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的,能夠設置回調。
舉個例子來講明一下。
傳統的寫法:
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),從函數的調用者那裏拷貝函數的參數,而後將控制權返回給調用者。此時,已經能夠安全地調用該函數了。當函數執行完畢,事情並無就此結束,函數的返回參數又被拷貝至調用者的棧結構中,而後釋放無用的棧空間。
經過這個過程,有效地實現了棧內存的無限使用。假設你並非不斷地在兩個棧之間往返,通俗地講叫棧分割,則代價是十分低廉的。
簡單來講: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.htmljavascript