001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.regionserver; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertNotNull; 022import static org.junit.Assert.assertTrue; 023import static org.junit.Assert.fail; 024import static org.mockito.Mockito.mock; 025import static org.mockito.Mockito.when; 026 027import java.io.IOException; 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.List; 031import java.util.Random; 032import org.apache.hadoop.conf.Configuration; 033import org.apache.hadoop.fs.FileSystem; 034import org.apache.hadoop.fs.Path; 035import org.apache.hadoop.hbase.CellComparatorImpl; 036import org.apache.hadoop.hbase.CellUtil; 037import org.apache.hadoop.hbase.HBaseClassTestRule; 038import org.apache.hadoop.hbase.HBaseTestingUtility; 039import org.apache.hadoop.hbase.KeyValue; 040import org.apache.hadoop.hbase.KeyValueUtil; 041import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 042import org.apache.hadoop.hbase.client.Scan; 043import org.apache.hadoop.hbase.io.hfile.BlockCache; 044import org.apache.hadoop.hbase.io.hfile.CacheConfig; 045import org.apache.hadoop.hbase.io.hfile.CompoundBloomFilter; 046import org.apache.hadoop.hbase.io.hfile.CompoundBloomFilterWriter; 047import org.apache.hadoop.hbase.io.hfile.HFile; 048import org.apache.hadoop.hbase.io.hfile.HFileContext; 049import org.apache.hadoop.hbase.io.hfile.HFileContextBuilder; 050import org.apache.hadoop.hbase.io.hfile.RandomKeyValueUtil; 051import org.apache.hadoop.hbase.testclassification.MediumTests; 052import org.apache.hadoop.hbase.testclassification.RegionServerTests; 053import org.apache.hadoop.hbase.util.BloomFilterFactory; 054import org.apache.hadoop.hbase.util.BloomFilterUtil; 055import org.apache.hadoop.hbase.util.Bytes; 056import org.junit.Before; 057import org.junit.ClassRule; 058import org.junit.Test; 059import org.junit.experimental.categories.Category; 060import org.slf4j.Logger; 061import org.slf4j.LoggerFactory; 062 063/** 064 * Tests writing Bloom filter blocks in the same part of the file as data 065 * blocks. 066 */ 067@Category({RegionServerTests.class, MediumTests.class}) 068public class TestCompoundBloomFilter { 069 070 @ClassRule 071 public static final HBaseClassTestRule CLASS_RULE = 072 HBaseClassTestRule.forClass(TestCompoundBloomFilter.class); 073 074 private static final HBaseTestingUtility TEST_UTIL = 075 new HBaseTestingUtility(); 076 077 private static final Logger LOG = LoggerFactory.getLogger( 078 TestCompoundBloomFilter.class); 079 080 private static final int NUM_TESTS = 9; 081 private static final BloomType BLOOM_TYPES[] = { BloomType.ROW, 082 BloomType.ROW, BloomType.ROWCOL, BloomType.ROWCOL, BloomType.ROW, 083 BloomType.ROWCOL, BloomType.ROWCOL, BloomType.ROWCOL, BloomType.ROW }; 084 085 private static final int NUM_KV[]; 086 static { 087 final int N = 10000; // Only used in initialization. 088 NUM_KV = new int[] { 21870, N, N, N, N, 1000, N, 7500, 7500}; 089 assert NUM_KV.length == NUM_TESTS; 090 } 091 092 private static final int BLOCK_SIZES[]; 093 static { 094 final int blkSize = 65536; 095 BLOCK_SIZES = new int[] { 512, 1000, blkSize, blkSize, blkSize, 128, 300, 096 blkSize, blkSize }; 097 assert BLOCK_SIZES.length == NUM_TESTS; 098 } 099 100 /** 101 * Be careful not to specify too high a Bloom filter block size, otherwise 102 * there will only be one oversized chunk and the observed false positive 103 * rate will be too low. 104 */ 105 private static final int BLOOM_BLOCK_SIZES[] = { 1000, 4096, 4096, 4096, 106 8192, 128, 1024, 600, 600 }; 107 static { assert BLOOM_BLOCK_SIZES.length == NUM_TESTS; } 108 109 private static final double TARGET_ERROR_RATES[] = { 0.025, 0.01, 0.015, 110 0.01, 0.03, 0.01, 0.01, 0.07, 0.07 }; 111 static { assert TARGET_ERROR_RATES.length == NUM_TESTS; } 112 113 /** A false positive rate that is obviously too high. */ 114 private static final double TOO_HIGH_ERROR_RATE; 115 static { 116 double m = 0; 117 for (double errorRate : TARGET_ERROR_RATES) 118 m = Math.max(m, errorRate); 119 TOO_HIGH_ERROR_RATE = m + 0.03; 120 } 121 122 private static Configuration conf; 123 private static CacheConfig cacheConf; 124 private FileSystem fs; 125 private BlockCache blockCache; 126 127 /** 128 * A message of the form <code>in test#<number></code>" to include in logging. 129 */ 130 private String testIdMsg; 131 132 private static final int GENERATION_SEED = 2319; 133 private static final int EVALUATION_SEED = 135; 134 135 @Before 136 public void setUp() throws IOException { 137 conf = TEST_UTIL.getConfiguration(); 138 139 // This test requires the most recent HFile format (i.e. v2). 140 conf.setInt(HFile.FORMAT_VERSION_KEY, HFile.MAX_FORMAT_VERSION); 141 142 fs = FileSystem.get(conf); 143 144 cacheConf = new CacheConfig(conf); 145 blockCache = cacheConf.getBlockCache(); 146 assertNotNull(blockCache); 147 } 148 149 private List<KeyValue> createSortedKeyValues(Random rand, int n) { 150 List<KeyValue> kvList = new ArrayList<>(n); 151 for (int i = 0; i < n; ++i) 152 kvList.add(RandomKeyValueUtil.randomKeyValue(rand)); 153 Collections.sort(kvList, CellComparatorImpl.COMPARATOR); 154 return kvList; 155 } 156 157 @Test 158 public void testCompoundBloomFilter() throws IOException { 159 conf.setBoolean(BloomFilterFactory.IO_STOREFILE_BLOOM_ENABLED, true); 160 for (int t = 0; t < NUM_TESTS; ++t) { 161 conf.setFloat(BloomFilterFactory.IO_STOREFILE_BLOOM_ERROR_RATE, 162 (float) TARGET_ERROR_RATES[t]); 163 164 testIdMsg = "in test #" + t + ":"; 165 Random generationRand = new Random(GENERATION_SEED); 166 List<KeyValue> kvs = createSortedKeyValues(generationRand, NUM_KV[t]); 167 BloomType bt = BLOOM_TYPES[t]; 168 Path sfPath = writeStoreFile(t, bt, kvs); 169 readStoreFile(t, bt, kvs, sfPath); 170 } 171 } 172 173 /** 174 * Validates the false positive ratio by computing its z-value and comparing 175 * it to the provided threshold. 176 * 177 * @param falsePosRate experimental positive rate 178 * @param nTrials the number of Bloom filter checks 179 * @param zValueBoundary z-value boundary, positive for an upper bound and 180 * negative for a lower bound 181 * @param cbf the compound Bloom filter we are using 182 * @param additionalMsg additional message to include in log output and 183 * assertion failures 184 */ 185 private void validateFalsePosRate(double falsePosRate, int nTrials, 186 double zValueBoundary, CompoundBloomFilter cbf, String additionalMsg) { 187 double p = BloomFilterFactory.getErrorRate(conf); 188 double zValue = (falsePosRate - p) / Math.sqrt(p * (1 - p) / nTrials); 189 190 String assortedStatsStr = " (targetErrorRate=" + p + ", falsePosRate=" 191 + falsePosRate + ", nTrials=" + nTrials + ")"; 192 LOG.info("z-value is " + zValue + assortedStatsStr); 193 194 boolean isUpperBound = zValueBoundary > 0; 195 196 if (isUpperBound && zValue > zValueBoundary || 197 !isUpperBound && zValue < zValueBoundary) { 198 String errorMsg = "False positive rate z-value " + zValue + " is " 199 + (isUpperBound ? "higher" : "lower") + " than " + zValueBoundary 200 + assortedStatsStr + ". Per-chunk stats:\n" 201 + cbf.formatTestingStats(); 202 fail(errorMsg + additionalMsg); 203 } 204 } 205 206 private void readStoreFile(int t, BloomType bt, List<KeyValue> kvs, 207 Path sfPath) throws IOException { 208 HStoreFile sf = new HStoreFile(fs, sfPath, conf, cacheConf, bt, true); 209 sf.initReader(); 210 StoreFileReader r = sf.getReader(); 211 final boolean pread = true; // does not really matter 212 StoreFileScanner scanner = r.getStoreFileScanner(true, pread, false, 0, 0, false); 213 214 { 215 // Test for false negatives (not allowed). 216 int numChecked = 0; 217 for (KeyValue kv : kvs) { 218 byte[] row = CellUtil.cloneRow(kv); 219 boolean present = isInBloom(scanner, row, CellUtil.cloneQualifier(kv)); 220 assertTrue(testIdMsg + " Bloom filter false negative on row " 221 + Bytes.toStringBinary(row) + " after " + numChecked 222 + " successful checks", present); 223 ++numChecked; 224 } 225 } 226 227 // Test for false positives (some percentage allowed). We test in two modes: 228 // "fake lookup" which ignores the key distribution, and production mode. 229 for (boolean fakeLookupEnabled : new boolean[] { true, false }) { 230 BloomFilterUtil.setFakeLookupMode(fakeLookupEnabled); 231 try { 232 String fakeLookupModeStr = ", fake lookup is " + (fakeLookupEnabled ? 233 "enabled" : "disabled"); 234 CompoundBloomFilter cbf = (CompoundBloomFilter) r.getGeneralBloomFilter(); 235 cbf.enableTestingStats(); 236 int numFalsePos = 0; 237 Random rand = new Random(EVALUATION_SEED); 238 int nTrials = NUM_KV[t] * 10; 239 for (int i = 0; i < nTrials; ++i) { 240 byte[] query = RandomKeyValueUtil.randomRowOrQualifier(rand); 241 if (isInBloom(scanner, query, bt, rand)) { 242 numFalsePos += 1; 243 } 244 } 245 double falsePosRate = numFalsePos * 1.0 / nTrials; 246 LOG.debug(String.format(testIdMsg 247 + " False positives: %d out of %d (%f)", 248 numFalsePos, nTrials, falsePosRate) + fakeLookupModeStr); 249 250 // Check for obvious Bloom filter crashes. 251 assertTrue("False positive is too high: " + falsePosRate + " (greater " 252 + "than " + TOO_HIGH_ERROR_RATE + ")" + fakeLookupModeStr, 253 falsePosRate < TOO_HIGH_ERROR_RATE); 254 255 // Now a more precise check to see if the false positive rate is not 256 // too high. The reason we use a relaxed restriction for the real-world 257 // case as opposed to the "fake lookup" case is that our hash functions 258 // are not completely independent. 259 260 double maxZValue = fakeLookupEnabled ? 1.96 : 2.5; 261 validateFalsePosRate(falsePosRate, nTrials, maxZValue, cbf, 262 fakeLookupModeStr); 263 264 // For checking the lower bound we need to eliminate the last chunk, 265 // because it is frequently smaller and the false positive rate in it 266 // is too low. This does not help if there is only one under-sized 267 // chunk, though. 268 int nChunks = cbf.getNumChunks(); 269 if (nChunks > 1) { 270 numFalsePos -= cbf.getNumPositivesForTesting(nChunks - 1); 271 nTrials -= cbf.getNumQueriesForTesting(nChunks - 1); 272 falsePosRate = numFalsePos * 1.0 / nTrials; 273 LOG.info(testIdMsg + " False positive rate without last chunk is " + 274 falsePosRate + fakeLookupModeStr); 275 } 276 277 validateFalsePosRate(falsePosRate, nTrials, -2.58, cbf, 278 fakeLookupModeStr); 279 } finally { 280 BloomFilterUtil.setFakeLookupMode(false); 281 } 282 } 283 284 r.close(true); // end of test so evictOnClose 285 } 286 287 private boolean isInBloom(StoreFileScanner scanner, byte[] row, BloomType bt, 288 Random rand) { 289 return isInBloom(scanner, row, RandomKeyValueUtil.randomRowOrQualifier(rand)); 290 } 291 292 private boolean isInBloom(StoreFileScanner scanner, byte[] row, 293 byte[] qualifier) { 294 Scan scan = new Scan().withStartRow(row).withStopRow(row, true); 295 scan.addColumn(Bytes.toBytes(RandomKeyValueUtil.COLUMN_FAMILY_NAME), qualifier); 296 HStore store = mock(HStore.class); 297 when(store.getColumnFamilyDescriptor()) 298 .thenReturn(ColumnFamilyDescriptorBuilder.of(RandomKeyValueUtil.COLUMN_FAMILY_NAME)); 299 return scanner.shouldUseScanner(scan, store, Long.MIN_VALUE); 300 } 301 302 private Path writeStoreFile(int t, BloomType bt, List<KeyValue> kvs) 303 throws IOException { 304 conf.setInt(BloomFilterFactory.IO_STOREFILE_BLOOM_BLOCK_SIZE, 305 BLOOM_BLOCK_SIZES[t]); 306 conf.setBoolean(CacheConfig.CACHE_BLOCKS_ON_WRITE_KEY, true); 307 cacheConf = new CacheConfig(conf); 308 HFileContext meta = new HFileContextBuilder().withBlockSize(BLOCK_SIZES[t]).build(); 309 StoreFileWriter w = new StoreFileWriter.Builder(conf, cacheConf, fs) 310 .withOutputDir(TEST_UTIL.getDataTestDir()) 311 .withBloomType(bt) 312 .withFileContext(meta) 313 .build(); 314 315 assertTrue(w.hasGeneralBloom()); 316 assertTrue(w.getGeneralBloomWriter() instanceof CompoundBloomFilterWriter); 317 CompoundBloomFilterWriter cbbf = 318 (CompoundBloomFilterWriter) w.getGeneralBloomWriter(); 319 320 int keyCount = 0; 321 KeyValue prev = null; 322 LOG.debug("Total keys/values to insert: " + kvs.size()); 323 for (KeyValue kv : kvs) { 324 w.append(kv); 325 326 // Validate the key count in the Bloom filter. 327 boolean newKey = true; 328 if (prev != null) { 329 newKey = !(bt == BloomType.ROW ? CellUtil.matchingRows(kv, 330 prev) : CellUtil.matchingRowColumn(kv, prev)); 331 } 332 if (newKey) 333 ++keyCount; 334 assertEquals(keyCount, cbbf.getKeyCount()); 335 336 prev = kv; 337 } 338 w.close(); 339 340 return w.getPath(); 341 } 342 343 @Test 344 public void testCompoundBloomSizing() { 345 int bloomBlockByteSize = 4096; 346 int bloomBlockBitSize = bloomBlockByteSize * 8; 347 double targetErrorRate = 0.01; 348 long maxKeysPerChunk = BloomFilterUtil.idealMaxKeys(bloomBlockBitSize, 349 targetErrorRate); 350 351 long bloomSize1 = bloomBlockByteSize * 8; 352 long bloomSize2 = BloomFilterUtil.computeBitSize(maxKeysPerChunk, 353 targetErrorRate); 354 355 double bloomSizeRatio = (bloomSize2 * 1.0 / bloomSize1); 356 assertTrue(Math.abs(bloomSizeRatio - 0.9999) < 0.0001); 357 } 358 359 @Test 360 public void testCreateKey() { 361 byte[] row = "myRow".getBytes(); 362 byte[] qualifier = "myQualifier".getBytes(); 363 // Mimic what Storefile.createBloomKeyValue() does 364 byte[] rowKey = KeyValueUtil.createFirstOnRow(row, 0, row.length, new byte[0], 0, 0, row, 0, 0).getKey(); 365 byte[] rowColKey = KeyValueUtil.createFirstOnRow(row, 0, row.length, 366 new byte[0], 0, 0, qualifier, 0, qualifier.length).getKey(); 367 KeyValue rowKV = KeyValueUtil.createKeyValueFromKey(rowKey); 368 KeyValue rowColKV = KeyValueUtil.createKeyValueFromKey(rowColKey); 369 assertEquals(rowKV.getTimestamp(), rowColKV.getTimestamp()); 370 assertEquals(Bytes.toStringBinary(rowKV.getRowArray(), rowKV.getRowOffset(), 371 rowKV.getRowLength()), Bytes.toStringBinary(rowColKV.getRowArray(), rowColKV.getRowOffset(), 372 rowColKV.getRowLength())); 373 assertEquals(0, rowKV.getQualifierLength()); 374 } 375 376 377} 378