共享资源的互斥访问一直是很多业务系统需要解决的问题。在分布式系统中,通常采用分布式锁的通用方案。本文将讨论分布式锁的实现原理、技术选型以及阿里云存储的具体实践。从单机锁到分布式锁在单机环境下,当共享资源本身不能提供互斥能力时,为了防止多线程/多进程同时读写共享资源造成数据损坏,第三种需要一方提供的互斥能力往往是内核或者提供互斥能力的类库。如下图所示,进程首先从内核/类库中获取互斥锁,获得锁的进程可以独占访问共享资源。要演变成分布式环境,我们需要一个提供相同功能的分布式服务。不同的机器通过这个服务获得锁,获得锁的机器可以独占访问共享资源。我们统称此类服务为分布式。锁服务,锁也叫分布式锁。单机锁到分布式锁这抽象了分布式锁的概念。首先,分布式锁需要是一个可以提供并发控制并输出独占状态的资源,即:lock=resource+concurrencycontrol+ownership以普通的单机锁为例:Spinlock=BOOL+CAS(乐观锁)Mutex=BOOL+CAS+notification(悲观锁)Spinlock和Mutex都是Bool资源,通过原子CAS指令:当现在为0时,将其设置为1。如果成功,则持有锁,如果失败,它不持有锁。如果不提供所有权显示,比如AtomicInteger,它也是使用resource(Interger)+CAS,但是不会显式提示所有权,所以不会被认为是一个Locks,当然可以认为更多的是wrapper用于某种形式的服务交付。在单机环境下,内核具有“上帝视角”,可以知道进程的存活情况。当进程挂掉时,可以释放进程持有的锁资源。但是,当它发展到分布式环境时,这就成了一个挑战。为了应对各种机器故障、停机等情况,有必要为锁具提供一个新特性:可用性。如下图所示,任何提供这三个特性的服务都可以提供分布式锁的能力。资源可以是文件、KV等,通过创建文件、KV等原子操作,通过创建成功结果来表明所有权的归属,同时通过TTL或session来保证Lock的可用性。分布式锁的特点及实现分布式锁的系统分类根据锁资源的安全性,我们将分布式锁分为两大阵营:基于异步复制的分布式系统,如mysql、tail、redis等;基于异步复制的分布式共识系统paxos协议,如zookeeper、etcd、consul等。基于异步复制的分布式系统存在数据丢失(丢锁)的风险,安全性不够。它往往通过TTL机制来承担细粒度的锁服务。该系统易于访问,适用于时间敏感的应用程序。有效期短,执行短期任务,丢锁对业务的影响相对可控。基于paxos协议的分布式系统通过一致性协议保证数据的多副本,数据安全性高。它往往通过租约(session)机制来承担粗粒度的锁服务。系统需要一定的门槛,适合对安全性非常敏感,希望长期持有锁,不希望有失锁服务。三阿里云存储分布式锁阿里云存储在长期的实践过程中,对于如何提高分布式锁的正确性,保证锁的可用性,提高锁的切换效率,积累了大量的经验。1严格互斥互斥是分布式锁最基本的要求。对于用户来说,“一把锁占一把锁”是不可能发生的。存储分布式锁如何避免这种情况呢?答案是每次服务器端都绑定一个唯一的session,client通过周期性发送心跳来保证session的有效性,也保证了锁的归属。当无法维持心跳时,会话和关联的锁节点将被释放,锁节点可以再次被抢占。这里有一个关键点,就是如何保证客户端和服务端的同步。当服务器上的会话过期时,客户端也能感知到。如下图所示,客户端和服务端都维护会话的有效期。客户端从发送心跳的时间(S0)开始计时,服务端从收到请求(S1)开始计时,这样可以保证客户端会在服务端之前过期。用户创建锁后,核心工作线程可以在执行核心操作之前判断是否有足够的有效期。同时,我们不再依赖墙上的时间,而是根据系统时钟来判断时间。系统时钟更准确,不会向前或向后移动(秒级误差为毫秒级,同时在NTP跳转的场景下,最多修改时钟速率)。存储场景的使用基于分布式锁的互斥。我们达到完美了吗?不是这样,还是有一种情况,基于分布式锁服务的业务访问互斥会被破坏。我们看下面的例子:如下图所示,客户端在时间点(S0)尝试抢锁,在时间点(S1)成功在后端抢到锁,所以一个有效期窗口的分布式锁也被生成。在有效期内,时间点(S2)进行了访问存储的操作,很快完成。然后在时间点(S3),判断锁的有效期仍然有效,继续访问存储的操作。结果这个操作耗时比较长,超过了分布式锁的过期时间,那么有可能这个时候分布式锁已经被其他客户端成功抢占了,有可能是两个客户端操作了同时处理同一批数据。这种可能性是存在的,虽然概率很小。越界场景对于这种场景,具体的解决方案是保证操作数据时有足够的锁有效期窗口。当然,如果业务本身提供回滚机制,方案会更完善。该方案在存储产品中也使用了分布式锁。过程中采纳。还有一个更好的解决方案,就是存储系统本身引入IOFence能力。这里不得不提一下MartinKleppmann和redis的作者antirez的讨论。redis为了防止异步复制带来的锁丢失问题,引入了redlock。该方案引入了多数机制,需要获得多数锁来最大程度地保证可用性和正确性。但是仍然存在两个问题:上层时间(NTP时间)的不可靠性异构系统无法实现严格的正确性。walltime可以用非walltimeMonoticTime解决(redis还是依赖walltime),但是异构系统只有一个系统。方法保证完全正确。如下图,Client1获取了锁,在操作数据的时候发生了GC。当GC完成后,锁的所有权丢失,导致数据不一致。异构系统无法实现完全正确。因此需要两个系统同时配合才能完成完全正确的互斥访问。存储系统引入IOFence能力。如下图,全局锁服务提供全局自增token,Client1拿到锁后返回的token为33,带入存储系统,发生GC。当Client2成功抢到锁返回34后,被带入存储系统,存储系统会拒绝令牌较小的请求。时间长了,fullgc会重启当恢复的Client1再次写入数据时,因为存储层记录的token已经更新,所以携带token值为33的请求会被直接拒绝,从而达到数据的效果保护(如Chubby的论文中所述,也是Kleppmann提出的Martin解决方案)。IOFence能力的引入,与阿里云分布式存储平台盘古的设计思路不谋而合。盘古支持类似IOFence的写保护能力,引入InlineFile文件类型,配合SealFile操作,具有类似IOFence的写保护。保护能力。首先,SealFile操作用于关闭cs上已经打开的文件,防止旧的Owner继续写入数据;其次,InlineFile可以防止旧所有者打开新文件。事实上,这两个功能在存储系统中也提供了token支持。2可用性存储分布式锁通过不断的心跳来保证锁的健壮性,使得用户不需要在可用性上花费大量的精力,但是异常的用户进程可能会继续占用锁。针对这种场景,为了保证锁最终能够被调度,提供了可以安全释放锁的会话黑化机制。当用户需要释放假死进程持有的锁时,可以查询session信息,将session拉黑。之后就不会再正常维持心跳,最终session过期,安全释放锁节点。这里我们并不是强制删除锁,而是选择禁用心跳,原因如下:删除锁的操作本身是不安全的。如果锁已经正常被别人抢占,请求删除锁会导致误删除。锁删除后,持有锁的人session还是正常的,它仍然认为自己持有锁,这样就会打破锁的互斥原则。3切换效率当一个进程持有的锁需要重新调度时,持有者可以主动删除锁节点,但是当持有者发生异常(如进程重启、机器宕机等)时,新的进程需要再次被抢占。需要等待原来的session过期,才有机会抢占成功。默认情况下,分布式锁使用的会话生命周期是几十秒。当持有锁的进程意外退出(没有主动释放锁)时,需要很长时间才能再次抢占锁节点。客户端和服务分别维护过期时间以提高切换精度,本质上压缩了会话生命周期,也意味着更快的心跳频率和更大的后端访问压力。我们优化了session周期,让session周期可以进一步压缩。同时结合具体的业务场景,比如daemon发现持有锁的进程挂掉的场景,提供锁的CAS释放操作,让进程零等待抢到锁。例如,利用锁节点中存储的进程唯一标识,强行释放不再使用的锁,重新争夺。这种方式可以完全避免进程升级或意外重启后抢到锁所需的等待时间。四个结论分布式锁在分布式环境中提供了对共享资源的互斥访问。业务要么依赖分布式锁追求效率提升,要么依赖分布式锁追求访问的绝对互斥。同时,在访问分布式锁服务的过程中,需要考虑访问成本、服务可靠性、分布式锁切换的准确性、正确性等问题。正确合理的使用分布式锁需要不断的思考和优化。参考文章Howtododistributedlocking-MartinKleppmannIsRedlocksafe?-antirezchubby纸-谷歌
