Skip to content

Commit 1703e33

Browse files
authored
feat(cache): introduce three-tier LRU cache (active→inactive→idle) (alibaba#1209)
* refactor(cache): introduce three-tier cache (active→inactive→idle) * add adaptive threshold * fix build for gcc12 * use string-keyed unordered set
1 parent fb38b48 commit 1703e33

3 files changed

Lines changed: 345 additions & 139 deletions

File tree

fs/cache/full_file_cache/cache_pool.cpp

Lines changed: 120 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ limitations under the License.
2020
#include <sys/stat.h>
2121

2222
#include <algorithm>
23+
#include <numeric>
2324
#include <sys/statvfs.h>
2425

2526
#include "cache_store.h"
@@ -79,11 +80,7 @@ ICacheStore* FileCachePool::do_open(std::string_view pathname, int flags, mode_t
7980
return nullptr;
8081
}
8182

82-
// Check idle container first
83-
auto idleIt = idleFileIndex_.find(pathname);
84-
if (idleIt != idleFileIndex_.end()) {
85-
promoteFromIdle(idleIt);
86-
}
83+
promoteToHot(pathname);
8784

8885
auto find = fileIndex_.find(pathname);
8986
if (find == fileIndex_.end()) {
@@ -96,15 +93,7 @@ ICacheStore* FileCachePool::do_open(std::string_view pathname, int flags, mode_t
9693
find->second->openCount++;
9794
}
9895

99-
// If LRU exceeds the limit, demote the tail (only if not open) to idle
100-
while (lru_.size() > demoteThreshold_) {
101-
auto tailIt = lru_.back();
102-
if (tailIt->second->openCount == 0) {
103-
demoteToIdle(tailIt);
104-
} else {
105-
break;
106-
}
107-
}
96+
demoteToCold();
10897

10998
return new FileCacheStore(this, localFile, refillUnit_, find);
11099
}
@@ -139,10 +128,13 @@ int FileCachePool::stat(CacheStat* stat, std::string_view pathname) {
139128
}
140129

141130
int FileCachePool::evict(std::string_view filename) {
142-
// Check idle container first
143-
auto idleIt = idleFileIndex_.find(filename);
144-
if (idleIt != idleFileIndex_.end()) {
145-
return evictIdleEntry(idleIt) >= 0 ? 0 : -1;
131+
// Check cold tiers first
132+
for (auto* tier : coldTiers_) {
133+
if (tier->contains(filename)) {
134+
auto freed = truncateAndUnlink(filename);
135+
tier->remove(filename);
136+
return freed >= 0 ? 0 : -1;
137+
}
146138
}
147139

148140
auto fileIter = fileIndex_.find(filename);
@@ -262,10 +254,18 @@ void FileCachePool::eviction() {
262254

263255
isFull_ = true;
264256

265-
// Phase 1: evict from idle tier first.
266-
actualEvict -= evictIdleWhenFull(actualEvict);
257+
// Evict from cold tiers first in reverse order
258+
for (int i = coldTiers_.size() - 1; i >= 0; i--) {
259+
auto* tier = coldTiers_[i];
260+
while (actualEvict > 0 && !tier->empty() && !exit_) {
261+
auto name = tier->victim();
262+
auto freed = truncateAndUnlink(name);
263+
tier->remove(name);
264+
if (freed >= 0) actualEvict -= freed;
265+
photon::thread_yield();
266+
}
267+
}
267268

268-
// Phase 2: fall back to LRU eviction when idle tier is exhausted
269269
while (actualEvict > 0 && !lru_.empty() && !exit_) {
270270
auto fileIter = lru_.back();
271271
const auto& fileName = fileIter->first;
@@ -347,34 +347,72 @@ int FileCachePool::insertFile(std::string_view file) {
347347
}
348348
auto fileSize = st.st_blocks * kDiskBlockSize;
349349

350-
if (lru_.size() >= demoteThreshold_) {
351-
auto idleLruIt = idleLru_.push_front(idleFileIndex_.end());
352-
auto iter = idleFileIndex_.emplace(file, idleLruIt).first;
353-
idleLru_.front() = iter;
354-
} else {
355-
auto lruIter = lru_.push_front(fileIndex_.end());
356-
auto entry = std::unique_ptr<LruEntry>(new LruEntry{lruIter, 0, fileSize});
357-
auto iter = fileIndex_.emplace(file, std::move(entry)).first;
358-
lru_.front() = iter;
359-
}
350+
auto lruIter = lru_.push_front(fileIndex_.end());
351+
auto entry = std::unique_ptr<LruEntry>(new LruEntry{lruIter, 0, fileSize});
352+
auto iter = fileIndex_.emplace(file, std::move(entry)).first;
353+
lru_.front() = iter;
360354
totalUsed_ += fileSize;
355+
356+
demoteToCold();
361357
return 0;
362358
}
363359

364-
// Demote a LRU entry (openCount must be 0) to the idle container.
365-
void FileCachePool::demoteToIdle(FileNameMap::iterator iter) {
366-
auto idleLruIt = idleLru_.push_front(idleFileIndex_.end());
367-
auto idleIndexIt = idleFileIndex_.emplace(iter->first, idleLruIt).first;
368-
idleLru_.front() = idleIndexIt;
360+
void FileCachePool::adaptThresholds() {
361+
uint64_t count = std::accumulate(tierHits_.begin(), tierHits_.end(), 0ULL);
362+
for (size_t i = 0; i + 1 < tierHits_.size(); i++) {
363+
if (count > 0) {
364+
double ratio = static_cast<double>(tierHits_[i]) / count;
365+
// High ratio => this tier absorbs the bulk of hits, it has room to shrink.
366+
// Low ratio => hits leak to colder tiers, grow it.
367+
if (ratio > 0.90) thresholds_[i].adapt(0.80);
368+
else if (ratio < 0.50) thresholds_[i].adapt(1.25);
369+
}
370+
count -= tierHits_[i];
371+
}
369372

370-
lru_.remove(iter->second->lruIter);
371-
fileIndex_.erase(iter);
373+
promoteCount_ = 0;
374+
tierHits_.fill(0);
372375
}
373376

374-
// Promote an idle entry back to the front of LRU.
375-
void FileCachePool::promoteFromIdle(IdleFileNameMap::iterator idleIt) {
376-
uint32_t idleLruIter = idleIt->second;
377-
const auto& filename = idleIt->first;
377+
void FileCachePool::demoteToCold() {
378+
while (lru_.size() > thresholds_[0].value) {
379+
auto tailIt = lru_.back();
380+
if (tailIt->second->openCount != 0) break;
381+
382+
coldTiers_[0]->insert(tailIt->first);
383+
lru_.remove(tailIt->second->lruIter);
384+
fileIndex_.erase(tailIt);
385+
386+
for (size_t i = 1; i < coldTiers_.size(); i++) {
387+
if (coldTiers_[i-1]->size() > thresholds_[i].value) {
388+
auto key = coldTiers_[i-1]->victim();
389+
coldTiers_[i]->insert(key);
390+
coldTiers_[i-1]->remove(key);
391+
} else break;
392+
}
393+
}
394+
}
395+
396+
void FileCachePool::promoteToHot(std::string_view filename) {
397+
auto find = fileIndex_.find(filename);
398+
if (find != fileIndex_.end()) {
399+
tierHits_[0]++;
400+
return;
401+
}
402+
403+
bool found = false;
404+
for (size_t i = 0; i < coldTiers_.size(); ++i) {
405+
if (coldTiers_[i]->contains(filename)) {
406+
tierHits_[i + 1]++;
407+
found = true;
408+
coldTiers_[i]->remove(filename);
409+
break;
410+
}
411+
}
412+
if (!found) return;
413+
414+
promoteCount_++;
415+
if (promoteCount_ >= kPromotesPerAdapt) adaptThresholds();
378416

379417
struct stat st = {};
380418
uint64_t fileSize = 0;
@@ -386,15 +424,9 @@ void FileCachePool::promoteFromIdle(IdleFileNameMap::iterator idleIt) {
386424
auto entry = std::unique_ptr<LruEntry>(new LruEntry{lruIter, 0, fileSize});
387425
auto iter = fileIndex_.emplace(filename, std::move(entry)).first;
388426
lru_.front() = iter;
389-
390-
idleLru_.remove(idleLruIter);
391-
idleFileIndex_.erase(idleIt);
392427
}
393428

394-
// Evict the idle entry pointed to by idleIt; return freed bytes or -1 on error.
395-
ssize_t FileCachePool::evictIdleEntry(IdleFileNameMap::iterator idleIt) {
396-
const auto& filename = idleIt->first;
397-
429+
ssize_t FileCachePool::truncateAndUnlink(std::string_view filename) {
398430
struct stat st = {};
399431
uint64_t fileSize = 0;
400432
if (mediaFs_->stat(filename.data(), &st) == 0) {
@@ -414,22 +446,49 @@ ssize_t FileCachePool::evictIdleEntry(IdleFileNameMap::iterator idleIt) {
414446
// we still evict fileSize bytes even if unlink fails
415447
LOG_ERRNO_RETURN(0, fileSize, "unlink failed, name : `", filename);
416448
}
417-
418-
uint32_t idleLruIter = idleIt->second;
419-
idleLru_.remove(idleLruIter);
420-
idleFileIndex_.erase(idleIt);
421449
return static_cast<ssize_t>(fileSize);
422450
}
423451

424-
uint64_t FileCachePool::evictIdleWhenFull(uint64_t needEvict) {
425-
uint64_t evictSize = 0;
426-
while (evictSize < needEvict && !idleLru_.empty() && !exit_) {
427-
auto r = evictIdleEntry(idleLru_.back());
428-
if (r >= 0) evictSize += static_cast<uint64_t>(r);
429-
photon::thread_yield();
430-
}
431-
return evictSize;
452+
// --- InactiveCacheTier ---
453+
bool InactiveCacheTier::contains(std::string_view name) {
454+
return index_.find(name) != index_.end();
432455
}
433456

457+
void InactiveCacheTier::remove(std::string_view name) {
458+
auto it = index_.find(name);
459+
if (it == index_.end()) return;
460+
lru_.remove(it->second);
461+
index_.erase(it);
462+
}
463+
464+
void InactiveCacheTier::insert(std::string_view name) {
465+
auto lruIt = lru_.push_front(index_.end());
466+
auto it = index_.emplace(name, lruIt).first;
467+
lru_.front() = it;
468+
}
469+
470+
size_t InactiveCacheTier::size() { return index_.size(); }
471+
bool InactiveCacheTier::empty() { return lru_.empty(); }
472+
std::string_view InactiveCacheTier::victim() { return lru_.back()->first; }
473+
474+
// --- IdleCacheTier ---
475+
bool IdleCacheTier::contains(std::string_view name) {
476+
return index_.find(std::string(name)) != index_.end();
477+
}
478+
479+
void IdleCacheTier::remove(std::string_view name) {
480+
auto it = index_.find(std::string(name));
481+
if (it == index_.end()) return;
482+
index_.erase(it);
483+
}
484+
485+
void IdleCacheTier::insert(std::string_view name) {
486+
index_.emplace(name);
487+
}
488+
489+
size_t IdleCacheTier::size() { return index_.size(); }
490+
bool IdleCacheTier::empty() { return index_.empty(); }
491+
std::string_view IdleCacheTier::victim() { return *index_.begin(); }
492+
434493
}
435494
}

fs/cache/full_file_cache/cache_pool.h

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ limitations under the License.
1616

1717
#pragma once
1818

19+
#include <array>
1920
#include <map>
2021
#include <memory>
2122
#include <string>
22-
#include <unordered_map>
2323
#include <vector>
2424
#include <photon/thread/thread.h>
2525
#include <photon/thread/timer.h>
@@ -32,6 +32,49 @@ limitations under the License.
3232
namespace photon {
3333
namespace fs {
3434

35+
// Abstract base for cold (non-hot) cache tiers.
36+
class ColdCacheTier {
37+
public:
38+
virtual ~ColdCacheTier() = default;
39+
virtual bool contains(std::string_view name) = 0;
40+
virtual void remove(std::string_view name) = 0;
41+
virtual void insert(std::string_view name) = 0;
42+
virtual size_t size() = 0;
43+
virtual bool empty() = 0;
44+
// The returned view is valid until remove().
45+
virtual std::string_view victim() = 0;
46+
};
47+
48+
class InactiveCacheTier : public ColdCacheTier {
49+
public:
50+
typedef map_string_key<uint32_t> IndexMap;
51+
typedef photon::fs::LRU<IndexMap::iterator, uint32_t> LRUContainer;
52+
53+
bool contains(std::string_view name) override;
54+
void remove(std::string_view name) override;
55+
void insert(std::string_view name) override;
56+
size_t size() override;
57+
bool empty() override;
58+
std::string_view victim() override;
59+
60+
private:
61+
LRUContainer lru_;
62+
IndexMap index_;
63+
};
64+
65+
class IdleCacheTier : public ColdCacheTier {
66+
public:
67+
bool contains(std::string_view name) override;
68+
void remove(std::string_view name) override;
69+
void insert(std::string_view name) override;
70+
size_t size() override;
71+
bool empty() override;
72+
std::string_view victim() override;
73+
74+
private:
75+
unordered_set_string_key<> index_;
76+
};
77+
3578
class FileCachePool : public photon::fs::ICachePool {
3679
public:
3780
FileCachePool(photon::fs::IFileSystem *mediaFs, uint64_t capacityInGB, uint64_t periodInUs,
@@ -42,8 +85,6 @@ class FileCachePool : public photon::fs::ICachePool {
4285
static const uint64_t kDiskBlockSize = 512; // stat(2)
4386
static const uint64_t kDeleteDelayInUs = 1000;
4487
static const uint32_t kWaterMarkRatio = 90;
45-
// max inuse-lru entries before demoting tail to idle-lru (default: 100w)
46-
static const uint32_t kDemoteThreshold = 1'000'000;
4788

4889
void Init();
4990

@@ -112,19 +153,37 @@ class FileCachePool : public photon::fs::ICachePool {
112153
// filename -> lruEntry
113154
FileNameMap fileIndex_;
114155

115-
uint32_t demoteThreshold_ = kDemoteThreshold;
156+
// Cold tiers: inactive (LRU-ordered) and idle (unordered).
157+
// Eviction/promote cascades iterate coldTiers_ in order.
158+
InactiveCacheTier inactiveTier_;
159+
IdleCacheTier idleTier_;
160+
std::array<ColdCacheTier*, 2> coldTiers_ = {&inactiveTier_, &idleTier_};
161+
162+
// Adaptive threshold tuning.
163+
struct AdaptiveThreshold {
164+
uint32_t min;
165+
uint32_t max;
166+
uint32_t value;
167+
168+
AdaptiveThreshold(int min, int max)
169+
: min(min), max(max), value(min) {}
170+
171+
void adapt(double ratio) {
172+
value = std::max(min, std::min(max, static_cast<uint32_t>(value * ratio)));
173+
}
174+
};
175+
std::array<AdaptiveThreshold, 2> thresholds_ =
176+
{AdaptiveThreshold(1'000, 100'000), AdaptiveThreshold(100'000, 100'000'000)};
177+
178+
static constexpr uint64_t kPromotesPerAdapt = 10'000;
179+
uint64_t promoteCount_ = 0;
180+
std::array<uint64_t, 3> tierHits_{};
116181

117-
// idleFileIndex_ stores filename -> lruContainer's iterator
118-
// idleLru_ stores iterators into idleFileIndex_; std::map nodes are stable.
119-
typedef map_string_key<uint32_t> IdleFileNameMap;
120-
typedef photon::fs::LRU<IdleFileNameMap::iterator, uint32_t> IdleLRUContainer;
121-
IdleLRUContainer idleLru_;
122-
IdleFileNameMap idleFileIndex_;
182+
void adaptThresholds();
183+
void demoteToCold();
123184

124-
void demoteToIdle(FileNameMap::iterator iter);
125-
void promoteFromIdle(IdleFileNameMap::iterator idleIter);
126-
uint64_t evictIdleWhenFull(uint64_t needEvictSize);
127-
ssize_t evictIdleEntry(IdleFileNameMap::iterator idleIter);
185+
void promoteToHot(std::string_view filename);
186+
ssize_t truncateAndUnlink(std::string_view filename);
128187

129188
friend struct FileCachePoolTest;
130189
};

0 commit comments

Comments
 (0)