這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
因為 雲巴 系統對高並發、低延遲的需求,我們對各個語言、平台做了很多的調研比較工作。這自然就包括致力於開發高並發應用的 Go 和 Erlang。
並發
Go 對高並發的支援通過 goroutine 實現。goroutine 可以理解為輕量級的 線程(thread)。同一個 Go 應用建立的 goroutine 共用地址空間。
Erlang 的高並發通過輕量級 進程(process)實現,每一個進程都有獨立的狀態記錄。
另外,使用 goroutine 要注意,goroutine 運行完畢後,佔用的記憶體放回記憶體池備用,不會釋放。
對於每一個任務都需要有獨立狀態的情境,Erlang 的 process 更有優勢。
搶佔式調度
Erlang 的任務調度器有一個 reduction budget 的概念。進程的任何操作都會造成預算消耗,包括 函數調用、調用 BIF、進程堆記憶體回收、ETS 讀寫、發訊息(目標郵箱堆積的訊息越多,消耗越大)。Erlang 的 Regex庫 也被做了修改以支援 reductions。所以如果進程在長時間執行Regex匹配,也一樣會消耗 reductions,也會被搶佔。
Go 之前的調度器只在 syscall 發生時調度,最佳化後可以在任何函數調用時調度。但是要注意,如果在 goroutine 裡寫一個死迴圈,Go 的調度器不能有效搶佔,同一個調度器的 其他 goroutine 會被掛起。
記憶體回收
像 Java 一樣,Go 的記憶體回收是全域的,這意味著一旦記憶體回收被觸發,所有的 goroutine 都會被暫停,造成一段時間的業務延遲。
Erlang 的記憶體回收是 進程 層級的,每一個進程都有自己獨立的記憶體回收行程,一個進程的記憶體回收被觸發,不會造成其他進程被掛起。相對來說帶來的業務延遲小。
錯誤處理
Erlang 的每一個進程都有 進程 ID (PID),同時也可以給進程註冊名字,也就是說每一個進程都有獨立的身份,可以有效監控每一個進程的狀態。進程異常退出時,可以捕捉到退出事件,並重啟進程(參見 otp 的 supervisor/worker)。
Go 的 goroutine 沒有身份識別,goroutine 的狀態沒辦法監控。
動態反射
Erlang 動態語言的特點,使它天然支援 REPL,另外 Erlang 支援 remote shell,我們可以在 Erlang 運行時,串連到 remote shell 與任何一個進程互動。這些特性對一個需要長期啟動並執行複雜系統的維護帶來了極大的便利。開發階段也能有一些便利。
Go 是靜態語言,不支援 REPL。
靜態編譯
Erlang 是動態語言,有所有動態語言的所有缺點:
- 運行速度慢
- 不能做早期錯誤檢查,需要依賴全覆蓋單元測試
- 代碼規模大了,給編寫帶來困擾
Erlang 現在也引入了 spec,對函數的參數傳回值在編譯時間做類型檢查,但是跟靜態語言比起來效果差的很遠。
不過正是因為是動態語言,Erlang 實現了運行時代碼替換,這個特性對一個需要長時間啟動並執行工業級產品,是一個非常重要的功能。
Go 是靜態語言,運行速度快,編譯時間做嚴格的類型檢查,可以避免很多隱患。
架構
Erlang 的 OTP 架構支援伺服器端開發常見的幾種模式(applications, supervisors, wokers),方便代碼的組織。
Go 暫時沒看到類似的架構。
第三方庫支援
Go 是一個相對比較新的語言,雖說現在很多項目都開支援 Go,但很多第三方庫的成熟度等級暫時不如 Erlang。
總結
對於有求低延遲、高並發的後端服務,我們近期還是採用 Erlang 為主。但使用 Erlang 的過程中,Erlang 缺乏靜態檢查的手段,也是一個很麻煩的問題,目前的做法是要求大家都使用 IntelliJ IDEA 編寫代碼,可以通過 IDE 提前發現部分語言問題。
同時我們會持續關注 Go 的發展。
weibo: @Tiger_張虎