1998:Erlang 開源#
事件#
Erlang 由 Joe Armstrong、Robert Virding 與 Mike Williams 於 1986 年在瑞典 Ericsson 計算機科學實驗室創造,最初是為了開發電信交換系統而設計的內部語言。1998 年,Ericsson 決定將 Erlang 開源(採 EPL 授權),同時一度宣布停止內部 Erlang 產品線、改用外部方案——隨後又在該年反轉決定,繼續以 Erlang 開發 AXD301 交換機。這台交換機之後達到了著名的 99.9999999%(九個九)可用性,成為分散式容錯系統的教科書案例。
語法/特性加入的理論與實用原因#
1. Actor 行程:輕量、隔離、訊息傳遞#
- 理論原因:Erlang 的併發模型源自 actor 模型(Hewitt 1973):行程(process)之間不共用記憶體,只透過非同步訊息傳遞溝通,理論上可免除鎖與競態條件。
- 實用原因:Erlang 行程極輕量(KB 級、數百萬個可並存),每個行程有獨立 GC,行程崩潰不影響他者;電信系統正是「大量小型對話並存」的天然 actor 問題。
- 彌補缺陷:用 C/C++ 寫容錯系統時,指標錯誤與記憶體共用會讓一個 bug 拖垮整個系統;行程隔離使錯誤可以局部化。
%% 行程建立與訊息傳遞
Pid = spawn(fun loop/0),
Pid ! {hello, self()},
receive
{reply, Msg} -> io:format("~p~n", [Msg])
end.
完整的收發迴路:行程以尾遞迴接收訊息,模式匹配決定行為——這就是 Erlang 行程的「狀態機」本質:
-module(counter).
-export([start/0, loop/1]).
start() -> spawn(counter, loop, [0]).
%% 每則訊息用模式匹配分派;狀態以函數參數(單一賦值)攜帶
loop(N) ->
receive
{incr} -> loop(N + 1); %% 尾遞迴:常數堆疊
{get, From} -> From ! {count, N}, loop(N);
stop -> io:format("共 ~p 次~n", [N]) %% 不再遞迴,行程結束
end.
%% 使用端:
%% Pid = counter:start(),
%% Pid ! {incr}, Pid ! {get, self()},
%% receive {count, N} -> io:format("~p~n", [N]) end.
對照組——用鎖保護共享計數器(Java/C++ 風格)與 Erlang 訊息傳遞的差異:
// Java:共用記憶體,必須加鎖,忘記加鎖就是競態條件
class Counter {
private int n = 0;
synchronized void incr() { n = n + 1; } // 鎖住整個物件
synchronized int get() { return n; }
}
Erlang 沒有鎖:狀態只存在於一個行程內,其他行程只能發訊息請求,天然免除競態條件。
2. 監督樹與「let it crash」哲學、OTP 框架#
- 理論原因:與其預防所有錯誤(防禦式程式設計),不如讓行程在出錯時立即崩潰,由監督者(supervisor)行程偵測並重啟它——錯誤處理回歸到「重啟到已知良好狀態」的簡單策略。
- 實用原因:1998 年前後 Ericsson 將這些模式標準化為 OTP(Open Telecom Platform)框架:supervisor、gen_server、application 等行為模組,成為工業級容錯的標準配方。
- 彌補缺陷:傳統例外處理程式碼往往比業務邏輯還多且不可靠;監督樹把容錯從「程式碼層」提升到「行程結構層」。
「let it crash」的概念碼——用 link/exit/monitor 串起監督關係,子行程崩潰由監督者重啟,而不是在業務邏輯裡包滿 try/catch:
%% 監督者:linked 子行程,崩潰時收到 {'EXIT', Pid, Reason}
supervisor(Child) ->
process_flag(trap_exit, true), %% 攔截 EXIT 訊號
receive
{'EXIT', Child, Reason} ->
io:format("子行程崩潰:~p,重啟~n", [Reason]),
NewChild = spawn_link(fun risky/0), %% 直接重啟到已知良好狀態
supervisor(NewChild)
end.
%% 子行程:不防禦,出錯就直接崩潰(let it crash)
risky() ->
receive
{div, A, B} -> A / B %% B=0 時整個行程立即崩潰
end,
risky().
%% monitor:單向觀察,監督者不隨之崩潰
%% Ref = erlang:monitor(process, Pid),
%% receive {'DOWN', Ref, process, Pid, Reason} -> ... end.
OTP supervisor 的實際樣式(宣告式定義重啟策略):
%% OTP:以 child 規格宣告監督樹,框架代管啟動與重啟
init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 5, period => 10},
Children = [#{id => counter,
start => {counter, start_link, []},
restart => permanent}],
{ok, {SupFlags, Children}}.
3. 熱程式碼替換(hot code swapping)#
- 理論原因:Erlang 的函數版本機制(兩個版本共存、行程自主切換)在理論上支援不停機更新,根源於電信交換機不能停機的需求。
- 實用原因:升級交換機軟體不需要斷線;這也使 Erlang 系統可以連續運行數年不重啟。
- 彌補缺陷:傳統系統每次更新都要停機維護,對電信等高可用性領域不可接受。
熱替換的概念碼:模組載入新版本後,新呼叫用新版、仍在遞迴中的舊行程在進入下一次遞迴時切換:
%% 載入新版模組;既有行程在下次 loop 進入時自動使用新版
code:load_file(counter). %% 編譯並載入新版本的 beam 檔
loop(N) ->
receive
{incr} -> loop(N + 1)
end.
%% 兩個版本的 loop 共存:fully-qualified 呼叫 counter:loop/1 用「新版」,
%% 尾遞迴 loop(N + 1) 用「當前版」;行程在回到頂層遞迴時切換版本
函數式核心:函數頭模式匹配 + 尾遞迴,一行一個分支,邏輯一目瞭然:
%% 以模式匹配在函數頭分支:處理串列的 map
double([]) -> []; %% 空串列:基礎情況
double([H|T]) -> [H * 2 | double(T)]. %% 尾遞迴式建構:常數堆疊
4. 函數式核心#
- 理論原因:單一賦值變數(single assignment)、模式匹配、尾遞迴,使行程邏輯易於驗證與除錯;輕量行程本身就是「狀態機」的封裝。
- 實用原因:Armstrong 在 2003 年的博士論文中論證:Erlang 原始碼的錯誤密度顯著低於對照語言,函數式核心是主因之一。
- 彌補缺陷:電信程式的並行狀態管理在命令式語言中極易出錯。
單一賦值的保證:變數一旦綁定就不可改,重複綁定直接編譯錯誤,消除一大類「狀態被意外竄改」的 bug:
X = 10,
X = X + 1 %% 編譯錯誤:X 已綁定為 10,無法重新賦值
彌補了什麼缺陷#
Erlang 彌補了用 C/C++ 建構分散式容錯系統的困難:行程隔離、監督樹、熱替換共同支撐了 AXD301 的九個九可用性。開源之後,Erlang 的影響力擴散:2009 年 WhatsApp 以少數工程師用 Erlang 撐起數億用戶的巨大併發量,成為著名案例;Elixir(2012)也建立在 Erlang VM(BEAM)之上。