前言
众所周知在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()