[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)

若是你對這個話題感興趣,大力拍打?點贊按鈕吧。

相關文章
相關標籤/搜索