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
end

Supervisor 定義了當子 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 不是好選擇。

補充 S02:為什麼 Rust 的 borrow checker 對後端重要