如何看待許式偉談Go Erlang並發編程差異?

《再談CERL:詳論GO與ERLANG的並發編程模型差異》-許式偉

[ECUG專題回顧]《再談CERL:詳論Go與Erlang的並發編程模型差異》


這次的 ECUG 我有參加做了分享 http://weibo.com/5140853887/BErNE2xdj

現在的 ECUG 有多人都是推崇 Go 的,但是我們 雲巴 目前後端主要用 Erlang,本想到 ECUG 想跟大家分享下我們採用 Erlang 的心得,為什麼暫時沒有大量用 Go,但是沒有實現。

  • 不執迷

首先說明下,我本人不執迷於任何東西,包括語言。現在 雲巴 之所以採用 Erlang 我們同樣是基於很多現實的考慮。

  • 什麼語言/平台好?

幾乎所有的語言都是為了解決一些特定場景的問題而被發明的。除了 彙編語言、C 這兩種最基本的系統級別的語言,它們主要用來編寫操作系統、其他語言。

所以討論一個 語言/平台 好與不好,要針對具體的場景。

  • 雲巴 的場景

雲巴 主要是為了解決跨平台的實時數據交換的問題,包括手機 App、Web、PC、智能設備等。定位為一個大並發、軟實時、高可靠系統。

具體我們的系統架構我在 InfoQ 的活動上做過演講:實時系統架構與實踐

我們需要同時處理大量並發,處理過程涉及到多個模塊的 IO 通信。這個場景目前能用的解決方案大致可以有:

  1. 非同步 IO。比如 libev,node.js
  2. 輕量級線程、進程平台。比如 Erlang, Akka, Go

我們團隊基於 libev 開發過東西,效果很不錯,但是涉及到多個模塊 IO 做起來工作量非常巨大。

系統的第一個版本我們採用的 node.js,node.js 開發效率很不錯,但是在處理海量並發時, javascript 語言本身還是太沉重,後來我們決定切換到輕量級進程的平台。

  • 為什麼用 Erlang/OTP

我們最先從比較 Go、Erlang 開始,基於一些具體考量選擇了 Erlang,我的博客有介紹:Go vs Erlang

簡單的總結促成我們現在選擇 Erlang 的具體原因:

  1. OTP 的容錯能力。Supervisor, Worker 的架構,Go 目前沒有。
  2. Erlang 的 process 有獨立狀態。有沒有獨立狀態本身無所謂好與不好,只是我們大部分場景需要每個 process 維護一個狀態。
  3. Erlang 的 process 有獨立身份。對於我們這個需要長時間不間斷運行的系統,可以隨時探測某一個 process 的運行情況很重要,Erlang 可以很方便通過 Pid,registered name 跟具體 process 通信。
  4. Erlang process 有獨立 GC。Go/Java 的全局 GC 在系統壓力巨大時,都會造成抖動,在我們的場景可能會造成很多請求超時。Erlang 每一個 process 有獨立 的 GC,一個 process 發生 GC 時不會造成其他 process 掛起。
  5. Erlang/OTP 的網路能力。我們很方便通過 process pool 把海量任務動態分配給不同伺服器處理。Go 在這方面完全沒有涉及。

當然 Erlang 是動態語言,它在執行效率、內存用量上對比 Go 有明顯劣勢。對於大型系統,編寫代碼時,動態類型也帶來諸多不便。

總體來說,Go 目前除了有 goroutine 解決同時處理大量任務的問題,在一個高可用、大並發、軟實時、基於集群的實際產品里需要面臨的諸多問題,不像 Erlang 那樣試圖去解決。

感覺 Go 更多還是定位為一個類似 C++ 的通用語言,而 Erlang 是專為編寫大並發的通訊系統設計的。

  • Erlang 的 IO 問題

演講中老許提到了多次 Erlang 還是需要用到非同步 IO。以我們 雲巴 的實際情況,我們會用 process pool 去處理,每個 process 都是採用的同步 IO,沒有使用非同步 IO。依賴 Erlang 的調度器有效分配 CPU 時間。


先吐槽:許大牛別天天光cerl這個那個模型了,都7年時間了,不開源代碼,不講數據,還把它拉到erlang和go層面比較。從我對erlang和go語言實現層面的理解,cerl實在不在一個層上,甚至難聽點就是一坨狗屎。輸了我余字倒著寫。理由是erlang單進程調度實現至少有二萬行c代碼,從第一版發布到現在超過9年,還在大規模完善調整,時不時還爆出bug。我自己早年也做了很多這樣的狗屎,深有體會做類似基礎框架的難度。

大家做技術的,show me the code 才是最真的,一堆雲里霧裡東西確實沒看懂,從文章對Erlang消息和調度工作原理的表述離譜到到想糾正都不知道從何入手,照理說許大牛不應該犯這種錯誤的。

再,語言和系統存在的目的是為了解決問題,都可以找到最適合自己的場景。erlang存在20多年繼續往前持續改進,有自己鮮明的特點,會走他自己的路。我們能做的是在路上一起完善系統,而不是費時在這口水。事實上我們很多工具類的系統也是用go實現的。

另:我們開源了erlang多pollset模塊,對網路密集型的後端伺服器性能提升很大:https://github.com/alibaba/erlang_multi_pollset


Erlang為什麼沒有鎖呢?實際上Erlang的伺服器是單進程(Process)的,是邏輯上就無並發的東西。一個Process就是一個執行體,所以Erlang的伺服器和Go的伺服器不一樣,Go的伺服器必然是多進程(goroutine)一起構成一個伺服器的,每個請求一個獨立的進程(goroutine)。但是Erlang不一樣,一個Erlang伺服器是一個單進程的東西,既然是一個單進程的首先所有的並發請求都進入了進程郵箱(後面會談這個進程郵箱),然後這個伺服器從進程郵箱裡面取郵件(請求的內容)然後處理,所以Erlang的單個伺服器並沒有並發的請求,這個是他不需要鎖的根本原因,其實並不是因為它沒有變數,變數不可變這些。因為大家都知道單線程的伺服器一定是沒有鎖的。那麼可能會有人問,那Erlang怎麼做高並發呢?其實是兩點:第一是每個Erlang物理的進程會有很多的伺服器,每個伺服器相互是無干擾的,它們可以並發。第二是單伺服器想要高並發怎麼辦?Erlang對這個問題的回答就是請非同步IO。

這裡的process應該指erlang process而不是os process,否則不應該與goroutine相提並論。但erlang伺服器怎麼可能只使用一個erlang process呢?每個請求一個獨立的erlang process也是erlang的通常做法,和go的做法一樣。

golang通常這樣寫

package main

import (
"log"
"net"
)

func main() {
ln, err := net.Listen("tcp", ":8989")
if err != nil {
log.Fatal(err)
}
for {
conn, err := ln.Accept()
if err != nil {
continue
}
go func() {
// handle connection
_ = conn
}()
}
}

erlang不用OTP,可以這樣

-module(a).
-export([start/0]).

start() -&>
{ok, Ln} = gen_tcp:listen(8990, [binary]),
accept(Ln).

accept(Ln) -&>
{ok, Conn} = gen_tcp:accept(Ln),
spawn(fun() -&> handle(Conn) end),
accept(Ln).

handle(Conn) -&>
% handle connection
ok.

模型上沒有區別。erlang不需要鎖是因為erlang process間不允許共享內存,僅此而已。

既然是一個請求一個process處理,那處理I/O就和go一樣,用同步的方法就可以了。這個前提不成立,那後面幾段討論非同步I/O和鎖之類的,就毫無意義了。

最後一個細節是我剛剛講過的次重要的概念,它是 Erlang的進程郵箱,所有發給Erlang進程的消息都會發到這個進程郵箱,Erlang提供郵箱收發消息的元語。Go則提供了channel這樣的通訊設施,這個channel可以輕易創建很多個,然後用它進行進程通訊。相比之下,Go的消息機制抽象更輕盈。消息隊列和進程是完全獨立的設施。

erlang是弱類型語言,所以mailbox里可以放任意類型的消息,通過selective receive來讀取特定的消息,所以不需要創建多個,一個就足夠了。go是強類型語言,一個channel通常只傳遞一種消息,多種消息通過多個channel傳遞。這兩個設計都無所謂誰優誰劣。

那麼最後一個問題,Erlang中是不是可以實施Go的並發模型?在Go裡面實施Erlang的並發模型是比較容易的,但是反過來想Erlang裡面可不可以實現Go的並發模型呢?原則上是不能。因為在Erlang當中進程不能實現共享狀態,這個是他反對鎖的最重要的基點。進程不能共享狀態,所以不用鎖,但其實我認為這個是最大的問題,為什麼呢?因為Erlang收到請求以後沒有辦法創建一個子的執行體,然後讓它處理某一個具體的請求不用再管它。但是Erlang裡面進程沒有共享狀態,你要改伺服器狀態必須用非同步IO的方式,把事情做了再把消息扔給伺服器對他說你自己改狀態。通過消息改伺服器狀態,這個成本是比較大的,而且帶來了很多問題。所以我認為Erlang的用消息改這個狀態是不好的做法,繞了一大圈沒有本質改變任何的東西。當然,如果我在Erlang裡面非要做到Go的並發模型也可以,這需要對Erlang做一個閹割,如果我們讓Erlang的伺服器都無狀態的話,是可以實施Go的並發模型。什麼樣的伺服器是無狀態的?大家可能很容易想到PHP伺服器。它把狀態交給所有的外部的存儲服務,由存儲服務來維持狀態。如果說Erlang的伺服器是無狀態的是可以實施Go的並發模型,因為所有的狀態都通過修改外部的存儲。但是這樣的話Erlang程序員肯定是很傷心,看起來Erlang語言並沒有帶來什麼實質性的好處。所以我的結論是:是時候放棄Erlang了。

erlang process之間共享狀態可以:

直接傳遞消息

使用ETS

使用Mnesia

不能共享的是內存,也就是不能操作同一塊內存,不是不能共享狀態。

erlang收到請求後,可以spawn一個erlang process進行處理,「不用再管它」,上面的代碼已經演示了。


他從來都是只談優點不然坑,讓我說坑爹呢。


幾年前參加給一次技術聚會,老許和霸爺都是主講,都在講erlang,其樂融融的樣子。唉,當初叫人家小甜甜,現在叫人家牛夫人。


這篇文字最大的問題是對很多概念的混淆不清。例如作者說"Erlang 伺服器是單進程的。」 什麼是"Erlang伺服器"?是以Erlang為支撐的網路伺服器還是Erlang虛擬機?連概念都交代不明白還討論什麼技術問題?

所以說科班訓練和學歷水平確實有它的用處,至少可以決定一個人是不是一張嘴就是一股民科味兒。


這哥們中邪了,對golang有種宗教狂熱分子般的偏激。雖然我個人覺得golang用用還不錯。

=================補充一下,再多說兩句其他的==============================

每個社區都有自己的文化,其中golang社區有種很搞笑的特點:====&>&>&>過度自戀,甚至稱為自戀狂也不過分。不信綜合看看國內國外各個golang 開發人員/愛好者 的言論/演講(不是說每個人都這樣,只是說整體上)。這個社區總有種,就他們聰明,就他們知道啥是好的,把其他人當SB的優越感,點個大的,rob pike就是典型。至於很多愛好者,特別是國內的一些同學(這裡並非針對某個個體),表現的更像星宿派教眾。不信看看這些人的weibo。最後說一句,我個人覺得golang還不錯,最大的優點就是簡單實用,就這樣。


最近回去看 erlang 和 elixir,嘗試分析看看老許的這篇文章。請霸爺 @余鋒 分析指正。不耐心看的,我提前總結下要點,就是說閱讀老許這篇文章,如果沒有對 erlang 有所了解,會帶來相當大的誤導。

Erlang為什麼沒有鎖呢?實際上Erlang的伺服器是單進程(Process)的,是邏輯上就無並發的東
西。一個Process就是一個執行體,所以Erlang的伺服器和Go的伺服器不一樣,Go的伺服器必然是多
進程(goroutine)一起構成一個伺服器的,每個請求一個獨立的進程(goroutine)。但是Erlang
不一樣,一個Erlang伺服器是一個單進程的東西,既然是一個單進程的首先所有的並發請求都進入了進
程郵箱(後面會談這個進程郵箱),然後這個伺服器從進程郵箱裡面取郵件(請求的內容)然後處理,
所以Erlang的單個伺服器並沒有並發的請求,這個是他不需要鎖的根本原因,其實並不是因為它沒有變
量,變數不可變這些。因為大家都知道單線程的伺服器一定是沒有鎖的。那麼可能會有人問,那Erlang
怎麼做高並發呢?其實是兩點:第一是每個Erlang物理的進程會有很多的伺服器,每個伺服器相互是無
干擾的,它們可以並發。第二是單伺服器想要高並發怎麼辦?Erlang對這個問題的回答就是請非同步
IO。

這段話,很大的問題是對於不了解 erlang 的人容易造成誤導,這裡提到的 Erlang 伺服器,其實應該指的是 GenServer 這個 behavior,而非我們普通理解意義上的所謂伺服器程序,其次所謂單進程 Process 也應該強調指出是指 Erlang 的輕量級 Process,而不是我們通常說的操作系統進程。不了解 erlang 的人看到這段話,還以為 erlang 這麼挫,伺服器單進程,多進程並發,原來是類似 apache 進程模型?

如果這樣理解就大錯特錯了。GenServer 是 Erlang OTP 提供的框架,他負責啟動一個 Erlang 的輕量級的 Process,維護一個狀態,通過 mailbox 收發處理消息來改變這個狀態。而 erlang 的 process 有多輕量級呢?一個數據,啟動 100 萬的 Erlang Process 進行首尾相接的消息傳遞,在我的 MPB 15 最新型機器上只要 7.1 秒(Elixir process chain test. 路 GitHub)。其次,Erlang 的物理進程可以支持無限量(理論是內存上限)的這樣的輕量級 process 啟動、運行、銷毀,這些 procss 由 Erlang VM 負責調度、執行,而 Erlang 的調度器是經過長期演化的,他的優秀毋庸置疑(參見 Erlang的調度原理(譯文)。 Erlang 在 06 年就支持 SMP 將調度器擴展到多核,充分利用 CPU。

關於調度器需要多說一點, Erlang 的調度器是搶佔式的,當某個 process 消耗躲過的時候,他會會被搶佔,調度器分配給其他 process 執行。這是很多協作式 coroutinue 實現無法解決的問題。這也是 Erlang 號稱軟實時的基礎。

繼續分析第二段:

第二是單伺服器想要高並發怎麼辦?Erlang對這個問題的回答就是請非同步IO。

但是非同步IO給Erlang帶來了什麼麻煩呢?首先是伺服器狀態變複雜了,這個複雜是非常非常要命的,
這導致我最後認為Erlang一旦引入了非同步IO之後,其實比正統的非同步IO編程模型還要糟糕。我們看幾

這裡老許提出的問題,雖然我沒有 Erlang 實戰經驗,但是也在 Erlang 社區內看到幾次有人抱怨 Erlang 單個 process 的處理能力有限,特別是 Erlang 的文件模塊,本質上也是一個 process 在處理,導致這個單進程性能似乎很成問題。從 Erlang 社區給出的解決方案,任務可以並行化的,可以用 poolboy 這樣的進程池(devinus/poolboy · GitHub)方案來並發加速,請注意上文描述, Erlang process 的代價是如此之輕,你要完全改變過去的認知,process 不再代價昂貴,需要的用的時候儘管使用,並且在 process 里採用同步的編程模型。其次,網路型應用,可以採用非阻塞 IO 模型( gen_tcp 的 async 模式),請注意非阻塞 IO 並非非同步 IO。非阻塞模型的問題就是所謂狀態機的引入,但是 Erlang 提供了 gen_fsm 這樣的方案,加上 binary 語法以及 pattern matching的強大威力,其實編寫這個狀態機的複雜度是極大降低了。

Golang 的部分不予評論,因為我對它並沒有深入做過了解,稍微的讀過的資料和文章給我的感覺就是一個類似 lua 的 coroutine 實現(multithreading),加上一個類似隊列緩衝區的 channel 機制。但是他沒有像 coroutine 明確地提供 yield/resume 這樣的操作,而是將這樣的協作操作『隱藏』在了 channel 的入隊出隊、IO 讀寫等『系統』調用里。Golang runtime 幫你識別或者說規定出可能的『阻塞』操作,並提供協作。訂正: goroutine 從 1.2 開始也加入初級的搶佔式調度(搶佔式調度 | 深入解析Go),應該也是意識到了協程調度存在的問題。

最後一個細節是我剛剛講過的次重要的概念,它是 Erlang的進程郵箱,所有發給Erlang進程的消息
都會發到這個進程郵箱,Erlang提供郵箱收發消息的元語。Go則提供了channel這樣的通訊設施,這
個channel可以輕易創建很多個,然後用它進行進程通訊。相比之下,Go的消息機制抽象更輕盈。消息
隊列和進程是完全獨立的設施。

這一點我不是很贊同, Erlang 的 mailbox 基於模式匹配的 selective receive 明顯強大得多。

update: 修改引用格式,訂正拼寫。


同時黑了我C++和Erlang。

許世偉的意思是Go有鎖,所以Go是先進的,Erlang都是消息,你要自己互斥,所以是落後的。C++無法導出一致的編程模型,也是落後的。即使這是對的,但Erlang也好,C++也好,這兩個可不只是用於這一種常見,所以,請不要再黑我大C++和Erlang了。


Talk is cheap, show me the benchmark.


他幾乎不懂Erlang


我印象中這位兄台每次討論會的主題都是這個啊:原來如何如何愛Erlang,Go如何的好,如何的從Erlang切換到Go來。道理可能沒錯,但是說多了意思就不大了。另外作為一個CTO,討論技術問題的維度總是在語言層面,似乎不夠高吧?


作者 @Belleve為什麼 [X 語言] 比 [Y 語言] 更好

最近我聽到很多人談論 [X 語言] 和 [Y 語言]。總結起來,這兩種 [平台] 上的 [編程範式] 語言,他們提倡的 [風格] 式編程能讓你在 [寫垃圾代碼] 的時候更加靈活。

在用兩種語言寫出 [很簡單的小應用不超過 10 萬行] 之後我有了自己的感受。考慮一下這個問題:[給北大青鳥程序員當作業的問題],如果用 [Y] 寫出來將會是這樣:

[很垃圾的 Y 代碼]

而用 [X] 寫呢,將會是這樣:

[仔細寫出的 X 代碼,要 show 語法糖!]

高下立判。

再比較下它們的類型系統。[關於動態類型 / 靜態類型的萬用對噴用詞。]沒錯,[Y] 向你提供了 [Y 的類型系統的好處],然而這要付出 [Y 的類型系統的壞處] 這樣慘痛的代價,值得嗎?

此外比較一下構建工具。當 [Y] 還在用 [一眼都沒看過的工具] 的時候,[X] 已經有了更加先進的 [剛剛知道怎麼用的工具],這已經足夠讓你投入 [X] 的懷抱了。


後,來比較下它們的完成度。[X] 有 [還在 Pre-Alpha 的 X 專用 IDE],而且還可以和 [已經有 50
年悠久歷史,快捷鍵如拳皇出招表的編輯器]、[某個人人都恨的 IDE] 完美集成。儘管 [Y] 也有類似的東西,但是使用它非常的生澀和痛苦。

總結起來,儘管 [平台] 這山能容二虎,不過若想讓這世界更美好的話,我還是希望 [Y] 程序員可以放棄 [Y] 轉行搞 [X],與我們共同進步。當然 [Y] 也不是那麼不堪,它還是挺贊的不是嗎?


你們爭什麼爭呀,erlang和go都沒有php好,PHP才是世界上最好的語言,沒有之一。:)


請允許我轉下 dcaoyuan 先生在微博上的評論:

基本同意文中對進程概念的描述。不過我傾向於「不共享狀態」。誠然,「狀態」是我們處理的主要對象,但狀態總是有主語的,也即狀態總是應該屬於某個主體的,當然也是有不同級別的,Actor 模式的好處就是 actor hold 自己的狀態。不管是很細粒度的 actor,還是 node 級別的,還是 cluster 級別的。

對於某個主體(actor),其狀態只通過消息驅動是最自然的並行、分布處理方式。如果別的 actor 需要了解,狀態可以全部或部分用消息傳遞出去,或者說,所謂的共享其實只是共享了非同一條河流的瞬時狀態,這個瞬時狀態一旦傳遞出去就是消息而已。

對不同級別的狀態, Akka 設計了不同的 actor pattern,比如 cluster 級別的狀態可用 ClusterSingleton actor,除了可以查詢,也可以通過 gossip 等方式傳播到每個 node 一遍就近查詢,甚至廣播到某類 actors。Akka 的 Actor 模式在我看來是最接近業務領域本身的概念、從而也是最自然的模式。


Erlang 最大的缺點是,那個語法太亂了 ,寫出來的代碼根本沒法看


分頁阅读: 1 2