聊聊分布式互斥
前言
在之前的两篇博客中,完成了分布式的基础入门,也就是知道了什么是分布式,以及分布式有哪些指标。下面一个篇章,将会聚焦于分布式协调与同步的内容。简单来说就是,如何让程序通过协作共同去达成一个业务目标。
什么是分布式互斥?
假设如下的场景,你正在吃自选餐,刚刚寻找到你想吃的菜品,突然在这时有一个抠脚大汉过来,把你挤到一边。看着他那体型,你只好耐着性子等他打完菜,再继续打自己的菜。结果你夹菜到一半他又回来中断了你的过程。如果这样反复来几次,你肯定会想,这老几就是来找茬的,马上就会来一场“有你没我,有我没你”的格斗了。
上述的场景,也同样存在于分布式集群环境。就像我们在选菜过程中不希望被打断一样,对于同一共享资源,一个程序正在使用的时候也不希望被其它程序进程所打扰。这就要求在同一时刻只能有一个程序能够访问这种资源。
在分布式系统里,这种排他性的资源访问形式,叫作分布式互斥,而这种被互斥访问的共享资源叫作临界资源。
接下来,一起看看如何才能让分布式系统中的程序互斥地访问临界资源。
集中式算法
对于前文提到的打菜问题,是不是可以增加一个“协调者”来约束大家文明就餐,从而避免强行插入打断别人进程的问题。
那么,按照这种方式,我们可以在分布式集群中,引入一个协调者程序,得到一个分布式互斥算法。每个程序在需要访问临界资源时,先给协调者发送一个请求。如果当前这个资源处于空闲状态,也就是没有程序正在使用,协调者直接授权请求程序访问;否则,可以按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源,则通知协调者,协调者从“排号”的队列里面取出排在最前面的请求,并给它发送授权消息。拿到授权消息的程序,可以直接去访问临界资源。
这就是 FIFO 的模式,这个互斥算法,就是我们常说的集中式算法。之所以这么称呼,是因为协调者代表着集中程序,如下图所示:
程序 1、2、3、4 为普通运行的程序,另一个程序为协调者。当程序 2 和 程序 4 需要使用临界资源时,会先向协调者发送申请,请求协调者授权。
不巧的是,程序 3 正在使用临界资源。这时,协调者会根据程序 2 和 4 的申请顺序,依次将它们放入等待队列。比如当前的案例是,程序 4 的申请时间早于程序 2,因此排在程序 2 的前面。
程序 3 使用完临界资源后,通知协调者释放授权。此时,协调者从等待队列中通知程序 4,并给它发放授权。这时,程序 4 就可以使用临界资源了。依次类推,直到所有申请结束。
基于上述流程可以看出,一个程序完成一次临界资源访问,需要如下几个流程和信息交互:
- 向协调者发送请求授权信息,1 次消息交互;
- 协调者向程序发放授权信息,1 次消息交互;
- 程序使用完临界资源后,向协调者发送释放授权,1 次信息交互。
因此,每个程序完成一次临界资源访问,至少需要进行 3 次信息交互。
集中式算法的优点在于直观、简单、信息交互量少、易于实现,并且所有程序只需和协调者通信,程序之间无需通信。但缺点也很明显,就是在协调者自身,原因如下:
- 一方面,协调者会成为系统的性能瓶颈。假设如果有 200 个程序要访问临界资源,那么协调者要处理 200*3=600 条消息。也就是说,协调者处理的消息数量会随着需要访问临界资源的程序数量线性增加;
- 另一方面,容易引发单点故障。当协调者的受到的请求访问量过大,如果某一进程特别重,可能会引发协调者故障,就会导致所有程序都无法访问临界资源,可能会进一步造成系统处于不可用状态。
因此,在使用集中算法的时候,一定要选择性能好、可靠性高的服务器来运行协调者。
分布式算法
既然引入协调者会带来一些问题,那不用协调者是否可以实现对临界资源的互斥访问呢?比如试试看,能不能协商着来。在访问某块资源之前,先征求其他人的意见,在确认其他人都没有意向使用的时候,就属于来到自己的主场了。
同理,我们可以把这种方式用于分布式系统。当一个程序要访问临界资源时,先向系统中的其他程序发送一条请求信息,在接受到所有程序返回的同意消息后,才可以访问临界资源。其中请求消息需要包含请求的资源、请求者的 ID,以及发送请求的时间。这就是民主协商法。
如图所示,程序 1、2、3 需要访问资源 A。在时间戳为 8 的时刻,程序 1 想要使用资源 A,于是向程序 2 和 3 发起使用资源 A 的申请,希望得到它们的同意。在时间戳为 12 的时刻,程序 3 想要使用资源 A,于是向程序 1 和 2 发起访问资源 A 的请求。
如果此时程序 2 暂时不访问资源 A,因此同意了程序 1 和 3 的资源访问请求。对于程序 3 来说,由于程序 1 提出请求的实践更早,因此同意程序 1 先使用资源,并等待程序 1 返回用以消息。
程序 1接收到其他所有程序的同意消息之后,开始用资源 A。当程序 1 使用完资源 A 后,释放使用权限,向请求队列中需要使用资源 A 的程序 3 发送同意使用资源的消息,并将程序 3 从请求队列中删除。此时,程序 3 收到了其他所有程序的同意消息,获得了使用资源 A 的权限,开始用临界资源 A。
从上述流程可以看出,一个程序完成一次临界资源的访问,需要进行如下的信息交互:
- 向其他 n-1 个程序发送访问临界资源的请求,总共需要 n-1 次消息交互;
- 需要接收到其他 n-1 个程序回复的同意消息,方可访问资源,总共需要 n-1 次消息交互。
可以得出以下结论,一个程序要成功访问临界资源,至少需要 2*(n-1) 次消息交互。假设,现在系统中的 n 个程序都要访问临界资源,则会同时产生 2(n-1) *n 条消息。总结来说,在大型系统中使用分布式算法,消息数量会随着需要访问临界资源的程序数量呈指数级增加,容易导致高昂的“沟通成本”。
从上述分析不难看出,分布式算法根据“先到先得”以及“投票全票通过”的机制,让每个程序按时间顺序公平地访问资源,简单粗暴、易于实现。但,这种算法可用性很低,主要包括以下两个方面:
- 当系统内需要访问临界资源的程序增多时,容易产生“信令风暴”,也就是程序收到的请求完全超过了自己的处理能力,从而导致自己正常的业务无法继续开展;
- 一旦某一程序发生故障,无法发送同意消息,那么其他程序均除以等待回复的状态中,使得整个系统处于停滞状态,导致整个系统不可用。所以,相对于集中算法的协调者故障,分布式算法的可用性更低。
针对可用性低的一种改进办法是,如果检测到一个程序故障,则直接忽略这个程序,比如网络阻塞,长时间接收不到返回的消息,既然等不到那么就无需再等咯。
这就好比在自助餐厅,一个人离开了餐厅,你想选某个菜品也就无需再征求他的建议。但这样的话,每个程序都需要对其他程序进行故障检测,这无疑带来了更大的复杂性。
因此,分布式算法适合节点数目少且变动不频繁的系统,且由于每个程序均需通信交互,因此适合 P2P 结构的系统。比如,运行在局域网中的分布式文件系统。
令牌环算法
既然集中式算法、分布式算法都存在一定的缺陷,那么还有什么方法可以实现分布式互斥嘛?答案是肯定的。比较方法总比问题多。轮值 CEO 其实就给了我们一个很好的启示:在轮值 CEO 体系里,CEO 就是临界资源,同时只能有一个人担任,由多名高管轮流出任 CEO。
类似的,程序访问临界资源问题也可以按照轮值 CEO 的思路实现,如下图所示,所有程序构建一个环结构,令牌按照指定的顺序(顺时针或逆时针)方向在程序之间传递,收到令牌的程序有权访问临界资源,访问完成后将令牌传送给下一个程序;若该程序不需要访问临界资源,则直接把令牌传送给下一个程序。
在分布式领域,通常将这个算法叫作令牌环算法。为了便于理解记忆,可以形象地称之为轮值 CEO 算法。
因为在使用临界资源前,不需要像分布式算法那样挨个征求其他程序的意见了,所以相对而言,在令牌环算法里单个程序具有更高的通信效率。同时,在一个周期内,每个程序都能访问到临界资源,因此令牌环算法的公平性很好。
但是,不管环中的程序是否想要访问资源,都需要接收并传递令牌,所以也会带来一些无效通信。假设系统中有 100 个程序,那么程序 1 访问完资源后,即使其它 99 个程序不需要访问,也必须要等令牌在其他 99 个程序传递完后,才能重新访问资源,这就降低了系统的实时性。
综上,令牌环算法的公平性高,在改进单点故障后,稳定性也很高,适用于系统规模较小,并且系统中每个程序使用临界资源的频率高且使用时间比较短的场景。
总结
在这篇博客中,我们介绍了分布式互斥的概念和几种实现方法,包括集中式算法,分布式算法和令牌环算法。
集中式算法引入了一个协调者程序,每个程序在需要访问临界资源时,先给协调者发送一个请求。协调者根据请求的顺序,决定哪个程序可以访问资源。集中式算法的优点是简单和易于实现,但是协调者可能会成为系统的性能瓶颈,或者引发单点故障。
分布式算法的思路是,一个程序在访问临界资源时,先向其他程序发送请求。只有当所有程序都同意之后,该程序才可以访问资源。这种方式可以避免单点故障的问题,但是可能会产生大量的消息交互,导致“信令风暴”。此外,如果某个程序发生故障,那么整个系统可能会处于停滞状态。
令牌环算法则是所有程序构成一个环,令牌按照一定的顺序在程序之间传递。只有拿到令牌的程序才能访问资源。这种方法避免了无效的通信,提高了通信效率。同时,由于在一个周期内,每个程序都能访问到资源,因此令牌环算法的公平性很好。但是,不论程序是否需要访问资源,都需要接收并传递令牌,这可能会降低系统的实时性。
总的来说,每种算法都有其适用的场景和局限性。在实际使用中,需要根据系统的规模,程序的数量,以及临界资源的使用频率和使用时间等因素,选择最合适的算法。同时,也可以考虑使用混合的方式,结合多种算法的优点,以满足不同的需求。