1986 年,Ericsson 的工程師 Joe Armstrong 有一個問題:
電信交換機不能停機。「不能停機」不是比喻——電話系統每年只能有幾秒鐘的中斷(nine 9s: 99.9999999%)。軟體 bug 不可避免,硬體故障不可避免,你需要在「有錯誤發生」的前提下設計系統,而不是假設系統不會出錯。
Erlang 就是為這個問題設計的。三十年後,WhatsApp 用 Erlang 讓每台伺服器撐兩百萬並行連線,Discord 用 Elixir 服務幾千萬個即時連線。
Erlang 的並行模型,和主流語言(Go、Java、Python)的根本差異不是語法,而是「崩潰應該怎麼處理」的哲學。
輕量行程(Lightweight Process)
Erlang 的基本並行單元叫 process,但和 OS process 完全不同:
- OS process:記憶體 MB 等級,建立需要 ms
- Erlang process:記憶體約 300 bytes 起,建立需要 μs
pid = spawn(fn ->
receive do
{:hello, name} -> IO.puts("Hello, #{name}!")
end
end)
send(pid, {:hello, "World"})一台普通的機器可以同時跑幾百萬個 Erlang process。每個 process 有自己的 heap,GC 在個別 process 層面獨立跑(不是全局 GC),GC pause 分散而不是全局暫停。
沒有共享記憶體
所有通訊都透過訊息傳遞(message passing)。Process 之間不共享記憶體,也不能直接讀寫對方的狀態。
# 訊息傳遞是唯一的跨 process 通訊方式
send(pid, {:update, new_value})
receive do
{:result, value} -> handle(value)
after 5000 -> handle_timeout() # 超時處理
end沒有共享記憶體,就沒有 mutex、沒有 deadlock、沒有 race condition。
代價:訊息傳遞有序列化/複製的成本(訊息在 process 之間是複製的,不是共享指標)。在需要傳大量資料的場景,這個成本是真實的。
Let It Crash:崩潰是正常的
Erlang 最獨特的哲學:不要寫防禦性程式碼,讓 process 在遇到它不懂得處理的情況時崩潰,讓 supervisor 重啟它。
defmodule Worker do
def process(data) do
# 如果 data 格式不對,pattern match 失敗,process crash
{:ok, result} = transform(data)
result
end
end這不是粗心,而是設計:Worker process 崩潰,不影響任何其他 process(沒有共享狀態);Supervisor 監控 Worker,崩潰後立刻重啟;重啟的 process 是新的,不帶任何損壞的狀態。
對比傳統做法:
# 傳統:到處寫 try/except,防止錯誤擴散
def process(data):
try:
result = transform(data)
return result
except Exception as e:
logger.error(e)
return None # 用 None 表示「有東西出錯了」用 None 的問題:你在一個可能損壞的狀態裡繼續運行,後續的 None propagation 讓 debug 更難。
OTP Supervisor Tree
Erlang 的 OTP(Open Telecom Platform) 是一套設計模式,最核心的是 Supervisor tree:
defmodule MyApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
children = [
MyApp.Database,
MyApp.Cache,
{MyApp.WorkerPool, size: 10},
]
Supervisor.init(children, strategy: :one_for_one)
end
endSupervisor 定義了當子 process 崩潰時怎麼處理(strategy):
:one_for_one:只重啟崩潰的那個,其他繼續:one_for_all:任何一個崩潰,全部重啟:rest_for_one:崩潰的那個和它之後定義的都重啟(用於有依賴順序的服務)
整個系統是一棵樹,根部是頂層 Supervisor,葉節點是 Worker process,中間節點是子 Supervisor。系統的「崩潰後能活著」的保證,由這棵樹的設計來提供。
熱更新(Hot Code Reloading)
Erlang 能在不停止 process 的情況下更新程式碼——這在電信系統裡是必要的,因為你不能讓電話在升級的時候中斷。
# 生產環境運行中
1. 上傳新版本的 .beam 檔(Erlang 的 bytecode)
2. VM 載入新版本,舊 process 繼續用舊版本
3. 新的訊息用新版本處理
4. 所有 process 遷移到新版本
這在 Go、Python、Java 裡幾乎不可能優雅實作——你通常是 rolling restart(逐步替換行程),而不是真正的熱更新。
Elixir:現代的 Erlang 生態
Elixir(2012)是跑在 Erlang VM(BEAM)上的語言,繼承了 Erlang 的所有並行模型優點,但語法更現代:
# Elixir 的 Phoenix LiveView:用 GenServer 管理每個連線的狀態
defmodule MyAppWeb.CounterLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
end每個 LiveView 連線是一個 GenServer(Erlang 的標準 server process 模式),狀態在 process 裡,崩潰重啟,沒有共享狀態的複雜度。
何時 Erlang/Elixir 的模型是最好的解
Erlang/Elixir 不是每個問題的最佳解,但在幾個場景它的模型特別適合:
- 需要九個 9 可用性的系統:supervisor tree + let it crash 是最成熟的 fault-tolerance 模型
- 大量並行連線,每個連線有獨立狀態:即時聊天、多人協作、遊戲伺服器
- 需要熱更新:電信、金融交易系統
- 分散式系統:Erlang 的 process 可以跨節點,
send(pid, msg)對本地和遠端行為相同
對計算密集型(ML training、圖片處理)、需要大量 shared memory 的場景,Erlang 不是好選擇。