Skip to content

Commit df85b3b

Browse files
committed
Add in-memory cache for getLastEntryInLedger to reduce RocksDB getFloor CPU cost
1 parent cbb3367 commit df85b3b

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/storage/ldb/EntryLocationIndex.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.bookkeeper.conf.ServerConfiguration;
3636
import org.apache.bookkeeper.stats.StatsLogger;
3737
import org.apache.bookkeeper.util.collections.ConcurrentLongHashSet;
38+
import org.apache.bookkeeper.util.collections.ConcurrentLongLongHashMap;
3839

3940
/**
4041
* Maintains an index of the entry locations in the EntryLogger.
@@ -45,15 +46,25 @@
4546
@CustomLog
4647
public class EntryLocationIndex implements Closeable {
4748

49+
static final String LAST_ENTRY_CACHE_MAX_SIZE = "dbStorage_lastEntryCacheMaxSize";
50+
private static final long DEFAULT_LAST_ENTRY_CACHE_MAX_SIZE = 10_000;
51+
4852
private final KeyValueStorage locationsDb;
4953
private final ConcurrentLongHashSet deletedLedgers = ConcurrentLongHashSet.newBuilder().build();
54+
private final ConcurrentLongLongHashMap lastEntryCache;
55+
private final long lastEntryCacheMaxSize;
5056
private final EntryLocationIndexStats stats;
5157
private boolean isCompacting;
5258

5359
public EntryLocationIndex(ServerConfiguration conf, KeyValueStorageFactory storageFactory, String basePath,
5460
StatsLogger stats) throws IOException {
5561
locationsDb = storageFactory.newKeyValueStorage(basePath, "locations", DbConfigType.EntryLocation, conf);
5662

63+
this.lastEntryCacheMaxSize = conf.getLong(LAST_ENTRY_CACHE_MAX_SIZE, DEFAULT_LAST_ENTRY_CACHE_MAX_SIZE);
64+
this.lastEntryCache = ConcurrentLongLongHashMap.newBuilder()
65+
.expectedItems((int) Math.min(lastEntryCacheMaxSize, Integer.MAX_VALUE))
66+
.build();
67+
5768
this.stats = new EntryLocationIndexStats(
5869
stats,
5970
() -> {
@@ -114,6 +125,18 @@ public long getLastEntryInLedger(long ledgerId) throws IOException {
114125
}
115126

116127
private long getLastEntryInLedgerInternal(long ledgerId) throws IOException {
128+
// Check in-memory cache first to avoid expensive getFloor() calls.
129+
// ConcurrentLongLongHashMap.get() returns -1 if not found.
130+
long cachedLastEntry = lastEntryCache.get(ledgerId);
131+
if (cachedLastEntry >= 0) {
132+
if (log.isDebugEnabled()) {
133+
log.debug("Found last entry for ledger {} in cache: {}", ledgerId, cachedLastEntry);
134+
}
135+
stats.getGetLastEntryInLedgerStats()
136+
.registerSuccessfulEvent(0, TimeUnit.NANOSECONDS);
137+
return cachedLastEntry;
138+
}
139+
117140
LongPairWrapper maxEntryId = LongPairWrapper.get(ledgerId, Long.MAX_VALUE);
118141

119142
long startTimeNanos = MathUtils.nowInNano();
@@ -139,6 +162,11 @@ private long getLastEntryInLedgerInternal(long ledgerId) throws IOException {
139162
.attr("ledgerId", ledgerId)
140163
.attr("lastEntryId", lastEntryId)
141164
.log("Found last page in storage db for ledger");
165+
// Populate cache for future lookups
166+
if (lastEntryCache.size() >= lastEntryCacheMaxSize) {
167+
lastEntryCache.clear();
168+
}
169+
lastEntryCache.put(ledgerId, lastEntryId);
142170
return lastEntryId;
143171
} else {
144172
throw new Bookie.NoEntryException(ledgerId, -1);
@@ -173,6 +201,18 @@ public void addLocation(Batch batch, long ledgerId, long entryId, long location)
173201
key.recycle();
174202
value.recycle();
175203
}
204+
205+
// Update the last entry cache if this entry is newer.
206+
// ConcurrentLongLongHashMap.get() returns -1 if not found.
207+
long cachedLastEntry = lastEntryCache.get(ledgerId);
208+
if (cachedLastEntry < entryId) {
209+
// Clear the cache if it exceeds the max size to bound memory usage.
210+
// The cache will quickly repopulate with hot ledgers on subsequent reads.
211+
if (cachedLastEntry < 0 && lastEntryCache.size() >= lastEntryCacheMaxSize) {
212+
lastEntryCache.clear();
213+
}
214+
lastEntryCache.put(ledgerId, entryId);
215+
}
176216
}
177217

178218
public void updateLocations(Iterable<EntryLocation> newLocations) throws IOException {
@@ -196,6 +236,7 @@ public void delete(long ledgerId) throws IOException {
196236
// We need to find all the LedgerIndexPage records belonging to one specific
197237
// ledgers
198238
deletedLedgers.add(ledgerId);
239+
lastEntryCache.remove(ledgerId);
199240
}
200241

201242
public String getEntryLocationDBPath() {

bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/storage/ldb/EntryLocationIndexTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222

2323
import static org.junit.Assert.assertEquals;
2424
import static org.junit.Assert.assertTrue;
25+
import static org.junit.Assert.fail;
2526

2627
import java.io.File;
2728
import java.io.IOException;
29+
import org.apache.bookkeeper.bookie.Bookie;
2830
import org.apache.bookkeeper.conf.ServerConfiguration;
2931
import org.apache.bookkeeper.stats.NullStatsLogger;
3032
import org.apache.bookkeeper.test.TestStatsProvider;
@@ -205,6 +207,52 @@ public void testDeleteSpecialEntry() throws IOException {
205207
assertEquals(0, idx.getLocation(40312, 10));
206208
}
207209

210+
@Test
211+
public void testGetLastEntryInLedgerCache() throws Exception {
212+
File tmpDir = File.createTempFile("bkTest", ".dir");
213+
tmpDir.delete();
214+
tmpDir.mkdir();
215+
tmpDir.deleteOnExit();
216+
217+
EntryLocationIndex idx = new EntryLocationIndex(serverConfiguration, KeyValueStorageRocksDB.factory,
218+
tmpDir.getAbsolutePath(), NullStatsLogger.INSTANCE);
219+
220+
// Add entries for ledger 1
221+
idx.addLocation(1, 0, 100);
222+
idx.addLocation(1, 1, 101);
223+
idx.addLocation(1, 2, 102);
224+
225+
// Add entries for ledger 2
226+
idx.addLocation(2, 0, 200);
227+
idx.addLocation(2, 5, 205);
228+
229+
// First call should hit RocksDB and populate cache
230+
assertEquals(2, idx.getLastEntryInLedger(1));
231+
assertEquals(5, idx.getLastEntryInLedger(2));
232+
233+
// Second call should hit cache and return same result
234+
assertEquals(2, idx.getLastEntryInLedger(1));
235+
assertEquals(5, idx.getLastEntryInLedger(2));
236+
237+
// Adding a newer entry should update cache
238+
idx.addLocation(1, 10, 110);
239+
assertEquals(10, idx.getLastEntryInLedger(1));
240+
241+
// Delete should invalidate cache
242+
idx.delete(1);
243+
try {
244+
idx.getLastEntryInLedger(1);
245+
fail("Should have thrown NoEntryException");
246+
} catch (Bookie.NoEntryException e) {
247+
// expected
248+
}
249+
250+
// Ledger 2 should still work
251+
assertEquals(5, idx.getLastEntryInLedger(2));
252+
253+
idx.close();
254+
}
255+
208256
@Test
209257
public void testEntryIndexLookupLatencyStats() throws IOException {
210258
File tmpDir = File.createTempFile("bkTest", ".dir");

0 commit comments

Comments
 (0)