Skip to content

Commit c29c06e

Browse files
authored
Changed random eviction cache to accounts only (#4650)
# Description This change adds a configurable memory based cap to the LiveBucketList random eviction cache. It also modifies the cache such that only accounts are stored. Following initial testing, the random eviction cache improved TX flooding performance, but had virtually no effect on apply time. This makes sense, since apply time already leverages the ltx cache and only makes at most one disk read per ledger entry. TX flooding on the other hand has no such queue and constantly re reads entries off disk. Given that we already have an ltx cache for apply time, it makes sense to only cache the entry types that are used by transaction flooding (i.e. `ACCOUNT`). My initial default setting is to cache 3 GB. Currently accounts take up about 2.5 GB in the BucketList, so this allows us to cache all accounts with some room for growth, and 3 GB seems like a modest increase in RAM requirements for validators. # Checklist - [x] Reviewed the [contributing](https://github.com/stellar/stellar-core/blob/master/CONTRIBUTING.md#submitting-changes) document - [x] Rebased on top of master (no merge commits) - [x] Ran `clang-format` v8.0.0 (via `make format` or the Visual Studio extension) - [x] Compiles - [x] Ran all tests - [ ] If change impacts performance, include supporting evidence per the [performance document](https://github.com/stellar/stellar-core/blob/master/performance-eval/performance-eval.md)
2 parents 5752780 + bdc97ad commit c29c06e

19 files changed

+410
-88
lines changed

docs/stellar-core_example.cfg

+6-7
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,12 @@ MAX_DEX_TX_OPERATIONS_IN_TX_SET = 0
235235
# 0, indiviudal index is always used. Default page size 16 kb.
236236
BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 14
237237

238-
# BUCKETLIST_DB_CACHED_PERCENT (Integer) default 10
239-
# Percentage of entries cached by BucketListDB when Bucket size is larger
240-
# than BUCKETLIST_DB_INDEX_CUTOFF. Note that this value does not impact
241-
# Buckets smaller than BUCKETLIST_DB_INDEX_CUTOFF, as they are always
242-
# completely held in memory. Roughly speaking, RAM usage for BucketList
243-
# cache == BucketListSize * (BUCKETLIST_DB_CACHED_PERCENT / 100).
244-
BUCKETLIST_DB_CACHED_PERCENT = 10
238+
# BUCKETLIST_DB_MEMORY_FOR_CACHING (Integer) default 3000
239+
# Memory used for caching entries by BucketListDB when Bucket size is
240+
# larger than BUCKETLIST_DB_INDEX_CUTOFF, in MB. Note that this value does
241+
# not impact Buckets smaller than BUCKETLIST_DB_INDEX_CUTOFF, as they are
242+
# always completely held in memory. If set to 0, caching is disabled.
243+
BUCKETLIST_DB_MEMORY_FOR_CACHING = 3000
245244

246245
# BUCKETLIST_DB_INDEX_CUTOFF (Integer) default 100
247246
# Size, in MB, determining whether a bucket should have an individual

src/bucket/BucketIndexUtils.cpp

+1-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ namespace stellar
1919
std::streamoff
2020
getPageSizeFromConfig(Config const& cfg)
2121
{
22-
if (cfg.BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT == 0 ||
23-
cfg.BUCKETLIST_DB_CACHED_PERCENT == 100)
22+
if (cfg.BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT == 0)
2423
{
2524
return 0;
2625
}

src/bucket/BucketIndexUtils.h

+4
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,17 @@ std::streamoff getPageSizeFromConfig(Config const& cfg);
9898
// Builds index for given bucketfile. This is expensive (> 20 seconds
9999
// for the largest buckets) and should only be called once. Constructs a
100100
// DiskIndex or InMemoryIndex depending on config and Bucket size.
101+
// Note: Constructor does not initialize the cache for live bucket indexes,
102+
// as this must be done when the Bucket is being added to the BucketList
101103
template <class BucketT>
102104
std::unique_ptr<typename BucketT::IndexT const>
103105
createIndex(BucketManager& bm, std::filesystem::path const& filename,
104106
Hash const& hash, asio::io_context& ctx, SHA256* hasher);
105107

106108
// Loads index from given file. If file does not exist or if saved
107109
// index does not have expected version or pageSize, return null
110+
// Note: Constructor does not initialize the cache for live bucket indexes,
111+
// as this must be done when the Bucket is being added to the BucketList
108112
template <class BucketT>
109113
std::unique_ptr<typename BucketT::IndexT const>
110114
loadIndex(BucketManager const& bm, std::filesystem::path const& filename,

src/bucket/BucketManager.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -1291,6 +1291,8 @@ BucketManager::assumeState(HistoryArchiveState const& has,
12911291
mLiveBucketList->getLevel(i).setNext(nextFuture);
12921292
}
12931293

1294+
mLiveBucketList->maybeInitializeCaches(mConfig);
1295+
12941296
if (restartMerges)
12951297
{
12961298
mLiveBucketList->restartMerges(mApp, maxProtocolVersion,

src/bucket/BucketUtils.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,18 @@ BucketEntryCounters::numEntries() const
348348
return num;
349349
}
350350

351+
BucketEntryCounters::BucketEntryCounters()
352+
{
353+
for (uint32_t type =
354+
static_cast<uint32_t>(LedgerEntryTypeAndDurability::ACCOUNT);
355+
type < static_cast<uint32_t>(LedgerEntryTypeAndDurability::NUM_TYPES);
356+
++type)
357+
{
358+
entryTypeCounts[static_cast<LedgerEntryTypeAndDurability>(type)] = 0;
359+
entryTypeSizes[static_cast<LedgerEntryTypeAndDurability>(type)] = 0;
360+
}
361+
}
362+
351363
template void
352364
BucketEntryCounters::count<LiveBucket>(LiveBucket::EntryT const& be);
353365
template void BucketEntryCounters::count<HotArchiveBucket>(

src/bucket/BucketUtils.h

+2
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ struct BucketEntryCounters
208208
{
209209
ar(entryTypeCounts, entryTypeSizes);
210210
}
211+
212+
BucketEntryCounters();
211213
};
212214

213215
template <class BucketT>

src/bucket/LiveBucket.cpp

+19
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,17 @@ LiveBucket::apply(Application& app) const
308308
}
309309
counters.logInfo("direct", 0, app.getClock().now());
310310
}
311+
312+
size_t
313+
LiveBucket::getMaxCacheSize() const
314+
{
315+
if (!isEmpty())
316+
{
317+
return getIndex().getMaxCacheSize();
318+
}
319+
320+
return 0;
321+
}
311322
#endif // BUILD_TESTS
312323

313324
std::optional<std::pair<std::streamoff, std::streamoff>>
@@ -431,6 +442,14 @@ LiveBucket::getBucketVersion() const
431442
return it.getMetadata().ledgerVersion;
432443
}
433444

445+
void
446+
LiveBucket::maybeInitializeCache(size_t totalBucketListAccountsSizeBytes,
447+
Config const& cfg) const
448+
{
449+
releaseAssert(mIndex);
450+
mIndex->maybeInitializeCache(totalBucketListAccountsSizeBytes, cfg);
451+
}
452+
434453
BucketEntryCounters const&
435454
LiveBucket::getBucketEntryCounters() const
436455
{

src/bucket/LiveBucket.h

+8
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class LiveBucket : public BucketBase<LiveBucket, LiveBucketIndex>,
8383
// entry in the database (respectively; if the entry is dead (a
8484
// tombstone), deletes the corresponding entry in the database.
8585
void apply(Application& app) const;
86+
size_t getMaxCacheSize() const;
8687
#endif
8788

8889
// Returns [lowerBound, upperBound) of file offsets for all offers in the
@@ -120,6 +121,13 @@ class LiveBucket : public BucketBase<LiveBucket, LiveBucketIndex>,
120121

121122
uint32_t getBucketVersion() const;
122123

124+
// Initializes the random eviction cache if it has not already been
125+
// initialized. totalBucketListAccountsSizeBytes is the total size, in
126+
// bytes, of all BucketEntries in the BucketList that hold ACCOUNT entries,
127+
// including INIT, LIVE and DEAD entries.
128+
void maybeInitializeCache(size_t totalBucketListAccountsSizeBytes,
129+
Config const& cfg) const;
130+
123131
BucketEntryCounters const& getBucketEntryCounters() const;
124132

125133
friend class LiveBucketSnapshot;

src/bucket/LiveBucketIndex.cpp

+107-16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "bucket/LiveBucketIndex.h"
66
#include "bucket/BucketIndexUtils.h"
77
#include "bucket/BucketManager.h"
8+
#include "bucket/BucketUtils.h"
89
#include "bucket/DiskIndex.h"
910
#include "util/Fs.h"
1011
#include "util/GlobalChecks.h"
@@ -65,21 +66,6 @@ LiveBucketIndex::LiveBucketIndex(BucketManager& bm,
6566
pageSize, filename);
6667
mDiskIndex = std::make_unique<DiskIndex<LiveBucket>>(
6768
bm, filename, pageSize, hash, ctx, hasher);
68-
69-
auto percentCached = bm.getConfig().BUCKETLIST_DB_CACHED_PERCENT;
70-
if (percentCached > 0)
71-
{
72-
auto const& counters = mDiskIndex->getBucketEntryCounters();
73-
auto cacheSize = (counters.numEntries() * percentCached) / 100;
74-
75-
// Minimum cache size of 100 if we are going to cache a non-zero
76-
// number of entries
77-
// We don't want to reserve here, since caches only live as long as
78-
// the lifetime of the Bucket and fill relatively slowly
79-
mCache = std::make_unique<CacheT>(std::max<size_t>(cacheSize, 100),
80-
/*separatePRNG=*/false,
81-
/*reserve=*/false);
82-
}
8369
}
8470
}
8571

@@ -95,6 +81,77 @@ LiveBucketIndex::LiveBucketIndex(BucketManager const& bm, Archive& ar,
9581
releaseAssertOrThrow(pageSize != 0);
9682
}
9783

84+
void
85+
LiveBucketIndex::maybeInitializeCache(size_t totalBucketListAccountsSizeBytes,
86+
Config const& cfg) const
87+
{
88+
// Everything is already in memory, no need for a redundant cache.
89+
if (mInMemoryIndex)
90+
{
91+
return;
92+
}
93+
94+
// Cache is already initialized
95+
if (std::shared_lock<std::shared_mutex> lock(mCacheMutex); mCache)
96+
{
97+
return;
98+
}
99+
100+
releaseAssert(mDiskIndex);
101+
102+
auto accountsInThisBucket =
103+
mDiskIndex->getBucketEntryCounters().entryTypeCounts.at(
104+
LedgerEntryTypeAndDurability::ACCOUNT);
105+
106+
// Nothing to cache
107+
if (accountsInThisBucket == 0)
108+
{
109+
return;
110+
}
111+
112+
// Convert from MB to bytes, max size for entire BucketList cache
113+
auto maxBucketListBytesToCache =
114+
cfg.BUCKETLIST_DB_MEMORY_FOR_CACHING * 1024 * 1024;
115+
116+
std::unique_lock<std::shared_mutex> lock(mCacheMutex);
117+
if (totalBucketListAccountsSizeBytes < maxBucketListBytesToCache)
118+
{
119+
// We can cache the entire bucket
120+
mCache = std::make_unique<CacheT>(accountsInThisBucket);
121+
}
122+
else
123+
{
124+
// The random eviction cache has an entry limit, but we expose a memory
125+
// limit in the validator config. We can't do an exact 1 to 1 mapping
126+
// because account entries have different sizes.
127+
//
128+
// First we take the fraction of the total BucketList size that this
129+
// bucket occupies to figure out how much memory to allocate. Then we
130+
// use the average account size to convert that to an entry count for
131+
// the cache.
132+
133+
auto accountBytesInThisBucket =
134+
mDiskIndex->getBucketEntryCounters().entryTypeSizes.at(
135+
LedgerEntryTypeAndDurability::ACCOUNT);
136+
137+
double fractionOfTotalBucketListBytes =
138+
static_cast<double>(accountBytesInThisBucket) /
139+
totalBucketListAccountsSizeBytes;
140+
141+
size_t bytesAvailableForBucketCache = static_cast<size_t>(
142+
maxBucketListBytesToCache * fractionOfTotalBucketListBytes);
143+
144+
double averageAccountSize =
145+
static_cast<double>(accountBytesInThisBucket) /
146+
accountsInThisBucket;
147+
148+
auto accountsToCache = static_cast<size_t>(
149+
bytesAvailableForBucketCache / averageAccountSize);
150+
151+
mCache = std::make_unique<CacheT>(accountsToCache);
152+
}
153+
}
154+
98155
LiveBucketIndex::IterT
99156
LiveBucketIndex::begin() const
100157
{
@@ -133,7 +190,7 @@ LiveBucketIndex::markBloomMiss() const
133190
std::shared_ptr<BucketEntry const>
134191
LiveBucketIndex::getCachedEntry(LedgerKey const& k) const
135192
{
136-
if (shouldUseCache())
193+
if (shouldUseCache() && isCachedType(k))
137194
{
138195
std::shared_lock<std::shared_mutex> lock(mCacheMutex);
139196
auto cachePtr = mCache->maybeGet(k);
@@ -262,6 +319,24 @@ LiveBucketIndex::getBucketEntryCounters() const
262319
return mInMemoryIndex->getBucketEntryCounters();
263320
}
264321

322+
bool
323+
LiveBucketIndex::shouldUseCache() const
324+
{
325+
if (mDiskIndex)
326+
{
327+
std::shared_lock<std::shared_mutex> lock(mCacheMutex);
328+
return mCache != nullptr;
329+
}
330+
331+
return false;
332+
}
333+
334+
bool
335+
LiveBucketIndex::isCachedType(LedgerKey const& lk)
336+
{
337+
return lk.type() == ACCOUNT;
338+
}
339+
265340
void
266341
LiveBucketIndex::maybeAddToCache(
267342
std::shared_ptr<BucketEntry const> const& entry) const
@@ -270,6 +345,10 @@ LiveBucketIndex::maybeAddToCache(
270345
{
271346
releaseAssertOrThrow(entry);
272347
auto k = getBucketLedgerKey(*entry);
348+
if (!isCachedType(k))
349+
{
350+
return;
351+
}
273352

274353
// If we are adding an entry to the cache, we must have missed it
275354
// earlier.
@@ -308,6 +387,18 @@ LiveBucketIndex::operator==(LiveBucketIndex const& in) const
308387

309388
return true;
310389
}
390+
391+
size_t
392+
LiveBucketIndex::getMaxCacheSize() const
393+
{
394+
if (shouldUseCache())
395+
{
396+
std::shared_lock<std::shared_mutex> lock(mCacheMutex);
397+
return mCache->maxSize();
398+
}
399+
400+
return 0;
401+
}
311402
#endif
312403

313404
template LiveBucketIndex::LiveBucketIndex(BucketManager const& bm,

src/bucket/LiveBucketIndex.h

+24-10
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ class LiveBucketIndex : public NonMovableOrCopyable
5959
RandomEvictionCache<LedgerKey, std::shared_ptr<BucketEntry const>>;
6060

6161
private:
62-
std::unique_ptr<DiskIndex<LiveBucket> const> mDiskIndex;
63-
std::unique_ptr<InMemoryIndex const> mInMemoryIndex;
64-
std::unique_ptr<CacheT> mCache;
62+
std::unique_ptr<DiskIndex<LiveBucket> const> mDiskIndex{};
63+
std::unique_ptr<InMemoryIndex const> mInMemoryIndex{};
6564

6665
// The indexes themselves are thread safe, as they are immutable after
6766
// construction. The cache is not, all accesses must first acquire this
6867
// mutex.
68+
mutable std::unique_ptr<CacheT> mCache{};
6969
mutable std::shared_mutex mCacheMutex;
7070

7171
medida::Meter& mCacheHitMeter;
@@ -83,11 +83,12 @@ class LiveBucketIndex : public NonMovableOrCopyable
8383
return std::get<InMemoryIndex::IterT>(iter);
8484
}
8585

86-
bool
87-
shouldUseCache() const
88-
{
89-
return mDiskIndex && mCache;
90-
}
86+
bool shouldUseCache() const;
87+
88+
// Because we already have an LTX level cache at apply time, we only need to
89+
// cache entry types that are used during TX validation and flooding.
90+
// Currently, that is only ACCOUNT.
91+
static bool isCachedType(LedgerKey const& lk);
9192

9293
// Returns nullptr if cache is not enabled or entry not found
9394
std::shared_ptr<BucketEntry const> getCachedEntry(LedgerKey const& k) const;
@@ -97,14 +98,28 @@ class LiveBucketIndex : public NonMovableOrCopyable
9798
inline static const uint32_t BUCKET_INDEX_VERSION = 5;
9899

99100
// Constructor for creating new index from Bucketfile
101+
// Note: Constructor does not initialize the cache
100102
LiveBucketIndex(BucketManager& bm, std::filesystem::path const& filename,
101103
Hash const& hash, asio::io_context& ctx, SHA256* hasher);
102104

103105
// Constructor for loading pre-existing index from disk
106+
// Note: Constructor does not initialize the cache
104107
template <class Archive>
105108
LiveBucketIndex(BucketManager const& bm, Archive& ar,
106109
std::streamoff pageSize);
107110

111+
// Initializes the random eviction cache if it has not already been
112+
// initialized. The random eviction cache itself has an entry limit, but we
113+
// expose a memory limit in the validator config. To account for this, we
114+
// check what percentage of the total number of accounts are in this bucket
115+
// and allocate space accordingly.
116+
//
117+
// bucketListTotalAccountSizeBytes is the total size, in bytes, of all
118+
// BucketEntries in the BucketList that hold ACCOUNT entries, including
119+
// INIT, LIVE and DEAD entries.
120+
void maybeInitializeCache(size_t bucketListTotalAccountSizeBytes,
121+
Config const& cfg) const;
122+
108123
// Returns true if LedgerEntryType not supported by BucketListDB
109124
static bool typeNotSupported(LedgerEntryType t);
110125

@@ -131,8 +146,7 @@ class LiveBucketIndex : public NonMovableOrCopyable
131146
void markBloomMiss() const;
132147
#ifdef BUILD_TESTS
133148
bool operator==(LiveBucketIndex const& in) const;
134-
135-
void clearCache() const;
149+
size_t getMaxCacheSize() const;
136150
#endif
137151
};
138152
}

0 commit comments

Comments
 (0)