分布式系统中的同步机制

共识问题( consensus problem )是在分布式系统中一组节点如何达成共识的问题。 - 共识可能是一个数值,一种行动或决定。共识允许一个分布式系统作为一个单一的实体,其中每一个节点知道并且遵从整个网络的一致的行动(Wikipedia)

 

如果数据副本分布在不同的节点之间,每个节点都可以进行读写操作,这就产生共识问题。 系统如何处理“并发”修改和版本控制,保证数据集最终收敛?

数据库厂商很久前就认识到有必要支持分区数据库,并引入了称为2PC(两阶段提交)的技术,提供跨多个数据库实例的数据严格一致性的保证。该协议是分成两个阶段:

首先,事务处理协调器要求所涉及的每个数据库预提交操作,并注明是否提交是可能的。如果所有数据库同意提交,然后可以进行第2阶段的开始。

事务协调要求每个数据库提交数据。如果提交的任何数据库否决,所有的数据库将还原各自的事务。

2PC的缺点是什么?它是一个阻塞协议。如果协调者出现永久性故障,一些同伙将永远没法解决自己的事务处理任务;在同伙将协议报文发送给协调器后,​​同伙将处于阻止状态,直到接收到提交或恢复的指令。我们通过阻塞协议获得了跨越分区的一致性。如果的布鲁尔是正确的,那么我们就必须牺牲可用性,为什么呢?

任何系统的可用性是操作所需的组件的可用性的乘积。 涉及两个数据库的2PC提交的的可用性将是每个数据库的可用性的乘积。例如如果我们假设每个数据库有99.9%的可用性,那么事务的可用性变成99.8%,或额外的停机时间为每月43分钟。

在某些系统的行为假设下, 在维护两阶段提交协议的优点的同时,有可能通过协议优化和协议操作而降低成本, 但是按照 布鲁尔定律, 2PC 不可能提供系统高的可用性。

 

另一种思路是避免使用同步锁定, 而使用类似RDBMS中的乐观锁(Optimistic Locking)的更加宽松的加锁机制。 乐观锁大多是基于数据版本( Version )记录机制来实现的。而数据版本的获得需要正确的 事件发生的时间顺序。 时钟同步(Clock Synchronization) 可以用来处理并发进程中的事件发生的时间顺序, 以保证在不同机器上运行的独立进程对系统内事件发生顺序的一致性的解读。

使用机器上的时钟来标记时间戳(Timestamps)似乎是解决事件发生时间顺序的一个明显方案。这种方案假定有一个全局性的时间钟,因此也叫做全局时间顺序(Global Time Ordering); 其本质是把不同机器上的事件顺序问题转化为不同时钟的同步问题。 然而,即使我们能够周期性的同步不同机器上的时钟, 不同时钟依然存在快慢问题; 而且时间戳不捕捉因果关系, 如果不同机器上事件发生的时间戳完全一致,无法找到一个一致性的决断标准来认定事件的先后顺序 。

 

为了解决这个问题,Lamport引入了逻辑时钟(Logical Clock)概念。逻辑时钟和物理时钟无关,记录了事件发生的序列值,用以分布式系统内的事件间的比较。

定义事件AB A → B 表示 A发生于B之前。 → 关系满足 传递性:A→B B→C =A → C .

如果A表示发出消息的时间值,B表示接受到该消息的时间值,AB之间存在因果关系,A必然发生在B之前,因此 A → B 必然成立。

 

创建可扩展性系统-4-2_系统架构 可扩展性

在图-1中每个事件被自己的进程赋予一个单调增加的时间戳,但是该时间戳在进程之间没法比较。在事件be 之间 b → e, 但是时间戳的值违反了→的定义。

 

 

Lamport 算法 解决了这个问题。

  1. 每个进程有一个逻辑时钟,对事件赋予一个单调增加的时间戳。

  2. 进程间传送的消息带有消息发送进程的发送消息时的时间戳T

  3. 如果接受进程的时间戳T' < T, 小于消息时间戳,调整 T' = T + 1

 

创建可扩展性系统-4-2_系统架构 可扩展性_02

 

Lamport 算法保证了,如果AB之间存在因果关系,A的逻辑时间小于B的逻辑时间。 但是如果AB之间没有因果关系,或者可以说是并发的关系, 在AB之间选择谁先发生并不重要, 只要系统内所有进程都做出一致的选择。在这种情况下, 我们可以直接使用属于各个进程的事件的时间戳; 这样带来一个问题, 事件的时间戳在进程内部是单调递增因而其值是唯一的,但是进程之间的时间戳的值并不能保证唯一性。 在这种情况下需要引入新的参数。简单的做法是考虑全局唯一性的进程ID,如果i表示进程IDTi 表示进程i Lamport时间戳, 那么 (Ti,i) < (Tj,j) 的充分必要条件是 Ti < Tj 或者 Ti = Tj 并且 i < j.

 

在实际应用中, 往往组合使用 线程ID,进程ID IP 地址来获得分布式系统全局唯一性的进程ID

 

 

创建可扩展性系统-4-2_系统架构 可扩展性_03

这样, 通过使用 lamport时间戳L(e) ,系统中所有的事件e 获得了完全的排序。

Lamport逻辑时间的局限是没法从 Lamport时间戳 推断 事件的因果关系。 如果事件 A → B , 那么 L(A) < L(B); 反之并不成立。 L(A) < L(B) 并不能得出 A → B

向量时钟解决了这个问题。向量时钟是在分布式系统中的获取更新事件顺序和因果关系的替代方案。一个具有N个进程的系统的向量时钟是含有N个整数的向量。

每个进程Pi维护一个向量时钟Vi。 向量时钟的更新规则如下

  1. 初始化 Vi[j]=0 I,j = [1,..,N]

  2. Pi的每一个事件, Vi[i] = Vi[i] + 1

  3. Pi 发出的每一个消息都带有 t = Vi

  4. Pj接受Pi发出的消息, Vj[k] = max(Vj[k], t[k]), k = [1...N]

 

并且定义ViVj 之间的比较关系如下

Vi=Vj 当且仅当 V[k] = V'[k] k = [1...N]

Vi <= Vj 当且仅当 V[k] <= V'[k] k = [1...N]

这样对于任何两个事件e, e',

  1. 如果 e → e' V(e)< V(e')

  2. 如果 V(e) < v(e') e → e'

  3. 如果 V(e)< V(e') V(e')< V(e) 都不成立,则 e e' 并发的。

 

 

创建可扩展性系统-4-2_系统架构 可扩展性_04

 

除了上面讨论的逻辑时钟,物理时钟也可以用于分布式系统中的事件发生时序。有专门的算法(用于绝对时钟同步的Cristian算法,相对时钟同步的Berkeley算法),和协议(NTP, SNTP)来解决物理时钟之间的同步问题。

再以AmazonDynamo为例, 为了支持写操作的高可用性, 在写操作时不考虑数据冲突,而在读操作时解决数据冲突问题。 在读操作过程中, 如果发现数据冲突,比如只有部分机器有最新的数据, Dynamo 使用 向量时钟记录的数据对象版本历史来解决冲突。如果最新数据是和老数据有 → 关系,系统返回最新数据; 否则返回所有数据给用户来解决冲突问题。