经常阅读 ljzn(就是在下)的专栏的朋友们可能知道,他平时最爱两样技术:beam虚拟机和 bitcoin网络。究其原因,可能是两者都在追求构建一个永生的网络集群。目标类似,那么实现方法一定会相似,我们现在就来盘点一些 distributed erlang 和 bitcoin network 究竟有多少类似的地方。
全联通网络
Bitcoin
Bitcoin 的矿工节点之间是高度联通的,这是由比特币的挖矿机制决定的。新的区块头中需要包含之前一个区块头的hash,换句话说,矿工必须时刻观察网络中是否有新的区块出现,然后跟在最新的区块后面进行挖矿。否则,矿工挖到的块很可能变成孤块,得不到任何奖励。每慢一秒钟观察到新的区块,矿工的算力就被浪费了1秒。另外,矿工也会主动把自己挖到的块发送给其它所有矿工,因为每慢一秒发出去,自己的块变成孤块的可能性就增加一分。所以最好的策略是连接到所有其它矿工的节点。
Beam
分布式 Erlang 的节点之间是全联通的。我们可以做一个小小的测试:
- 在2个终端启动2个 erlang node:
$ iex --sname bob@localhost
$ iex --sname carl@localhost
- 使用 carl 连接到 bob:
iex(carl@localhost)2> Node.connect :bob@localhost
true
iex(carl@localhost)3> Node.list
[:bob@localhost]
- 可以在 bob 看到 carl:
iex(bob@localhost)4> Node.list
[:carl@localhost, :alice@localhost]
- 在新的终端启动 alice,并连接到 bob:
zhanglinjie@MacBook-Pro-2 ~ % iex --sname alice@localhost
Erlang/OTP 22 [erts-10.7.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]
Interactive Elixir (1.10.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(alice@localhost)1> Node.connect :bob@localhost
true
- 可以在 carl 看到 alice,因为分布式 erlang 的节点会主动引导新加入的节点与其它节点连接上:
iex(carl@localhost)4> Node.list
[:bob@localhost, :alice@localhost]
全局锁
Bitcoin
Bitcoin 使用区块来更新整个账本的状态,可以说整个区块链只有一个锁 ——— 区块。例如,目前的区块高度是 60001,那么全部的矿工都在竞争 60002 区块的所有权,而获胜的矿工将可以使用 60002 区块来更新账本状态,之后全部矿工再进入到 60003 区块的竞争中去。
当出现竞争情况的时候,Bitcoin采取的是一种 “博弈” 模式,比如矿工 A 和矿工 B 同时爆出了高度 60002 的区块,并且第一时间发布到网络中。矿工 C 首先接受到矿工 A 的区块 a,于是接受了区块 a 并在其基础上开始挖 60003 区块。接着在很短的时间内,矿工 C 又收到了矿工 B 的区块 b,此时矿工 C 不会立刻将其丢弃,因为 a 和 b 区块都有可能成为被全网承认的 60002 区块,同时,a 和 b 区块都有可能变成孤块。所以,最好的策略是,将两个区块都保存下来作为候选,同时判断哪个区块更有可能被全网接受,就在那个区块上接下去挖。
那么占据大量算力的矿工有可能实现区块垄断吗?(从分布式锁的语境来看,即拒绝交出锁的所有权)。实际上这种行为是不经济的,假设刚才的情况中,时间 T0 爆出区块 a 和 b. 假设在时间 T1 有矿工在区块 b 之后爆出了新块 b' , 这时矿工A想用区块 a 去赢得竞选就很难。因为矿工在区块 a 上挖了9分钟没有出块并不意味着下一分钟内继续在 a 上挖出块的可能性就会增加,道理和扔了9次硬币都是反面,第10次正面的概率不会增加一样。这时矿工 A 如果继续在 a 上挖,它的对手是全网所有的其它矿工,因为它们都已经在 b’ 上开始挖矿。
这时候对于矿工 A 来说 good ending 是挖到新块 a', 使得 a' 和 b' 继续公平竞争(高度相同); bad ending 是其它矿工 在时间 T2 挖到 b'', 区块a 将落后两个区块, 被其它矿工接受的可能性继续降低. good ending 和 bad ending 发生的概率比等于矿工 A 和网络中其余矿工的算力比. 且 good ending 的收益是 0 (因为还要继续竞争), bad ending 的收益是 -(A 的算力在 T1 到 T2 这段时间出块的可能性)*(b'' 的区块奖励)
. 这样计算出选择 "看到 b' 之后继续在 a 上挖矿" 的收益期望始终是负的.
Beam
在分布式 erlang里提供了全局锁的功能.
- 在 alice 获取一个全局锁,锁的 id 是
:lock1
, 元组的第二个元素是请求者的 id
iex(alice@localhost)12> :global.set_lock {:lock1, self()}
true
- 在 carl 尝试获取这个锁,会发现被挂起。此时是在定时重试获取:
iex(carl@localhost)6> :global.set_lock {:lock1, self()}
- 在 alice 释放这个锁:
iex(alice@localhost)13> :global.del_lock {:lock1, self()}
true
- 几秒后,看到 carl 获取成功:
iex(carl@localhost)6> :global.set_lock {:lock1, self()}
true
从内部实现来看,分布式erlang的全局锁采取的是一种 “退缩” 策略。核心的代码是这段:
check_replies([{_Node, false=Reply} | _T], Id, Replies) ->
TrueReplyNodes = [N || {N, true} <- Replies],
?trace({check_replies, {true_reply_nodes, TrueReplyNodes}}),
gen_server:multi_call(TrueReplyNodes, global_name_server, {del_lock, Id}),
Reply;
这里的逻辑可以这样解释:节点 A 想获得分布式锁 L, 它告诉其它节点,我要占有锁 L。其它节点收到消息后,如果检查一下自己的本地状态,如果发现锁 L 没人占用,就会更新锁状态,然后回复 true,反之回复 false。节点 A 在全部回复都是 true 的情况下才会完成锁的占有,只要有1个节点返回 false,节点 A 就会立刻 ”退缩“,并且发消息给所有返回了 true 的节点,让它们赶快把刚才记录删除。
所以在出现竞争情况的时候,双方都可能会退缩,并且重试,直到一个占有流程中不存在任何冲突时才会成功。
全局注册名
Bitcoin
Bitcoin 使用随机数来保证每个用户拥有唯一的地址,用户使用 256bits 的私钥生成公钥(进而转换成地址),重复的可能性微乎其微。缺点也很明显,地址是人类难以记忆的的长度,需要第三方的注册名管理服务,例如 交易所,钱包 等来帮助管理。
目前 Bitcoin 里尚未有原生的管理 “可读的注册名” -> "地址“ 的映射的手段,但我们可能通过比特币脚本来实现,这依旧是十分前沿的技术,暂且不表。
Beam
在 erlang 里使用自增的计数器来让每个进程都拥有一个唯一的 pid。对于远程节点的进程,其pid里会加上节点的信息,使得其全局唯一。另外,erlang提供了全局的注册名管理机制,我们来试验一下:
- 在 carl 里将 shell 进程注册为 :god
iex(carl@localhost)10> :global.register_name :god, self()
:yes
- 在 bob 里尝试注册 :god, 返回 :no,表示注册失败,因为已经被占用
iex(bob@localhost)17> :global.register_name :god, self()
:no
- 在 carl 里注销 :god
iex(carl@localhost)14> :global.unregister_name :god
:ok
- 现在在 bob 里可以注册 :god
iex(bob@localhost)18> :global.register_name :god, self()
:yes
内部实现就是用了上面提到的全局锁。在修改注册名信息的时候要先获取到锁。这里做了一些优化,首先尝试在一个节点(The Boss)上获取锁,获取成功后才会尝试在其它全部节点上获取锁。
%% Use the same convention (a boss) as lock_nodes_safely. Optimization.
case set_lock_on_nodes(Id, [Boss]) of
true ->
case lock_on_known_nodes(Id, Known, Nodes) of
true ->
Nodes;
false ->
del_lock(Id, [Boss]),
random_sleep(Times),
set_lock_known(Id, Times+1)
end;
false ->
random_sleep(Times),
set_lock_known(Id, Times+1)
end.
小结
我们粗略对比了一下这两样迥然不同的技术,发现了其中一些有趣的共同点:
- 全联通网络 (yes)
- 全局锁 (yes)
- 全局注册名 (maybe)
如果你对这个话题感兴趣,大力拍打点赞按钮吧。