/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hdfs; import java.io.IOException; import java.util.Collection; import java.util.Arrays; import java.util.Comparator; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.*; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hdfs.metrics.LookasideMetrics.LocalMetrics; /** * The cache is a typical implementation of a LRU cache. */ public class LookasideCache { public static final Log LOG = LogFactory.getLog(LookasideCache.class); // The size of the cache and its default value. public static String CACHESIZE = "fs.lookasidecache.size"; public static long CACHESIZE_DEFAULT = 10 * 1024 * 1024; // The cache eviction percentage and its default value public static String CACHEEVICT_PERCENT = "fs.lookasidecache.evict.percent"; public static long CACHEEVICT_PERCENT_DEFAULT = 10; // A static object to record metrics for all instances of this cache public static LocalMetrics localMetrics = new LocalMetrics(); /* * Metrics are locally gathers in the static structure called 'stats'. * These are returned here to the metrics module, and the local values * in 'stats' are zeroed out. */ public static LocalMetrics copyZeroLocalMetrics() { return new LocalMetrics(localMetrics); } /** * Returns a copy of the local Metrics */ static LocalMetrics getLocalMetrics() { return localMetrics; } // The configuration private Configuration conf; // The global stamp is a monotonically increasing number, simulates // the passage of time. private static AtomicLong globalStamp = new AtomicLong(0); // a hashMap that keeps a record of all entries in the cache. The // key to this hashMap is the full path name of the hdfs file. The // value is a record that describes the local file and its age. private ConcurrentHashMap<Path, CacheEntry> cacheMap; // the total size occupied by the cache. This is a virtual size, // the application can specify a size with each entry to be cached. private AtomicLong cacheSize = new AtomicLong(0); // is eviction in progress? private boolean evictionInProgress = false; // The maximum size of the cache. If it exceeds this number, // then eviction will start. However, current threads may // temporarily exceed this specified threshold. private long cacheSizeMax; // The percentage of the cache that will be evicted // if (and when) it becomes full. private long cacheEvictPercent; // call back into the application when an entry needs to be evicted private final Eviction evictionIface; // The details of each cache entry. It maps a hdfs path to // a local path. static class CacheEntry { Path hdfsPath; Path localPath; long genStamp; // the timestamp of last access long entrySize; // the size of this entry CacheEntry(Path hdfsPath, Path localPath, long entrySize) { this.hdfsPath = hdfsPath; this.localPath = localPath; this.genStamp = globalStamp.incrementAndGet(); this.entrySize = entrySize; } // stamp the latest timestamp in this entry synchronized void setGenstamp(long newValue) { assert this.genStamp < newValue; this.genStamp = newValue; } } // a comparator to sort CacheEntry in ascending order of genStamp static class CacheEntryComparator implements Comparator<CacheEntry> { public int compare(CacheEntry t1, CacheEntry t2) { if (t1.genStamp < t2.genStamp) { return -1; } else if (t1.genStamp > t2.genStamp) { return 1; } else { return 0; } } } static CacheEntryComparator LRU_COMPARATOR = new CacheEntryComparator(); /** * The application that uses this cachemodule have to define this * interface so that the cache module can call back into the applcation * when an entry needs to be evicted from the cache. */ interface Eviction { void evictCache(Path hdfsPath, Path localPath, long size) throws IOException; } /** * Create an instance of this Cache. Eviction notices are not * served to the application. */ LookasideCache(Configuration conf) throws IOException { this(conf, null); } /** * Create an instance of this Cache. It has a maximum size. If * it fills up, we evict oldest entries from the cache until it has * a configured percentage of free space. */ LookasideCache(Configuration conf, Eviction evictIface) throws IOException { this.conf = conf; this.cacheMap = new ConcurrentHashMap<Path, CacheEntry>(); this.cacheSizeMax = conf.getLong(CACHESIZE, CACHESIZE_DEFAULT); this.cacheEvictPercent = conf.getLong(CACHEEVICT_PERCENT, CACHEEVICT_PERCENT_DEFAULT); this.evictionIface = evictIface; if (LOG.isDebugEnabled()) { LOG.debug("Cache size " + this.cacheSizeMax + " Cache evict percentage " + this.cacheEvictPercent); } } /** * Returns the max size of the cache */ long getCacheMaxSize() { return cacheSizeMax; } /** * Returns the eviction percentage of the cache */ long getCacheEvictPercent() { return cacheEvictPercent; } /** * Returns the current size of the cache */ long getCacheSize() { return cacheSize.get(); } /** * Adds an entry into the cache. * The size is the virtual size of this entry. */ void addCache(Path hdfsPath, Path localPath, long size) throws IOException { localMetrics.numAdd++; CacheEntry c = new CacheEntry(hdfsPath, localPath, size); CacheEntry found = cacheMap.putIfAbsent(hdfsPath, c); if (found != null) { // If entry was already in the cache, update its timestamp assert size == found.entrySize; assert localPath.equals(found.localPath); found.setGenstamp(globalStamp.incrementAndGet()); localMetrics.numAddExisting++; if (LOG.isDebugEnabled()) { LOG.debug("LookasideCache updating path " + hdfsPath); } } else { // We just inserted an entry in the cache. Increment the // recorded size of the cache. cacheSize.addAndGet(size); localMetrics.numAddNew++; if (LOG.isDebugEnabled()) { LOG.debug("LookasideCache add new path:" + hdfsPath + " cachedPath:" + localPath + " size " + size); } } // check if we need to evict because cache is full if (cacheSize.get() > cacheSizeMax) { checkEvict(); } } /** * Change the localPath in the cache. The size remains the * same. The accesstime is updated. */ void renameCache(Path oldhdfsPath, Path newhdfsPath, Path localPath) throws IOException { CacheEntry found = cacheMap.remove(oldhdfsPath); if (found == null) { String msg = "LookasideCache error renaming path: " + oldhdfsPath + " to: " + newhdfsPath + " Path " + newhdfsPath + " because it does not exists in the cache."; LOG.warn(msg); return; } // Update its timestamp and localPath found.hdfsPath = newhdfsPath; found.setGenstamp(globalStamp.incrementAndGet()); found.localPath = localPath; // add it back to the cache CacheEntry empty = cacheMap.putIfAbsent(newhdfsPath, found); if (empty != null) { String msg = "LookasideCache error renaming path: " + oldhdfsPath + " to: " + newhdfsPath + " Path " + newhdfsPath + " already exists in the cache."; LOG.warn(msg); throw new IOException(msg); } localMetrics.numRename++; if (LOG.isDebugEnabled()) { LOG.debug("LookasideCache renamed path:" + oldhdfsPath + " to:" + newhdfsPath + " cachedPath: " + localPath); } } /** * Delete an entry from the cache. */ void removeCache(Path hdfsPath) { CacheEntry c = cacheMap.remove(hdfsPath); if (c != null) { cacheSize.addAndGet(-c.entrySize); localMetrics.numRemove++; if (LOG.isDebugEnabled()) { LOG.debug("LookasideCache removed path:" + hdfsPath + " freed up size: " + c.entrySize); } } } /** * Evicts an entry from the cache. This calls back into * the application to indicate that a cache entry has been * reclaimed. */ void evictCache(Path hdfsPath) throws IOException { CacheEntry c = cacheMap.remove(hdfsPath); if (c != null) { cacheSize.addAndGet(-c.entrySize); if (evictionIface != null) { evictionIface.evictCache(c.hdfsPath, c.localPath, c.entrySize); } localMetrics.numEvict++; if (LOG.isDebugEnabled()) { LOG.debug("LookasideCache removed path:" + hdfsPath + " freed up size: " + c.entrySize); } } } /** * Maps the hdfs pathname to a local pathname. Returns null * if this is not found in the cache. */ Path getCache(Path hdfsPath) { CacheEntry c = cacheMap.get(hdfsPath); localMetrics.numGetAttempts++; if (c != null) { // update the accessTime before returning to caller c.setGenstamp(globalStamp.incrementAndGet()); localMetrics.numGetHits++; return c.localPath; } return null; // not in cache } /** * Eviction occurs if the cache is full, we free up a specified * percentage of the cache on every run. This method is synchronized * so that only one thread is doing the eviction. */ synchronized void checkEvict() throws IOException { if (cacheSize.get() < cacheSizeMax) { return; // nothing to do, plenty of free space } // Only one thread should be doing the eviction. Do not block // current thread, it is ok to oversubscribe the cache size // temporarily. if (evictionInProgress) { return; } // record the fact that eviction has started. evictionInProgress = true; try { // if the cache has reached a threshold size, then free old entries. long curSize = cacheSize.get(); // how much to evict in one iteration long targetSize = cacheSizeMax - (cacheSizeMax * cacheEvictPercent)/100; if (LOG.isDebugEnabled()) { LOG.debug("Cache size " + curSize + " has exceeded the " + " maximum configured cacpacity " + cacheSizeMax + ". Eviction has to reduce cache size to " + targetSize); } // sort all entries based on their accessTimes Collection<CacheEntry> values = cacheMap.values(); CacheEntry[] records = values.toArray(new CacheEntry[values.size()]); Arrays.sort(records, LRU_COMPARATOR); for (int i = 0; i < records.length; i++) { if (cacheSize.get() <= targetSize) { break; // we reclaimed everything we wanted to } CacheEntry c = records[i]; evictCache(c.hdfsPath); } } finally { evictionInProgress = false; // eviction done. } if (LOG.isDebugEnabled()) { LOG.debug("Cache eviction complete. Current cache size is " + cacheSize.get()); } } }