[轉]Golang適合高併發場景的緣由分析


來源:http://blog.csdn.net/ghj1976/article/details/27996095javascript

做者:蟈蟈俊html



典型的兩個現實案例:

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


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億個心跳包/天,佔大多數。node

 

京東雲消息推送系統

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


C10K問題

爲何可以支撐這麼高併發的請求呢?咱們先從C10K問題提及:2001年左右的時候,有一個叫Dan Kegel的人在網上提出:現在的硬件應該可以讓一臺機器支持10000個併發的client。linux

而後他討論了用不一樣的方式實現大規模併發服務的技術。golang

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

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

固然, 現在C10K 已經不是問題了, 不論什麼一個普通的程序猿, 都能利用手邊的語言和庫, 輕鬆地寫出 C10K 的server. 這既得益於軟件的進步, 也得益於硬件性能的提升。現在應該擴展討論的是應該是C10M問題了。編程

參考資料:

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


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

不論線程仍是進程,都不可能一個鏈接建立一個。對應的成本太大。多進程和多線程都有資源耗費比較大的問題。因此在高併發量的server端使用並很少。

解決方式是一個線程或者進程處理多個鏈接,更詳細的現在比較主流的是:Coroutine模型 和 非堵塞/異步IO(callback),在分析這兩個以前,咱們先看看多進程和多線程的狀況。


多進程

這樣的模型在linux如下的服務程序普遍採用。比方大名鼎鼎的apache。

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

1234514831_ddvip_588

這樣的架構的最大的優勢是隔離性,子進程萬一crash並不會影響到父進程。缺點就是對系統的負擔太重,想像一下假設有上萬的鏈接。會需要多少進程來處理。

因此這樣的模型比較合適那種不需要太多併發量的server程序。

另外,進程間的通信效率也是一個瓶頸之中的一個。大部分會採用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的問題在於server的性能會隨着鏈接數的增多而變差 
關鍵點:性能和可擴展性並不是一回事。當人們談論規模時,他們每每是在談論性能,但是規模和性能是不一樣的,比方Apache。 
持續幾秒的短時間鏈接,比方快速事務。假設每秒處理1000個事務,僅僅有約1000個併發鏈接到server。 
事務延長到10秒,要維持每秒1000個事務,必須打開1萬個併發鏈接。這樣的狀況下:雖然你不顧DoS攻擊。Apache也會性能陡降;同一時候大量的下載操做也會使Apache崩潰。 
假設每秒處理的鏈接從5千添加到1萬,你會怎麼作?比方說。你升級硬件而且提升處理器速度到原來的2倍。

發生了什麼?你獲得兩倍的性能。但你沒有獲得兩倍的處理規模。每秒處理的鏈接可能僅僅達到了6000。你繼續提快速度,狀況也沒有改善。

甚至16倍的性能時,仍然不能處理1萬個併發鏈接。因此說性能和可擴展性是不同的。 
問題在於Apache會建立一個CGI進程,而後關閉,這個步驟並無擴展。 
爲何呢?內核使用的O(N^2)算法使server沒法處理1萬個併發鏈接。 
內核中的兩個基本問題: 
鏈接數=線程數/進程數。當一個數據包進來。內核會遍歷其所有進程以決定由哪一個進程來處理這個數據包。

 
鏈接數=選擇數/輪詢次數(單線程)。

相同的可擴展性問題,每個包都要走一遭列表上所有的socket。

 
解決方法:改進內核使其在常數時間內查找。 
使線程切換時間與線程數量無關。 
使用一個新的可擴展epoll()/IOCompletionPort常數時間去作socket查詢。

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

 


多線程


這樣的模型在windows如下比較常見。它使用一個線程來處理一個client。他的優勢是編程簡單,最重要的是你會有一個清晰連續順序的work flow。

簡單意味着不easy出錯。

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

 

線程和進程的成本

普通的線程,需要消耗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,這樣server就可以持續運轉,而不需要等待,可以使用很是少的線程,即便僅僅有一個也可以。需要按期的任務可以採取定時器來觸發。把這樣的架構發揮到極致的就是node.js,一個用javascript來寫server端程序的框架。

在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使用了語言級別的支持。使用keywordgo來啓動一個coroutine(從這個keyword可以看出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語言經過系統的線程來多路派遣這些函數的運行,使得每個用gokeyword運行的函數可以運行成爲一個單位協程。當一個協程堵塞的時候。調度器就會本身主動把其它協程安排到另外的線程中去運行,從而實現了程序無等待並行化運行。而且調度的開銷很是小,一顆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
相關文章
相關標籤/搜索