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}