這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
在傳統的伺服器編程模型中,我們會為每一個請求分配一個線程,請求結束後終止該線程,或者把線程放回線程池。 Java 的 Servlet 就屬於這種模型的典型。對於 PHP, Ruby, Python 這些語言,要麼對於多線程支援不好,要麼在多線程下表現不好,通常我們會為每個請求分配一個進程,但整體模型都差不多。
通常我們的請求並不是一直在消耗CPU, 一個請求的大部分時間都花在等待磁碟,資料庫,cache,其他服務返回上面,或者說等待一個事件發生(比如 web 版的 IM, 通常會建立一個 HTTP 要求,等待其他人給你發訊息)。 所以對於一個8核的 CPU,支撐100個並發,CPU消耗並不大,負載也不會太高,甚至 1000 左右的並發請求數也不會有大問題。但在高並發下,首先是由於線程數/進程數過多消耗了太多的記憶體資源,同時大部分CPU都消耗在環境切換上邊,所以系統負載很高,伺服器效率大幅度降低,服務響應變慢甚至不可用。
為了讓伺服器支援高並發,通常我們需要一個新的架構來處理這些請求,比如 Java 這邊就有 comet和netty, python 有 tornado, ruby 有 eventmachine。還有很多直接基於 epoll 系統調用, 或者基於 libevent, libev, libcoro的應用。
在解決這個問題時,發展出了 reactor[1] 和 proactor[2] 兩種設計模式, 關於 reactor 模式,可以參考一下 libevent, netty 的編程範例。關於 proactor 模式,可以參考一下 node.js, Java NIO.2[3] 的編程範例,在此不做解釋。對於 libcoro, jscex[4],從實現層面上仍然可以認為是 proactor 模式,但避免了寫大量的 callback 方法,所以更加易用。
go 語言用於解決高並發的技術被成為 goroutine[5], 與 erlang 的 process 類似,但仍然有如下的不同
- erlang 接收訊息的主題是 erlang process, go 語言是 channel
- 一個 goroutine 可以引用多個 channel, 同樣一個 channel 也可以被多個 goroutine 引用 (對於常規的生產者-消費者模型, goroutine 比 erlang 方便)
- channel 屬於強型別,在編譯期可以檢查出更多的錯誤,如果一個 goroutine 希望接收多種類型的訊息,一般可以引用多個channel
- channel 可以設定為堵塞型或者設定一個 buffer size, 並且可以用select測試 channel 是否已滿, erlang 則無法測試(?)
在編程模型上, goroutine 應該屬於reactor 模式,但與常規的 reactor 模式有很大的不同。常規的 reactor 模式中,每處等待都需要佔用一個線程,所以通常我們只會使用 1-2 個線程來做等待(比如接收請求處放一個,在非同步作業時放一個),在等待處再根據事件類型來進行事件分發和後續處理。但由於我們需要區分所有的事件,通常我們會設計出一組單根,複雜的事件類型(比如 Java 下),這對編程人員來說很不利,特別是需要各個模組協作的時候,同時在等待處會出現代碼龐大,商務邏輯集中在一處的問題。對於 goroutine, 通常每個 goroutine 會有自己的 reactor 代碼(即 select),更多是通過語言方面的機制來避免每處 select 佔用一個線程, 商務邏輯被分解到各個 goroutine 內部,無需集中在一處,對開發有利。
[1] http://en.wikipedia.org/wiki/Reactor_pattern
[2] http://en.wikipedia.org/wiki/Proactor_pattern
[3] http://jcp.org/en/jsr/detail?id=203
[4] https://github.com/JeffreyZhao/jscex
[5] http://golang.org/doc/GoCourseDay3.pdf