1. 程式人生 > >Golang適合高併發場景的原因分析

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。然後他討論了用不同的方式實現大規模併發服務的技術。

     

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

 

參考資料:

   

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

 

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

 

多程序

 

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

 

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

 

1234514831_ddvip_588

 

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

 

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

     

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查詢。

     

多執行緒

 

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

 

這種模型的問題就是太多的執行緒會減低軟體的執行效率。

   

執行緒和程序的成本

     

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

 

針對這個問題,有兩類架構解決它:基於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的效果隱式事件排程,

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

程序、執行緒、協程的關係和區別:

  • 程序擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,程序由作業系統排程。
  •  
  • 執行緒擁有自己獨立的棧和共享的堆,共享堆,不共享棧,執行緒亦由作業系統排程(標準執行緒是的)。
  •  
  • 協程和執行緒一樣共享堆,不共享棧,協程由程式設計師在協程的程式碼裡顯示排程。

協程和執行緒的區別是:協程避免了無意義的排程,由此可以提高效能,但也因此,程式設計師必須自己承擔排程的責任。

執行協程只需要極少的棧記憶體(大概是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.”。

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

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

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

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

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

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

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