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.mob; 019 020import java.io.IOException; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.List; 024import java.util.Map; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.concurrent.Executors; 027import java.util.concurrent.ScheduledExecutorService; 028import java.util.concurrent.TimeUnit; 029import java.util.concurrent.atomic.AtomicLong; 030import java.util.concurrent.atomic.LongAdder; 031import java.util.concurrent.locks.ReentrantLock; 032import org.apache.hadoop.conf.Configuration; 033import org.apache.hadoop.fs.FileSystem; 034import org.apache.hadoop.fs.Path; 035import org.apache.hadoop.hbase.io.hfile.CacheConfig; 036import org.apache.hadoop.hbase.regionserver.StoreContext; 037import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTracker; 038import org.apache.hadoop.hbase.regionserver.storefiletracker.StoreFileTrackerFactory; 039import org.apache.hadoop.hbase.util.IdLock; 040import org.apache.yetus.audience.InterfaceAudience; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043 044import org.apache.hbase.thirdparty.com.google.common.hash.Hashing; 045import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; 046 047/** 048 * The cache for mob files. This cache doesn't cache the mob file blocks. It only caches the 049 * references of mob files. We are doing this to avoid opening and closing mob files all the time. 050 * We just keep references open. 051 */ 052@InterfaceAudience.Private 053public class MobFileCache { 054 055 private static final Logger LOG = LoggerFactory.getLogger(MobFileCache.class); 056 057 /* 058 * Eviction and statistics thread. Periodically run to print the statistics and evict the lru 059 * cached mob files when the count of the cached files is larger than the threshold. 060 */ 061 static class EvictionThread extends Thread { 062 MobFileCache lru; 063 064 public EvictionThread(MobFileCache lru) { 065 super("MobFileCache.EvictionThread"); 066 setDaemon(true); 067 this.lru = lru; 068 } 069 070 @Override 071 public void run() { 072 lru.evict(); 073 } 074 } 075 076 // a ConcurrentHashMap, accesses to this map are synchronized. 077 private Map<String, CachedMobFile> map = null; 078 // caches access count 079 private final AtomicLong count = new AtomicLong(0); 080 private long lastAccess = 0; 081 private final LongAdder miss = new LongAdder(); 082 private long lastMiss = 0; 083 private final LongAdder evictedFileCount = new LongAdder(); 084 private long lastEvictedFileCount = 0; 085 086 // a lock to sync the evict to guarantee the eviction occurs in sequence. 087 // the method evictFile is not sync by this lock, the ConcurrentHashMap does the sync there. 088 private final ReentrantLock evictionLock = new ReentrantLock(true); 089 090 // stripes lock on each mob file based on its hash. Sync the openFile/closeFile operations. 091 private final IdLock keyLock = new IdLock(); 092 093 private final ScheduledExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(1, 094 new ThreadFactoryBuilder().setNameFormat("MobFileCache #%d").setDaemon(true).build()); 095 private final Configuration conf; 096 097 // the count of the cached references to mob files 098 private final int mobFileMaxCacheSize; 099 private final boolean isCacheEnabled; 100 private float evictRemainRatio; 101 102 public MobFileCache(Configuration conf) { 103 this.conf = conf; 104 this.mobFileMaxCacheSize = 105 conf.getInt(MobConstants.MOB_FILE_CACHE_SIZE_KEY, MobConstants.DEFAULT_MOB_FILE_CACHE_SIZE); 106 isCacheEnabled = (mobFileMaxCacheSize > 0); 107 map = new ConcurrentHashMap<>(mobFileMaxCacheSize); 108 if (isCacheEnabled) { 109 long period = conf.getLong(MobConstants.MOB_CACHE_EVICT_PERIOD, 110 MobConstants.DEFAULT_MOB_CACHE_EVICT_PERIOD); // in seconds 111 evictRemainRatio = conf.getFloat(MobConstants.MOB_CACHE_EVICT_REMAIN_RATIO, 112 MobConstants.DEFAULT_EVICT_REMAIN_RATIO); 113 if (evictRemainRatio < 0.0) { 114 evictRemainRatio = 0.0f; 115 LOG.warn(MobConstants.MOB_CACHE_EVICT_REMAIN_RATIO + " is less than 0.0, 0.0 is used."); 116 } else if (evictRemainRatio > 1.0) { 117 evictRemainRatio = 1.0f; 118 LOG.warn(MobConstants.MOB_CACHE_EVICT_REMAIN_RATIO + " is larger than 1.0, 1.0 is used."); 119 } 120 this.scheduleThreadPool.scheduleAtFixedRate(new EvictionThread(this), period, period, 121 TimeUnit.SECONDS); 122 123 if (LOG.isDebugEnabled()) { 124 LOG.debug("MobFileCache enabled with cacheSize=" + mobFileMaxCacheSize + ", evictPeriods=" 125 + period + "sec, evictRemainRatio=" + evictRemainRatio); 126 } 127 } else { 128 LOG.info("MobFileCache disabled"); 129 } 130 } 131 132 /** 133 * Evicts the lru cached mob files when the count of the cached files is larger than the 134 * threshold. 135 */ 136 public void evict() { 137 if (isCacheEnabled) { 138 // Ensure only one eviction at a time 139 if (!evictionLock.tryLock()) { 140 return; 141 } 142 printStatistics(); 143 List<CachedMobFile> evictedFiles = new ArrayList<>(); 144 try { 145 if (map.size() <= mobFileMaxCacheSize) { 146 return; 147 } 148 List<CachedMobFile> files = new ArrayList<>(map.values()); 149 Collections.sort(files); 150 int start = (int) (mobFileMaxCacheSize * evictRemainRatio); 151 if (start >= 0) { 152 for (int i = start; i < files.size(); i++) { 153 String name = files.get(i).getFileName(); 154 CachedMobFile evictedFile = map.remove(name); 155 if (evictedFile != null) { 156 evictedFiles.add(evictedFile); 157 } 158 } 159 } 160 } finally { 161 evictionLock.unlock(); 162 } 163 // EvictionLock is released. Close the evicted files one by one. 164 // The closes are sync in the closeFile method. 165 for (CachedMobFile evictedFile : evictedFiles) { 166 closeFile(evictedFile); 167 } 168 evictedFileCount.add(evictedFiles.size()); 169 } 170 } 171 172 /** 173 * Evicts the cached file by the name. 174 * @param fileName The name of a cached file. 175 */ 176 public void evictFile(String fileName) { 177 if (isCacheEnabled) { 178 IdLock.Entry lockEntry = null; 179 try { 180 // obtains the lock to close the cached file. 181 lockEntry = keyLock.getLockEntry(hashFileName(fileName)); 182 CachedMobFile evictedFile = map.remove(fileName); 183 if (evictedFile != null) { 184 evictedFile.close(); 185 evictedFileCount.increment(); 186 } 187 } catch (IOException e) { 188 LOG.error("Failed to evict the file " + fileName, e); 189 } finally { 190 if (lockEntry != null) { 191 keyLock.releaseLockEntry(lockEntry); 192 } 193 } 194 } 195 } 196 197 /** 198 * Opens a mob file. 199 * @param fs The current file system. 200 * @param path The file path. 201 * @param cacheConf The current MobCacheConfig 202 * @return A opened mob file. 203 */ 204 public MobFile openFile(FileSystem fs, Path path, CacheConfig cacheConf, 205 StoreContext storeContext) throws IOException { 206 StoreFileTracker sft = StoreFileTrackerFactory.create(conf, false, storeContext); 207 if (!isCacheEnabled) { 208 MobFile mobFile = MobFile.create(fs, path, conf, cacheConf, sft); 209 mobFile.open(); 210 return mobFile; 211 } else { 212 String fileName = path.getName(); 213 CachedMobFile cached = map.get(fileName); 214 IdLock.Entry lockEntry = keyLock.getLockEntry(hashFileName(fileName)); 215 try { 216 if (cached == null) { 217 cached = map.get(fileName); 218 if (cached == null) { 219 if (map.size() > mobFileMaxCacheSize) { 220 evict(); 221 } 222 cached = CachedMobFile.create(fs, path, conf, cacheConf, sft); 223 cached.open(); 224 map.put(fileName, cached); 225 miss.increment(); 226 } 227 } 228 cached.open(); 229 cached.access(count.incrementAndGet()); 230 } finally { 231 keyLock.releaseLockEntry(lockEntry); 232 } 233 return cached; 234 } 235 } 236 237 /** 238 * Closes a mob file. 239 * @param file The mob file that needs to be closed. 240 */ 241 public void closeFile(MobFile file) { 242 IdLock.Entry lockEntry = null; 243 try { 244 if (!isCacheEnabled) { 245 file.close(); 246 } else { 247 lockEntry = keyLock.getLockEntry(hashFileName(file.getFileName())); 248 file.close(); 249 } 250 } catch (IOException e) { 251 LOG.error("MobFileCache, Exception happen during close " + file.getFileName(), e); 252 } finally { 253 if (lockEntry != null) { 254 keyLock.releaseLockEntry(lockEntry); 255 } 256 } 257 } 258 259 public void shutdown() { 260 this.scheduleThreadPool.shutdown(); 261 for (int i = 0; i < 100; i++) { 262 if (!this.scheduleThreadPool.isShutdown()) { 263 try { 264 Thread.sleep(10); 265 } catch (InterruptedException e) { 266 LOG.warn("Interrupted while sleeping"); 267 Thread.currentThread().interrupt(); 268 break; 269 } 270 } 271 } 272 273 if (!this.scheduleThreadPool.isShutdown()) { 274 List<Runnable> runnables = this.scheduleThreadPool.shutdownNow(); 275 LOG.debug("Still running " + runnables); 276 } 277 } 278 279 /** 280 * Gets the count of cached mob files. 281 * @return The count of the cached mob files. 282 */ 283 public int getCacheSize() { 284 return map == null ? 0 : map.size(); 285 } 286 287 /** 288 * Gets the count of accesses to the mob file cache. 289 * @return The count of accesses to the mob file cache. 290 */ 291 public long getAccessCount() { 292 return count.get(); 293 } 294 295 /** 296 * Gets the count of misses to the mob file cache. 297 * @return The count of misses to the mob file cache. 298 */ 299 public long getMissCount() { 300 return miss.sum(); 301 } 302 303 /** 304 * Gets the number of items evicted from the mob file cache. 305 * @return The number of items evicted from the mob file cache. 306 */ 307 public long getEvictedFileCount() { 308 return evictedFileCount.sum(); 309 } 310 311 /** 312 * Gets the hit ratio to the mob file cache. 313 * @return The hit ratio to the mob file cache. 314 */ 315 public double getHitRatio() { 316 return count.get() == 0 ? 0 : ((float) (count.get() - miss.sum())) / (float) count.get(); 317 } 318 319 /** 320 * Prints the statistics. 321 */ 322 public void printStatistics() { 323 long access = count.get() - lastAccess; 324 long missed = miss.sum() - lastMiss; 325 long evicted = evictedFileCount.sum() - lastEvictedFileCount; 326 int hitRatio = access == 0 ? 0 : (int) (((float) (access - missed)) / (float) access * 100); 327 LOG.info("MobFileCache Statistics, access: " + access + ", miss: " + missed + ", hit: " 328 + (access - missed) + ", hit ratio: " + hitRatio + "%, evicted files: " + evicted); 329 lastAccess += access; 330 lastMiss += missed; 331 lastEvictedFileCount += evicted; 332 } 333 334 /** 335 * Use murmurhash to reduce the conflicts of hashed file names. We should notice that the hash 336 * conflicts may bring deadlocks, when opening mob files with evicting some other files, as 337 * described in HBASE-28047. 338 */ 339 private long hashFileName(String fileName) { 340 return Hashing.murmur3_128().hashString(fileName, java.nio.charset.StandardCharsets.UTF_8) 341 .asLong(); 342 } 343}