Open abbshr opened 8 years ago
广义上的分布式系统都是满足AP或CP的系统, 除了如交易等对强一致性敏感的系统之外, 大多数系统在海量数据处理&高并发请求的压力下都折衷选择了AP. trade-off就是在性能优先的前提下只要满足最终一致性即可.
相比严格要求强一致性的系统来说, 满足最终一致性的系统在理论设计上容易得多. gossip协议就是为了提供最终一致性保障而设计的, 并已广泛应用于追求性能的分布式系统中如cassandra,redis-cluster,consul,dynamo等.
不料Google上讲解gossip的文章不多, (为图方便)最开始学习时我找了不少资料, 多数是千篇一律的"点到为止". 后来不得不求助于paper原文,好在其中描述的十分严谨清晰, 方得点拨.
我们先从gossip的分类说起.
gossip分为两种: anti-entropy和rumor-mongering. 前者称为"反熵", 也是被最多采纳的gossip. 本质上很容易理解: 节点之间交换差异数据以达到状态的一致.
在anti-entropy的基础上, 又产生三种gossip模式: push, pull, pull/push. 表示任意两个节点之间如何进行gossip.
而后, 考虑到网络带宽与CPU时间的资源占用问题, 研究出了两种传输策略: precise reconciliation和scuttlebutt reconciliation. 表示gossip时具体传输哪些内容(数据子集).
接下来, 结合paper介绍一下gossip.
P表示节点的集合:
P
P = {p, q, ...}
V和N分别表示value和version的集合.
V
N
S表示状态集合: (状态即从key到V×N的映射)
S
S = K → V × N
σ ∈ S表示S中一个状态(也就是一个k→v×n的函数):
σ ∈ S
σ(k) = (v, n)
设key的初始映射为:(⊥表示最小version)
⊥
k → (., ⊥)
μp: P → S表示在节点p上, 一个节点到其状态的映射.
μp: P → S
⊕操作符有两个操作数, 是两个状态, 对他们进行"合并"和"协调", 得到另一个状态:
⊕
σ = σ1 ⊕ σ2: σ1(k) = (v1, n1) σ2(k) = (v2, n2) σ(k) = if n1 ≥ n2 then (v1, n1) else (v2, n2)
每个节点p都有一份节点集合Fp, 表示gossipee: Fp ⊆ P - {p}
Fp
Fp ⊆ P - {p}
max(σ)表示σ∈S中具有的最大version
max(σ)
p周期性从Fp中随机选择一个q, gossip之后应用⊕到各自的节点状态拷贝中. p只能直接修改自身的状态μp(p), 而μp(q)则只能通过gossip间接修改. gossip的标准信息是一些tuple的集合: {(r, k, v, n) ∈ P × K × V × N | μp(r)(k) = (v, n)}, 这个集合称做deltas.
μp(p)
μp(q)
{(r, k, v, n) ∈ P × K × V × N | μp(r)(k) = (v, n)}
对于gossip的三种传输模式:
push模式, 即p → q, 对q来说, 有: ∀r ∈ P → μ'q(r) = μq(r) ⊕ μp(r), 反之q → p同理.
∀r ∈ P → μ'q(r) = μq(r) ⊕ μp(r)
pull模式. p生成一份μp的摘要digest, 其中μp不包含v, 相当于tuple:(r,k,n). q得到digest后把必要的更新发给p.
pull/push模式是在pull的基础上, q在响应的同时包含一份比digest中version旧的digest给p, p再发给q一份更新. 是最高效的传输模式.
但是受限于网络资源, 这里限制一个gossip信息最多不超过mtu大小.
对于|deltas| > mtu, 发送deltas的哪些子集由协调机制π决定: 将deltas按<π排序, 结果只取优先级最高的tuples.
|deltas| > mtu
π
<π
precise简单粗暴, 就是互相发送最新的状态, 在pull/push模式下, 通过digest得到deltas:
△precise(p → q) = {(r, k, v, n) ∈ P × K × V × N | μp(r)(k) = (v, n) ∧ μq(r)(k)=(.,n′) ∧ n > n′}
然后如何给deltas中的tuples排序呢? 这里的<precise有两个排序方案:
<precise
precise-oldest: 优先选择最旧的更新. precise-newest: 优先选择最新的更新(可能导致饥饿, 因为旧的更新可能得不到gossip的机会).
precise-oldest
precise-newest
另外对于两种排序策略, 因为涉及到时间先后顺序, 需要在节点之间维护一个同步时钟以便所有的更新可以从这里得到时间戳(版本).
scuttlebutt是paper种建议的协调机制, 进一步节省带宽与CPU时间.
与precise的一个不同是版本变化的方式. scuttlebutt严格要求状态更新使用的版本号必须是在当前最大版本号之上的:
# k0 → (v, n) => k0 → (v0, n0) σp = μp(p) # 构造一个新函数σ', 使 σ'p(k0) = (v0, n0) # 满足 n0 > max(σp) ∧ ∀ k ≠ k0, σp(k) = σ'p(k)
scuttlebutt下的digest也与precise略有差别, tuple中的version是一个节点副本中最大version:
digest(p → q) = {(r, max(μp(r))) | r ∈ P}
q得到digest, 构造updates:
△scuttle(q → p) = {(r, k, v, n) ∈ P × K × V × N | μq(r)(k) = (v, n) ∧ n > max(μp(r))}
在排序上, 对于每个节点内部的tuple, scuttlebutt严格按照version升序排列, 即满足: n > n' => (r, k, v, n) <scuttle (r, k', v', n')
n > n' => (r, k, v, n) <scuttle (r, k', v', n')
到这里你可能会有疑问, scuttlebutt 每次gossip的都是比对方max version还要大的deltas, 这难到不会出现饥饿么?
现在举个栗子吧. 这里有三个节点: p,q,r. 在t1时刻, 各自维护的r节点状态副本(k1,k2,k3)的版本号分别为:
这时p与r发生一次gossip, q也与r发生一次gossip, 根据scuttlebutt, r给p和q的deltas是从n=9开始的, 假如这里只允许r节点的deltas只包含一个tuple(r, k1, ., 9). 那么t2时刻的状态如下:
接着p与q发生一次gossip, 对于r的状态副本, 由于q中没有大于max(μp(r))的版本, 因此不会返回updates给p, 同理p也不会发送updates给q.
因为p只能直接修改自身的状态μp(p), 而μp(q)则只能通过gossip间接修改, 所以r的状态虽然在p,q中都存有副本, 但不允许p,q的直接更新, 所以k1,k2,k3的版本变化都是通过r节点控制的, 也就是p,q中r的副本最终都能通过后续的几次gossip完成k2,k3的更新而不会产生饥饿.
用一个逻辑关系可以约束以上过程, 有:
C(p,q) ≡ ∀k ∈ K, μp(p)(k) = μq(p)(k) ∨ μp(p)(k).n > max(μq(p))
scuttlebutt满足不变式C. 即对于p的状态副本, 或者是已经与p节点状态达到一致, 否则p节点自身状态中的任何一个key的version都大于其他节点中p状态副本的最大版本号.
这里与precise最根本的差异就体现出来了, precise是尽量消除所有的差异, 而scuttlebutt在gossip时并不把所有的差异全部消除, 而是只选择清除小部分差异(大于max(μp(r))的一部分).
看起来precise的收敛速度相对更快, 其实不然, scuttlebutt可以利用空闲空间发送更多其他节点的副本. 并且只要遵循不变式C(p,q), 就有:
max(μq(p)) = max(μp(p)) ⇒ μq(p) = μp(p)
即完成同步.
前面提到过scuttlebutt中的deltas排序约束条件: n > n' => (r, k, v, n) <scuttle (r, k', v', n')
因为这是针对一个节点r内部的排序方法, 一个deltas中可能包含不同节点的状态副本, 这时整体的排序方案就产生了两种:
(1)的思路是尽量包含更多的节点, 为每个节点内部的状态评级从0开始, 版本越低的评级越低, 排序按评级的升序排列, 当遇到几个节点的评级相同但是gossip大小达到了mtu, 这是可以进行随机节点选择. (2)的思路是尽量包含一个节点中的更多状态, 取包含updates数量最多的节点封包, 如果几个节点的updates数量相同, 那么同样可以随机排序, 并使节点内部的排序仍满足约束条件.
截止目前, 已上所讨论的都是一份digest恰好装在一个mtu的情况, 对于节点数量巨大的系统来说, 这种情况可能无法满足. 为此可以将digest拆分成多个packet发送, 或者仅发送一个mtu packet, 里面的digest随机选取, 但这种做法会增加收敛时间.
当我们已经确定这个理论是站的住脚, 就可以将其应用于设计&编码中. 上面强调了一下不变式C, 为了满足它就要保证: 每个节点负责自己的key集合, 不同节点负责不同的key集合. 这暗示着基于gossip的分布式系统可能需要进行一些改动, 或者需要一致性hash等方法来帮助划分key空间以及负载均衡了.
下一篇谈谈构建最终一致性集群.
这里有一个CoffeeScript实现的gossip库: https://github.com/abbshr/leviathan-gossip
你现在在哪个公司?
@barretlee 我来又拍云了
广义上的分布式系统都是满足AP或CP的系统, 除了如交易等对强一致性敏感的系统之外, 大多数系统在海量数据处理&高并发请求的压力下都折衷选择了AP. trade-off就是在性能优先的前提下只要满足最终一致性即可.
相比严格要求强一致性的系统来说, 满足最终一致性的系统在理论设计上容易得多. gossip协议就是为了提供最终一致性保障而设计的, 并已广泛应用于追求性能的分布式系统中如cassandra,redis-cluster,consul,dynamo等.
不料Google上讲解gossip的文章不多, (为图方便)最开始学习时我找了不少资料, 多数是千篇一律的"点到为止". 后来不得不求助于paper原文,好在其中描述的十分严谨清晰, 方得点拨.
分类
我们先从gossip的分类说起.
gossip分为两种: anti-entropy和rumor-mongering. 前者称为"反熵", 也是被最多采纳的gossip. 本质上很容易理解: 节点之间交换差异数据以达到状态的一致.
在anti-entropy的基础上, 又产生三种gossip模式: push, pull, pull/push. 表示任意两个节点之间如何进行gossip.
而后, 考虑到网络带宽与CPU时间的资源占用问题, 研究出了两种传输策略: precise reconciliation和scuttlebutt reconciliation. 表示gossip时具体传输哪些内容(数据子集).
接下来, 结合paper介绍一下gossip.
一些符号说明
P
表示节点的集合:V
和N
分别表示value和version的集合.S
表示状态集合: (状态即从key到V×N的映射)σ ∈ S
表示S中一个状态(也就是一个k→v×n的函数):设key的初始映射为:(
⊥
表示最小version)μp: P → S
表示在节点p上, 一个节点到其状态的映射.⊕
操作符有两个操作数, 是两个状态, 对他们进行"合并"和"协调", 得到另一个状态:每个节点p都有一份节点集合
Fp
, 表示gossipee:Fp ⊆ P - {p}
max(σ)
表示σ∈S中具有的最大versiongossiping
p周期性从
Fp
中随机选择一个q, gossip之后应用⊕
到各自的节点状态拷贝中. p只能直接修改自身的状态μp(p)
, 而μp(q)
则只能通过gossip间接修改. gossip的标准信息是一些tuple的集合:{(r, k, v, n) ∈ P × K × V × N | μp(r)(k) = (v, n)}
, 这个集合称做deltas.对于gossip的三种传输模式:
push模式, 即p → q, 对q来说, 有:
∀r ∈ P → μ'q(r) = μq(r) ⊕ μp(r)
, 反之q → p同理.pull模式. p生成一份μp的摘要digest, 其中μp不包含v, 相当于tuple:(r,k,n). q得到digest后把必要的更新发给p.
pull/push模式是在pull的基础上, q在响应的同时包含一份比digest中version旧的digest给p, p再发给q一份更新. 是最高效的传输模式.
但是受限于网络资源, 这里限制一个gossip信息最多不超过mtu大小.
gossip哪些状态
对于
|deltas| > mtu
, 发送deltas的哪些子集由协调机制π
决定: 将deltas按<π
排序, 结果只取优先级最高的tuples.precise协调机制
precise简单粗暴, 就是互相发送最新的状态, 在pull/push模式下, 通过digest得到deltas:
然后如何给deltas中的tuples排序呢? 这里的
<precise
有两个排序方案:precise-oldest
: 优先选择最旧的更新.precise-newest
: 优先选择最新的更新(可能导致饥饿, 因为旧的更新可能得不到gossip的机会).另外对于两种排序策略, 因为涉及到时间先后顺序, 需要在节点之间维护一个同步时钟以便所有的更新可以从这里得到时间戳(版本).
scuttlebutt协调机制
scuttlebutt是paper种建议的协调机制, 进一步节省带宽与CPU时间.
与precise的一个不同是版本变化的方式. scuttlebutt严格要求状态更新使用的版本号必须是在当前最大版本号之上的:
scuttlebutt下的digest也与precise略有差别, tuple中的version是一个节点副本中最大version:
q得到digest, 构造updates:
在排序上, 对于每个节点内部的tuple, scuttlebutt严格按照version升序排列, 即满足:
n > n' => (r, k, v, n) <scuttle (r, k', v', n')
谁来保证最终一致性?
到这里你可能会有疑问, scuttlebutt 每次gossip的都是比对方max version还要大的deltas, 这难到不会出现饥饿么?
现在举个栗子吧. 这里有三个节点: p,q,r. 在t1时刻, 各自维护的r节点状态副本(k1,k2,k3)的版本号分别为:
这时p与r发生一次gossip, q也与r发生一次gossip, 根据scuttlebutt, r给p和q的deltas是从n=9开始的, 假如这里只允许r节点的deltas只包含一个tuple(r, k1, ., 9). 那么t2时刻的状态如下:
接着p与q发生一次gossip, 对于r的状态副本, 由于q中没有大于max(μp(r))的版本, 因此不会返回updates给p, 同理p也不会发送updates给q.
因为p只能直接修改自身的状态
μp(p)
, 而μp(q)
则只能通过gossip间接修改, 所以r的状态虽然在p,q中都存有副本, 但不允许p,q的直接更新, 所以k1,k2,k3的版本变化都是通过r节点控制的, 也就是p,q中r的副本最终都能通过后续的几次gossip完成k2,k3的更新而不会产生饥饿.用一个逻辑关系可以约束以上过程, 有:
scuttlebutt满足不变式C. 即对于p的状态副本, 或者是已经与p节点状态达到一致, 否则p节点自身状态中的任何一个key的version都大于其他节点中p状态副本的最大版本号.
这里与precise最根本的差异就体现出来了, precise是尽量消除所有的差异, 而scuttlebutt在gossip时并不把所有的差异全部消除, 而是只选择清除小部分差异(大于max(μp(r))的一部分).
看起来precise的收敛速度相对更快, 其实不然, scuttlebutt可以利用空闲空间发送更多其他节点的副本. 并且只要遵循不变式C(p,q), 就有:
即完成同步.
scuttlebutt中的deltas排序方案
前面提到过scuttlebutt中的deltas排序约束条件:
n > n' => (r, k, v, n) <scuttle (r, k', v', n')
因为这是针对一个节点r内部的排序方法, 一个deltas中可能包含不同节点的状态副本, 这时整体的排序方案就产生了两种:
(1)的思路是尽量包含更多的节点, 为每个节点内部的状态评级从0开始, 版本越低的评级越低, 排序按评级的升序排列, 当遇到几个节点的评级相同但是gossip大小达到了mtu, 这是可以进行随机节点选择. (2)的思路是尽量包含一个节点中的更多状态, 取包含updates数量最多的节点封包, 如果几个节点的updates数量相同, 那么同样可以随机排序, 并使节点内部的排序仍满足约束条件.
发送哪些digest?
截止目前, 已上所讨论的都是一份digest恰好装在一个mtu的情况, 对于节点数量巨大的系统来说, 这种情况可能无法满足. 为此可以将digest拆分成多个packet发送, 或者仅发送一个mtu packet, 里面的digest随机选取, 但这种做法会增加收敛时间.
用gossip建立&维护集群关系(To be continued)
当我们已经确定这个理论是站的住脚, 就可以将其应用于设计&编码中. 上面强调了一下不变式C, 为了满足它就要保证: 每个节点负责自己的key集合, 不同节点负责不同的key集合. 这暗示着基于gossip的分布式系统可能需要进行一些改动, 或者需要一致性hash等方法来帮助划分key空间以及负载均衡了.
下一篇谈谈构建最终一致性集群.