[elixir! #0070] 不死的网络,细数 Beam 集群和 Bitcoin 的相似之处

常常阅读 ljzn(就是在下)的专栏的朋友们可能知道,他平时最爱两样技术:beam虚拟机和 bitcoin网络。究其缘由,多是二者都在追求构建一个永生的网络集群。目标相似,那么实现方法必定会类似,咱们如今就来盘点一些 distributed erlang 和 bitcoin network 究竟有多少相似的地方。node

全联通网络

Bitcoin

Bitcoin 的矿工节点之间是高度联通的,这是由比特币的挖矿机制决定的。新的区块头中须要包含以前一个区块头的hash,换句话说,矿工必须时刻观察网络中是否有新的区块出现,而后跟在最新的区块后面进行挖矿。不然,矿工挖到的块极可能变成孤块,得不到任何奖励。每慢一秒钟观察到新的区块,矿工的算力就被浪费了1秒。另外,矿工也会主动把本身挖到的块发送给其它全部矿工,由于每慢一秒发出去,本身的块变成孤块的可能性就增长一分。因此最好的策略是链接到全部其它矿工的节点。shell

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 区块的竞争中去。dom

当出现竞争状况的时候,Bitcoin采起的是一种 “博弈” 模式,好比矿工 A 和矿工 B 同时爆出了高度 60002 的区块,而且第一时间发布到网络中。矿工 C 首先接受到矿工 A 的区块 a,因而接受了区块 a 并在其基础上开始挖 60003 区块。接着在很短的时间内,矿工 C 又收到了矿工 B 的区块 b,此时矿工 C 不会马上将其丢弃,由于 a 和 b 区块都有可能成为被全网认可的 60002 区块,同时,a 和 b 区块都有可能变成孤块。因此,最好的策略是,将两个区块都保存下来做为候选,同时判断哪一个区块更有可能被全网接受,就在那个区块上接下去挖。async

那么占据大量算力的矿工有可能实现区块垄断吗?(从分布式锁的语境来看,即拒绝交出锁的全部权)。实际上这种行为是不经济的,假设刚才的状况中,时间 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 的节点,让它们赶快把刚才记录删除。code

因此在出现竞争状况的时候,双方均可能会退缩,而且重试,直到一个占有流程中不存在任何冲突时才会成功。

全局注册名

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)

若是你对这个话题感兴趣,大力拍打?点赞按钮吧。

相关文章
相关标签/搜索