Skip to content

Commit ebd1ffa

Browse files
committed
Add in-memory cache for getLastEntryInLedger to reduce RocksDB getFloor CPU cost
1 parent 8a8f4d6 commit ebd1ffa

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
@@ -34,6 +34,7 @@
3434
import org.apache.bookkeeper.conf.ServerConfiguration;
3535
import org.apache.bookkeeper.stats.StatsLogger;
3636
import org.apache.bookkeeper.util.collections.ConcurrentLongHashSet;
37+
import org.apache.bookkeeper.util.collections.ConcurrentLongLongHashMap;
3738
import org.slf4j.Logger;
3839
import org.slf4j.LoggerFactory;
3940

@@ -45,15 +46,25 @@
4546
*/
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
() -> {
@@ -115,6 +126,18 @@ public long getLastEntryInLedger(long ledgerId) throws IOException {
115126
}
116127

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

120143
long startTimeNanos = MathUtils.nowInNano();
@@ -139,6 +162,11 @@ private long getLastEntryInLedgerInternal(long ledgerId) throws IOException {
139162
if (log.isDebugEnabled()) {
140163
log.debug("Found last page in storage db for ledger {} - last entry: {}", ledgerId, lastEntryId);
141164
}
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);
@@ -171,6 +199,18 @@ public void addLocation(Batch batch, long ledgerId, long entryId, long location)
171199
key.recycle();
172200
value.recycle();
173201
}
202+
203+
// Update the last entry cache if this entry is newer.
204+
// ConcurrentLongLongHashMap.get() returns -1 if not found.
205+
long cachedLastEntry = lastEntryCache.get(ledgerId);
206+
if (cachedLastEntry < entryId) {
207+
// Clear the cache if it exceeds the max size to bound memory usage.
208+
// The cache will quickly repopulate with hot ledgers on subsequent reads.
209+
if (cachedLastEntry < 0 && lastEntryCache.size() >= lastEntryCacheMaxSize) {
210+
lastEntryCache.clear();
211+
}
212+
lastEntryCache.put(ledgerId, entryId);
213+
}
174214
}
175215

176216
public void updateLocations(Iterable<EntryLocation> newLocations) throws IOException {
@@ -195,6 +235,7 @@ public void delete(long ledgerId) throws IOException {
195235
// We need to find all the LedgerIndexPage records belonging to one specific
196236
// ledgers
197237
deletedLedgers.add(ledgerId);
238+
lastEntryCache.remove(ledgerId);
198239
}
199240

200241
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)