View Javadoc

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, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase.io.hfile;
20  
21  import static org.junit.Assert.assertEquals;
22  import static org.junit.Assert.assertTrue;
23  
24  import java.io.IOException;
25  import java.lang.management.ManagementFactory;
26  import java.lang.management.MemoryUsage;
27  import java.nio.ByteBuffer;
28  import java.util.Map;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.hadoop.conf.Configuration;
33  import org.apache.hadoop.fs.FileSystem;
34  import org.apache.hadoop.fs.Path;
35  import org.apache.hadoop.hbase.HBaseConfiguration;
36  import org.apache.hadoop.hbase.HBaseTestingUtility;
37  import org.apache.hadoop.hbase.HConstants;
38  import org.apache.hadoop.hbase.testclassification.LargeTests;
39  import org.apache.hadoop.hbase.io.hfile.bucket.BucketCache;
40  import org.apache.hadoop.hbase.io.util.HeapMemorySizeUtil;
41  import org.apache.hadoop.hbase.util.Threads;
42  import org.junit.After;
43  import org.junit.Before;
44  import org.junit.Test;
45  import org.junit.experimental.categories.Category;
46  
47  /**
48   * Tests that {@link CacheConfig} does as expected.
49   */
50  // This test is marked as a large test though it runs in a short amount of time
51  // (seconds).  It is large because it depends on being able to reset the global
52  // blockcache instance which is in a global variable.  Experience has it that
53  // tests clash on the global variable if this test is run as small sized test.
54  @Category(LargeTests.class)
55  public class TestCacheConfig {
56    private static final Log LOG = LogFactory.getLog(TestCacheConfig.class);
57    private Configuration conf;
58  
59    static class Deserializer implements CacheableDeserializer<Cacheable> {
60      private final Cacheable cacheable;
61      private int deserializedIdentifier = 0;
62  
63      Deserializer(final Cacheable c) {
64        deserializedIdentifier = CacheableDeserializerIdManager.registerDeserializer(this);
65        this.cacheable = c;
66      }
67  
68      @Override
69      public int getDeserialiserIdentifier() {
70        return deserializedIdentifier;
71      }
72  
73      @Override
74      public Cacheable deserialize(ByteBuffer b, boolean reuse) throws IOException {
75        LOG.info("Deserialized " + b + ", reuse=" + reuse);
76        return cacheable;
77      }
78  
79      @Override
80      public Cacheable deserialize(ByteBuffer b) throws IOException {
81        LOG.info("Deserialized " + b);
82        return cacheable;
83      }
84    };
85  
86    static class IndexCacheEntry extends DataCacheEntry {
87      private static IndexCacheEntry SINGLETON = new IndexCacheEntry();
88  
89      public IndexCacheEntry() {
90        super(SINGLETON);
91      }
92  
93      @Override
94      public BlockType getBlockType() {
95        return BlockType.ROOT_INDEX;
96      }
97    }
98  
99    static class DataCacheEntry implements Cacheable {
100     private static final int SIZE = 1;
101     private static DataCacheEntry SINGLETON = new DataCacheEntry();
102     final CacheableDeserializer<Cacheable> deserializer;
103 
104     DataCacheEntry() {
105       this(SINGLETON);
106     }
107 
108     DataCacheEntry(final Cacheable c) {
109       this.deserializer = new Deserializer(c);
110     }
111 
112     @Override
113     public String toString() {
114       return "size=" + SIZE + ", type=" + getBlockType();
115     };
116 
117     @Override
118     public long heapSize() {
119       return SIZE;
120     }
121 
122     @Override
123     public int getSerializedLength() {
124       return SIZE;
125     }
126 
127     @Override
128     public void serialize(ByteBuffer destination) {
129       LOG.info("Serialized " + this + " to " + destination);
130     }
131 
132     @Override
133     public CacheableDeserializer<Cacheable> getDeserializer() {
134       return this.deserializer;
135     }
136 
137     @Override
138     public BlockType getBlockType() {
139       return BlockType.DATA;
140     }
141   };
142 
143   static class MetaCacheEntry extends DataCacheEntry {
144     @Override
145     public BlockType getBlockType() {
146       return BlockType.INTERMEDIATE_INDEX;
147     }
148   }
149 
150   @Before
151   public void setUp() throws Exception {
152     CacheConfig.GLOBAL_BLOCK_CACHE_INSTANCE = null;
153     this.conf = HBaseConfiguration.create();
154   }
155 
156   @After
157   public void tearDown() throws Exception {
158     // Let go of current block cache.
159     CacheConfig.GLOBAL_BLOCK_CACHE_INSTANCE = null;
160   }
161 
162   /**
163    * @param cc
164    * @param doubling If true, addition of element ups counter by 2, not 1, because element added
165    * to onheap and offheap caches.
166    * @param sizing True if we should run sizing test (doesn't always apply).
167    */
168   void basicBlockCacheOps(final CacheConfig cc, final boolean doubling,
169       final boolean sizing) {
170     assertTrue(cc.isBlockCacheEnabled());
171     assertTrue(CacheConfig.DEFAULT_IN_MEMORY == cc.isInMemory());
172     BlockCache bc = cc.getBlockCache();
173     BlockCacheKey bck = new BlockCacheKey("f", 0);
174     Cacheable c = new DataCacheEntry();
175     // Do asserts on block counting.
176     long initialBlockCount = bc.getBlockCount();
177     bc.cacheBlock(bck, c, cc.isInMemory(), cc.isCacheDataInL1());
178     assertEquals(doubling? 2: 1, bc.getBlockCount() - initialBlockCount);
179     bc.evictBlock(bck);
180     assertEquals(initialBlockCount, bc.getBlockCount());
181     // Do size accounting.  Do it after the above 'warm-up' because it looks like some
182     // buffers do lazy allocation so sizes are off on first go around.
183     if (sizing) {
184       long originalSize = bc.getCurrentSize();
185       bc.cacheBlock(bck, c, cc.isInMemory(), cc.isCacheDataInL1());
186       assertTrue(bc.getCurrentSize() > originalSize);
187       bc.evictBlock(bck);
188       long size = bc.getCurrentSize();
189       assertEquals(originalSize, size);
190     }
191   }
192 
193   /**
194    * @param cc
195    * @param filename
196    * @return
197    */
198   private long cacheDataBlock(final CacheConfig cc, final String filename) {
199     BlockCacheKey bck = new BlockCacheKey(filename, 0);
200     Cacheable c = new DataCacheEntry();
201     // Do asserts on block counting.
202     cc.getBlockCache().cacheBlock(bck, c, cc.isInMemory(), cc.isCacheDataInL1());
203     return cc.getBlockCache().getBlockCount();
204   }
205 
206   @Test
207   public void testCacheConfigDefaultLRUBlockCache() {
208     CacheConfig cc = new CacheConfig(this.conf);
209     assertTrue(cc.isBlockCacheEnabled());
210     assertTrue(CacheConfig.DEFAULT_IN_MEMORY == cc.isInMemory());
211     basicBlockCacheOps(cc, false, true);
212     assertTrue(cc.getBlockCache() instanceof LruBlockCache);
213   }
214 
215   /**
216    * Assert that the caches are deployed with CombinedBlockCache and of the appropriate sizes.
217    */
218   @Test
219   public void testOffHeapBucketCacheConfig() {
220     this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap");
221     doBucketCacheConfigTest();
222   }
223 
224   @Test
225   public void testOnHeapBucketCacheConfig() {
226     this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "heap");
227     doBucketCacheConfigTest();
228   }
229 
230   @Test
231   public void testFileBucketCacheConfig() throws IOException {
232     HBaseTestingUtility htu = new HBaseTestingUtility(this.conf);
233     try {
234       Path p = new Path(htu.getDataTestDir(), "bc.txt");
235       FileSystem fs = FileSystem.get(this.conf);
236       fs.create(p).close();
237       this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "file:" + p);
238       doBucketCacheConfigTest();
239     } finally {
240       htu.cleanupTestDir();
241     }
242   }
243 
244   private void doBucketCacheConfigTest() {
245     final int bcSize = 100;
246     this.conf.setInt(HConstants.BUCKET_CACHE_SIZE_KEY, bcSize);
247     CacheConfig cc = new CacheConfig(this.conf);
248     basicBlockCacheOps(cc, false, false);
249     assertTrue(cc.getBlockCache() instanceof CombinedBlockCache);
250     // TODO: Assert sizes allocated are right and proportions.
251     CombinedBlockCache cbc = (CombinedBlockCache)cc.getBlockCache();
252     BlockCache [] bcs = cbc.getBlockCaches();
253     assertTrue(bcs[0] instanceof LruBlockCache);
254     LruBlockCache lbc = (LruBlockCache)bcs[0];
255     assertEquals(HeapMemorySizeUtil.getLruCacheSize(this.conf), lbc.getMaxSize());
256     assertTrue(bcs[1] instanceof BucketCache);
257     BucketCache bc = (BucketCache)bcs[1];
258     // getMaxSize comes back in bytes but we specified size in MB
259     assertEquals(bcSize, bc.getMaxSize() / (1024 * 1024));
260   }
261 
262   /**
263    * Assert that when BUCKET_CACHE_COMBINED_KEY is false, the non-default, that we deploy
264    * LruBlockCache as L1 with a BucketCache for L2.
265    */
266   @Test (timeout=10000)
267   public void testBucketCacheConfigL1L2Setup() {
268     this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap");
269     // Make lru size is smaller than bcSize for sure.  Need this to be true so when eviction
270     // from L1 happens, it does not fail because L2 can't take the eviction because block too big.
271     this.conf.setFloat(HConstants.HFILE_BLOCK_CACHE_SIZE_KEY, 0.001f);
272     long lruExpectedSize = HeapMemorySizeUtil.getLruCacheSize(this.conf);
273     final int bcSize = 100;
274     long bcExpectedSize = 100 * 1024 * 1024; // MB.
275     assertTrue(lruExpectedSize < bcExpectedSize);
276     this.conf.setInt(HConstants.BUCKET_CACHE_SIZE_KEY, bcSize);
277     this.conf.setBoolean(CacheConfig.BUCKET_CACHE_COMBINED_KEY, false);
278     CacheConfig cc = new CacheConfig(this.conf);
279     basicBlockCacheOps(cc, false, false);
280     assertTrue(cc.getBlockCache() instanceof LruBlockCache);
281     // TODO: Assert sizes allocated are right and proportions.
282     LruBlockCache lbc = (LruBlockCache)cc.getBlockCache();
283     assertEquals(lruExpectedSize, lbc.getMaxSize());
284     BlockCache bc = lbc.getVictimHandler();
285     // getMaxSize comes back in bytes but we specified size in MB
286     assertEquals(bcExpectedSize, ((BucketCache) bc).getMaxSize());
287     // Test the L1+L2 deploy works as we'd expect with blocks evicted from L1 going to L2.
288     long initialL1BlockCount = lbc.getBlockCount();
289     long initialL2BlockCount = bc.getBlockCount();
290     Cacheable c = new DataCacheEntry();
291     BlockCacheKey bck = new BlockCacheKey("bck", 0);
292     lbc.cacheBlock(bck, c, false, false);
293     assertEquals(initialL1BlockCount + 1, lbc.getBlockCount());
294     assertEquals(initialL2BlockCount, bc.getBlockCount());
295     // Force evictions by putting in a block too big.
296     final long justTooBigSize = lbc.acceptableSize() + 1;
297     lbc.cacheBlock(new BlockCacheKey("bck2", 0), new DataCacheEntry() {
298       @Override
299       public long heapSize() {
300         return justTooBigSize;
301       }
302 
303       @Override
304       public int getSerializedLength() {
305         return (int)heapSize();
306       }
307     });
308     // The eviction thread in lrublockcache needs to run.
309     while (initialL1BlockCount != lbc.getBlockCount()) Threads.sleep(10);
310     assertEquals(initialL1BlockCount, lbc.getBlockCount());
311     long count = bc.getBlockCount();
312     assertTrue(initialL2BlockCount + 1 <= count);
313   }
314 
315   /**
316    * Test the cacheDataInL1 flag.  When set, data blocks should be cached in the l1 tier, up in
317    * LruBlockCache when using CombinedBlockCcahe.
318    */
319   @Test
320   public void testCacheDataInL1() {
321     this.conf.set(HConstants.BUCKET_CACHE_IOENGINE_KEY, "offheap");
322     this.conf.setInt(HConstants.BUCKET_CACHE_SIZE_KEY, 100);
323     CacheConfig cc = new CacheConfig(this.conf);
324     assertTrue(cc.getBlockCache() instanceof CombinedBlockCache);
325     CombinedBlockCache cbc = (CombinedBlockCache)cc.getBlockCache();
326     // Add a data block.  Should go into L2, into the Bucket Cache, not the LruBlockCache.
327     cacheDataBlock(cc, "1");
328     LruBlockCache lrubc = (LruBlockCache)cbc.getBlockCaches()[0];
329     assertDataBlockCount(lrubc, 0);
330     // Enable our test flag.
331     cc.setCacheDataInL1(true);
332     cacheDataBlock(cc, "2");
333     assertDataBlockCount(lrubc, 1);
334     cc.setCacheDataInL1(false);
335     cacheDataBlock(cc, "3");
336     assertDataBlockCount(lrubc, 1);
337   }
338 
339   private void assertDataBlockCount(final LruBlockCache bc, final int expected) {
340     Map<BlockType, Integer> blocks = bc.getBlockTypeCountsForTest();
341     assertEquals(expected, blocks == null? 0:
342       blocks.get(BlockType.DATA) == null? 0:
343       blocks.get(BlockType.DATA).intValue());
344   }
345 }