文章目录
前言
在分布式系统中,我们常常使用锁来保证操作的一致性控制。但是锁的存在则意味着必然存在着锁竞争的情况。而且这种竞争会随着外部请求量的激增而变得更为的激烈。因此我们改进的一个方向是改变锁的粗细力度,从较为简单的粗粒度锁变为更细粒度的锁。细粒度锁相较于粗粒度锁来说,毫无疑问,它能减缓激烈的锁竞争的情情况,但是它在实现上会增加额外的复杂度。这个很好理解,在server端原先只需要维护一把锁就行了,现在则要可能维护一定规模量的小锁。本文笔者来聊聊其中一种细粒度锁的方案实现。
锁的细粒度级别
这里我们首先来了解锁的粗细粒度的级别,对于锁来说,我们可以做到何种粗细粒度的级别呢?
下面我们从粗粒度到细粒度的程度来说,
首先,比较粗粒度的锁,全局单一锁控制,一般做到读写锁分离的程度就够了,特点实现较为的简单直接。但是扩展性不好,容易达到性能瓶颈。
紧接着,稍微细粒度一点的锁,按照partition进行分片的分片锁。这种一个典型的例子是我们将一个大namespace按照规则划分成若干个小namespace,然后给每个namespace分派一个锁做各自namespace下的并发操作的控制。在这种情况下,锁的数量也还是能在一定数量控制之下的。
最后一种,完全意义上的细粒度锁控制,这个粒度甚至可以到ID级别,或者每个数据块Block级别的锁类似这种。到了这个粒度时,锁的数量可以达到成千上万的规模级别,因为它可以具体到最小单位数据粒度级别了。服务端如果此时硬是在内存里存储这些小锁,内存空间也倒是不支持。下面我们就来聊聊这种情况的锁设计实现。
基于小概率锁碰撞的lock pool实现方案
刚刚上面也已经提过,当锁的控制粒度降为每个最小单位数据粒度时。维护全部的小锁并不是一种切实的方案。
那么这个时候怎么办呢?这里推荐的一种方案是lock pool,锁池的方案。这个锁池会维护一定规模的锁。主要关键的一点是,这个锁池内的锁在被释放后,能被其它请求拿去使用,达到锁复用的程度。这里基于的一个背景其实是:
一段时间内,往往只同时存在部分被操作的数据,因此我们实际只需要维护这部分操作需要的锁,而这个数量规模其实是有限的。
然后呢,当这些锁被用完后,它可以被释放回锁池,然后后面的请求可以再次获取锁对象。
当然还存在一种锁碰撞的情况发生,当系统同时在操作的单位数需要获取到同一个锁的时候,这个时候就会发生锁等待的情况。当然这种情况是一种低概率的事情,当我们锁池内锁维护的锁总数越大的时候,这个碰撞的概率会越低。
OK,我们这里介绍两种简单锁池模型的实现。
第一种,维护一个固定大小的锁列表对象,然后根据映射规则去取对应的锁。比如下面逻辑的代码,去获取object对象锁。
blockOpLocks = new Object[blockOpLocksSize];
for (int i = 0; i < blockOpLocksSize; i++) {
blockOpLocks[i] = new Object();
}
...
/** * Gets the locks used in block operations. * @param blockId the given block id. * @return the lock used in the operation of the given block. */
private Object getBlockOpLock(long blockId) {
return blockOpLocks[Math.abs((int) (blockId % blockOpLocksSize))];
}
以上是HDFS社区JIRA HDFS-9668: Optimize the locking in FsDatasetImpl 部分的代码,按照BlockId取余来得到其应该获取的对象锁。
另外一种简单实现的方法是利用外部工具Guava的Striped类,此类实现功能类似于可复用entry的ConcurrentHashMap。此类能够保证的一点是,相同key的对象一定会映射到同一个lock上,但是它并不能保证不同的key完全不会映射到同一个lock上的可能性。这取决于给定的最小数量lock的指定。
在Alluxio代码中使用了Striped类做细粒度级别锁的控制,
首先是定义:
/** * 10k locks balances between keeping a small memory footprint and avoiding unnecessary lock * contention. Each stripe is around 100 bytes, so this takes about 1MB. Block locking critical * sections are short, so it is acceptable to occasionally have conflicts where two different * blocks want to lock the same stripe. */
private final Striped<Lock> mBlockLocks = Striped.lock(10_000);
上面数字指定越大,锁冲突概率越小,但是相应的会消耗更多memory空间去保留锁,然后是锁的获取,直接从Striped类里获取,
private LockResource lockBlock(long blockId) {
return new LockResource(mBlockLocks.get(blockId));
}
以上是本文对于带有锁冲突概率的细粒度锁实现的思考。
引用
[1]. https://issues.apache.org/jira/browse/HDFS-9668