前言
衆所周知在HDFS中,爲了保證元數據更新的一致性,它所使用的是全局鎖的模式。不過這在一定模式下會導致激烈的鎖競爭的情況發生,尤其當集羣規模日趨膨脹的時候,獲取鎖的這種代價就會變得越來越高。如果在不降低集羣請求吞吐量的情況下,我們如何優化這一點呢?一種很自然的想法是將鎖的粒度變細,鎖粒度變細的一個原則前提是:某些不必要的場合,我們不一定非得要獲取到全局鎖的程度。本文筆者通過Ozone內部的鎖控制器的實現來簡單聊聊細粒度鎖模式的設計與實現。
全局鎖的細粒度拆分化
HDFS之所以採用全局鎖的模式,是因爲它的元數據是存儲於其內部的一個類似map的映射結構中,更爲關鍵的一點是,它並沒有對着裏面的數據進行partition的分片區分。簡單來說,就是當NN在更新其內部元數據的時候,就要把它整個的metadata數據上鎖。這個時候,其它打算修改metadata的操作請求都會被拒之門外。
但其實倘若NN能夠對其維護的海量元數據進行簡單的partition區分,比如根據INodeFile Id值或者文件Path的哈希值等等。那麼對於不相關的metadata請求更新,則完全可以獲取對應獨立的鎖,毫無疑問這種模式下集羣的concurrency請求處理無疑會提高許多。
因此全局鎖粒度的拆分前提的關鍵一點在於其所保護Resource的可拆分化,不同Resource之間的處理毫不受影響。鎖只用來保護特定Resource內的並行請求處理。
OK,如果我們實現的系統內部鎖保護Resource滿足以上的特徵,那麼很好我們後面就可以進行基於Resource的鎖管理器模型實現了。
Ozone內部基於Resource的鎖管理器模型
下面本文以Ozone內部基於Resource的鎖管理器實現爲例子,聊聊這個細粒度鎖的設計實現。
這裏有個Ozone的背景,Ozone作爲對象存儲系統,它的metadata是基於Volume,Bucket,Key的形式進行組織的。簡單來說,Ozone內部的保護的Resource其實是Volume和Bucket。我們只需要將鎖申請到具體某個Resource(具體某個Volume,Bucket)下即可,Key的操作會被其所屬的Bucket鎖所保護。當然,如果我們還是採用HDFS全局單一鎖模式來做Ozone metadata的一致性控制,也是完全沒問題的。
基於上述的實現思路,Ozone內部使用了鎖對象池的方式實現了鎖管理器模型,鎖管理器模型如下所示:
上面的鎖申請過程其實比較簡單:
1)Client端向Server端準備發起Resource訪問行爲,向鎖管理器發出指定Resource的鎖申請行爲。
2)鎖管理器向鎖對象池申請可用的鎖實例,並返回對應此Resource的當前可用的鎖。
3)Client拿到這個Resource鎖後,進行後續的數據訪問操作行爲。
4)Client Resource訪問結束後,執行了釋放鎖操作,鎖管理器將鎖歸還到鎖池子內。
在這個過程中,對象池的運用是爲了避免鎖實例的重複創建和銷燬行爲,達到更高效的複用率。其次鎖管理器在其內部會維護一個當前實時的資源與其資源鎖的一個mapping映射。
Ozone內部鎖管理器實現
上面我們瞭解了鎖管理器的原理實現,下面我們來簡單看看其中的代碼實現邏輯。
首先Ozone實際創建使用的鎖本質還是JDK自帶的Reentrant讀寫鎖,不過額外增加了多餘鎖的引用數:
/**
* Lock implementation which also maintains counter.
*/
public final class ActiveLock {
// ActiveLock鎖的本質是Reentrant讀寫鎖帶上一個reference引用數
private ReadWriteLock lock;
private AtomicInteger count;
/**
* Use ActiveLock#newInstance to create instance.
*
* @param fairness - if true the lock uses a fair ordering policy, else
* non-fair ordering.
*/
private ActiveLock(boolean fairness) {
this.lock = new ReentrantReadWriteLock(fairness);
this.count = new AtomicInteger(0);
}
/**
* Creates a new instance of ActiveLock.
*
* @return new ActiveLock
*/
public static ActiveLock newInstance(boolean fairness) {
return new ActiveLock(fairness);
}
void readLock() {
lock.readLock().lock();
}
void readUnlock() {
lock.readLock().unlock();
}
void writeLock() {
lock.writeLock().lock();
}
void writeUnlock() {
lock.writeLock().unlock();
}
/**
* Increment the active count of the lock.
*/
void incrementActiveCount() {
count.incrementAndGet();
}
/**
* Decrement the active count of the lock.
*/
void decrementActiveCount() {
count.decrementAndGet();
}
/**
* Returns the active count on the lock.
*
* @return Number of active leases on the lock.
*/
int getActiveLockCount() {
return count.get();
}
/**
* Resets the active count on the lock.
*/
void resetCounter() {
count.set(0);
}
@Override
public String toString() {
return lock.toString();
}
}
然後這裏使用了第三庫的對象池方法,進行一個方法繼承操作,
/**
* Pool factory to create {@code ActiveLock} instances.
*/
public class PooledLockFactory extends BasePooledObjectFactory<ActiveLock> {
private boolean fairness;
PooledLockFactory(boolean fair) {
this.fairness = fair;
}
@Override
public ActiveLock create() throws Exception {
return ActiveLock.newInstance(fairness);
}
@Override
public PooledObject<ActiveLock> wrap(ActiveLock activeLock) {
return new DefaultPooledObject<>(activeLock);
}
@Override
public void activateObject(PooledObject<ActiveLock> pooledObject) {
// 鎖實例對象重新激活使用時,只需進行引用計數的重置即可
pooledObject.getObject().resetCounter();
}
}
下面是最終LockManager的實現,
/**
* Manages the locks on a given resource. A new lock is created for each
* and every unique resource. Uniqueness of resource depends on the
* {@code equals} implementation of it.
*/
public class LockManager<R> {
private static final Logger LOG = LoggerFactory.getLogger(LockManager.class);
// 當前使用的活躍鎖,對應其鎖保護Resource的映射
private final Map<R, ActiveLock> activeLocks = new ConcurrentHashMap<>();
// 鎖對象池
private final GenericObjectPool<ActiveLock> lockPool;
...
/**
* Creates new LockManager instance with the given Configuration.
*
* @param conf Configuration object
* @param fair - true to use fair lock ordering, else non-fair lock ordering.
*/
public LockManager(final Configuration conf, boolean fair) {
// 鎖池子的初始化
lockPool =
new GenericObjectPool<>(new PooledLockFactory(fair));
lockPool.setMaxTotal(-1);
}
...
}
這裏我們重點來看鎖的申請和釋放的過程,
public class LockManager<R> {
...
public void writeLock(final R resource) {
acquire(resource, ActiveLock::writeLock);
}
private void acquire(final R resource, final Consumer<ActiveLock> lockFn) {
lockFn.accept(getLockForLocking(resource));
}
/**
* Returns {@link ActiveLock} instance for the given resource,
* on which the lock can be acquired.
*
* @param resource on which the lock has to be acquired
* @return {@link ActiveLock} instance
*/
private ActiveLock getLockForLocking(final R resource) {
/*
* While getting a lock object for locking we should
* atomically increment the active count of the lock.
*
* This is to avoid cases where the selected lock could
* be removed from the activeLocks map and returned to
* the object pool.
*/
return activeLocks.compute(resource, (k, v) -> {
final ActiveLock lock;
try {
if (v == null) {
// 從鎖池子中借鎖,如果當前對應Resource目前沒有鎖
lock = lockPool.borrowObject();
} else {
// 否則從當前的mapping中獲取之前已經從鎖池中借出的鎖
lock = v;
}
// 增加此鎖的引用計數值
lock.incrementActiveCount();
} catch (Exception ex) {
LOG.error("Unable to obtain lock.", ex);
throw new RuntimeException(ex);
}
return lock;
});
}
public void writeUnlock(final R resource)
throws IllegalMonitorStateException {
release(resource, ActiveLock::writeUnlock);
}
private void release(final R resource, final Consumer<ActiveLock> releaseFn) {
// 取出當前Resource對應的活躍鎖實例
final ActiveLock lock = getLockForReleasing(resource);
// 執行鎖對象的釋放鎖操作
releaseFn.accept(lock);
// 減少鎖的引用數
decrementActiveLockCount(resource);
}
private void decrementActiveLockCount(final R resource) {
activeLocks.computeIfPresent(resource, (k, v) -> {
v.decrementActiveCount();
if (v.getActiveLockCount() != 0) {
return v;
}
// 如果鎖的當前引用數爲0,則說明鎖沒有被其它Client所持有,進行鎖對象的歸還
lockPool.returnObject(v);
return null;
});
}
...
}
以上的LockManager實際的Resource可以根據具體使用場景做具體化的定義,不一定非得是具體某個類的實例,一個簡單的字符串代表一個唯一的Resource也是沒有問題的。
鎖的多層級約束處理
在某些場景中,還會涉及到多層級鎖的約束情況。比如在Ozone中,涉及到Bucket的創建操作的時候,需要事先拿到Bucket所屬Volume的Volume鎖。這是爲了避免在創建過程中,其它請求對Volume進行操作,導致Bucket在Volume內的創建操作出現問題。又或者,比如未來支持Bucket的rename操作,爲了保證rename操作在Volume下的原子性,我們就得有Volume鎖這樣高一級鎖的約束了。
OK,以上就是本文所要闡述的內容了,感興趣的同學可閱讀引用鏈接,來了解Ozone LockManager全部代碼實現的邏輯。
引用
[1].https://github.com/apache/hadoop-ozone/blob/master/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/lock/LockManager.java
[2].https://github.com/apache/hadoop-ozone/blob/master/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/lock/ActiveLock.java
[3].https://github.com/apache/hadoop-ozone/blob/master/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/lock/PooledLockFactory.java