2 分布式系统原理
2.4 Quorum 机制
2.4.5 基于 Quorum 机制选择 primary
本节介绍一种介于quorum 机制选择 primary 的技术。回忆 2.2.2 节,基本 primary-secondary 协 议中,primary 负责进行更新操作的同步工作。现在基本 primary-secondary 协议中引入 quorum 机制,
即primary 成功更新 W 个副本(含 primary 本身)后向用户返回成功。读取数据时依照一致性要求的不 同可以有不同的做法:如果需要强一致性的立刻读取到最新的成功提交的数据,则可以简单的只读 取 primary 副本上的数据即可,也可以通过上节的方式读取;如果需要会话一致性,则可以根据之 前已经读到的数据版本号在各个副本上进行选择性读取;如果只需要弱一致性,则可以选择任意副 本读取。
在primary-secondary 协议中,当 primary 异常时,需要选择出一个新的 primary,之后 secondary 副本与primary 同步数据。通常情况下,选择新的 primary 的工作是由某一中心节点完成的,在引入 quorum 机制后,常用的 primary 选择方式与读取数据的方式类似,即中心节点读取 R 个副本,选择 R 个副本中版本号最高的副本作为新的 primary。新 primary 与至少 W 个副本完成数据同步后作为新 的primary 提供读写服务。首先,R 个副本中版本号最高的副本一定蕴含了最新的成功提交的数据。
再者,虽然不能确定最高版本号的数是一个成功提交的数据,但新的primary 在随后与 secondary 同 步数据,使得该版本的副本个数达到W,从而使得该版本的数据成为成功提交的数据。
例2.4.5:在 N=5,W=3,R=3 的系统中,某时刻副本最大版本号为(v2 v2 v1 v1 v1),此时v1是 系统的最新的成功提交的数据,v2是一个处于中间状态的未成功提交的数据。假设此刻原 primary 副本异常,中心节点进行 primary 切换工作。这类“中间态”数据究竟作为“脏数据”被删除,还 是作为新的数据被同步后成为生效的数据,完全取决于这个数据能否参与新 primary 的选举。下面
分别分析这两种情况。
图 2-12 基于 quorum 选择 primary 情况 1
第一、如图 2-12,若中心节点与其中 3 个副本通信成功,读取到的版本号为(v1 v1 v1),则任 选一个副本作为primary,新 primary 以 v1 作为最新的成功提交的版本并与其他副本同步,当与第 1、
第2 个副本同步数据时,由于第 1、第 2 个副本版本号大于 primary,属于脏数据,可以按照 2.2.2.4 节中介绍的处理脏数据的方式解决。实践中,新 primary 也有可能与后两个副本完成同步后就提供 数据服务,随后自身版本号也更新到 v2,如果系统不能保证之后的 v2与之前的 v2完全一样,则新 primary 在与第 1、2 个副本同步数据时不但要比较数据版本号还需要比较更新操作的具体内容是否 一样。
第二、若中心节点与其他3 个副本通信成功,读取到的版本号为(v2 v1 v1),则选取版本号为 v2 的副本作为新的 primary,之后,一旦新 primary 与其他 2 个副本完成数据同步,则符合 v2 的副 本个数达到W个,成为最新的成功提交的副本,新primary 可以提供正常的读写服务。
2.4.6 工程投影
2.4.6.1 GFS 中的 Quorum
GFS 使用 WARO 机制读写副本,即如果更新所有副本成功则认为更新成功,一旦更新成功,
则可以任意选择一个副本读取数据;如果更新某个副本失败,则更显失败,副本之间处于不一致的 状态。GFS 系统不保证异常状态时副本的一致性,GFS 系统需要上层应用通过 Checksum 等机制自 行判断数据是否合法。值得注意的是GFS 中的 append 操作,一旦 append 操作某个 chunck 的副本上 失败,GFS 系统会自动新增一个 chunck 并尝试 append 操作,由于可以让新增的 chunck 在正常的机 器上创建,从而解决了由于WARO 造成的系统可用性下降问题。进而在 GFS 中,append 操作不保 证一定在文件的结尾进行,由于在新增的chunk 上重试 append,append 的数据可能会出现多份重复
同步,删脏数据
v
2v
2v
2v
2v
2同步,删脏数据
同步
v
2v
2v
1v
1v
1同步
v
2v
2v
1v
1v
1v
1v
1v
1v
1v
1的现象,但每个append 操作会返回用户最终成功的 offset 位置,在这个位置上,任意读取某个副本 一定可以读到写入的数据。这种在新增chunk 上进行尝试的思路,大大增大了系统的容错能力,提 高了系统可用性,是一种非常值得借鉴的设计思路。
2.4.6.2 Dynamo 中的 Quorum
Dynamo/Cassandra 是一种去中心化的分布式存储系统。Dynamo 使用 Quorum 机制来管理副本。
用户可以配置N、R、W 的参数,并保证满足 R+W>N 的 quorum 要求。与其他系统的 Quorum 机制 类似,更新数据时,至少成功更新W 个副本返回用户成功,读取数据时至少返回 R 个副本的数据。
然而Dynamo 是一个没有 primary 中的去中心化系统,由于缺乏中心控制,每次更新操作都可能由 不同的副本主导,在出现并发更新、系统异常时,其副本的一致性完全无法得到保障。
下面着重分析Dynamo 在异常时副本的一致性情况。首先,Dynamo 使用一致性哈希分布数据,
理论上,即使出现一个节点异常,更新操作也可以顺着一致性哈希环的顺序找到N 个节点完成。不 过,这里我们简化其模型,认为始终只有初始的N 个副本,在实际中可以等效为网络异常造成用户 只能和初始的N 个副本通信。更复杂的是,虽然可以沿哈希环找到下一个节点临时加入,但无法解 决异常节点又重新加入的问题,所以这里的这种简化模型是完全合理的。我们通过一个例子来考察 Dynamo 的一致性。
例2.4.6,在 Dynamo 系统中,N=3,R=2,W=2,初始时,数据 3 个副本(A、B、C)上的数 据一致,这里假设数据值都为1,即(1,1,1)。
某次更新操作需在原有数据的基础上增加新数据,这里假设为+1 操作,该操作由副本 A 主导,
副本A 成功更新自己及副本 C,由于异常,更新副本 B 失败,由于已经满足 W=2 的要求,返回用 户更新成功。此时3 个副本上的数据分别为(2,1,2)。
接着,进行新的更新操作,该操作需要在原有数据的基础上增加新数据,假设为+2 操作,假设 用户端由于异常联系副本A 失败,联系副本 B 成功,本次更新操作由副本 B 主导,副本 B 读取本 地数据1,完成加 2 操作后同步给其他副本,假设同步副本 C 成功,此时满足 W=2 的要求,返回用 户更新成功。此时3 个副本上的数据分别为(2, 3, 3)。这里需要说明的是在 Dynamo 中,副本 C 必须 要接受副本B 发过来的更新并覆盖自身数据,即使从全局角度说该更新与副本 C 上的已有数据是冲 突的,但副本C 自身无法判断自己的数据是否有效。假如第一次副本 A 主导的更新只在副本 C 上 成功,那么此时副本C 上的数据本身就是错误的脏数据,被副本 B 主导的这次更新覆盖也是完全应 该的。
最后,用户读取数据,假设成功读取副本A 及副本 B 上的数据,满足 R=2 的需求,用户将拿 到两个完全不一致的数据2 与 3,Dynamo 将解决这种不一致的情况留给了用户进行。
为了帮助用户解决这种不一致的情况,Dynamo 提出了一种 clock vector 的方法,该方法的思路
就是记录数据的版本变化,以类似MVCC(2.7 )的方式帮助用户解决数据冲突。所谓 clock vector 即记录了数据变化的路径的向量,为每个更新操作维护分配一个向量元素,记录数据的版本号及主 导该次更新的副本名字。接着例2.4.7 来介绍 clock vector 的过程。
例2.4.8:在 Dynamo 系统中,N=3,R=2,W=2,初始时,数据 3 个副本(A、B、C)上的数 据一致,这里假设数据值都为1,即(1,1,1),此时三个副本的 clock vector 都为空([], [], [])。
某次更新操作需在原有数据的基础上增加新数据,这里假设为+1 操作,该操作由副本 A 主导,
副本A 成功更新自己及副本 C,返回用户更新成功。此时 3 个副本上的数据分别为(2,1,2),而 三个副本的clock vector 为([(1, A)], [], [(1, A)]),A、C 的 clock vector 表示数据版本号为 1,更新是 有副本A 主导的。
接着,进行新的更新操作,该操作需要在原有数据的基础上增加新数据,假设为+2 操作,假设 本次更新操作由副本B 主导,副本 B 读取本地数据 1,完成加 2 操作后同步给其他副本,假设同步 副本C 成功,。此时 3 个副本上的数据分别为(2, 3, 3),此时三个副本的 clock vector 为([(1, A)], [(1, B)], [(1, B)])。
为了说明clock vector,这里再加入一次+3 操作,并由副本 A 主导,更新副本 A 及副本 C 成功,
此时数据为(5, 3, 5),此时三个副本的 clock vector 为([(2, A), (1, A)], [(1, B)], [(2, A), (1, A)])。
最后,用户读取数据,假设成功读取副本A 及副本 B 上的数据,得到两个完全不一致的数据 5 与3,及这两个数据的版本信息[(2, A), (1, A)], [(1, B)]。用户可以根据自定义的策略进行合并,例如 假设用户判断出,其实这些加法操作可以合并,那么最终的数据应该是 7,又例如用户可以选择保 留一个数据例如5 作为自己的数据。
由于提供了clock vector 信息,不一致的数据其实成为了多版本数据,用户可以通过自定义策略 选择合并这些多版本数据。Dynamo 建议可以简单的按照数据更新的时间戳进行合并,即用数据时 间戳较新的数据替代较旧的数据。如果是简单的覆盖写操作,例如设置某个用户属性,这样的策略 是有效且正确的。然而类似上例中这类并发的加法操作(例如“向购物车中增加商品”),简单的用 新数据替代旧数据的方式就是不正确的,会造成数据丢失。
2.4.6.3 Zookeeper 中的 Quorum
Zookeeper 使用的 paxos 协议本身就是利用了 Quorum 机制,在 2.8 中有详细分析,这里不赘述。
当利用paxos 协议外选出 primary 后,Zookeeper 的更新流量由 primary 节点控制,每次更新操作,
primary 节点只需更新超过半数(含自身)的节点后就返回用户成功。每次更新操作都会递增各个节 点的版本号(xzid)。当 primary 节点异常,利用 paxos 协议选举新的 primary 时,每个节点都会以自 己的版本号发起paxos 提议,从而保证了选出的新 primary 是某个超过半数副本集合中版本号最大的 副本。这个原则与2.4.5 中描述的完全一致。值得一提的是,在 2.4.5 中分析到,新 primary 的版本
号未必是一个最新已提交的版本,可能是一个只更新了少于半数副本的中间态的更新版本,此时新 primary 完成与超过半数的副本同步后,这个版本的数据自动满足 quorum 的半数要求;另一方面,
号未必是一个最新已提交的版本,可能是一个只更新了少于半数副本的中间态的更新版本,此时新 primary 完成与超过半数的副本同步后,这个版本的数据自动满足 quorum 的半数要求;另一方面,