This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
<audio id="audio" title="03 | 分布式互斥:有你没我,有我没你" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d6/b3/d6b3d17b33b4375c757c8c52ab698cb3.mp3"></audio>
你好!我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
通过前面的两篇文章,相信你已经对此次的分布式世界之行有了一个初步了解,想必对此次旅行也是充满期待。今天,我就带你正式踏上第一站:分布式协调与同步。在这一站,我将带你学习如何让分布在不同计算机上的程序具有“团队精神”,换句话说就是如何让程序通过协作共同去达成一个业务目标。
在本站,我将带你打卡的第一个景点是分布式互斥。那,什么是分布式互斥呢?
## 什么是分布式互斥?
想象一下,你正在一家餐厅使用自助咖啡机泡制咖啡,突然有个人过来挪走了你的杯子,开始泡制他自己的咖啡。你耐着性子等他操作完,继续泡制自己的咖啡。结果你开始没多久,他又回来中断了你泡制咖啡的过程。相信要不了几个回合,你和他就会上演一场“有你没我,有我没你”的格斗了。
这样现实的问题也同样存在于分布式世界。就像我们使用自助咖啡机时不希望被打扰一样,对于同一共享资源,一个程序正在使用的时候也不希望被其他程序打扰。这,就要求同一时刻只能有一个程序能够访问这种资源。
在分布式系统里,这种排他性的资源访问方式,叫作**分布式互斥Distributed Mutual Exclusion<strong>而这种被互斥访问的共享资源就叫作**临界资源Critical Resource</strong>
接下来,我带你一起看看如何才能让分布式系统里的程序互斥地访问临界资源。
## 霸道总裁:集中式算法
对于前面提到的咖啡机问题,我们首先想到的就是,增加一个“协调者”来约束大家使用自助咖啡机,解决强行插入打断别人的问题。
类似的,我们引入一个协调者程序,得到一个分布式互斥算法。每个程序在需要访问临界资源时,先给协调者发送一个请求。如果当前没有程序使用这个资源,协调者直接授权请求程序访问;否则,按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源,则通知协调者,协调者从“排号”的队列里取出排在最前面的请求,并给它发送授权消息。拿到授权消息的程序,可以直接去访问临界资源。
这个互斥算法,就是我们所说的**集中式算法**,也可以叫做中央服务器算法。之所以这么称呼,是因为协调者代表着集中程序或中央服务器。
集中式算法的示意图如下所示:
<img src="https://static001.geekbang.org/resource/image/35/9c/35e3acf117901ebe4d966eed56660e9c.jpg" alt="">
如图所示程序1、2、3、4为普通运行程序另一个程序为协调者。当程序2和程序4需要使用临界资源时它们会向协调者发起申请请求协调者授权。
不巧的是程序3正在使用临界资源。这时协调者根据程序2和4的申请时间顺序依次将它们放入等待队列。在这个案例里程序4的申请时间早于程序2因此排在程序2的前面。
程序3使用完临界资源后通知协调者释放授权。此时协调者从等待队列中取出程序4并给它发放授权。这时程序4就可以使用临界资源了。
从上述流程可以看出,**一个程序完成一次临界资源访问,需要如下几个流程和消息交互**
1. 向协调者发送请求授权信息1次消息交互
1. 协调者向程序发放授权信息1次消息交互
1. 程序使用完临界资源后向协调者发送释放授权1次消息交互。
因此每个程序完成一次临界资源访问需要进行3次消息交互。
不难看出,集中式算法的优点在于直观、简单、信息交互量少、易于实现,并且所有程序只需和协调者通信,程序之间无需通信。但是,这个算法的问题也出在了协调者身上。
- 一方面协调者会成为系统的性能瓶颈。想象一下如果有100个程序要访问临界资源那么协调者要处理100*3=300条消息。也就是说协调者处理的消息数量会随着需要访问临界资源的程序数量线性增加。
- 另一方面,容易引发单点故障问题。协调者故障,会导致所有的程序均无法访问临界资源,导致整个系统不可用。
因此,在使用集中式算法的时候,一定要选择性能好、可靠性高的服务器来运行协调者。
**小结一下:**集中式算法具有简单、易于实现的特点,但可用性、性能易受协调者影响。在可靠性和性能有一定保障的情况下,比如中央服务器计算能力强、性能高、故障率低,或者中央服务器进行了主备,主故障后备可以立马升为主,且数据可恢复的情况下,集中式算法可以适用于比较广泛的应用场景。
## 民主协商:分布式算法
既然引入协调者会带来一些问题,这时你可能就会想,不用协调者是否可以实现对临界资源的互斥访问呢?想象一下,当你需要使用自助咖啡机的时候,是不是可以先去征求其他人的意见,在确认其他人都没在使用也暂时不会使用咖啡机时,你就可以放心大胆地去泡制自己的咖啡了呢?
同理我们可以把这套算法用于分布式系统。当一个程序要访问临界资源时先向系统中的其他程序发送一条请求消息在接收到所有程序返回的同意消息后才可以访问临界资源。其中请求消息需要包含所请求的资源、请求者的ID以及发起请求的时间。
这,就是民主协商法。**在分布式领域中,我们称之为分布式算法,或者使用组播和逻辑时钟的算法。**
如图所示程序1、2、3需要访问共享资源A。在时间戳为8的时刻程序1想要使用资源A于是向程序2和3发起使用资源A的申请希望得到它们的同意。在时间戳为12的时刻程序3想要使用资源A于是向程序1和2发起访问资源A的请求。
<img src="https://static001.geekbang.org/resource/image/ef/8a/ef72edf91f407aa10d9def74ea66088a.jpg" alt="">
如图所示此时程序2暂时不访问资源A因此同意了程序1和3的资源访问请求。对于程序3来说由于程序1提出请求的时间更早因此同意程序1先使用资源并等待程序1返回同意消息。
<img src="https://static001.geekbang.org/resource/image/cb/68/cb9b513d9bcd9cf655a3a8df6ba11b68.jpg" alt="">
如图所示程序1接收到其他所有程序的同意消息之后开始使用资源A。当程序1使用完资源A后释放使用权限向请求队列中需要使用资源A的程序3发送同意使用资源的消息并将程序3从请求队列中删除。此时程序3收到了其他所有程序的同意消息获得了使用资源A的权限开始使用临界资源A的旅程。
<img src="https://static001.geekbang.org/resource/image/64/5f/64de3bf824a39c99aa3360af6bdcc55f.jpg" alt="">
从上述流程可以看出,一个程序完成一次临界资源的访问,需要进行如下的信息交互:
1. 向其他n-1个程序发送访问临界资源的请求总共需要n-1次消息交互
1. 需要接收到其他n-1个程序回复的同意消息方可访问资源总共需要n-1次消息交互。
可以看出一个程序要成功访问临界资源至少需要2*(n-1)次消息交互。假设现在系统中的n个程序都要访问临界资源则会同时产生2**n**(n-1)条消息。总结来说,**在大型系统中使用分布式算法,消息数量会随着需要访问临界资源的程序数量呈指数级增加,容易导致高昂的“沟通成本”。**
从上述分析不难看出,分布式算法根据“先到先得”以及“投票全票通过”的机制,让每个程序按时间顺序公平地访问资源,简单粗暴、易于实现。但,这个算法可用性很低,主要包括两个方面的原因:
- 当系统内需要访问临界资源的程序增多时,容易产生“信令风暴”,也就是程序收到的请求完全超过了自己的处理能力,而导致自己正常的业务无法开展。
- 一旦某一程序发生故障,无法发送同意消息,那么其他程序均处在等待回复的状态中,使得整个系统处于停滞状态,导致整个系统不可用。所以,相对于集中式算法的协调者故障,分布式算法的可用性更低。
**针对可用性低的一种改进办法是**,如果检测到一个程序故障,则直接忽略这个程序,无需再等待它的同意消息。这就好比在自助餐厅,一个人离开餐厅了,那你在使用咖啡机前,也无需征得他的同意。但这样的话,每个程序都需要对其他程序进行故障检测,这无疑带来了更大的复杂性。
因此,**分布式算法适合节点数目少且变动不频繁的系统且由于每个程序均需通信交互因此适合P2P结构的系统**。比如运行在局域网中的分布式文件系统具有P2P结构的系统等。
那么,**在我们工作中,什么样的场景适合采用分布式算法呢?**
Hadoop是我们非常熟悉的分布式系统其中的分布式文件系统HDFS的文件修改就是一个典型的应用分布式算法的场景。
如下图所示处于同一个局域网内的计算机1、2、3中都有同一份文件的备份信息且它们可以相互通信。这个共享文件就是临界资源。当计算机1想要修改共享的文件时需要进行如下操作
1. 计算机1向计算机2、3发送文件修改请求
1. 计算机2、3发现自己不需要使用资源因此同意计算机1的请求
1. 计算机1收到其他所有计算机的同意消息后开始修改该文件
1. 计算机1修改完成后向计算机2、3发送文件修改完成的消息并发送修改后的文件数据
1. 计算机2和3收到计算机1的新文件数据后更新本地的备份文件。
<img src="https://static001.geekbang.org/resource/image/cf/b3/cfe4a4b49d03781fab8e75c3904b09b3.jpg" alt="">
**归纳一下:**分布式算法是一个“先到先得”和“投票全票通过”的公平访问机制,但通信成本较高,可用性也比集中式算法低,适用于临界资源使用频度较低,且系统规模较小的场景。
## 轮值CEO令牌环算法
那么除了集中式算法、分布式算法以外还有什么方法可以实现分布式互斥吗答案是肯定的。毕竟方法总比问题多。华为独创的轮值CEO其实就给了我们一个很好的启示。在华为的轮值CEO体系里CEO就是临界资源同时只能有一个人担任由多名高管轮流出任CEO。
类似的程序访问临界资源问题也可按照轮值CEO的思路实现。 如下图所示,所有程序构成一个环结构,令牌按照顺时针(或逆时针)方向在程序之间传递,收到令牌的程序有权访问临界资源,访问完成后将令牌传送到下一个程序;若该程序不需要访问临界资源,则直接把令牌传送给下一个程序。
在分布式领域这个算法叫作令牌环算法也可以叫作基于环的算法。为了便于理解与记忆你完全可以把这个方法形象地理解为轮值CEO法。
<img src="https://static001.geekbang.org/resource/image/2d/cf/2de1f7015480ce4ac34cce85920df7cf.jpg" alt="">
因为在使用临界资源前,不需要像分布式算法那样挨个征求其他程序的意见了,所以相对而言,在令牌环算法里单个程序具有更高的通信效率。同时,在一个周期内,每个程序都能访问到临界资源,因此令牌环算法的公平性很好。
但是不管环中的程序是否想要访问资源都需要接收并传递令牌所以也会带来一些无效通信。假设系统中有100个程序那么程序1访问完资源后即使其它99个程序不需要访问也必须要等令牌在其他99个程序传递完后才能重新访问资源这就降低了系统的实时性。
综上,**令牌环算法非常适合通信模式为令牌环方式的分布式系统**,例如移动自组织网络系统。一个典型的应用场景就是无人机通信。
无人机在通信时,工作原理类似于对讲机,同一时刻只能发送信息或接收信息。因此,通信中的上行链路(即向外发送信息的通信渠道)是临界资源。
如下图所示,所有的无人机组成一个环,按照顺时针方向通信。每个无人机只知道其前一个发送信息的无人机,和后一个将要接收信息的无人机。拥有令牌的无人机可以向外发送信息,其他无人机只能接收数据。拥有令牌的无人机通信完成后,会将令牌传送给后一个无人机。
所有的无人机轮流通信并传输数据,从而消除了多个无人机对通信资源的争夺,使得每个无人机都能接收到其他无人机的信息,降低了通信碰撞导致的丢包率,保证了网络通信的稳定性,提高了多个无人机之间的协作效率。
<img src="https://static001.geekbang.org/resource/image/79/3a/7941b455807db8257fbf794f4967303a.jpg" alt="">
令牌环算法是一种更加公平的算法,通常会与通信令牌结合,从而取得很好的效果。特别是当系统支持广播或组播通信模式时,该算法更加高效、可行。
对于集中式和分布式算法都存在的单点故障问题在令牌环中若某一个程序例如上图的无人机2出现故障则直接将令牌传递给故障程序的下一个程序例如上图中无人机1直接将令牌传送给无人机3从而很好地解决单点故障问题提高系统的健壮性带来更好的可用性。但这就要求每个程序都要记住环中的参与者信息这样才能知道在跳过一个参与者后令牌应该传递给谁。
**小结一下:**令牌环算法的公平性高,在改进单点故障后,稳定性也很高,适用于系统规模较小,并且系统中每个程序使用临界资源的频率高且使用时间比较短的场景。
## 知识扩展:有适合大规模系统中的分布式互斥算法吗?
可以看到上面提到的集中式、分布式和令牌环3个互斥算法都不适用于规模过大、节点数量过多的系统。那么什么样的互斥算法适用于大规模系统呢
由于大规模系统的复杂性,我们很自然地想到要用一个相对复杂的互斥算法。时下有一个很流行的互斥算法,**两层结构的分布式令牌环算法,**把整个广域网系统中的节点组织成两层结构,可以用于节点数量较多的系统,或者是广域网系统。
我们知道广域网由多个局域网组成因此在该算法中局域网是较低的层次广域网是较高的层次。每个局域网中包含若干个局部进程和一个协调进程。局部进程在逻辑上组成一个环形结构在每个环形结构上有一个局部令牌T在局部进程间传递。局域网与局域网之间通过各自的协调进程进行通信这些协调进程同样组成一个环结构这个环就是广域网中的全局环。在这个全局环上有一个全局令牌在多个协调进程间传递。
## 总结
我首先结合生活中的案例带你剖析了什么是分布式互斥以及为什么需要分布式互斥。然后我和你介绍了3类典型的分布式互斥方法集中式算法、分布式算法以及令牌环算法并列举了对应的适用场景相信你通过今天的学习一定可以为你的场景选择一个合适的分布式互斥算法了加油
接下来,我把今天的内容通过下面的一张思维导图再全面总结下。
<img src="https://static001.geekbang.org/resource/image/42/c6/4210e133d9d94ea22917db55458c11c6.png" alt="">
## 课后问题
最后,我想请你思考以下两个问题:
1. 你认为集中式算法、分布式算法和令牌环算法,还有什么可以改进的地方吗?
1. 传统单机上的互斥方法,为什么不能用于分布式环境呢?
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!

View File

@@ -0,0 +1,150 @@
<audio id="audio" title="04 | 分布式选举:国不可一日无君" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/27/d46c30b2b3fa661a8a7028ee71386927.mp3"></audio>
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
相信你对集群的概念并不陌生。简单说,集群一般是由两个或两个以上的服务器组建而成,每个服务器都是一个节点。我们经常会听到数据库集群、管理集群等概念,也知道数据库集群提供了读写功能,管理集群提供了管理、故障恢复等功能。
接下来,你开始好奇了,对于一个集群来说,多个节点到底是怎么协同,怎么管理的呢。比如,数据库集群,如何保证写入的数据在每个节点上都一致呢?
也许你会说,这还不简单,选一个“领导”来负责调度和管理其他节点就可以了啊。
这个想法一点儿也没错。这个“领导”,在分布式中叫做主节点,而选“领导”的过程在分布式领域中叫作分布式选举。
然后,你可能还会问,怎么选主呢。那接下来,我们就一起去揭开这个谜底吧。
## 为什么要有分布式选举?
主节点,在一个分布式集群中负责对其他节点的协调和管理,也就是说,其他节点都必须听从主节点的安排。
主节点的存在,就可以保证其他节点的有序运行,以及数据库集群中的写入数据在每个节点上的一致性。这里的一致性是指,数据在每个集群节点中都是一样的,不存在不同的情况。
当然,如果主故障了,集群就会天下大乱,就好比一个国家的皇帝驾崩了,国家大乱一样。比如,数据库集群中主节点故障后,可能导致每个节点上的数据会不一致。
**这,就应了那句话“国不可一日无君”,对应到分布式系统中就是“集群不可一刻无主”**。总结来说,选举的作用就是选出一个主节点,由它来协调和管理其他节点,以保证集群有序运行和节点间数据的一致性。
## 分布式选举的算法
那么,如何在集群中选出一个合适的主呢?这是一个技术活儿,目前常见的选主方法有基于序号选举的算法( 比如Bully算法、多数派算法比如Raft算法、ZAB算法等。接下来就和我一起来看看这几种算法吧。
### 长者为大Bully算法
Bully算法是一种霸道的集群选主算法为什么说是霸道呢因为它的选举原则是“长者”为大即在所有活着的节点中选取ID最大的节点作为主节点。
在Bully算法中节点的角色有两种普通节点和主节点。初始化时所有节点都是平等的都是普通节点并且都有成为主的权利。但是当选主成功后有且仅有一个节点成为主节点其他所有节点都是普通节点。当且仅当主节点故障或与其他节点失去联系后才会重新选主。
Bully算法在选举过程中需要用到以下3种消息
- Election消息用于发起选举
- Alive消息对Election消息的应答
- Victory消息竞选成功的主节点向其他节点发送的宣誓主权的消息。
Bully算法选举的原则是“长者为大”意味着它的**假设条件是集群中每个节点均知道其他节点的ID。**在此前提下,其具体的选举过程是:
1. 集群中每个节点判断自己的ID是否为当前活着的节点中ID最大的如果是则直接向其他节点发送Victory消息宣誓自己的主权
1. 如果自己不是当前活着的节点中ID最大的则向比自己ID大的所有节点发送Election消息并等待其他节点的回复
1. 若在给定的时间范围内本节点没有收到其他节点回复的Alive消息则认为自己成为主节点并向其他节点发送Victory消息宣誓自己成为主节点若接收到来自比自己ID大的节点的Alive消息则等待其他节点发送Victory消息
1. 若本节点收到比自己ID小的节点发送的Election消息则回复一个Alive消息告知其他节点我比你大重新选举。
<img src="https://static001.geekbang.org/resource/image/91/54/91385c487255ba0179d8e9538ed8f154.png" alt="">
目前已经有很多开源软件采用了Bully算法进行选主比如MongoDB的副本集故障转移功能。MongoDB的分布式选举中采用节点的最后操作时间戳来表示ID时间戳最新的节点其ID最大也就是说时间戳最新的、活着的节点是主节点。
**小结一下**。Bully算法的选择特别霸道和简单谁活着且谁的ID最大谁就是主节点其他节点必须无条件服从。这种算法的优点是选举速度快、算法复杂度低、简单易实现。
但这种算法的缺点在于需要每个节点有全局的节点信息因此额外信息存储较多其次任意一个比当前主节点ID大的新节点或节点故障后恢复加入集群的时候都可能会触发重新选举成为新的主节点如果该节点频繁退出、加入集群就会导致频繁切主。
### 民主投票Raft算法
Raft算法是典型的多数派投票选举算法其选举机制与我们日常生活中的民主投票机制类似核心思想是“少数服从多数”。也就是说Raft算法中获得投票最多的节点成为主。
采用Raft算法选举集群节点的角色有3种
- **Leader**即主节点同一时刻只有一个Leader负责协调和管理其他节点
- **Candidate**即候选者每一个节点都可以成为Candidate节点在该角色下才可以被选为新的Leader
- **Follower**Leader的跟随者不可以发起选举。
Raft选举的流程可以分为以下几步
1. 初始化时所有节点均为Follower状态。
1. 开始选主时所有节点的状态由Follower转化为Candidate并向其他节点发送选举请求。
1. 其他节点根据接收到的选举请求的先后顺序,回复是否同意成为主。这里需要注意的是,在每一轮选举中,一个节点只能投出一张票。
1. 若发起选举请求的节点获得超过一半的投票则成为主节点其状态转化为Leader其他节点的状态则由Candidate降为Follower。Leader节点与Follower节点之间会定期发送心跳包以检测主节点是否活着。
1. 当Leader节点的任期到了即发现其他服务器开始下一轮选主周期时Leader节点的状态由Leader降级为Follower进入新一轮选主。
节点的状态迁移如下所示图中的term指的是选举周期
<img src="https://static001.geekbang.org/resource/image/fc/b8/fc0f00a3b7c9290bc91cb4d8721dc6b8.png" alt="">
请注意,**每一轮选举,每个节点只能投一次票。**这种选举就类似人大代表选举正常情况下每个人大代表都有一定的任期任期到后会触发重新选举且投票者只能将自己手里唯一的票投给其中一个候选者。对应到Raft算法中选主是周期进行的包括选主和任值两个时间段选主阶段对应投票阶段任值阶段对应节点成为主之后的任期。但也有例外的时候如果主节点故障会立马发起选举重新选出一个主节点。
Google开源的Kubernetes擅长容器管理与调度为了保证可靠性通常会部署3个节点用于数据备份。这3个节点中有一个会被选为主其他节点作为备。Kubernetes的选主采用的是开源的etcd组件。而etcd的集群管理器etcds是一个高可用、强一致性的服务发现存储仓库就是采用了Raft算法来实现选主和一致性的。
**小结一下。**Raft算法具有选举速度快、算法复杂度低、易于实现的优点缺点是它要求系统内每个节点都可以相互通信且需要获得过半的投票数才能选主成功因此通信量大。该算法选举稳定性比Bully算法好这是因为当有新节点加入或节点故障恢复后会触发选主但不一定会真正切主除非新节点或故障后恢复的节点获得投票数过半才会导致切主。
### 具有优先级的民主投票ZAB算法
ZABZooKeeper Atomic Broadcast选举算法是为ZooKeeper实现分布式协调功能而设计的。相较于Raft算法的投票机制ZAB算法增加了通过节点ID和数据ID作为参考进行选主节点ID和数据ID越大表示数据越新优先成为主。相比较于Raft算法ZAB算法尽可能保证数据的最新性。所以ZAB算法可以说是对Raft算法的改进。
使用ZAB算法选举时集群中每个节点拥有3种角色
- **Leader**,主节点;
- **Follower**,跟随者节点;
- **Observer**,观察者,无投票权。
选举过程中集群中的节点拥有4个状态
- **Looking状态**即选举状态。当节点处于该状态时它会认为当前集群中没有Leader因此自己进入选举状态。
- **Leading状态**即领导者状态表示已经选出主且当前节点为Leader。
- **Following状态**即跟随者状态集群中已经选出主后其他非主节点状态更新为Following表示对Leader的追随。
- **Observing状态**即观察者状态表示当前节点为Observer持观望态度没有投票权和选举权。
投票过程中,每个节点都有一个唯一的三元组(server_id, server_zxID, epoch)其中server_id表示本节点的唯一IDserver_zxID表示本节点存放的数据ID数据ID越大表示数据越新选举权重越大epoch表示当前选取轮数一般用逻辑时钟表示。
ZAB选举算法的核心是“少数服从多数ID大的节点优先成为主”因此选举过程中通过(vote_id, vote_zxID)来表明投票给哪个节点其中vote_id表示被投票节点的IDvote_zxID表示被投票节点的服务器zxID。**ZAB算法选主的原则是server_zxID最大者成为Leader若server_zxID相同则server_id最大者成为Leader。**
接下来我以3个Server的集群为例此处每个Server代表一个节点与你介绍ZAB选主的过程。
第一步当系统刚启动时3个服务器当前投票均为第一轮投票即epoch=1且zxID均为0。此时每个服务器都推选自己并将选票信息&lt;epoch, vote_id, vote_zxID&gt;广播出去。
<img src="https://static001.geekbang.org/resource/image/2f/29/2fddb05e71c14c7af437e9a5d558dc29.png" alt="">
第二步根据判断规则由于3个Server的epoch、zxID都相同因此比较server_id较大者即为推选对象因此Server 1和Server 2将vote_id改为3更新自己的投票箱并重新广播自己的投票。
<img src="https://static001.geekbang.org/resource/image/25/57/25a37bb2edb2894dc7f4ea6fe2cce757.png" alt="">
第三步此时系统内所有服务器都推选了Server 3因此Server 3当选Leader处于Leading状态向其他服务器发送心跳包并维护连接Server1和Server2处于Following状态。
<img src="https://static001.geekbang.org/resource/image/ee/c8/ee3612f641c037021595e383eb5336c8.png" alt="">
**小结一下**。ZAB算法性能高对系统无特殊要求采用广播方式发送信息若节点中有n个节点每个节点同时广播则集群中信息量为n*(n-1)个消息容易出现广播风暴且除了投票还增加了对比节点ID和数据ID这就意味着还需要知道所有节点的ID和数据ID所以选举时间相对较长。但该算法选举稳定性比较好当有新节点加入或节点故障恢复后会触发选主但不一定会真正切主除非新节点或故障后恢复的节点数据ID和节点ID最大且获得投票数过半才会导致切主。
### 三种选举算法的对比分析
好了我已经带你理解了分布式选举的3种经典算法即Bully算法、Raft算法和ZAB算法。那么接下来我就从消息传递内容、选举机制和选举过程的维度对这3种算法进行一个对比分析以帮助你理解记忆。
<img src="https://static001.geekbang.org/resource/image/e4/7e/e411f24b0b03991ad761134dfc3dff7e.jpg" alt="">
## 知识扩展:为什么“多数派”选主算法通常采用奇数节点,而不是偶数节点呢?
多数派选主算法的核心是少数服从多数,获得投票多的节点胜出。想象一下,如果现在采用偶数节点集群,当两个节点均获得一半投票时,到底应该选谁为主呢?
答案是,在这种情况下,无法选出主,必须重新投票选举。但即使重新投票选举,两个节点拥有相同投票数的概率也会很大。因此,多数派选主算法通常采用奇数节点。
也是大家通常看到ZooKeeper、 etcd、Kubernetes等开源软件选主均采用奇数节点的一个关键原因。
## 总结
今天我首先与你讲述了什么是分布式选举以及为什么需要分布式选举。然后我和你介绍了实现分布式选举的3种方法Bully算法、Raft算法以及ZooKeeper中的ZAB算法并通过实例与你展示了各类方法的选举流程。
我将今天的主要内容总结为了如下所示的思维导图,来帮助你加深理解与记忆。
<img src="https://static001.geekbang.org/resource/image/04/bd/04dfd1e4b8a1558fcbfa1bb8a9b077bd.png" alt="">
## 思考题
1. 分布式选举和一致性的关系是什么?
1. 你是否见到过一个集群中存在双主的场景呢?
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!

View File

@@ -0,0 +1,157 @@
<audio id="audio" title="05 | 分布式共识:存异求同" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6c/e2/6ca70efd3bee316865873e1e7ecdf2e2.mp3"></audio>
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
首先,我们来回忆下上篇文章的分布式选举。分布式选举问题,是从多个节点中选出一个主节点,相关的选举方法几乎都有一个共同特点:每个节点都有选举权和被选举权。大部分选举方法采用多数策略,也就是说一个节点只有得到了大部分节点的同意或认可才能成为主节点,然后主节点向其他节点宣告主权。
其实,这个选主过程就是一个分布式共识问题,因为每个节点在选出主节点之前都可以认为自己会成为主节点,也就是说集群节点“存异”;而通过选举的过程选出主节点,让所有的节点都认可该主节点,这叫“求同”。由此可见,**分布式共识的本质就是“存异求同”。**
所以,**从本质上看,分布式选举问题,其实就是传统的分布式共识方法,主要是基于多数投票策略实现的。**基于多数投票策略的分布式选举方法,如果用于分布式在线记账一致性问题中,那么记账权通常会完全掌握到主节点的手里,这使得主节点非常容易造假,且存在性能瓶颈。因此,分布式选举不适用于分布式在线记账的一致性问题。在今天这篇文章中,我就带你了解另外一种用于解决分布式在线记账一致性问题的分布式共识技术。
这里所说的分布式在线记账,是指在没有集中的发行方,也就是没有银行参与的情况下,任意一台接入互联网的电脑都能参与买卖,所有看到该交易的服务器都可以记录这笔交易,并且记录信息最终都是一致的,以保证交易的准确性。而如何保证交易的一致性,就是该场景下的分布式共识问题。
接下来,我们就一起学习下分布式共识技术吧。
## 什么是分布式共识?
假设现在有5台服务器分散在美国华盛顿、英国伦敦、法国巴黎、中国北京、中国上海分别对应着用户{A,B,C,D,E}。现在用户A给用户B转了100元。
在传统方法中我们通过银行进行转账并记录该笔交易。但分布式在线记账方法中没有银行这样的一个集中方而是由上述5台服务器来记录该笔交易。但是这5台服务器均是有各自想法的个体都可以自主操作或记录那么如何保证记录的交易是一致的呢就是分布式共识技术要解决的问题。
可以看出,**分布式共识就是在多个节点均可独自操作或记录的情况下,使得所有节点针对某个状态达成一致的过程。**通过共识机制,我们可以使得分布式系统中的多个节点的数据达成一致。
看到这里,相信你已经看出来了,我在这里说的分布式在线记账,就是近几年比较火的区块链技术解决的问题。而分布式共识技术,就是区块链技术共识机制的核心。
接下来,请和我一起看看分布式共识是如何实现的,有哪些方法吧。
## 分布式共识方法
为了不影响你理解分布式共识的核心技术,我会先和你分享区块链中的一个核心概念:挖矿。
在传统的交易方式中用户A给用户B转账需要银行来实行具体的转账操作并记录交易银行会从中收取相应的手续费。而采用分布式在线记账的话参与记录这笔交易的服务器也可以从中获得一些奖励这些奖励在区块链技术中可以换成钱。所有服务器帮助记录交易并达成一致的过程就是区块链中的“挖矿”。
区块链是一种链式数据结构,由包含交易信息的区块通过哈希指针、根据时间顺序连接而成,也是一种分布式数据库。区块是区块链的主要组成部分,每个区块由区块头和区块内容数据构成。区块头记录了时间戳,并用于保证区块链的连接性;区块内容数据中包含了多条交易信息。如果你对区块链技术的其他概念感兴趣的话,可以自行查阅更多资料。
接下来我将与你介绍3种主流的解决分布式在线记账一致性问题的共识技术PoWProof-of-Work工作量证明、PoSProof-of-Stake权益证明和DPoSDelegated Proof of Stake委托权益证明
### PoW
从分布式选举问题可以看出,同一轮选举中有且仅有一个节点成为主节点。同理,在分布式在线记账问题中,针对同一笔交易,有且仅有一个节点或服务器可以获得记账权,然后其他节点或服务器同意该节点或服务器的记账结果,达成一致。
也就是说,**分布式共识包括两个关键点,获得记账权和所有节点或服务器达成一致**。
**PoW算法**,是以每个节点或服务器的计算能力(即“算力”)来竞争记账权的机制,因此是一种**使用工作量证明机制的共识算法**。也就是说,谁的计算力强、工作能力强,谁获得记账权的可能性就越大。
那么,如何体现节点的“算力”呢?答案就是,每个节点都去解一道题,谁能先解决谁的能力就强。
假设每个节点会划分多个区块用于记录用户交易PoW算法获取记账权的原理是利用区块的index、前一个区块的哈希值、交易的时间戳、区块数据和nonce值通过SHA256哈希算法计算出一个哈希值并判断前k个值是否都为0。如果不是则递增nonce值重新按照上述方法计算如果是则本次计算的哈希值为要解决的题目的正确答案。谁最先计算出正确答案谁就获得这个区块的记账权。
**请注意**nonce值是用来找到一个满足哈希值的数字k为哈希值前导零的个数标记了计算的难度0越多计算难度越大。
达成共识的过程,就是获得记账权的节点将该区块信息广播给其他节点,其他节点判断该节点找到的区块中的所有交易都是有效且之前未存在过的,则认为该区块有效,并接受该区块,达成一致。
接下来,**我以上文提到的分散在世界各地的5台服务器为例和你说明基于PoW的共识记账过程。**
假设客户端A产生一个新的交易基于PoW的共识记账过程为
- 客户端A产生新的交易向全网进行广播要求对交易进行记账。
- 每个记账节点接收到这个请求后,将收到的交易信息放入一个区块中。
- 每个节点通过PoW算法计算本节点的区块的哈希值尝试找到一个具有足够工作量难度的工作量证明。
<img src="https://static001.geekbang.org/resource/image/77/05/77a1cbcd3830acf730f3a0a820710205.png" alt="">
- 若节点D找到了一个工作量证明向全网广播。当然当且仅当包含在该区块中的交易都是有效且之前未存在过的其他节点才会认同该区块的有效性。
- 其他节点接收到广播信息后,若该区块有效,接受该区块,并跟随在该区块的末尾,制造新区块延长该链条,将被接受的区块的随机哈希值视为新区块的随机哈希值。
可以看出PoW算法中谁的计算能力强获得记账权的可能性就越大。但必须保证其记账的区块是有效的并在之前未存在过才能获得其他节点的认可。
目前比特币平台采用了PoW算法属于区块链1.0阶段其重心在于货币比特币大约10min 才会产生一个区块,区块的大小也只有 1MB仅能够包含 30004000 笔交易,平均每秒只能够处理 5~7个位数笔交易。
PoW通过“挖矿”的方式发行新币把比特币分散给个人实现了相对的公平。PoW的容错机制允许全网50%的节点出错因此如果要破坏系统则需要投入极大成本若你有全球51%的算力,则可尝试攻击比特币)。
PoW机制每次达成共识需要全网共同参与运算增加了每个节点的计算量并且如果题目过难会导致计算时间长、资源消耗多而如果题目过于简单会导致大量节点同时获得记账权冲突多。这些问题都会增加达成共识的时间。
所以PoW机制的缺点也很明显共识达成的周期长、效率低资源消耗大。
### PoS
为了解决PoW算法的问题引入了PoS算法。它的核心原理是由系统权益代替算力来决定区块记账权拥有的权益越大获得记账权的概率就越大。
这里所谓的权益就是每个节点占有货币的数量和时间而货币就是节点所获得的奖励。PoS算法充分利用了分布式在线记账中的奖励鼓励“利滚利”。
在股权证明PoS模式下根据你持有货币的数量和时间给你发利息。每个币每天产生1币龄比如你持有100个币总共持有了50天那么你的币龄就为5000。这个时候如果你发现了一个PoS区块你的币龄就会被减少365。每被减少365币龄你就可以从区块中获得0.05个币的利息(可理解为年利率5%)。
在这个案例中,利息 = 5000*5% /365 = 0.68个币。这下就有意思了,持币有利息。
**基于PoS算法获得区块记账权的方法与基于PoW的方法类似不同之处在于**节点计算获取记账权的方法不一样PoW是利用区块的index、前一个区块的哈希值、交易的时间戳、区块数据和nonce值通过SHA256哈希算法计算出一个哈希值并判断前k个值是否都为0而PoS是根据节点拥有的股权或权益进行计算的。
接下来我们看一个具体的案例。假设一个公链网络中共有3个节点A 、B和C。其中 A 节点拥有10000 个币总共持有30天而 B 和 C 节点分别有 1000 和 2000 个币分别持有15和20天。
通过PoS算法决定区块记账权的流程和PoW算法类似唯一不同的就是每个节点在计算自己记账权的时候通过计算自己的股权或权益来评估如果发现自己权益最大则将自己的区块广播给其他节点当然必须保证该区块的有效性。
<img src="https://static001.geekbang.org/resource/image/be/db/be44bacadba9a47b8491156e646d59db.png" alt="">
以太坊平台属于区块链2.0阶段在区块链1.0的基础上进一步强调了合约采用了PoS算法。12年发布的点点币PPC综合了PoW工作量证明及PoS权益证明方式从而在安全和节能方面实现了创新。
可以看出PoS将算力竞争转变成权益竞争。与PoW相比PoS不需要消耗大量的电力就能够保证区块链网络的安全性同时也不需要在每个区块中创建新的货币来激励记账者参与当前网络的运行这也就在一定程度上缩短了达成共识所需要的时间。所以基于PoS算法的以太坊每秒大概能处理 30 笔左右的交易。
PoS算法中持币越多或持币越久币龄就会越高持币人就越容易挖到区块并得到激励而持币少的人基本没有机会这样整个系统的安全性实际上会被持币数量较大的一部分人掌握容易出现垄断现象。
### DPoS
为了解决PoS算法的垄断问题2014年比特股BitShares的首席开发者丹尼尔 · 拉里默Dan Larimer提出了委托权益证明法也就是DPoS算法。
DPoS算法的原理类似股份制公司的董事会制度普通股民虽然拥有股权但进不了董事会他们可以投票选举代表受托人代他们做决策。DPoS是由被社区选举的可信帐户受托人比如得票数排行前101位来拥有记账权。
为了成为正式受托人用户要去社区拉票获得足够多的信任。用户根据自己持有的货币数量占总量的百分比来投票好比公司股票机制假设总的发行股票为1000现在股东A持股10那么股东A投票权为10/1000=1/100。如下图所示根据自己拥有的权益投票选出可代表自己的受托节点受托节点之间竞争记账权。
<img src="https://static001.geekbang.org/resource/image/dc/6d/dc2fde94ff17bc317fc755c2c7184c6d.png" alt="">
在DPos算法中通常会选出k(比如101)个受托节点,它们的权利是完全相等的。受托节点之间争取记账权也是根据算力进行竞争的。只要受托节点提供的算力不稳定,计算机宕机或者利用手中的权力作恶,随时可以被握着货币的普通节点投票踢出整个系统,而后备的受托节点可以随时顶上去。
DPoS在比特股和Steem上已运行多年整个网络中选举出的多个节点能够在 1s 之内对 99.9% 的交易进行确认。此外DPoS在EOSEnterprise Operation System为商用分布式应用设计的一款区块链操作系统中也有广泛应用被称为区块链3.0阶段。
DPoS是在PoW和PoS的基础上进行改进的相比于PoS算法DPoS引入了受托人优点主要表现在
- 由投票选举出的若干信誉度更高的受托人记账解决了所有节点均参与竞争导致消息量大、达成一致的周期长的问题。也就是说DPoS能耗更低具有更快的交易速度。
- 每隔一定周期会调整受托人,避免受托人造假和独权。
但是在DPoS中由于大多数持币人通过受托人参与投票投票的积极性并不高且一旦出现故障节点DPoS无法及时做出应对导致安全隐患。
### 三种分布式共识算法对比分析
好了现在我们已经理解了PoW、PoS和DPoS这3种分布式共识算法。接下来为了方便你理解与记忆我把这三种算法放在一起做下对比如下图所示。
<img src="https://static001.geekbang.org/resource/image/b2/29/b2a43f0e0239f083a2c89db7bce2f729.jpg" alt="">
## 知识扩展:一致性与共识的区别是什么?
在平常使用中,我们通常会混淆一致性和共识这两个概念,接下来我就为你分析下这两个概念吧。
**一致性**是指在分布式系统中,针对同一数据或状态以多个副本形式保存在不同节点上;当对某个数据或状态副本做出修改后,能保证多副本达到对外表现的数据一致性。
**共识**是指一个或多个进程提议某些修改后,采用一种大家认可的方法,使得系统中所有进程对该修改达成一致意见,该方法称为共识机制。
也就是说,共识重点在于达成一致的过程或方法,一致性问题在于最终对外表现的结果。
## 总结
今天我和你介绍了分布式在线记账问题中的3种常见共识算法PoW、PoS和DPoS。
PoW算法 以每个节点或服务器的计算能力即“算力”来竞争记账权的机制。类似于按劳分配谁工作量大谁拿的多。其实竞争的就是挖矿设备看谁的挖矿设备的CPU、GPU等更厉害缺点就是费电、污染环境。
PoS算法由系统权益代替算力来决定区块记账权拥有的权益越大获得记账权的概率就越大。这种方法的优点是节能不需要挖矿了但缺点是容易形成垄断。
DPoS算法是一种委托权益证明算法。持有币的人可以通过投票选举出一些节点来作为代表去记账类似于全国人民代表大会制度。
讲到这里我还希望你明确区块链中的共识技术并没那么难和神秘常用的算法就是PoW、PoS和DPoS。希望通过这篇文章你能对共识技术有一定的了解能勇敢、自信地去探索分布式共识技术和区块链技术。
最后,我再用思维导图概括一下今天的内容。
<img src="https://static001.geekbang.org/resource/image/be/8d/be023c879a365df13578c6979b5d498d.png" alt="">
## 思考题
你能描述出拜占庭将军问题是什么吗?你认为可以如何解决拜占庭将军的容错问题呢?
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!

View File

@@ -0,0 +1,260 @@
<audio id="audio" title="06 | 分布式事务All or nothing" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b0/9a/b0dfd7a26b72bd08e039ef34b9468a9a.mp3"></audio>
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
对于网上购物的每一笔订单来说,电商平台一般都会有两个核心步骤:一是订单业务采取下订单操作,二是库存业务采取减库存操作。
通常这两个业务会运行在不同的机器上甚至是运行在不同区域的机器上。针对同一笔订单当且仅当订单操作和减库存操作一致时才能保证交易的正确性。也就是说一笔订单只有这两个操作都完成才能算做处理成功否则处理失败充分体现了“All or nothing”的思想。
在分布式领域中,这个问题就是分布式事务问题。那么今天,我们就一起打卡分布式事务吧。
## 什么是分布式事务?
在介绍分布式事务之前,我们首先来看一下什么是事务。
事务Transaction提供一种机制将包含一系列操作的工作序列纳入到一个不可分割的执行单元。只有所有操作均被正确执行才能提交事务任意一个操作失败都会导致整个事务回滚Rollback到之前状态即所有操作均被取消。简单来说事务提供了一种机制使得工作要么全部都不做要么完全被执行即all or nothing。
通常情况下我们所说的事务指的都是本地事务也就是在单机上的事务。而事务具备四大基本特征ACID具体含义如下。
- **A原子性Atomicity**),即事务最终的状态只有两种,全部执行成功和全部不执行,不会停留在中间某个环节。若处理事务的任何一项操作不成功,就会导致整个事务失败。一旦操作失败,所有操作都会被取消(即回滚),使得事务仿佛没有被执行过一样。就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
- **C一致性Consistency**是指事务操作前和操作后数据满足完整性约束数据库保持一致性状态。比如用户A和用户B在银行分别有800元和600元总共1400元用户A给用户B转账200元分为两个步骤从A的账户扣除200元和对B的账户增加200元。一致性就是要求上述步骤操作后最后的结果是用户A还有600元用户B有800元总共1400元而不会出现用户A扣除了200元但用户B未增加的情况该情况用户A和B均为600元总共1200元
- **I隔离性Isolation**,是指当系统内有多个事务并发执行时,多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。
- **D持久性Durability**,也被称为永久性,是指一个事务被执行后,那么它对数据库所做的更新就永久地保存下来了。即使发生系统崩溃或宕机等故障,重新启动数据库系统后,只要数据库能够重新被访问,那么一定能够将其恢复到事务完成时的状态。就像消费者在网站上的购买记录,即使换了手机,也依然可以查到。
只有在数据操作请求满足上述四个特性的条件下,存储系统才能保证处于正确的工作状态。因此,无论是在传统的集中式存储系统还是在分布式存储系统中,任何数据操作请求都必须满足 ACID 特性。
**分布式事务,就是在分布式系统中运行的事务,由多个本地事务组合而成。**在分布式场景下,对事务的处理操作可能来自不同的机器,甚至是来自不同的操作系统。文章开头提到的电商处理订单问题,就是典型的分布式事务。
分布式事务由多个事务组成因此基本满足ACID其中的C是强一致性也就是所有操作均执行成功才提交最终结果以保证数据一致性或完整性。但随着分布式系统规模不断扩大复杂度急剧上升达成强一致性所需时间周期较长限定了复杂业务的处理。为了适应复杂业务出现了BASE理论该理论的一个关键点就是采用最终一致性代替强一致性。我会在“知识扩展”模块与你详细展开BASE理论这部分内容。
介绍完什么是事务和分布式事务,以及它们的基本特征后,就进入“怎么做”的阶段啦。所以接下来,我们就看看如何实现分布式事务吧。
## 如何实现分布式事务?
实际上分布式事务主要是解决在分布式环境下组合事务的一致性问题。实现分布式事务有以下3种基本方法
- 基于XA协议的二阶段提交协议方法
- 三阶段提交协议方法;
- 基于消息的最终一致性方法。
其中基于XA协议的二阶段提交协议方法和三阶段提交协议方法采用了强一致性遵从ACID。基于消息的最终一致性方法采用了最终一致性遵从BASE理论。下面我将带你一起学习这三种方法。
### 基于XA协议的二阶段提交方法
XA是一个分布式事务协议规定了事务管理器和资源管理器接口。因此XA协议包括事务管理器和本地资源管理器两个部分。
**XA实现分布式事务的原理就类似于我在**[**第3讲**](https://time.geekbang.org/column/article/141772)**中与你介绍的集中式算法**事务管理器相当于协调者负责各个本地资源的提交和回滚而资源管理器就是分布式事务的参与者通常由数据库实现比如Oracle、DB2等商业数据库都实现了XA接口。
基于 XA协议的二阶段提交方法中二阶段提交协议Two-phase Commit Protocol2PC用于保证分布式系统中事务提交时的数据一致性是XA在全局事务中用于协调多个资源的机制。
那么,**两阶段提交协议如何保证分布在不同节点上的分布式事务的一致性呢**?为了保证它们的一致性,我们需要引入一个协调者来管理所有的节点,并确保这些节点正确提交操作结果,若提交失败则放弃事务。接下来,我们看看两阶段提交协议的具体过程。
两阶段提交协议的执行过程分为投票Voting和提交Commit两个阶段。
首先,我们看一下**第一阶段投票**在这一阶段协调者Coordinator即事务管理器会向事务的参与者Cohort即本地资源管理器发起执行操作的CanCommit请求并等待参与者的响应。参与者接收到请求后会执行请求中的事务操作将操作信息记录到事务日志中但不提交即不会修改数据库中的数据待参与者执行成功则向协调者发送“Yes”消息表示同意操作若不成功则发送“No”消息表示终止操作。
当所有的参与者都返回了操作结果Yes或No消息**系统进入了第二阶段提交阶段**也可以称为执行阶段。在提交阶段协调者会根据所有参与者返回的信息向参与者发送DoCommit提交或DoAbort取消指令。具体规则如下
- 若协调者从参与者那里收到的都是“Yes”消息则向参与者发送“DoCommit”消息。参与者收到“DoCommit”消息后完成剩余的操作比如修改数据库中的数据并释放资源整个事务过程中占用的资源然后向协调者返回“HaveCommitted”消息
- 若协调者从参与者收到的消息中包含“No”消息则向所有参与者发送“DoAbort”消息。此时投票阶段发送“Yes”消息的参与者则会根据之前执行操作时的事务日志对操作进行回滚就好像没有执行过请求操作一样然后所有参与者会向协调者发送“HaveCommitted”消息
- 协调者接收到来自所有参与者的“HaveCommitted”消息后就意味着整个事务结束了。
接下来,**我以用户A要在网上下单购买100件T恤为例重点与你介绍下单操作和减库存操作这两个操作**,帮助你加深对二阶段提交协议的理解。
第一阶段订单系统中将与用户A有关的订单数据库锁住准备好增加一条关于用户A购买100件T恤的信息并将同意消息“Yes”回复给协调者。而库存系统由于T恤库存不足出货失败因此向协调者回复了一个终止消息“No”。
<img src="https://static001.geekbang.org/resource/image/8a/6a/8a880c358c5f1a1fe9c8cc8179d6b56a.png" alt="">
第二阶段由于库存系统操作不成功因此协调者就会向订单系统和库存系统发送“DoAbort”消息。订单系统接收到“DoAbort”消息后将系统内的数据退回到没有用户A购买100件T恤的版本并释放锁住的数据库资源。订单系统和库存系统完成操作后向协调者发送“HaveCommitted”消息表示完成了事务的撤销操作。
至此用户A购买100件T恤这一事务已经结束用户A购买失败。
<img src="https://static001.geekbang.org/resource/image/bd/5c/bd73d10eb000ee554a448d169344f95c.png" alt="">
由上述流程可以看出,**二阶段提交的算法思路可以概括为**:协调者向参与者下发请求事务操作,参与者接收到请求后,进行相关操作并将操作结果通知协调者,协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作。
虽然基于XA的二阶段提交算法尽量保证了数据的强一致性而且实现成本低但依然有些不足。主要有以下三个问题
- **同步阻塞问题**二阶段提交算法在执行过程中所有参与节点都是事务阻塞型的。也就是说当本地资源管理器占有临界资源时其他资源管理器如果要访问同一临界资源会处于阻塞状态。因此基于XA的二阶段提交协议不支持高并发场景。
- **单点故障问题:**该算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
- **数据不一致问题:**在提交阶段当协调者向所有参与者发送“DoCommit”请求时如果发生了局部网络异常或者在发送提交请求的过程中协调者发生了故障就会导致只有一部分参与者接收到了提交请求并执行提交操作但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。
### 三阶段提交方法
三阶段提交协议Three-phase Commit Protocol3PC是对二阶段提交2PC的改进。为了更好地处理两阶段提交的同步阻塞和数据不一致问题**三阶段提交引入了超时机制和准备阶段**。
- 与2PC只是在协调者引入超时机制不同3PC同时在协调者和参与者中引入了超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应就会根据当前的状态选择提交或者终止整个事务从而减少了整个集群的阻塞时间在一定程度上减少或减弱了2PC中出现的同步阻塞问题。
- 在第一阶段和第二阶段中间引入了一个准备阶段或者说把2PC的投票阶段一分为二也就是在提交阶段之前加入了一个预提交阶段。在预提交阶段尽可能排除一些不一致的情况保证在最后提交之前各参与节点的状态是一致的。
三阶段提交协议就有CanCommit、PreCommit、DoCommit三个阶段下面我们来看一下这个三个阶段。
**第一CanCommit阶段。**
协调者向参与者发送请求操作CanCommit请求询问参与者是否可以执行事务提交操作然后等待参与者的响应参与者收到CanCommit请求之后回复Yes表示可以顺利执行事务否则回复No。
3PC的CanCommit阶段与2PC的Voting阶段相比
- 类似之处在于协调者均需要向参与者发送请求操作CanCommit请求询问参与者是否可以执行事务提交操作然后等待参与者的响应。参与者收到CanCommit请求之后回复Yes表示可以顺利执行事务否则回复No。
- 不同之处在于在2PC中在投票阶段若参与者可以执行事务会将操作信息记录到事务日志中但不提交并返回结果给协调者。但在3PC中在CanCommit阶段参与者仅会判断是否可以顺利执行事务并返回结果。而操作信息记录到事务日志但不提交的操作由第二阶段预提交阶段执行。
CanCommit阶段不同节点之间的事务请求成功和失败的流程如下所示。
<img src="https://static001.geekbang.org/resource/image/56/7c/56fe63b378ed63d24e318af22419bb7c.png" alt="">
当协调者接收到所有参与者回复的消息后进入预提交阶段PreCommit阶段
**第二PreCommit阶段。**
协调者根据参与者的回复情况来决定是否可以进行PreCommit操作预提交阶段
<li>
如果所有参与者回复的都是“Yes”那么协调者就会执行事务的预执行
</li>
<li>
协调者向参与者发送PreCommit请求进入预提交阶段。
</li>
<li>
参与者接收到PreCommit请求后执行事务操作并将Undo和Redo信息记录到事务日志中。
</li>
<li>
如果参与者成功执行了事务操作则返回ACK响应同时开始等待最终指令。
</li>
<li>
假如任何一个参与者向协调者发送了“No”消息或者等待超时之后协调者都没有收到参与者的响应就执行中断事务的操作
</li>
<li>
协调者向所有参与者发送“Abort”消息。
</li>
<li>
参与者收到“Abort”消息之后或超时后仍未收到协调者的消息执行事务的中断操作。
</li>
预提交阶段,不同节点上事务执行成功和失败的流程,如下所示。
<img src="https://static001.geekbang.org/resource/image/4e/45/4edc9b1a1248825d62db5b9107b09045.png" alt="">
预提交阶段保证了在最后提交阶段DoCmmit阶段之前所有参与者的状态是一致的。
**第三DoCommit阶段。**
DoCmmit阶段进行真正的事务提交根据PreCommit阶段协调者发送的消息进入执行提交阶段或事务中断阶段。
<li>
**执行提交阶段:**
</li>
<li>
若协调者接收到所有参与者发送的Ack响应则向所有参与者发送DoCommit消息开始执行阶段。
</li>
<li>
参与者接收到DoCommit消息之后正式提交事务。完成事务提交之后释放所有锁住的资源并向协调者发送Ack响应。
</li>
<li>
协调者接收到所有参与者的Ack响应之后完成事务。
</li>
<li>
**事务中断阶段:**
</li>
<li>
协调者向所有参与者发送Abort请求。
</li>
<li>
参与者接收到Abort消息之后利用其在PreCommit阶段记录的Undo信息执行事务的回滚操作释放所有锁住的资源并向协调者发送Ack消息。
</li>
<li>
协调者接收到参与者反馈的Ack消息之后执行事务的中断并结束事务。
</li>
执行阶段不同节点上事务执行成功和失败(事务中断)的流程,如下所示。
<img src="https://static001.geekbang.org/resource/image/7e/0f/7e332feee4fb6b6fd67689b66cc2610f.png" alt="">
3PC协议在协调者和参与者均引入了超时机制。即当参与者在预提交阶段向协调者发送 Ack消息后如果长时间没有得到协调者的响应在默认情况下参与者会自动将超时的事务进行提交从而减少整个集群的阻塞时间在一定程度上减少或减弱了2PC中出现的同步阻塞问题。
但三阶段提交仍然存在数据不一致的情况比如在PreCommit阶段部分参与者已经接受到ACK消息进入执行阶段但部分参与者与协调者网络不通导致接收不到ACK消息此时接收到ACK消息的参与者会执行任务未接收到ACK消息且网络不通的参与者无法执行任务最终导致数据不一致。
### 基于分布式消息的最终一致性方案
2PC和3PC核心思想均是以集中式的方式实现分布式事务这两种方法都存在两个共同的缺点一是同步执行性能差二是数据不一致问题。为了解决这两个问题通过分布式消息来确保事务最终一致性的方案便出现了。
在eBay的分布式系统架构中架构师解决一致性问题的核心思想就是将需要分布式处理的事务通过消息或者日志的方式异步执行消息或日志可以存到本地文件、数据库或消息队列中再通过业务规则进行失败重试。这个案例就是使用**基于分布式消息的最终一致性方案**解决了分布式事务的问题。
基于分布式消息的最终一致性方案的事务处理引入了一个消息中间件在本案例中我们采用Message QueueMQ消息队列用于在多个应用之间进行消息传递。实际使用中阿里就是采用RocketMQ 机制来支持消息事务。
基于消息中间件协商多个节点分布式事务执行操作的示意图,如下所示。
<img src="https://static001.geekbang.org/resource/image/9c/30/9c48c611124574c64806f45f62f8b130.png" alt="">
仍然以网上购物为例。假设用户A在某电商平台下了一个订单需要支付50元发现自己的账户余额共150元就使用余额支付支付成功之后订单状态修改为支付成功然后通知仓库发货。
在该事件中,涉及到了订单系统、支付系统、仓库系统,这三个系统是相互独立的应用,通过远程服务进行调用。
<img src="https://static001.geekbang.org/resource/image/f6/45/f687a6a05dac8e974a4dac04e1ce1a45.png" alt="">
根据基于分布式消息的最终一致性方案用户A通过终端手机首先在订单系统上操作通过消息队列完成整个购物流程。然后整个购物的流程如下所示。
<img src="https://static001.geekbang.org/resource/image/d9/a4/d9b2d32660e49a4ea613871337b570a4.png" alt="">
1. 订单系统把订单消息发给消息中间件,消息状态标记为“待确认”。
1. 消息中间件收到消息后,进行消息持久化操作,即在消息存储系统中新增一条状态为“待发送”的消息。
1. 消息中间件返回消息持久化结果(成功/失败),订单系统根据返回结果判断如何进行业务操作。失败,放弃订单,结束(必要时向上层返回失败结果);成功,则创建订单。
1. 订单操作完成后,把操作结果(成功/失败)发送给消息中间件。
1. 消息中间件收到业务操作结果后,根据结果进行处理:失败,删除消息存储中的消息,结束;成功,则更新消息存储中的消息状态为“待发送(可发送)”,并执行消息投递。
1. 如果消息状态为“可发送”则MQ会将消息发送给支付系统表示已经创建好订单需要对订单进行支付。支付系统也按照上述方式进行订单支付操作。
1. 订单系统支付完成后会将支付消息返回给消息中间件中间件将消息传送给订单系统。若支付失败则订单操作失败订单系统回滚到上一个状态MQ中相关消息将被删除若支付成功则订单系统再调用库存系统进行出货操作操作流程与支付系统类似。
在上述过程中,可能会产生如下异常情况,其对应的解决方案为:
1. 订单消息未成功存储到MQ中则订单系统不执行任何操作数据保持一致
1. MQ成功将消息发送给支付系统或仓库系统但是支付系统或仓库系统操作成功的ACK消息回传失败由于通信方面的原因导致订单系统与支付系统或仓库系统数据不一致此时MQ会确认各系统的操作结果删除相关消息支付系统或仓库系统操作回滚使得各系统数据保持一致
1. MQ成功将消息发送给支付系统或仓库系统但是支付系统或仓库系统操作成功的ACK消息回传成功订单系统操作后的最终结果成功或失败未能成功发送给MQ此时各系统数据可能不一致MQ也需确认各系统的操作结果若数据一致则更新消息若不一致则回滚操作、删除消息。
基于分布式消息的最终一致性方案采用消息传递机制,并使用异步通信的方式,避免了通信阻塞,从而增加系统的吞吐量。同时,这种方案还可以屏蔽不同系统的协议规范,使其可以直接交互。
在不需要请求立即返回结果的场景下, 这些特性就带来了明显的通信优势,并且通过引入消息中间件,实现了消息生成方(如上述的订单系统)本地事务和消息发送的原子性,采用最终一致性的方式,只需保证数据最终一致即可,一定程度上解决了二阶段和三阶段方法要保证强一致性而在某些情况导致的数据不一致问题。
可以看出,分布式事务中,当且仅当所有的事务均成功时整个流程才成功。所以,**分布式事务的一致性是实现分布式事务的关键问题,目前来看还没有一种很简单、完美的方案可以应对所有场景。**
### 三种实现方式对比
现在,为了方便你理解并记忆这三种方法,我总结了一张表格,从算法一致性、执行方式、性能等角度进行了对比:
<img src="https://static001.geekbang.org/resource/image/9c/2b/9c789a486aa8df6d9d12182b953a862b.jpg" alt="">
## 知识扩展:刚性事务与柔性事务
在讨论事务的时候,我们经常会提到刚性事务与柔性事务,但却很难区分这两种事务。所以,今天的知识扩展内容,我就来和你说说什么是刚性事务、柔性事务,以及两者之间有何区别?
- 刚性事务遵循ACID原则具有强一致性。比如数据库事务。
- 柔性事务,其实就是根据不同的业务场景使用不同的方法实现最终一致性,也就是说我们可以根据业务的特性做部分取舍,容忍一定时间内的数据不一致。
总结来讲与刚性事务不同柔性事务允许一定时间内数据不一致但要求最终一致。而柔性事务的最终一致性遵循的是BASE理论。
那,**什么是BASE理论**呢?
eBay 公司的工程师 Dan Pritchett曾提出了一种分布式存储系统的设计模式——BASE理论。 BASE理论包括基本可用Basically Available、柔性状态Soft State和最终一致性Eventual Consistency
- 基本可用分布式系统出现故障的时候允许损失一部分功能的可用性保证核心功能可用。比如某些电商618大促的时候会对一些非核心链路的功能进行降级处理。
- 柔性状态:在柔性事务中,允许系统存在中间状态,且这个中间状态不会影响系统整体可用性。比如,数据库读写分离,写库同步到读库(主库同步到从库)会有一个延时,其实就是一种柔性状态。
- 最终一致性:事务在操作过程中可能会由于同步延迟等问题导致不一致,但最终状态下,所有数据都是一致的。
BASE理论为了支持大型分布式系统通过牺牲强一致性保证最终一致性来获得高可用性是对ACID原则的弱化。ACID 与 BASE 是对一致性和可用性的权衡所产生的不同结果但二者都保证了数据的持久性。ACID 选择了强一致性而放弃了系统的可用性。与ACID原则不同的是BASE理论保证了系统的可用性允许数据在一段时间内可以不一致最终达到一致状态即可也即牺牲了部分的数据一致性选择了最终一致性。
具体到今天的三种分布式事务实现方式二阶段提交、三阶段提交方法遵循的是ACID原则而消息最终一致性方案遵循的就是BASE理论。
## 总结
我从事务的ACID特性出发介绍了分布式事务的概念、特征以及如何实现分布式事务。在关于如何实现分布式的部分我以网购为例与你介绍了常见的三种实现方式即基于XA协议的二阶段提交方法三阶段方法以及基于分布式消息的最终一致性方法。
二阶段和三阶段方法是维护强一致性的算法它们针对刚性事务实现的是事务的ACID特性。而基于分布式消息的最终一致性方案更适用于大规模分布式系统它维护的是事务的最终一致性遵循的是BASE理论因此适用于柔性事务。
在分布式系统的设计与实现中,分布式事务是不可或缺的一部分。可以说,没有实现分布式事务的分布式系统,不是一个完整的分布式系统。分布式事务的实现过程看似复杂,但将方法分解剖析后,你就会发现分布式事务的实现是有章可循的。
我将实现分布式事务常用的三个算法整理为了一张思维导图,以帮助你加深理解与记忆。
<img src="https://static001.geekbang.org/resource/image/3d/13/3dabbddf3eab0297c2d154245ccb3c13.png" alt="">
## 思考题
你觉得分布式互斥与分布式事务之间的关系是什么呢?
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!

View File

@@ -0,0 +1,184 @@
<audio id="audio" title="07 | 分布式锁:关键重地,非请勿入" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b3/31/b353ee1f35dcef3a732080acc99be231.mp3"></audio>
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
我在[第3篇文章](https://time.geekbang.org/column/article/141772)中,与你一起学习了分布式互斥,领悟了其“有你没我,有我没你”的精髓,为你解释了同一临界资源同一时刻只能被一个程序访问的问题,并介绍了解决分布式互斥的算法。
不知道你有没有发现一个细节,在之前介绍的算法中,我主要讲了如何协调多个进程获取权限和根据权限有序访问共享资源,“获得访问权限的进程可以访问共享资源,其他进程必须等待拥有该权限的进程释放权限”。但是,我并没有介绍在访问共享资源时,这个权限是如何设置或产生的,以及设置或产生这个权限的工作原理是什么。
那么,在本讲,我就将带你一起打卡分布式锁,去学习分布式锁是如何解决这个问题的。
## 为什么要使用分布锁?
首先,我先带你认识一下什么是锁。
在单机系统中,经常会有多个线程访问同一种资源的情况,我们把这样的资源叫做共享资源,或者叫做临界资源。为了维护线程操作的有效性和正确性,我们需要某种机制来减少低效率的操作,避免同时对相同数据进行不一样的操作,维护数据的一致性,防止数据丢失。也就是说,我们需要一种互斥机制,按照某种规则对多个线程进行排队,依次、互不干扰地访问共享资源。
这个机制指的是,为了实现分布式互斥,在某个地方做个**标记**,这个标记每个线程都能看到,到标记不存在时可以设置该标记,当标记被设置后,其他线程只能等待拥有该标记的线程执行完成,并释放该标记后,才能去设置该标记和访问共享资源。这里的标记,就是我们常说的**锁**。
也就是说,锁是多线程同时访问同一资源的场景下,为了让线程互不干扰地访问共享资源,从而保证操作的有效性和正确性的一种标记。
与普通锁不同的是,**分布式锁**是指分布式环境下系统部署在多个机器中实现多进程分布式互斥的一种锁。为了保证多个进程能看到锁锁被存在公共存储比如Redis、Memcached、数据库等三方存储中以实现多个进程并发访问同一个临界资源同一时刻只有一个进程可访问共享资源确保数据的一致性。
那什么场景下需要使用分布式锁呢?
比如现在某电商要售卖某大牌吹风机以下简称“吹风机”库存只有2个但有5个来自不同地区的用户{A,B,C,D,E}几乎同时下单那么这2个吹风机到底会花落谁家呢
你可能会想,这还不简单,谁先提交订单请求,谁就购买成功呗。但实际业务中,为了高并发地接收大量用户订单请求,很少有电商网站真正实施这么简单的措施。
此外,对于订单的优先级,不同电商往往采取不同的策略,比如有些电商根据下单时间判断谁可以购买成功,而有些电商则是根据付款时间来判断。但,无论采用什么样的规则去判断谁能购买成功,都必须要保证吹风机售出时,数据库中更新的库存是正确的。为了便于理解,我在下面的讲述中,以下单时间作为购买成功的判断依据。
我们能想到的最简单方案就是,给吹风机的库存数加一个锁。当有一个用户提交订单后,后台服务器给库存数加一个锁,根据该用户的订单修改库存。而其他用户必须等到锁释放以后,才能重新获取库存数,继续购买。
在这里,吹风机的库存就是共享资源,不同的购买者对应着多个进程,后台服务器对共享资源加的锁就是告诉其他进程“**关键重地,非请勿入**”。
但问题就这样解决了吗?当然没这么简单。
想象一下用户A想买1个吹风机用户B想买2个吹风机。在理想状态下用户A网速好先买走了1个库存还剩下1个此时应该提示用户B库存不足用户B购买失败。但实际情况是用户A和用户B同时获取到商品库存还剩2个用户A买走1个在用户A更新库存之前用户B又买走了2个此时用户B更新库存商品还剩0个。这时电商就头大了总共2个吹风机却卖出去了3个。
不难看出,如果只使用单机锁将会出现不可预知的后果。因此,在高并发场景下,为了保证临界资源同一时间只能被一个进程使用,从而确保数据的一致性,我们就需要引入分布式锁了。
此外,在大规模分布式系统中,单个机器的线程锁无法管控多个机器对同一资源的访问,这时使用分布式锁,就可以把整个集群当作一个应用一样去处理,实用性和扩展性更好。
## 分布式锁的三种实现方法及对比
接下来我带你看看实现分布式锁的3种主流方法
- 基于数据库实现分布式锁,这里的数据库指的是关系型数据库;
- 基于缓存实现分布式锁;
- 基于ZooKeeper实现分布式锁。
### 基于数据库实现分布式锁
实现分布式锁最直接的方式通过数据库进行实现,首先创建一张表用于记录共享资源信息,然后通过操作该表的数据来实现共享资源信息的修改。
当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。数据库对共享资源做了唯一性约束,如果有多个请求被同时提交到数据库的话,数据库会保证只有一个操作可以成功,操作成功的那个线程就获得了访问共享资源的锁,可以进行操作。
基于数据库实现的分布式锁是最容易理解的。但是因为数据库需要落到硬盘上频繁读取数据库会导致IO开销大因此这种分布式锁**适用于并发量低,对性能要求低的场景**。对于双11、双12等需求量激增的场景数据库锁是无法满足其性能要求的。而在平日的购物中我们可以在局部场景中使用数据库锁实现对资源的互斥访问。
下面我们还是以电商售卖吹风机的场景为例。吹风机库存是2个有3个来自不同地区的用户{A,B,C}想要购买其中用户A想买1个用户B想买2个用户C想买1个。
用户A和用户B几乎同时下单但用户A的下单请求最先到达服务器。因此该商家的产品数据库中增加了一条关于用户A的记录用户A获得了锁他的订单请求被处理服务器修改吹风机库存数减去1后还剩下1个。
当用户A的订单请求处理完成后有关用户A的记录被删除服务器开始处理用户B的订单请求。这时库存只有1个了无法满足用户B的订单需求因此用户B购买失败。
从数据库中删除用户B的记录服务器开始处理用户C的订单请求库存中1个吹风机满足用户C的订单需求。所以数据库中增加了一条关于用户C的记录用户C获得了锁他的订单请求被处理服务器修改吹风机数量减去1后还剩下0个。
<img src="https://static001.geekbang.org/resource/image/f5/aa/f58d1ef2d7896a9da85dbbe98f8de9aa.png" alt="">
可以看出,**基于数据库实现分布式锁比较简单,绝招在于创建一张锁表,为申请者在锁表里建立一条记录,记录建立成功则获得锁,消除记录则释放锁。**该方法依赖于数据库,主要有两个缺点:
- **单点故障问题**。一旦数据库不可用,会导致整个系统崩溃。
- **死锁问题**。数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。倘若已获得共享资源访问权限的进程突然挂掉、或者解锁操作失败,使得锁记录一直存在数据库中,无法被删除,而其他进程也无法获得锁,从而产生死锁现象。
### 基于缓存实现分布式锁
数据库的性能限制了业务的并发量那么对于双11、双12等需求量激增的场景是否有解决方法呢
基于缓存实现分布式锁的方式,非常适合解决这种场景下的问题。**所谓基于缓存也就是说把数据存放在计算机内存中不需要写入磁盘减少了IO读写。**接下来我以Redis为例与你展开这部分内容。
Redis通常可以使用setnx(key, value)函数来实现分布式锁。key和value就是基于缓存的分布式锁的两个属性其中key表示锁idvalue = currentTime + timeOut表示当前时间+超时时间。也就是说某个进程获得key这把锁后如果在value的时间内未释放锁系统就会主动释放锁。
setnx函数的返回值有0和1
- 返回1说明该服务器获得锁setnx将key对应的value设置为当前时间 + 锁的有效时间。
- 返回0说明其他服务器已经获得了锁进程不能进入临界区。该服务器可以不断尝试setnx操作以获得锁。
我还是以电商售卖吹风机的场景为例,和你说明基于缓存实现的分布式锁,假设现在库存数量是足够的。
用户A的请求因为网速快最先到达Server2setnx操作返回1并获取到购买吹风机的锁用户B和用户C的请求几乎同时到达了Server1和Server3但因为这时Server2获取到了吹风机数据的锁所以只能加入等待队列。
Server2获取到锁后负责管理吹风机的服务器执行业务逻辑只用了1s就完成了订单。订单请求完成后删除锁的key从而释放锁。此时排在第二顺位的Server1获得了锁可以访问吹风机的数据资源。但不巧的是Server1在完成订单后发生了故障无法主动释放锁。
于是排在第三顺位的Server3只能等设定的有效时间比如30分钟到期锁自动释放后才能访问吹风机的数据资源也就是说用户C只能到00:30:01以后才能继续抢购。
<img src="https://static001.geekbang.org/resource/image/a5/0c/a5565f3f58ce13d7ce2f9679af6e730c.png" alt="">
总结来说,**Redis通过队列来维持进程访问共享资源的先后顺序**。Redis锁主要基于setnx函数实现分布式锁当进程通过setnx&lt;key,value&gt;函数返回1时表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁或者等到时间超时才能获得锁。
相对于基于数据库实现分布式锁的方案来说,**基于缓存实现的分布式锁的优势**表现在以下几个方面:
- 性能更好。数据被存放在内存而不是磁盘避免了频繁的IO操作。
- 很多缓存可以跨集群部署,避免了单点故障问题。
- 使用方便。很多缓存服务都提供了可以用来实现分布式锁的方法比如Redis的setnx和delete方法等。
- 可以直接设置超时时间例如expire key timeout来控制锁的释放因为这些缓存服务器一般支持自动删除过期数据。
这个方案的不足是,通过超时时间来控制锁的失效时间,并不是十分靠谱,因为一个进程执行时间可能比较长,或受系统进程做内存回收等影响,导致时间超时,从而不正确地释放了锁。
为了解决基于缓存实现的分布式锁的这些问题我们再来看看基于ZooKeeper实现的分布式锁吧。
### 基于ZooKeeper实现分布式锁
ZooKeeper基于树形数据存储结构实现分布式锁来解决多个进程同时访问同一临界资源时数据的一致性问题。ZooKeeper的树形数据存储结构主要由4种节点构成
- 持久节点PERSISTENT。这是默认的节点类型一直存在于ZooKeeper中。
- 持久顺序节点PERSISTENT_SEQUENTIAL。在创建节点时ZooKeeper根据节点创建的时间顺序对节点进行编号命名。
- 临时节点EPHEMERAL。当客户端与Zookeeper连接时临时创建的节点。与持久节点不同当客户端与ZooKeeper断开连接后该进程创建的临时节点就会被删除。
- 临时顺序节点EPHEMERAL_SEQUENTIAL。就是按时间顺序编号的临时节点。
**根据它们的特征ZooKeeper基于临时顺序节点实现了分布锁。**
还是以电商售卖吹风机的场景为例。假设用户A、B、C同时在11月11日的零点整提交了购买吹风机的请求ZooKeeper会采用如下方法来实现分布式锁
1. 在与该方法对应的持久节点shared_lock的目录下为每个进程创建一个临时顺序节点。如下图所示吹风机就是一个拥有shared_lock的目录当有人买吹风机时会为他创建一个临时顺序节点。
1. 每个进程获取shared_lock目录下的所有临时节点列表注册Watcher用于监听子节点变更的信息。当监听到自己的临时节点是顺序最小的则可以使用共享资源。
1. 每个节点确定自己的编号是否是shared_lock下所有子节点中最小的若最小则获得锁。例如用户A的订单最先到服务器因此创建了编号为1的临时顺序节点LockNode1。该节点的编号是持久节点目录下最小的因此获取到分布式锁可以访问临界资源从而可以购买吹风机。
<li>若本进程对应的临时节点编号不是最小的,则分为两种情况:
<ol>
1. 本进程为读请求,如果比自己序号小的节点中有写请求,则等待;
1. 本进程为写请求,如果比自己序号小的节点中有请求,则等待。
例如用户B也想要买吹风机但在他之前用户C想看看吹风机的库存量。因此用户B只能等用户A买完吹风机、用户C查询完库存量后才能购买吹风机。
<img src="https://static001.geekbang.org/resource/image/b1/4f/b1404782160c8f79a19a9d289d73234f.png" alt="">
可以看到使用ZooKeeper实现的分布式锁可以解决前两种方法提到的各种问题比如单点故障、不可重入、死锁等问题。但该方法实现较复杂且需要频繁地添加和删除节点所以性能不如基于缓存实现的分布式锁。
### 三种实现方式对比
我通过一张表格来对比一下这三种方式的特点,以方便你理解、记忆。
<img src="https://static001.geekbang.org/resource/image/da/b9/daea1d41a6b72c288d292adf45ad4bb9.jpg" alt="">
值得注意的是,**这里的实现复杂性,是针对同样的分布式锁的实现复杂性,与之前提到的基于数据库的实现非常简易不一样。**
基于数据库实现的分布式锁存在单点故障和死锁问题仅仅利用数据库技术去解决单点故障和死锁问题是非常复杂的。而ZooKeeper已定义相关的功能组件因此可以很轻易地解决设计分布式锁时遇到的各种问题。所以说要实现一个完整的、无任何缺陷的分布式锁ZooKeeper是一个最简单的选择。
总结来说,**ZooKeeper分布式锁的可靠性最高有封装好的框架很容易实现分布式锁的功能并且几乎解决了数据库锁和缓存式锁的不足因此是实现分布式锁的首选方法。**
从上述分析可以看出,为了确保分布式锁的可用性,我们在设计时应考虑到以下几点:
- 互斥性,即在分布式系统环境下,对于某一共享资源,需要保证在同一时间只能一个线程或进程对该资源进行操作。
- 具备锁失效机制,防止死锁。即使出现进程在持有锁的期间崩溃或者解锁失败的情况,也能被动解锁,保证后续其他进程可以获得锁。
- 可重入性,即进程未释放锁时,可以多次访问临界资源。
- 有高可用的获取锁和释放锁的功能,且性能要好。
## 知识扩展:如何解决分布式锁的羊群效应问题?
在分布式锁问题中,会经常遇到羊群效应。
所谓羊群效应就是在整个ZooKeeper分布式锁的竞争过程中大量的进程都想要获得锁去使用共享资源。每个进程都有自己的“Watcher”来通知节点消息都会获取整个子节点列表使得信息冗余资源浪费。
当共享资源被解锁后Zookeeper会通知所有监听的进程这些进程都会尝试争取锁但最终只有一个进程获得锁使得其他进程产生了大量的不必要的请求造成了巨大的通信开销很有可能导致网络阻塞、系统性能下降。
**那如何解决这个问题呢?**具体方法可以分为以下三步。
1. 在与该方法对应的持久节点的目录下,为每个进程创建一个临时顺序节点。
1. 每个进程获取所有临时节点列表,对比自己的编号是否最小,若最小,则获得锁。
1. 若本进程对应的临时节点编号不是最小的则注册Watcher监听自己的上一个临时顺序节点当监听到该节点释放锁后获取锁。
## 总结
我以电商购物为例首先带你剖析了什么是分布式锁以及为什么需要分布式锁然后与你介绍了三种实现分布式锁的方法包括基于数据库实现、基于缓存实现以Redis为例以及基于ZooKeeper实现。
分布式锁是解决多个进程同时访问临界资源的常用方法在分布式系统中非常常见比如开源的ZooKeeper、Redis中就有所涉及。通过今天这篇文章对分布式锁原理及方法的讲解我相信你会发现分布式锁不再那么神秘、难懂然后以此为基础对分布式锁进行更深入的学习和应用。
接下来,我把今天的内容通过下面的一张思维导图再全面总结下。
<img src="https://static001.geekbang.org/resource/image/0a/87/0ac5f2ac38f1eb46cf5ad681d5153887.png" alt="">
## 思考题
分布式锁与分布式互斥的关系是什么呢?
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!

View File

@@ -0,0 +1,151 @@
<audio id="audio" title="08 | 分布式技术是如何引爆人工智能的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/64/eb/64af37922fbff2ccb613fb4b4d872aeb.mp3"></audio>
你好,我是聂鹏程。今天,我来继续带你打卡分布式核心技术。
通过前面课程,相信你已经对分布式的起源以及什么是分布式有了一定的了解,从用户留言来看很多同学对分布式技术、分布式技术的应用,以及分布式技术的重要性非常感兴趣。所以,我将以人工智能技术为例,带你了解分布式技术的应用及其重要性。
## 什么是人工智能?
2016年3月Google AlphaGo以4:1的比分赢得了世界围棋冠军李世石。这场围棋人机大战将人工智能技术推向了高潮。现在人工智能已经广泛渗透到了我们的生活中比如手机拍照美化、人脸识别、平安城市、自然语言处理、语音识别等。
那么,到底什么是人工智能呢?
所谓人工智能,其实就是希望机器能够模拟人的思维,像人一样智能。目前,对人工智能的定义大多可划分为四类,即机器**“像人一样思考”“像人一样行动”“理性地思考”和“理性地行动”。**这里的行动,指的是采取行动或制定行动的决策。
那人工智能是如何让机器像人那样智能呢?人并不是天生就会解决问题的,我们经常会听到一句经典的话“见多识广”,人遇到新的问题,是通过学习新知识,然后结合自己的经验去解决的。比如,人并不是生来就认识香蕉,而是通过后天的学习(包括学习香蕉的形状、颜色、口味等)来获取识别香蕉的经验,当下次再看到香蕉时,就知道这是香蕉了。
人工智能要模拟人的智能也类似,需要通过大量的数据进行学习和分析获得规律(即建立一个模型),然后利用该规律或模型对未知数据进行预测,以判断是否与建模数据具有相同特征。
从人工智能的定义可以看出,**数据、模型(也叫作算法)、算力**是人工智能的三大核心。在一定程度上可以说,数据决定了机器学习能达到的上限,模型提供了方法。因此**数据处理和模型训练是人工智能的关键技术,算力决定了数据处理和模型训练的实用性能,而分布式技术就是解决算力的不二妙招。**
接下来,我就对数据处理和模型训练进行具体分析,来帮助你了解人工智能中需要用到哪些分布式技术来解决算力问题。
## 数据处理
数据处理又称数据预处理,是指通过数据统计、数据集成、数据清理、数据规约、数据变换等方法,对数据缺失、数据噪声、数据冗余、多数据源等问题进行处理以得到高质量数据,为模型训练提供高质量输入,是人工智能不可缺少的环节。
其实,**数据处理类似于我们的知识整理过程。**一个精心打造的、体系化梳理过的专栏文章,可以帮助我们在学习一门课程时,少走弯路、避免踩雷、达到事半功倍的效果。同样地,一个精心处理过的数据集,对于人工智能的模型训练也能起到事半功倍的效果,一方面可以缩短机器学习的周期,另一方面也可以提高机器学习的质量。
所以接下来,我们就一起看看数据预处理的方法吧。
**数据统计Data Statistics。**数据统计是数据预处理的第一步,其范围、规模、方式等会直接影响数据分析的结果。常见的统计特征有最大值、最小值、均值、中位数、方差、标准差等。
**数据集成Data Integration。**数据的收集有多种途径,比如文件数据、数据库数据、问卷数据等,而不同的数据源,其数据的存储方式、命名规则、单位等不尽相同,所以我们需要数据集成来将多个数据源的数据整合到一起,以保证数据结构、属性的一致性,并去除冗余数据,方便后续分析。
**数据清理Data Cleaning。**由于用户忘记或设备损坏,经常会造成部分数据缺失;由于仪器故障或用户填写错误,经常会出现数据错误(噪声数据)等。如果不对这些数据做任何处理,后面的模型训练过程将产生严重偏差。数据清理过程就是用来解决这个问题的,它可以通过平均值或众数等来填充丢失值或修改这些噪声值。
**数据规约Data Reduction。**由于机器学习中的数据量很大因此会导致很多重复的特征或者很多不重要的特征比如ID号等。数据规约通常指通过主成分分析法 (Principal Component AnalysisPCA)、小波变换(Wavelet TransformWT)等方法去除重复特征及不重要的特征,从而减少数据的维度或者数据量,降低问题复杂度,同时不影响后面训练的结果。
**数据变换Data Conversion。**数据变化是指通过标准化、离散化和分层化等方法对数据进行集成、清理、规约等操作,使得数据更加一致、更加容易被模型处理。数据变换方法主要有数据标准化、数据离散化和数据泛化三类。
可以看出,**数据预处理虽然很复杂,但可以拆分成多个步骤进行**。对于小样本数据处理时,单台机器的处理能力就足够了,所以采用单台机器进行处理即可。但是对于大规模数据来说,单台机器的处理能力已成为瓶颈,此时,不得不需要分布式数据处理了。
目前业界已经有很多大数据处理软件比如分布式计算框架MapReduce、Spark分布式存储框架HDFS、HBase等来进行分布式数据处理。
>
备注:我会在专栏的“第三站:分布式计算技术”和“第五站:分布式数据存储”与你详细讲述这些框架。
接下来,我们再一起看看分布式如何助力模型训练。
## 分布式模型训练
在了解什么是分布式模型训练之前,我们先看一下什么是模型训练。
### 什么是分布式模型训练?
**模型训练就是从已知数据中找到规律**。具体来说就是,不断通过已有数据进行学习寻找规律,并进行验证增强,最终给出最适合的模型参数,并根据该模型参数对给定的未知数据进行预测。
比如有一堆橘子和西瓜,可以通过模型训练得到:大的、绿色的判定为西瓜,小的、黄色的判定为橘子。那么当给出一个未知数据时,我们通过它的大小及颜色信息就可以判断该水果是橘子还是西瓜。这就是模型训练。
其中大小和颜色属于预测的两个特征而它们的具体数值比如大于10厘米等颜色RGB的数值范围就是模型参数。
随着大数据时代的到来人工智能技术逐渐向大规模训练数据、大模型训练等方向发展。比如百度的Deep Speech 2系统使用了11940小时的语音数据以及超过200万句表述来训练英语的语音识别模型2011年谷歌训练出拥有十亿个参数的超大神经网络模型。很明显单台计算机的存储能力、计算能力已经不能满足了因此分布式模型训练诞生了。
研究表明,在具有 GPU加速卡的单机上采用**ImageNet 数据集,完成一次训练大概需要多达一周的时间。**这还仅仅只是一次训练迭代的时间,如果是比较严格的生产级业务,至少需要数十次迭代,训练累计时间将会达到数十周。试想一下,如果一个业务仅仅是模型训练就花费数十周,那么等到真正上线,恐怕最佳时间窗口也已经过去了。
在多台机器上的分布式训练无疑能极大减少训练时间,近期研究中,基于**ImageNet数据集**,采用包含**2048个GPU 的集群将训练时间降低到了4分钟**。TensorFlow是由Google开源且在业内非常流行的机器学习计算框架它的分布式版本利用了 GPU 加速服务器的虚拟化集群,将深度学习的**训练时间从数周缩短到数小时。**
总结来讲分布式训练可以大大提升训练效率大幅缩短训练时间从而缩短业务面市周期所以各大公司都在研究分布式训练比如华为、IBM、阿里巴巴等。
那,什么是分布式模型训练呢?
**分布式模型训练**是利用分布式集群,将多个计算机的存储能力、计算能力等进行统一管理和调度,从而实现模型训练。
可以看到,分布式模型训练的前提是有一个**分布式集群**,因此一个高效、可靠的分布式集群是基础。而这个分布式集群的**架构、选主、调度、可靠性**等关键技术,奠定了分布式模型训练的基础。
>
备注:关于分布式集群的架构和调度,我会在本专栏的“第二站:分布式资源管理与负载调度”进行详细讲解;关于集群的选主,我已经在本专栏的“第一站:分布式协调与同步”中进行了讲解;而关于可靠性,我会在在本专栏的“第六站:分布式高可靠”进行讲解。
好了,有了分布式集群作为基础,接下来,我们要考虑的就是如何进行分布式模型训练了。不同的场景,采用的分布式模型训练的方法也不一样,主要包括数据分布式训练、模型分布式训练和混合模型训练三类。
接下来,我将带你了解这三种分布式模型训练模式,并带你了解其中涉及的分布式技术。
### 数据分布式训练
数据分布式训练主要是针对大规模训练数据的场景。如下图所示,数据分布式训练是在每个节点(假设,一台服务器代表一个节点)上都存储或运行一个完整的模型训练程序的基础上,将大规模数据进行划分,然后将划分后的数据子集分配到多个节点上,每个节点根据自己接收到的数据进行训练。
<img src="https://static001.geekbang.org/resource/image/8c/c2/8c3197afef4ca27cc155df39fcf64dc2.jpg" alt="">
每个节点会根据自己拥有的数据子集训练出一个子模型,并按照一定的规则与其他节点通信,比如各节点向其他节点传递本节点的子模型参数或参数更新等信息,以有效整合来自各个节点的训练结果,来得到全局的机器学习模型。比如,每个节点训练一个子模型得到自己的参数,最终的模型为多个节点的参数取平均值。
可以看出,数据分布式有如下两个重要信息:
1. 数据需拆分存储到不同的节点进行训练,因此涉及了**数据的拆分方法、数据的分布式存储和管理**,其中数据拆分方法主要有两类,对训练样本进行划分和对每个样本的维度进行划分,这是非常基础的方法。目前,市面上大部分的书籍均有介绍,如果你感兴趣的话可以自行学习。
1. **节点之间需要通信交互信息。**分布式通信是实现任何分布式技术的底座,没有分布式通信技术,分布式模型训练犹如纸上谈兵。
>
备注:数据的分布式存储和管理,是数据分布式的基础,我会在“第五站:分布式数据存储”中与你详细讲述;而关于分布式通信的相关技术,我会在“第四站:分布式通信技术”与你介绍。
### 模型分布式训练
了解了数据分布式训练,我们再来看一下模型分布式训练。它针对的主要是大模型训练场景,在分布式领域中也被称为任务并行或任务分布式。
如下图所示模型分布式训练是指将大模型进行拆分然后将拆分后的子模型分配到不同的节点上进行训练。与数据分布式训练不同的是首先每个节点上只存储和运行部分模型训练程序而不是完整的模型训练程序其次各个子模型之间存在较强的依赖关系比如节点1的输出是节点2和节点3子模型的输入因此节点之间需要进行中间计算结果的通信。
<img src="https://static001.geekbang.org/resource/image/89/e4/89efd0455d8397a1331f546686ba35e4.jpg" alt="">
可以看出,模型分布式训练包含如下两个关键信息:
1. 大模型拆分为多个小模型,其本质是将大任务拆分为多个子任务,这其实就是**分而治之策略**。而子任务之间的拆分,需要运用包括**流水线、MapReduce等**在内的多种分布式计算模式。
1. 不同节点上的子任务之间,需要通过通信交互中间计算结果,涉及分布式通信技术。
>
备注:任务拆分和流水线等分布式计算模式是模型分布式训练不可缺少的技术,我会在“第三站:分布式计算技术”与你详细介绍;而关于分布式通信的相关技术,你可以移步“第四站:分布式通信技术”中进行了解。
### 混合模型训练
混合模型训练,主要是针对大规模训练数据和大模型训练共存的场景。
所谓混合模型训练就是将数据分布式训练和模型分布式训练结合起来。如下图所示假设有一个多GPU集群系统首先对模型进行拆分将子模型分配到单节点上不同的GPU然后对数据进行划分每个节点负责训练一部分数据最后进行模型参数同步得到全局参数和全局模型。
<img src="https://static001.geekbang.org/resource/image/f9/ef/f96f4bc09fd9f95ddf9610b21d94e2ef.jpg" alt="">
从混合模型训练的流程可以看出:
- 单节点或多节点实现模型并行或模型分布式训练,涉及模型拆分、并行与分布式计算模式等;
- 多节点之间实现了数据分布式训练,涉及数据的拆分方法和数据的分布式存储和管理等技术;
- 单节点之间的模型分布式训练,需要单节点上多进程之间通信;多节点之间的分布式训练需要跨节点跨进程通信。
>
备注:我会在“第三站:分布式计算技术”“第四站:分布式通信技术”和“第五站:分布式数据存储”模块中,与你讲述这其中涉及的分布式技术。
## 总结
讲完分布式模型训练,这节课就告一段落了。分布式训练可以将深度学习的训练时间从数周缩短到数小时,极大提升了训练效率,这充分说明了分布式技术的重要性。接下来,我们一起总结下今天的主要内容吧。
首先,我与你介绍了什么是人工智能,让你先对其有了一个整体的理解。
然后,我与你介绍了人工智能中的数据预处理方法,包括数据清理、数据统计、数据集成、数据规约、数据变换等,在这其中分布式数据预处理是处理大规模数据的一个很好的方式。
最后我与你介绍了分布式模型训练包括数据分布式训练、模型分布式训练和混合模型训练3种方法并介绍了其中涉及的关键分布式技术比如数据的分布式存储和管理、分布式通信等。没有这些关键的分布式技术分布式模型训练其实就是空谈了。
现在,我将人工智能中涉及的关键分布式技术整理为了一张表格,以方便你学习。
<img src="https://static001.geekbang.org/resource/image/3b/43/3b626dcb58d4cec1b405e3c24eed0943.jpg" alt="">
我是聂鹏程,感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再会!