从在校大二开始到如今参加工作,接触了不少关于分布式的东西。但总是感觉分布式基础理论知识很含糊,不清晰。打算在这一周里梳理下相关的知识线路。
CAP理论
CAP理论又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
三个特性进行了如下归纳:
- 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。等同于所有节点访问同一份最新的数据副本。
- 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。对数据更新具备高可用性。
- 可扩展性/分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
CAP原理告诉我们,这三个因素最多只能满足两个,不可能三者兼顾。对于分布式系统来说,分区容错是基本要求,所以必然要放弃一致性。对于大型网站来说,分区容错和可用性的要求更高,所以一般都会选择适当放弃一致性。对应CAP理论,NoSQL追求的是AP,而传统数据库追求的是CA,这也可以解释为什么传统数据库的扩展能力有限的原因。
在CAP三者中,“可扩展性”是分布式系统的特有性质。分布式系统的设计初衷就是利用集群多机的能力处理单机无法解决的问题。当需要扩展系统性能时,一种做法是优化系统的性能或者升级硬件(scale up),一种做法就是“简单”的增加机器来扩展系统的规模(scale out)。好的分布式系统总在追求”线性扩展性”,即性能可以随集群数量增长而线性增长。
CAP定律其实也是衡量分布式系统的重要指标,另一个重要的指标是性能。
BASE理论
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写,BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
- 基本可用:基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用,举个例子:
- 响应时间上的损失:正常情况下,一个在线搜索引擎需要0.5秒内返回给用户相应的查询结果,但由于出现异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
- 功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
- 弱状态:也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
- 最终一致性:是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性
一致性模型
数据一致性来说,简单说有三种类型,当然,如果细分的话,还有很多一致性模型,如:顺序一致性,FIFO一致性,会话一致性,单读一致性,单写一致性等。下面介绍主要的三种一致性模型:
- Strong Consistency 强一致性:新的数据一旦写入,在任意副本任意时刻都能读到新值。比如:文件系统,RDBMS,Azure Table都是强一致性的。
- Eventually 最终一致性:当你写入一个新值后,有可能读不出来,但在某个时间窗口之后保证最终能读出来。比如:DNS,电子邮件、Amazon S3,Google搜索引擎这样的系统。
- Weak 弱一致性:当你写入一个新值后,读操作在数据副本上可能读出来,也可能读不出来。比如:某些cache系统,网络游戏其它玩家的数据和你没什么关系,VOIP这样的系统。
从这三种一致型的模型上来说,我们可以看到,Weak和Eventually一般来说是异步冗余的,而Strong一般来说是同步冗余的(多写),异步的通常意味着更好的性能,但也意味着更复杂的状态控制。同步意味着简单,但也意味着性能下降。
以及其他变体:
- Causal Consistency(因果一致性):如果Process A通知Process B它已经更新了数据,那么Process B的后续读取操作则读取A写入的最新值,而与A没有因果关系的C则可以最终一致性。
- Read-your-writes Consistency(读你所写一致性):如果Process A写入了最新的值,那么 Process A的后续操作都会读取到最新值。但是其它用户可能要过一会才可以看到。Facebook的数据同步就是采用这种原则。
- Session Consistency(会话一致性):一次会话内一旦读到某个值,不会读到更旧的值。
- Monotonic Read Consistency(单调一致性):一个用户一旦读到某个值,不会读到比这个值更旧的值,其他用户不一定。
Quorum W+R>N和vector clock
- N:复制的节点数,即一份数据被保存的份数为N
- W: 成功写操作的最小节点数 ,即每次写成功需要的份数。
- R: 成功读操作的最小节点数,即每次读取成功需要的份数
这三个因素决定了可用性,一致性和分区容错性。W+R>N可以保证数据的一致性(C),W越大数据一致性越高。这个NWR模型把CAP的选择权交给了用户,让用户自己在功能,性能和成本效益之间进行权衡。
配置的时候要求W+R > N。 因为W+R > N, 所以 R > N-W 这个是什么意思呢?就是读取的份数一定要比总备份数减去确保写成功的备份的差值要大。
对于一个分布式系统来说,N通常都大于3,也就说同一份数据需要保存在三个以上不同的节点上,以防止单点故障。W是成功写操作的最小节点数,这里的写成功可以理解为“同步”写,比如N=3,W=1,那么只要写成功一个节点就可以了,另外的两份数据是通过异步的方式复制的。R是成功读操作的最小节点数,读操作为什么要读多份数据呢?在分布式系统中,数据在不同的节点上可能存在着不一致的情况,继续往下看。
NWR模型的一些设置会造成脏数据的问题,因为这很明显不是像Paxos一样是一个强一致的东西,所以,可能每次的读写操作都不在同一个结点上,于是会出现一些结点上的数据并不是最新版本,但却进行了最新的操作。也就是说,如果你读出来数据的版本是v1,当你计算完成后要回填数据后,却发现数据的版本号已经被人更新成了v2,那么服务器就会拒绝你,这就是版本冲突问题。
于是,Amazon的Dynamo引入了Vector Clock(矢量钟)这个设计。这个设计让每个结点各自记录自己的版本信息,也就是说,对于同一个数据,需要记录两个事:1、谁更新的我,2、我的版本号是什么
看一个操作序列:
对节点A的D数据进行写操作。数据D会增加一个版本信息(A,1)。我们把这个时候的数据记做D1(A,1)。
然后继续对数据节点A的D数据进行写操作处,于是对节点A的D数据记录为D2(A,2)。这个时候,D2是可以覆盖D1的,不会有冲突产生。
现在我们假设D2传播到了所有节点(B和C),B和C收到的数据不是从客户产生的,而是别人复制给他们的,所以他们不产生新的版本信息,所以现在B和C所持有的数据还是D2(A,2)。于是A,B,C上的数据及其版本号都是一样的。
如果我们有一个新的写请求到了B结点上,于是B结点生成数据D3(A,2; B,1),意思是:数据D全局版本号为3,A升了两新,B升了一次。这就是所谓的代码版本的log
如果D3没有传播到C的时候又一个请求被C处理了,于是,以C结点上的数据是D4(A,2; C,1)
-
最精彩的事情来了:如果这个时候来了一个读请求,我们要记得,我们的W=1 那么R=N=3,所以R会从所有三个节点上读,此时,他会读到三个版本:
- A结点:D2(A,2)
- B结点:D3(A,2; B,1);
- C结点:D4(A,2; C,1)
这个时候可以判断出,D2已经是旧版本(已经包含在D3/D4中),可以舍弃。
但是D3和D4是明显的版本冲突。于是,交给调用方自己去做版本冲突处理。就像源代码版本管理一样。
Dynamo的配置用的是CAP里的A和P。Vector Clock(Version Vector)只能用于发现数据冲突,但是想要解决数据冲突还要留给用户去定夺(就好比git commit出现conflicts,需要手工解决一样),当然也可以设置某种策略来直接解决冲突(保留最新或集群内多数表决)。
lease机制
一般描述如下:
- Lease 是由授权者授予的在一段时间内的承诺。
- 授权者一旦发出 lease,则无论接受方是否收到,也无论后续接收方处于何种状态,只要 lease 不过期,授权者一定遵守承诺,按承诺的时间、内容执行。
- 接收方在有效期内可以使用颁发者的承诺,只要 lease 过期,接收方放弃授权,不再继续执行,要重新申请Lease。
- 可以通过版本号、时间周期,或者到某个固定时间点认为Lease证书失效
通俗解释一下:
- lease颁发过程只需要网络可以单向通信,同一个lease可以被颁发者不断重复向接受方发送。即使颁发者偶尔发送lease失败,颁发者也可以简单的通过重发的办法解决。
- 机器宕机对lease机制的影响不大。如果颁发者宕机,则宕机的颁发者通常无法改变之前的承诺,不会影响lease的正确性。在颁发者机恢复后,如果颁发者恢复出了之前的lease 信息,颁发者可以继续遵守lease的承诺。如果颁发者无法恢复lease信息,则只需等待一个最大的lease超时时间就可以使得所有的lease都失效,从而不破坏lease机制。
- lease机制依赖于有效期,这就要求颁发者和接收者的时钟是同步的。
- 如果颁发者的时钟比接收者的时钟慢,则当接收者认为lease已经过期的时候,颁发者依旧认为lease有效。接收者可以用在lease到期前申请新的lease的方式解决这个问题。
- 如果颁发者的时钟比接收者的时钟快,则当颁发者认为lease已经过期的时候,可能将lease颁发给其他节点,造成承诺失效,影响系统的正确性。对于这种时钟不同步,实践中的通常做法是将颁发者的有效期设置得比接收者的略大,只需大过时钟误差就可以避免对lease的有效性的影响。
即Lease是一种带期限的契约,在此期限内拥有Lease的节点有权利操作一些预设好的对象。从更深 层次上来看,Lease就是一把带有超时机制的分布式锁,如果没有Lease,分布式环境中的锁可能会因为锁拥有者的失败而导致死锁,有了lease死锁会被控制在超时时间之内。
一般的应用:
双主问题
如果有3副本A、B、C,并通过中心结点M来管理。A B C互主副本。其中A节点为primary,且同一时刻只能有一个primary节点处理方法是在每个副本与中心节点M中维护一个心跳,期望通过心跳是否存在而判断对方是否依旧存活。心跳方法其实根本无法解决分布式下的这个问题。考虑如下场景:
- M在某时刻未能预期收到主节点A的心跳,M认为A已经异常,于是从B、C中选取一个B作为主节点。但实际上A并未异常,而是由于网络瞬时阻塞、或是M本身出现异常使A这消息暂时未收到。这时,系统中出现A、B两个都是主节点的情况,称“双主”问题,从节点C可能同时从这两个主节点同步数据,这会引发很严重的数据错误。
lease(租期、承诺)机制就是用来解决这类问题的:
由中心节点向M其他节点发送lease,若某个节点持有有效的lease,则认为该节点正常可以提供服务。节点 A、B、C 依然周期性的发送heart beat报告自身状态,节点M收到heart beat后发送一个lease,表示节点M确认了节点 A、B、C 的状态,并允许节点在 lease 有效期内正常工作。节点M可以给 primary节点一个特殊的lease,表示节点可以作为primary工作。一旦节点M希望切换新的primary,则只需等前一个primary的lease过期,则就可以安全的颁发新的lease给新的primary节点,从而不会出现“双主”问题。实际工程实现时要考虑primary的lease过期的时间。
在实际系统中,若用一个中心节点发送lease也有很大的风险,一旦该中心节点宕机或网络异常,则所有的节点没有lease,从而造成系统高度不可用。为此,实际系统总是使用多个中心节点互为副本,成为一个小的集群,该小集群具有高可用性,对外颁发lease的功能。
读锁/写锁(分布式锁)
master和slave模型缓存系统中,其中master负责少量的读、所有的写和同步操作,slave负责读操作,如何保证读到缓存的一致性?ps(这种情况不适用于强一致性的应用)
当读请求到来时,m节点在向各s节点发送数据时同时向节点颁发一个lease,一旦真实时间超过这个时间点,则lease过期失效。在lease的有效期内,s保证不会修改对应数据的值。因此,节点收到数据和lease后,将数据加入本地Cache,一旦对应的lease超时,节点将对应的本地cache数据删除。m在修改数据时,首先阻塞所有写的读请求,并等待之前为该数据发出的所有lease超时过期,然后修改数据的值。之后重复上面的工作。
上述等lease失效的过程中,可能有新的请求请求到达,这时s又会继续颁发新的lease,使得lease一直不结束,形成“活锁”,即修改请求等待lease失效,而又源源不断颁发新lease而一直无法完成。此问题形成了“活锁”
- 解决活锁的办法:当有修改请求在等待着lease失效时,如果后续有读请求,则只返回请求数据而不颁发新lease,或者是只颁发目前最长的lease。
一致性哈希
传统hash(x) % N算法的弊端:不利于架构的伸缩性,我们为每个节点都增加一个备用节点,当某个节点失效时,就自动切换到备用节点上,类似于数据库的master和slave。但是依然无法解决增加或删除节点后,需要做hash重分布的问题,也就是无法动态增删节点。
这时就引入了一致性hash的概念 ,将所有的节点分布到一个hash环上,每个请求都落在这个hash环上的某个位置,只需要按照顺时针方向找到的第一个节点,就是自己需要的服务节点。当某个节点发生故障时,只需要在环上找到下一个可用节点即可。
虚拟节点:每个虚拟节点都有一个对应的物理节点,而每个物理节点可以对应若干个虚拟节点。并且这些虚拟节点连续的分配在环上,这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。
一篇不错的一致性哈希文章
敬请期待系列文章
- 2PC/3PC:分布式事务
- Paxos:强一致性协议
- Gossip协议:节点管理
- Raft
- 拜占庭将军问题
- ZAB协议
- MVCC
- 区块链