Skip to content

Commit c4c7b1c

Browse files
dao-junhangc0276
authored andcommitted
Optimize bounded batch reads by predicting entry count (#4741)
* Optimize batch-read to avoid wasted disk IO * Fix checkstyle * Address review comments * Address review comments * fix conflicts * Address review comments * Address review comments * fix checkstyle * fix code * Address review comment * address review comment (cherry picked from commit 8e88b03)
1 parent cbb3367 commit c4c7b1c

5 files changed

Lines changed: 285 additions & 15 deletions

File tree

bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/BatchedReadEntryProcessor.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,19 @@ protected ReferenceCounted readData() throws Exception {
5656
}
5757
long maxSize = Math.min(batchRequest.getMaxSize(), maxBatchReadSize);
5858
//See BookieProtoEncoding.ResponseEnDeCoderPreV3#encode on BatchedReadResponse case.
59-
long frameSize = 24 + 8 + 4;
59+
long frameSize = 24 + 8 + Integer.BYTES;
6060
for (int i = 0; i < maxCount; i++) {
6161
try {
6262
ByteBuf entry = requestProcessor.getBookie().readEntry(request.getLedgerId(), request.getEntryId() + i);
63-
frameSize += entry.readableBytes() + 4;
6463
if (data == null) {
64+
frameSize += entry.readableBytes() + Integer.BYTES;
6565
data = ByteBufList.get(entry);
66+
long perEntrySize = entry.readableBytes() + Integer.BYTES;
67+
long remainingBudget = maxSize - frameSize;
68+
long remainingEntries = remainingBudget > 0 ? remainingBudget / Math.max(perEntrySize, 1L) : 0L;
69+
maxCount = (int) Math.min(maxCount, 1L + remainingEntries);
6670
} else {
71+
frameSize += entry.readableBytes() + Integer.BYTES;
6772
if (frameSize > maxSize) {
6873
entry.release();
6974
break;

bookkeeper-server/src/test/java/org/apache/bookkeeper/client/BookKeeperTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,16 @@ public void testBatchReadWithV2Protocol() throws Exception {
816816
entries++;
817817
}
818818
assertEquals(expectEntriesNum, entries);
819+
820+
// The first entry is still returned even when maxSize is smaller than a single entry frame.
821+
entries = 0;
822+
for (Enumeration<LedgerEntry> readEntries = lh.batchReadEntries(0, 20, headerSize);
823+
readEntries.hasMoreElements();) {
824+
LedgerEntry entry = readEntries.nextElement();
825+
assertArrayEquals(data, entry.getEntry());
826+
entries++;
827+
}
828+
assertEquals(1, entries);
819829
}
820830
}
821831
}

bookkeeper-server/src/test/java/org/apache/bookkeeper/proto/BatchedReadEntryProcessorTest.java

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919
package org.apache.bookkeeper.proto;
2020

2121
import static org.junit.Assert.assertEquals;
22+
import static org.junit.Assert.assertNotNull;
2223
import static org.junit.Assert.assertTrue;
2324
import static org.mockito.ArgumentMatchers.any;
2425
import static org.mockito.ArgumentMatchers.anyLong;
26+
import static org.mockito.ArgumentMatchers.eq;
2527
import static org.mockito.Mockito.doAnswer;
2628
import static org.mockito.Mockito.mock;
29+
import static org.mockito.Mockito.never;
2730
import static org.mockito.Mockito.times;
2831
import static org.mockito.Mockito.verify;
2932
import static org.mockito.Mockito.when;
@@ -46,6 +49,7 @@
4649
import org.apache.bookkeeper.common.concurrent.FutureUtils;
4750
import org.apache.bookkeeper.proto.BookieProtocol.Response;
4851
import org.apache.bookkeeper.stats.NullStatsLogger;
52+
import org.apache.bookkeeper.util.ByteBufList;
4953
import org.junit.Before;
5054
import org.junit.Test;
5155

@@ -221,4 +225,171 @@ public void testNonFenceRequest() throws Exception {
221225
assertEquals(BookieProtocol.BATCH_READ_ENTRY, response.getOpCode());
222226
assertEquals(BookieProtocol.EOK, response.getErrorCode());
223227
}
224-
}
228+
229+
@Test
230+
public void testReadDataPredictsMaxCountFromUniformFirstEntrySize() throws Exception {
231+
long ledgerId = 1234L;
232+
long firstEntryId = 1L;
233+
int entrySize = 20;
234+
long maxSize = 24 + 8 + Integer.BYTES + (entrySize + Integer.BYTES) * 2L;
235+
236+
ByteBuf firstEntry = entryBuffer(entrySize);
237+
ByteBuf secondEntry = entryBuffer(entrySize);
238+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId))).thenReturn(firstEntry);
239+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId + 1))).thenReturn(secondEntry);
240+
241+
BatchedReadEntryProcessor processor = createProcessor(ledgerId, firstEntryId, 5, maxSize);
242+
ByteBufList data = (ByteBufList) processor.readData();
243+
assertNotNull(data);
244+
try {
245+
assertEquals(2, data.size());
246+
} finally {
247+
data.release();
248+
}
249+
250+
verify(bookie, times(1)).readEntry(eq(ledgerId), eq(firstEntryId));
251+
verify(bookie, times(1)).readEntry(eq(ledgerId), eq(firstEntryId + 1));
252+
verify(bookie, never()).readEntry(eq(ledgerId), eq(firstEntryId + 2));
253+
}
254+
255+
@Test
256+
public void testReadDataReturnsFirstEntryEvenIfItAloneExceedsMaxSize() throws Exception {
257+
long ledgerId = 1235L;
258+
long firstEntryId = 1L;
259+
int firstEntrySize = 20;
260+
long maxSize = 50;
261+
262+
ByteBuf firstEntry = entryBuffer(firstEntrySize);
263+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId))).thenReturn(firstEntry);
264+
265+
BatchedReadEntryProcessor processor = createProcessor(ledgerId, firstEntryId, 5, maxSize);
266+
ByteBufList data = (ByteBufList) processor.readData();
267+
assertNotNull(data);
268+
try {
269+
assertEquals(1, data.size());
270+
} finally {
271+
data.release();
272+
}
273+
274+
verify(bookie, times(1)).readEntry(eq(ledgerId), eq(firstEntryId));
275+
verify(bookie, never()).readEntry(eq(ledgerId), eq(firstEntryId + 1));
276+
}
277+
278+
@Test
279+
public void testReadDataReleasesOneOverReadEntryWhenSizesGrow() throws Exception {
280+
long ledgerId = 1236L;
281+
long firstEntryId = 1L;
282+
int firstEntrySize = 10;
283+
int secondEntrySize = 40;
284+
long maxSize = 80;
285+
286+
ByteBuf firstEntry = entryBuffer(firstEntrySize);
287+
ByteBuf secondEntry = entryBuffer(secondEntrySize);
288+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId))).thenReturn(firstEntry);
289+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId + 1))).thenReturn(secondEntry);
290+
291+
BatchedReadEntryProcessor processor = createProcessor(ledgerId, firstEntryId, 5, maxSize);
292+
ByteBufList data = (ByteBufList) processor.readData();
293+
assertNotNull(data);
294+
try {
295+
assertEquals(1, data.size());
296+
assertEquals(0, secondEntry.refCnt());
297+
} finally {
298+
data.release();
299+
}
300+
301+
verify(bookie, times(1)).readEntry(eq(ledgerId), eq(firstEntryId));
302+
verify(bookie, times(1)).readEntry(eq(ledgerId), eq(firstEntryId + 1));
303+
verify(bookie, never()).readEntry(eq(ledgerId), eq(firstEntryId + 2));
304+
}
305+
306+
@Test
307+
public void testReadDataStopsOnMissingSubsequentEntry() throws Exception {
308+
long ledgerId = 1237L;
309+
long firstEntryId = 1L;
310+
int firstEntrySize = 20;
311+
312+
ByteBuf firstEntry = entryBuffer(firstEntrySize);
313+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId))).thenReturn(firstEntry);
314+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId + 1)))
315+
.thenThrow(new Bookie.NoEntryException(ledgerId, firstEntryId + 1));
316+
317+
BatchedReadEntryProcessor processor = createProcessor(ledgerId, firstEntryId, 5, 1024);
318+
ByteBufList data = (ByteBufList) processor.readData();
319+
assertNotNull(data);
320+
try {
321+
assertEquals(1, data.size());
322+
} finally {
323+
data.release();
324+
}
325+
}
326+
327+
@Test
328+
public void testReadDataStopsOnIOExceptionAfterFirstEntry() throws Exception {
329+
long ledgerId = 1238L;
330+
long firstEntryId = 1L;
331+
332+
ByteBuf firstEntry = entryBuffer(20);
333+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId))).thenReturn(firstEntry);
334+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId + 1)))
335+
.thenThrow(new IOException("disk error"));
336+
337+
BatchedReadEntryProcessor processor = createProcessor(ledgerId, firstEntryId, 5, 1024);
338+
ByteBufList data = (ByteBufList) processor.readData();
339+
assertNotNull(data);
340+
try {
341+
assertEquals(1, data.size());
342+
} finally {
343+
data.release();
344+
}
345+
}
346+
347+
@Test
348+
public void testProcessPacketReturnsPrefixWhenSubsequentReadFails() throws Exception {
349+
ChannelPromise promise = new DefaultChannelPromise(channel);
350+
AtomicReference<Object> writtenObject = new AtomicReference<>();
351+
CountDownLatch latch = new CountDownLatch(1);
352+
doAnswer(invocationOnMock -> {
353+
writtenObject.set(invocationOnMock.getArgument(0));
354+
promise.setSuccess();
355+
latch.countDown();
356+
return promise;
357+
}).when(channel).writeAndFlush(any(Response.class));
358+
359+
long ledgerId = 1239L;
360+
long firstEntryId = 1L;
361+
ByteBuf firstEntry = entryBuffer(20);
362+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId))).thenReturn(firstEntry);
363+
when(bookie.readEntry(eq(ledgerId), eq(firstEntryId + 1)))
364+
.thenThrow(new IOException("disk error"));
365+
366+
BatchedReadEntryProcessor processor = createProcessor(ledgerId, firstEntryId, 5, 1024);
367+
processor.run();
368+
369+
latch.await();
370+
assertTrue(writtenObject.get() instanceof Response);
371+
BookieProtocol.BatchedReadResponse response = (BookieProtocol.BatchedReadResponse) writtenObject.get();
372+
try {
373+
assertEquals(BookieProtocol.EOK, response.getErrorCode());
374+
assertEquals(1, response.getData().size());
375+
} finally {
376+
response.release();
377+
}
378+
assertEquals(0, firstEntry.refCnt());
379+
}
380+
381+
private BatchedReadEntryProcessor createProcessor(long ledgerId, long entryId, int maxCount, long maxSize) {
382+
ExecutorService service = mock(ExecutorService.class);
383+
BookieProtocol.BatchedReadRequest request = BookieProtocol.BatchedReadRequest.create(
384+
BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, entryId, BookieProtocol.FLAG_NONE, new byte[] {},
385+
0L, maxCount, maxSize);
386+
return BatchedReadEntryProcessor.create(request, requestHandler, requestProcessor, service, true,
387+
5 * 1024 * 1024);
388+
}
389+
390+
private static ByteBuf entryBuffer(int size) {
391+
ByteBuf entry = ByteBufAllocator.DEFAULT.buffer(size);
392+
entry.writeZero(size);
393+
return entry;
394+
}
395+
}

bookkeeper-server/src/test/java/org/apache/bookkeeper/proto/MockBookies.java

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -140,19 +140,35 @@ public ByteBufList batchReadEntries(BookieId bookieId, int flags, long ledgerId,
140140
if (maxCount <= 0) {
141141
maxCount = Integer.MAX_VALUE;
142142
}
143-
long frameSize = 24 + 8 + 4;
143+
long frameSize = 24 + 8 + Integer.BYTES;
144144
for (long i = startEntryId; i < startEntryId + maxCount; i++) {
145-
ByteBuf entry = ledger.getEntry(i);
146-
frameSize += entry.readableBytes() + 4;
147-
if (data == null) {
148-
data = ByteBufList.get(entry);
149-
} else {
150-
if (frameSize > maxSize) {
151-
entry.release();
152-
break;
153-
}
154-
data.add(entry);
155-
}
145+
ByteBuf entry = ledger.getEntry(i);
146+
if (data == null) {
147+
if (entry == null) {
148+
LOG.warn("[{};L{}] entry({}) not found", bookieId, ledgerId, i);
149+
throw new BKException.BKNoSuchEntryException();
150+
}
151+
frameSize += entry.readableBytes() + Integer.BYTES;
152+
data = ByteBufList.get(entry);
153+
long perEntrySize = entry.readableBytes() + Integer.BYTES;
154+
long remainingBudget = maxSize - frameSize;
155+
long remainingEntries = remainingBudget > 0 ? remainingBudget / Math.max(perEntrySize, 1L) : 0L;
156+
maxCount = (int) Math.min(maxCount, 1L + remainingEntries);
157+
continue;
158+
}
159+
160+
if (entry == null) {
161+
LOG.warn("[{};L{}] entry({}) not found", bookieId, ledgerId, i);
162+
break;
163+
}
164+
165+
if (frameSize + entry.readableBytes() + Integer.BYTES > maxSize) {
166+
// MockLedgerData returns the stored shared buffer, so this path does not own the skipped entry.
167+
break;
168+
}
169+
170+
frameSize += entry.readableBytes() + Integer.BYTES;
171+
data.add(entry);
156172
}
157173
return data;
158174
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
*
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
*
20+
*/
21+
package org.apache.bookkeeper.proto;
22+
23+
import static org.junit.Assert.assertEquals;
24+
import static org.junit.Assert.assertNotNull;
25+
26+
import io.netty.buffer.ByteBuf;
27+
import io.netty.buffer.Unpooled;
28+
import org.apache.bookkeeper.net.BookieId;
29+
import org.apache.bookkeeper.util.ByteBufList;
30+
import org.junit.Test;
31+
32+
public class MockBookiesTest {
33+
34+
private static final BookieId BOOKIE_ID = BookieId.parse("127.0.0.1:3181");
35+
private static final long LEDGER_ID = 1L;
36+
private static final long BATCH_RESPONSE_HEADER_SIZE = 24 + 8 + 4;
37+
38+
@Test
39+
public void testBatchReadStopsOnMissingSubsequentEntry() throws Exception {
40+
MockBookies mockBookies = new MockBookies();
41+
mockBookies.addEntry(BOOKIE_ID, LEDGER_ID, 0L, newEntry(8));
42+
43+
ByteBufList data = mockBookies.batchReadEntries(BOOKIE_ID, 0, LEDGER_ID, 0L, 2, Long.MAX_VALUE);
44+
45+
assertNotNull(data);
46+
assertEquals(1, data.size());
47+
assertEquals(8, data.getBuffer(0).readableBytes());
48+
}
49+
50+
@Test
51+
public void testBatchReadDoesNotReleaseOversizedSkippedEntry() throws Exception {
52+
MockBookies mockBookies = new MockBookies();
53+
mockBookies.addEntry(BOOKIE_ID, LEDGER_ID, 0L, newEntry(8));
54+
mockBookies.addEntry(BOOKIE_ID, LEDGER_ID, 1L, newEntry(16));
55+
56+
long maxSize = BATCH_RESPONSE_HEADER_SIZE + 8 + Integer.BYTES + 16 + Integer.BYTES - 1;
57+
ByteBufList data = mockBookies.batchReadEntries(BOOKIE_ID, 0, LEDGER_ID, 0L, 2, maxSize);
58+
59+
assertNotNull(data);
60+
assertEquals(1, data.size());
61+
assertEquals(8, data.getBuffer(0).readableBytes());
62+
assertEquals(16, mockBookies.readEntry(BOOKIE_ID, 0, LEDGER_ID, 1L).readableBytes());
63+
}
64+
65+
private static ByteBuf newEntry(int size) {
66+
return Unpooled.buffer(size).writeZero(size);
67+
}
68+
}

0 commit comments

Comments
 (0)