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