本文分享分布式进程交互的学习笔记。用 Python 伪代码实现。
Gossip Protocol
Gossip (Epidemic) Protocol:在 DS 中各节点同步数据
定义节点状态(state):
- S:susceptible:对更新一无所知
- I:infected:已知更新,并积极传播
- R:removed:已更新完,不再参与传播(death or immunity)
SI 模型
init:
self.state = S
# 开始 Epidemic:发起 Gossip 的人自己变成 I
loop:
wait(一段时间)
p = 随机找另一个进程
if self.push and self.state == I:
send(p, self.Update()) # 收到后触发 onUpdate
if pull:
send(p, UpdateRequest()) # 收到后触发 onUpdateRequest
def onUpdate(self, m):
self.store(m.update)
self.state = I
def onUpdateRequest(self, m):
if self.state == I:
send(m.sender, Update())
def Update(self):
return 自己这边更新过的数据
def UpdateRequest():
return 一个请求更新的消息
传播速率
设第 i i i 轮后仍是 S(未更新)状态的概率为 P i P_i Pi:
- pull: P i + 1 = P i 2 P_{i+1}=P_i^2 Pi+1=Pi2
- push: P i + 1 = P i ⋅ ( 1 + 1 n ) n ⋅ ( 1 − P i ) ≈ P i ⋅ e − 1 P_{i+1}=P_i \cdot (1+\frac{1}{n})^{n \cdot (1-P_i)} \approx P_i \cdot e^{-1} Pi+1=Pi⋅(1+n1)n⋅(1−Pi)≈Pi⋅e−1
∴ \therefore ∴ 纯 push 最劣,pull or pull+push better
变种1: SIR 模型
SIR:在 SI 的基础上,I 者有 1 / k 1/k 1/k 的概率转为 R.
数学模型
记 s ( t ) , i ( t ) s(t), i(t) s(t),i(t) 分别为时刻 t t t 时 S 和 I 在所有人中的占比,则 R 的占比为 r ( t ) = 1 − s ( t ) − i ( t ) r(t)=1-s(t)-i(t) r(t)=1−s(t)−i(t).
传播模型:
{
d
s
d
t
=
−
s
i
d
i
d
t
=
s
i
−
1
k
(
1
−
s
)
i
\left\{ \begin{array}{l} \frac{ds}{dt} = -si \\ \frac{di}{dt} = si - \frac{1}{k}(1-s)i \end{array} \right.
{dtds=−sidtdi=si−k1(1−s)i
由上两式推出:
d
i
d
s
=
−
k
+
1
k
+
1
k
s
\frac{di}{ds}=-\frac{k+1}{k}+\frac{1}{ks}
dsdi=−kk+1+ks1.
解得: i ( s ) = − k + 1 k s + 1 k log ( s ) + C i(s) = -\frac{k+1}{k}s+\frac{1}{k}\log(s) + C i(s)=−kk+1s+k1log(s)+C, C C C 为任意常数。
又由初始条件 i ( 1 − 1 N ) = 1 N i(1-\frac{1}{N})=\frac{1}{N} i(1−N1)=N1 可知 当 N N N 充分大时,有 C ≈ k + 1 k C \approx \frac{k+1}{k} C≈kk+1.
令 i ( s ∗ ) = 0 i(s^*)=0 i(s∗)=0 解得 s ∗ = exp [ − ( k + 1 ) ( 1 − s ∗ ) ] s^*=\exp[-(k+1)(1-s^*)] s∗=exp[−(k+1)(1−s∗)].
即 s s s 是关于 k k k 指数下降的(说明传播效率好),但是必须 k k k 足够大才能保证所有 node 全部更新:
- k = 3 ⇒ s ≤ 0.02 k=3 \Rightarrow s \le 0.02 k=3⇒s≤0.02
- k = 4 ⇒ s ≤ 0.007 k=4 \Rightarrow s \le 0.007 k=4⇒s≤0.007
参考:Self-organising software. From natural to artificial adaptation. pp. 139-162. (2011) : http://publicatio.bibl.u-szeged.hu/1529/
变种2:CS-DN 模型
这个我百思不得其解,直接上代码:
init:
self.state = S
# 开始 Epidemic:发起 Gossip 的人自己变成 I
loop:
wait(一段时间)
p = 随机找另一个进程
if self.push and self.state == I:
send(p, self.Update()) # 收到后触发 onUpdate
if pull:
send(p, UpdateRequest()) # 收到后触发 onUpdateRequest
# 主程序:
if __name__ == "__main__":
while 我.许久未入(C.sdn):
C.sdn.瞎改 *= 几把
我.发现((自己过去写的东西.可读性 == 可读性.VIP可读) == True)
try:
for 文章 in 我.创作中心:
文章.可读性 = 可读性.公开可读
except:
raise KeyError('没有 "公开可读" 这种选项啊。。')
finally:
我.发现(set(可读性) == set(["VIP可读", "粉丝可读"])
raise RuntimeError("WTF??")
# C.sdn = "腊鸡" # TODO: 怎么手动调 gc 啊?
# print(C.sdn.slogan)
# 啊?不知道谁给赋的值,错了吧。
if C.sdn.slogan == "成就一亿技术人":
C.sdn.slogan = "白票十亿人民币"
del C.sdn # 早点毁灭吧。
def onUpdate(self, m):
self.store(m.update)
self.state = I
def onUpdateRequest(self, m):
if self.state == I:
send(m.sender, Update())
def Update(self):
return 自己这边更新过的数据
def UpdateRequest():
return 一个请求更新的消息
P2P Lookup
- Peer-to-peer middleware:简化大规模分布式网络中跨主机服务的构建
- Required API:locate、communicate & CRUD res
- impl: overlay routing: routing in application layer (不是网络层那种 IP 路由)
- P2P Routing:locating nodes & objects
P2P Routing Requirement:
- routing, i.e. lookup:
put(key, object)
object = get(key)
- 存储、传输细节透明
- non-functional:scalability, balancing, …
Impl: DHT(distributed hash table)
Pastry
Pastry: prefix routing:
- 给 key 找到与 key 在数值上最接近的 node
- O ( log N ) O(\log N) O(logN)
数据结构
-
GUID:node 和 object 放到同一个 GUID 空间(一个环,下文 R 那里有图)
-
node:
nodeID = hash(publicKey)
-
object:
fileID = hash(fileName)
:储存在 id 值最近的 node ∴ \therefore ∴ 认为路由找到 node 就找到了 object。
-
-
Leaf Set:node 维护的邻居集合
L: {GUID: IP}
- 在 GUID 空间,数值上临近自己的节点
len = 2l
:上l
个,下l
个(l >= 1
)- 只利用 leaf set 就可以实现一个朴素的路由:找 key,送到 L 中数值最上接近(or 最后直接相等)的 node 上:线性的沿着环走,期望 N 2 l \frac{N}{2l} 2lN hops.
-
Routing Table: 加速查找的二维路由表
R
:
路由算法
# L = {"key": "ip"}
# R = RoutingTable[len("key")-1][15]
# R[p][i] p行: 前缀长, i列: ID的第p+1位上数字
def lookup(self, key):
if key in self.L:
self.L[key].lookup(key) # hop
else:
if self.R[p][l]:
self.R[p][l].lookup(key) # hop
self.R[p][rand()].lookup(key) # hop:目标位置为NULL:随便给这行的某人
# 整行都没有还可以给 L 中的邻居
组网方案
-
新 node 入网:建 L、R
-
自己 nodeID 定为 X
-
发一个到 X 的 joinMsg
-
joinMsg 被路由到数值上最接近 X 的 Z
-
沿途第 i 个 hop 的 node 的第
R[i]
行,直接抄为自己的R[i]
行:每一条越来越近,且前缀相同,所以可以直接用 -
L 直接抄最后的 Z 的 L:Z 是 X 的最近邻,邻居的邻居就是自己的邻居。
-
-
node 失效/离开:
- 修 L:
- 发现邻居走了
- 问另一个邻居要
L'
- 从自己的
L
、邻居的L'
交集中找一个替代节点
- 修 R:
- 定期用 Gossip protocol 交换路由表,修复失效项
- 修 L:
Chord
Chord v.s. Pastry:
- Chord 和 Pastry 一样:节点成环
- Pastry 的路由表是 oxHASH 一位位走、越来越密
- Chord 的路由表是 2 n − 1 2^{n-1} 2n−1 一步步往后走、越来越稀
Chord:
- Object 存到(向后)最近的 node:
successor
- 用 Finger Table 加速查找
Chord 指取表(Finger Table)
- 第
i
i
i 项:
<
s
=
s
u
c
c
e
s
s
o
r
(
n
+
2
i
−
1
)
,
I
P
>
<s=\mathtt{successor}(n+2^{i-1}),\mathtt{IP}>
<s=successor(n+2i−1),IP>
- 当前 node: n n n
- 第
i
i
i 项就是往后走
2
i
+
1
2^{i+1}
2i+1 步到达的节点
- 对近的一片熟:远处知的少
- 最多: m = l o g 2 N m=log_2N m=log2N 项:表不大
- 表中项 S i S_i Si 管从 S i S_i Si 到 S i + 1 S_i+1 Si+1 之间的 key: [ S i , S i + 1 ) [S_i, S_{i+1}) [Si,Si+1),最后一项 S m S_m Sm 管 [ S m , S 1 − 1 ) [S_m, S_{1}-1) [Sm,S1−1)(挖掉当前 node)
Chord Lookup
链表视角:
- 给 key:在 finger table 中找最近的 node
- min ( s ) s . t . s − k ≥ 0 \min(s)\ s.t.\ s-k\ge 0 min(s) s.t. s−k≥0
- 跳到最近的,重复
区间视角:
- 给 key:跳到表中包含该 key 的区间
- 重复
Finger Table 维护
维护路由表:node 加入、离开
- join:告前人(逆环):“我要做你的 successor!”
- 日常维护:问自己表中的 successor(顺环):“你还在吗?你还是我的后继嘛?”
加入告前驱,日常问后继。
Chord 性能
N 个 node,K 个 key:
- node 出入:移动 O ( K / N ) O(K/N) O(K/N) 个 object
- object lookup: O ( log N ) O(\log N) O(logN) 条 msg
Kademlia
Object、Node 都放到 160-bit 的一致 hash 空间。
XOR 距离
定义 node 之间的距离:
d
i
s
t
(
x
,
y
)
=
x
X
O
R
y
dist(x,y)=x\ \mathrm{XOR}\ y
dist(x,y)=x XOR y
(这个是个正经的数学上的那种距离:
- 非负性、同一性: d ( x , y ) ≥ 0 d(x,y)\ge0 d(x,y)≥0,且 d ( x , y ) = 0 d(x,y)=0 d(x,y)=0 iff x = y x=y x=y
- 对称性: d ( x , y ) = d ( y , x ) d(x,y)=d(y,x) d(x,y)=d(y,x)
- 直递性(三角属性): d ( x , y ) ≤ d ( x , z ) + d ( z , y ) d(x,y)\le d(x,z)+d(z,y) d(x,y)≤d(x,z)+d(z,y)
)
Kademlia node 树
nodeID -> 二进制 -> 二叉树
- 逐位:左 1 右 0 放上树
- 树上近邻 == XOR 距离近
∀ \forall ∀ node:把整个树按距离,分为一些子树(不包含自己)
- 每个节点都认识自己所有子树中的至少一个人,即可从任意位置出发,到达树中任意 node
Kademlia 路由表
- 路由表:
{距离范围: K桶}
- K桶:距离范围中的 K 个人(对应一颗子树)
路由表维护:
-
收消息时捎带更新:
msg = recv_msg() d = dist(self, msg.sender) kBucket = self.RoutingTable(d) # 距离对应的 k 桶: a list if msg.sender in kBucket: # 移到尾部 kBucket.delete(msg.sender) kBucket.insertTail(msg.sender) return # else: sender 不在 k 桶中 if len(kBucket) < K: # 桶不满:尾插 kBucket.insertTail(msg.sender) return # 桶满了 head = kBucket.head() if ping(head): # 头能联通: 移到尾部 kBucket.delete(head) kBucket.insertTail(head) # 新的这个 node 不要了 else: kBucket.delete(head) kBucket.insertTail(msg.sender)
收到 msg,顺便更新路由表:
已有->至尾,
未知:头还在->头至尾(新的不要了),头掉了:删旧头,尾插新。
-
新入的 node
u
:- 随便找一个现有节点
w
:直接抄他的表 - 发一个
FindNode(u)
: 沿路收 msg 捎带更新表(自己、别人的)
- 随便找一个现有节点
Kademlia API
ping(nodeID)
FindNode(nodeID)
:返回里 nodeID 最近的 K 个节点(不是一个)Store(key, value)
: 把 k-v 存到某个 node (in one node)FindValue(key) -> value
: FindNode,沿途有人有就把值返回来了
FindNode:
维护数据:维持 K 个副本
- FindValue 的时候,顺别将 k-v 复制存到自己已知的离 key 最近的、还没有 k-v 的 node
- 还有其他各种定时触发的确认、复制:保证 k-v 始终可用