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

 

 

多線程


這種模型在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的,可以設置回調。

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

 var file = open(‘my.txt’);
 var data = file.read(); //block
 sleep(1);
 print(data); //block

node.js的寫法:

 fs.open(‘my.txt’,function(err,data){
    setTimeout(1000,function(){
       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),從函數的調用者那里拷貝函數的參數,然后將控制權返回給調用者。此時,已經可以安全地調用該函數了。當函數執行完畢,事情並沒有就此結束,函數的返回參數又被拷貝至調用者的棧結構中,然后釋放無用的棧空間。

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

 

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




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM