Skip to content

Commit 3acf920

Browse files
committed
HBASE-27264 Add options to consider compressed size when delimiting blocks during hfile writes (#4675)
Signed-off-by: Tak Lon (Stephen) Wu <[email protected]> Signed-off-by: Ankit Singhal <[email protected]>
1 parent aaad3a7 commit 3acf920

File tree

6 files changed

+283
-6
lines changed

6 files changed

+283
-6
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.io.hfile;
19+
20+
import org.apache.yetus.audience.InterfaceAudience;
21+
22+
/**
23+
* Allows for defining different compression rate predicates on its implementing classes. Useful
24+
* when compression is in place, and we want to define block size based on the compressed size,
25+
* rather than the default behaviour that considers the uncompressed size only. Since we don't
26+
* actually know the compressed size until we actual apply compression in the block byte buffer, we
27+
* need to "predicate" this compression rate and minimize compression execution to avoid excessive
28+
* resources usage. Different approaches for predicating the compressed block size can be defined by
29+
* implementing classes. The <code>updateLatestBlockSizes</code> allows for updating uncompressed
30+
* and compressed size values, and is called during block finishing (when we finally apply
31+
* compression on the block data). Final block size predicate logic is implemented in
32+
* <code>shouldFinishBlock</code>, which is called by the block writer once uncompressed size has
33+
* reached the configured BLOCK size, and additional checks should be applied to decide if the block
34+
* can be finished.
35+
*/
36+
@InterfaceAudience.Private
37+
public interface BlockCompressedSizePredicator {
38+
39+
String BLOCK_COMPRESSED_SIZE_PREDICATOR = "hbase.block.compressed.size.predicator";
40+
41+
String MAX_BLOCK_SIZE_UNCOMPRESSED = "hbase.block.max.size.uncompressed";
42+
43+
/**
44+
* Updates the predicator with both compressed and uncompressed sizes of latest block written. To
45+
* be called once the block is finshed and flushed to disk after compression.
46+
* @param context the HFileContext containg the configured max block size.
47+
* @param uncompressed the uncompressed size of last block written.
48+
* @param compressed the compressed size of last block written.
49+
*/
50+
void updateLatestBlockSizes(HFileContext context, int uncompressed, int compressed);
51+
52+
/**
53+
* Decides if the block should be finished based on the comparison of its uncompressed size
54+
* against an adjusted size based on a predicated compression factor.
55+
* @param uncompressed true if the block should be finished. n
56+
*/
57+
boolean shouldFinishBlock(int uncompressed);
58+
59+
}

hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/HFileBlock.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.hadoop.hbase.io.hfile;
1919

2020
import static org.apache.hadoop.hbase.io.ByteBuffAllocator.HEAP;
21+
import static org.apache.hadoop.hbase.io.hfile.BlockCompressedSizePredicator.BLOCK_COMPRESSED_SIZE_PREDICATOR;
2122
import static org.apache.hadoop.hbase.io.hfile.trace.HFileContextAttributesBuilderConsumer.CONTEXT_KEY;
2223

2324
import io.opentelemetry.api.common.Attributes;
@@ -64,6 +65,7 @@
6465
import org.apache.hadoop.hbase.util.ChecksumType;
6566
import org.apache.hadoop.hbase.util.ClassSize;
6667
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
68+
import org.apache.hadoop.util.ReflectionUtils;
6769
import org.apache.yetus.audience.InterfaceAudience;
6870
import org.slf4j.Logger;
6971
import org.slf4j.LoggerFactory;
@@ -463,7 +465,7 @@ int getOnDiskSizeWithoutHeader() {
463465
}
464466

465467
/** Returns the uncompressed size of data part (header and checksum excluded). */
466-
int getUncompressedSizeWithoutHeader() {
468+
public int getUncompressedSizeWithoutHeader() {
467469
return uncompressedSizeWithoutHeader;
468470
}
469471

@@ -740,6 +742,10 @@ private enum State {
740742
BLOCK_READY
741743
};
742744

745+
private int maxSizeUnCompressed;
746+
747+
private BlockCompressedSizePredicator compressedSizePredicator;
748+
743749
/** Writer state. Used to ensure the correct usage protocol. */
744750
private State state = State.INIT;
745751

@@ -818,11 +824,11 @@ EncodingState getEncodingState() {
818824
*/
819825
public Writer(Configuration conf, HFileDataBlockEncoder dataBlockEncoder,
820826
HFileContext fileContext) {
821-
this(conf, dataBlockEncoder, fileContext, ByteBuffAllocator.HEAP);
827+
this(conf, dataBlockEncoder, fileContext, ByteBuffAllocator.HEAP, fileContext.getBlocksize());
822828
}
823829

824830
public Writer(Configuration conf, HFileDataBlockEncoder dataBlockEncoder,
825-
HFileContext fileContext, ByteBuffAllocator allocator) {
831+
HFileContext fileContext, ByteBuffAllocator allocator, int maxSizeUnCompressed) {
826832
if (fileContext.getBytesPerChecksum() < HConstants.HFILEBLOCK_HEADER_SIZE) {
827833
throw new RuntimeException("Unsupported value of bytesPerChecksum. " + " Minimum is "
828834
+ HConstants.HFILEBLOCK_HEADER_SIZE + " but the configured value is "
@@ -845,6 +851,10 @@ public Writer(Configuration conf, HFileDataBlockEncoder dataBlockEncoder,
845851
// TODO: Why fileContext saved away when we have dataBlockEncoder and/or
846852
// defaultDataBlockEncoder?
847853
this.fileContext = fileContext;
854+
this.compressedSizePredicator = (BlockCompressedSizePredicator) ReflectionUtils.newInstance(
855+
conf.getClass(BLOCK_COMPRESSED_SIZE_PREDICATOR, UncompressedBlockSizePredicator.class),
856+
new Configuration(conf));
857+
this.maxSizeUnCompressed = maxSizeUnCompressed;
848858
}
849859

850860
/**
@@ -897,6 +907,15 @@ void ensureBlockReady() throws IOException {
897907
finishBlock();
898908
}
899909

910+
public boolean checkBoundariesWithPredicate() {
911+
int rawBlockSize = encodedBlockSizeWritten();
912+
if (rawBlockSize >= maxSizeUnCompressed) {
913+
return true;
914+
} else {
915+
return compressedSizePredicator.shouldFinishBlock(rawBlockSize);
916+
}
917+
}
918+
900919
/**
901920
* Finish up writing of the block. Flushes the compressing stream (if using compression), fills
902921
* out the header, does any compression/encryption of bytes to flush out to disk, and manages
@@ -911,6 +930,11 @@ private void finishBlock() throws IOException {
911930
userDataStream.flush();
912931
prevOffset = prevOffsetByType[blockType.getId()];
913932

933+
// We need to cache the unencoded/uncompressed size before changing the block state
934+
int rawBlockSize = 0;
935+
if (this.getEncodingState() != null) {
936+
rawBlockSize = blockSizeWritten();
937+
}
914938
// We need to set state before we can package the block up for cache-on-write. In a way, the
915939
// block is ready, but not yet encoded or compressed.
916940
state = State.BLOCK_READY;
@@ -931,13 +955,18 @@ private void finishBlock() throws IOException {
931955
onDiskBlockBytesWithHeader.reset();
932956
onDiskBlockBytesWithHeader.write(compressAndEncryptDat.get(),
933957
compressAndEncryptDat.getOffset(), compressAndEncryptDat.getLength());
958+
// Update raw and compressed sizes in the predicate
959+
compressedSizePredicator.updateLatestBlockSizes(fileContext, rawBlockSize,
960+
onDiskBlockBytesWithHeader.size());
961+
934962
// Calculate how many bytes we need for checksum on the tail of the block.
935963
int numBytes = (int) ChecksumUtil.numBytes(onDiskBlockBytesWithHeader.size(),
936964
fileContext.getBytesPerChecksum());
937965

938966
// Put the header for the on disk bytes; header currently is unfilled-out
939967
putHeader(onDiskBlockBytesWithHeader, onDiskBlockBytesWithHeader.size() + numBytes,
940968
baosInMemory.size(), onDiskBlockBytesWithHeader.size());
969+
941970
if (onDiskChecksum.length != numBytes) {
942971
onDiskChecksum = new byte[numBytes];
943972
}
@@ -1077,7 +1106,7 @@ int getUncompressedSizeWithoutHeader() {
10771106
/**
10781107
* The uncompressed size of the block data, including header size.
10791108
*/
1080-
int getUncompressedSizeWithHeader() {
1109+
public int getUncompressedSizeWithHeader() {
10811110
expectState(State.BLOCK_READY);
10821111
return baosInMemory.size();
10831112
}
@@ -1101,7 +1130,7 @@ public int encodedBlockSizeWritten() {
11011130
* block at the moment. Note that this will return zero in the "block ready" state as well.
11021131
* @return the number of bytes written
11031132
*/
1104-
int blockSizeWritten() {
1133+
public int blockSizeWritten() {
11051134
return state != State.WRITING ? 0 : this.getEncodingState().getUnencodedDataSizeWritten();
11061135
}
11071136

hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/HFileWriterImpl.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
*/
1818
package org.apache.hadoop.hbase.io.hfile;
1919

20+
import static org.apache.hadoop.hbase.io.hfile.BlockCompressedSizePredicator.MAX_BLOCK_SIZE_UNCOMPRESSED;
21+
2022
import java.io.DataOutput;
2123
import java.io.DataOutputStream;
2224
import java.io.IOException;
@@ -292,7 +294,8 @@ protected void finishInit(final Configuration conf) {
292294
throw new IllegalStateException("finishInit called twice");
293295
}
294296
blockWriter =
295-
new HFileBlock.Writer(conf, blockEncoder, hFileContext, cacheConf.getByteBuffAllocator());
297+
new HFileBlock.Writer(conf, blockEncoder, hFileContext, cacheConf.getByteBuffAllocator(),
298+
conf.getInt(MAX_BLOCK_SIZE_UNCOMPRESSED, hFileContext.getBlocksize() * 10));
296299
// Data block index writer
297300
boolean cacheIndexesOnWrite = cacheConf.shouldCacheIndexesOnWrite();
298301
dataBlockIndexWriter = new HFileBlockIndex.BlockIndexWriter(blockWriter,
@@ -319,6 +322,7 @@ protected void checkBlockBoundary() throws IOException {
319322
shouldFinishBlock = blockWriter.encodedBlockSizeWritten() >= hFileContext.getBlocksize()
320323
|| blockWriter.blockSizeWritten() >= hFileContext.getBlocksize();
321324
}
325+
shouldFinishBlock &= blockWriter.checkBoundariesWithPredicate();
322326
if (shouldFinishBlock) {
323327
finishBlock();
324328
writeInlineBlocks(false);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.io.hfile;
19+
20+
import org.apache.yetus.audience.InterfaceAudience;
21+
22+
/**
23+
* This BlockCompressedSizePredicator implementation adjusts the block size limit based on the
24+
* compression rate of the block contents read so far. For the first block, adjusted size would be
25+
* zero, so it performs a compression of current block contents and calculate compression rate and
26+
* adjusted size. For subsequent blocks, decision whether the block should be finished or not will
27+
* be based on the compression rate calculated for the previous block.
28+
*/
29+
@InterfaceAudience.Private
30+
public class PreviousBlockCompressionRatePredicator implements BlockCompressedSizePredicator {
31+
32+
private int adjustedBlockSize;
33+
private int compressionRatio = 1;
34+
private int configuredMaxBlockSize;
35+
36+
/**
37+
* Recalculates compression rate for the last block and adjusts the block size limit as:
38+
* BLOCK_SIZE * (uncompressed/compressed).
39+
* @param context HFIleContext containing the configured max block size.
40+
* @param uncompressed the uncompressed size of last block written.
41+
* @param compressed the compressed size of last block written.
42+
*/
43+
@Override
44+
public void updateLatestBlockSizes(HFileContext context, int uncompressed, int compressed) {
45+
configuredMaxBlockSize = context.getBlocksize();
46+
compressionRatio = uncompressed / compressed;
47+
adjustedBlockSize = context.getBlocksize() * compressionRatio;
48+
}
49+
50+
/**
51+
* Returns <b>true</b> if the passed uncompressed size is larger than the limit calculated by
52+
* <code>updateLatestBlockSizes</code>.
53+
* @param uncompressed true if the block should be finished. n
54+
*/
55+
@Override
56+
public boolean shouldFinishBlock(int uncompressed) {
57+
if (uncompressed >= configuredMaxBlockSize) {
58+
return uncompressed >= adjustedBlockSize;
59+
}
60+
return false;
61+
}
62+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.io.hfile;
19+
20+
import org.apache.yetus.audience.InterfaceAudience;
21+
22+
/**
23+
* This BlockCompressedSizePredicator implementation doesn't actually performs any predicate and
24+
* simply returns <b>true</b> on <code>shouldFinishBlock</code>. This is the default implementation
25+
* if <b>hbase.block.compressed.size.predicator</b> property is not defined.
26+
*/
27+
@InterfaceAudience.Private
28+
public class UncompressedBlockSizePredicator implements BlockCompressedSizePredicator {
29+
30+
/**
31+
* Empty implementation. Does nothing.
32+
* @param uncompressed the uncompressed size of last block written.
33+
* @param compressed the compressed size of last block written.
34+
*/
35+
@Override
36+
public void updateLatestBlockSizes(HFileContext context, int uncompressed, int compressed) {
37+
}
38+
39+
/**
40+
* Dummy implementation that always returns true. This means, we will be only considering the
41+
* block uncompressed size for deciding when to finish a block.
42+
* @param uncompressed true if the block should be finished. n
43+
*/
44+
@Override
45+
public boolean shouldFinishBlock(int uncompressed) {
46+
return true;
47+
}
48+
49+
}

0 commit comments

Comments
 (0)