Golang適合高併發場景的原因分析

典型的兩個現實案例:

我們先看兩個用Go做消息推送的案例實際處理能力。

360消息推送的數據:

16臺機器,標配:24個硬件線程,64GB內存 
Linux Kernel 2.6.32 x86_64 
單機80萬併發連接,load 0.2~0.4,CPU 總使用率 7%~10%,內存佔用20GB (res) 
目前接入的產品約1280萬在線用戶 
2分鐘一次GC,停頓2秒 (1.0.3 的 GC 不給力,直接升級到 tip,再次吃螃蟹) 
15億個心跳包/天,佔大多數。

 

京東雲消息推送系統

(團隊人數:4) 
單機併發tcp連接數峯值118w 
內存佔用23G(Res) 
Load 0.7左右 
心跳包 4k/s 
gc時間2-3.x s

C10K問題

爲什麼可以支撐這麼高併發的請求呢?我們先從C10K問題說起:2001年左右的時候,有一個叫Dan Kegel的人在網上提出:現在的硬件應該能夠讓一臺機器支持10000個併發的client。然後他討論了用不同的方式實現大規模併發服務的技術。

http://www.kegel.com/c10k.html (英文版)

http://www.oschina.net/translate/c10k (中文翻譯版) 
http://www.cnblogs.com/fll/archive/2008/05/17/1201540.html

當然, 現在C10K 已經不是問題了, 任何一個普通的程序員, 都能利用手邊的語言和庫, 輕鬆地寫出 C10K 的服務器. 這既得益於軟件的進步, 也得益於硬件性能的提高,現在應該擴展討論的是應該是C10M問題了。

參考資料:

千萬級併發實現的祕密:內核不是解決方案,而是問題所在! 
http://www.csdn.net/article/2013-05-16/2815317-The-Secret-to-10M-Concurrent-Connections

Coroutine模型 和 非阻塞/異步IO(callback)

不論線程還是進程,都不可能一個連接創建一個,相應的成本太大,多進程和多線程都有資源耗費比較大的問題,所以在高併發量的服務器端使用並不多。解決方案是一個線程或者進程處理多個連接,更具體的現在比較主流的是:Coroutine模型 和 非阻塞/異步IO(callback),在分析這兩個之前,我們先看看多進程和多線程的情況。

多進程

這種模型在linux下面的服務程序廣泛採用,比如大名鼎鼎的apache。

下圖說明了Apache的生命週期(prefork模式)。主進程負責監聽和管理連接,而具體的業務處理都會交給子進程來處理。

1234514831_ddvip_588

這種架構的最大的好處是隔離性,子進程萬一crash並不會影響到父進程。缺點就是對系統的負擔過重,想像一下如果有上萬的連接,會需要多少進程來處理。所以這種模型比較合適那種不需要太多併發量的服務器程序。另外,進程間的通訊效率也是一個瓶頸之一,大部分會採用share memory等技術來減低通訊開銷。

apache的處理能力,下面有幾篇文章:

2008年時的數據:http://www.blogjava.net/daniel-tu/archive/2008/12/29/248883.html

http://wenku.baidu.com/view/c527582a453610661ed9f40f.html

Apache的問題

Apache的問題在於服務器的性能會隨着連接數的增多而變差 
關鍵點:性能和可擴展性並不是一回事。當人們談論規模時,他們往往是在談論性能,但是規模和性能是不同的,比如Apache。 
持續幾秒的短期連接,比如快速事務,如果每秒處理1000個事務,只有約1000個併發連接到服務器。 
事務延長到10秒,要維持每秒1000個事務,必須打開1萬個併發連接。這種情況下:儘管你不顧DoS攻擊,Apache也會性能陡降;同時大量的下載操作也會使Apache崩潰。 
如果每秒處理的連接從5千增加到1萬,你會怎麼做?比方說,你升級硬件並且提高處理器速度到原來的2倍。發生了什麼?你得到兩倍的性能,但你沒有得到兩倍的處理規模。每秒處理的連接可能只達到了6000。你繼續提高速度,情況也沒有改善。甚至16倍的性能時,仍然不能處理1萬個併發連接。所以說性能和可擴展性是不一樣的。 
問題在於Apache會創建一個CGI進程,然後關閉,這個步驟並沒有擴展。 
爲什麼呢?內核使用的O(N^2)算法使服務器無法處理1萬個併發連接。 
內核中的兩個基本問題: 
連接數=線程數/進程數。當一個數據包進來,內核會遍歷其所有進程以決定由哪個進程來處理這個數據包。 
連接數=選擇數/輪詢次數(單線程)。同樣的可擴展性問題,每個包都要走一遭列表上所有的socket。 
解決方法:改進內核使其在常數時間內查找。 
使線程切換時間與線程數量無關。 
使用一個新的可擴展epoll()/IOCompletionPort常數時間去做socket查詢。

參考:http://www.csdn.net/article/2013-05-16/2815317-The-Secret-to-10M-Concurrent-Connections

 

多線程


這種模型在windows下面比較常見。它使用一個線程來處理一個client。他的好處是編程簡單,最重要的是你會有一個清晰連續順序的work flow。簡單意味着不容易出錯。

這種模型的問題就是太多的線程會減低軟件的運行效率。

 

線程和進程的成本

普通的線程,需要消耗1M的堆棧 
http://www.cnblogs.com/PurpleTide/archive/2010/11/12/1875763.html

多進程和多線程的優缺點...  
http://blog.163.com/ymguan@yeah/blog/static/140072872201147832740/

我們知道,操作系統的最小調度單元是“線程”,要執行任何一段代碼,都必須落實到“線程”上。可惜線程太重,資源佔用太高,頻繁創建銷燬會帶來比較嚴重的性能問題,於是又誕生出線程池之類的常見使用模式。也是類似的原因,“阻塞”一個線程往往不是一個好主意,因爲線程雖然暫停了,但是它所佔用的資源還在。線程的暫停和繼續對於調度器都會帶來壓力,而且線程越多,調度時的開銷便越大,這其中的平衡很難把握。

針對這個問題,有兩類架構解決它:基於callback和coroutine的架構。

 

Callback- 非阻塞/異步IO


這種架構的特點是使用非阻塞的IO,這樣服務器就可以持續運轉,而不需要等待,可以使用很少的線程,即使只有一個也可以。需要定期的任務可以採取定時器來觸發。把這種架構發揮到極致的就是node.js,一個用javascript來寫服務器端程序的框架。在node.js中,所有的io都是non-block的,可以設置回調。

舉個例子來說明一下。 
傳統的寫法:

 <span class="kwrd" style="color: rgb(0, 0, 255);">var</span> file = open(‘my.txt’);
 <span class="kwrd" style="color: rgb(0, 0, 255);">var</span> data = file.read(); <span class="rem" style="color: rgb(0, 128, 0);">//block</span>
 sleep(1);
 print(data); //block

node.js的寫法:

 fs.open(‘my.txt’,<span class="kwrd" style="color: rgb(0, 0, 255);">function</span>(err,data){
    setTimeout(1000,<span class="kwrd" style="color: rgb(0, 0, 255);">function</span>(){
       console.log(data);
    }
 }); //non-block

這種架構的好處是performance會比較好,缺點是編程複雜,把以前連續的流程切成了很多片段。另外也不能充分發揮多核的能力。

 

Coroutine-協程

coroutine本質上是一種輕量級的thread,它的開銷會比使用thread少很多。多個coroutine可以按照次序在一個thread裏面執行,一個coroutine如果處於block狀態,可以交出執行權,讓其他的coroutine繼續執行。

非阻塞I/O模型協程(Coroutines)使得開發者可以採用阻塞式的開發風格,卻能夠實現非阻塞I/O的效果隱式事件調度,

簡單來說:協程十分輕量,可以在一個進程中執行有數以十萬計的協程,依舊保持高性能。

進程、線程、協程的關係和區別:

  • 進程擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程由操作系統調度。
  • 線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程亦由操作系統調度(標準線程是的)。
  • 協程和線程一樣共享堆,不共享棧,協程由程序員在協程的代碼裏顯示調度。

堆和棧的區別請參看:http://www.cnblogs.com/ghj1976/p/3623037.html

協程和線程的區別是:協程避免了無意義的調度,由此可以提高性能,但也因此,程序員必須自己承擔調度的責任。

執行協程只需要極少的棧內存(大概是4~5KB),默認情況下,線程棧的大小爲1MB。

goroutine就是一段代碼,一個函數入口,以及在堆上爲其分配的一個堆棧。所以它非常廉價,我們可以很輕鬆的創建上萬個goroutine,但它們並不是被操作系統所調度執行。

Google go語言對coroutine使用了語言級別的支持,使用關鍵字go來啓動一個coroutine(從這個關鍵字可以看出Go語言對coroutine的重視),結合chan(類似於message queue的概念)來實現coroutine的通訊,實現了Go的理念 ”Do not communicate by sharing memory; instead, share memory by communicating.”。

 


http://my.oschina.net/Obahua/blog/144549

goroutine 的一個主要特性就是它們的消耗;創建它們的初始內存成本很低廉(與需要 1 至 8MB 內存的傳統 POSIX 線程形成鮮明對比)以及根據需要動態增長和縮減佔用的資源。這使得 goroutine 會從 4096 字節的初始棧內存佔用開始按需增長或縮減內存佔用,而無需擔心資源的耗盡。

爲了實現這個目標,鏈接器(5l、6l 和 8l)會在每個函數前插入一個序文,這個序文會在函數被調用之前檢查判斷當前的資源是否滿足調用該函數的需求(備註 1)。如果不滿足,則調用 runtime.morestack 來分配新的棧頁面(備註 2),從函數的調用者那裏拷貝函數的參數,然後將控制權返回給調用者。此時,已經可以安全地調用該函數了。當函數執行完畢,事情並沒有就此結束,函數的返回參數又被拷貝至調用者的棧結構中,然後釋放無用的棧空間。

通過這個過程,有效地實現了棧內存的無限使用。假設你並不是不斷地在兩個棧之間往返,通俗地講叫棧分割,則代價是十分低廉的。

 

簡單來說:Go語言通過系統的線程來多路派遣這些函數的執行,使得每個用go關鍵字執行的函數可以運行成爲一個單位協程。當一個協程阻塞的時候,調度器就會自動把其他協程安排到另外的線程中去執行,從而實現了程序無等待並行化運行。而且調度的開銷非常小,一顆CPU調度的規模不下於每秒百萬次,這使得我們能夠創建大量的goroutine,從而可以很輕鬆地編寫高併發程序,達到我們想要的目的。

 

Coroutine模型 和 非阻塞/異步IO(callback)性能對比

從性能角度來說,callback的典型node.js和golang的性能測試結果,兩者差不多,參考下面測試數據:

http://www.cnblogs.com/QLeelulu/archive/2012/08/12/2635261.html

不過從代碼可讀性角度來說,callback確實有點不太好。

 

 

 

參考資料:
風格之爭:Coroutine模型 vs 非阻塞/異步IO(callback)
http://blog.csdn.net/kjfcpua/article/details/15809703

Goroutine(協程)爲何能處理大併發?
http://www.cnblogs.com/ghj1976/p/3642513.html
python Eventlet 
http://www.360doc.com/content/14/0522/00/8504707_379786818.shtml
爲什麼我認爲goroutine和channel是把別的平臺上類庫的功能內置在語言裏
http://blog.zhaojie.me/2013/04/why-channel-and-goroutine-in-golang-are-buildin-libraries-for-other-platforms.html
Go-簡潔的併發
http://www.yankay.com/go-clear-concurreny/
GOROUTINE性能測試
http://www.kankanews.com/ICkengine/archives/115285.shtml
Golang特性介紹
http://mryufeng.iteye.com/blog/576968/
併發編程
http://book.2cto.com/201301/14436.html
原文:http://blog.csdn.net/ghj1976/article/details/27996095
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章