如何實現單機大規模併發SIP語音呼叫?

鎖定目標:單機5千

多大叫大,1千還是1萬?好吧,暫定爲5000或以上。帶寬不夠?千兆網。硬盤太慢?SSD。

本文不考慮IO的限制,只討論結構和模式。

開源世界Voip領域最響亮的牌子應該是FreeSwitch,使用者衆多,它能實現如此大規模的單機併發嗎?我認爲:不行。

爲什麼不行?因爲它線程太多,一個通道一個線程,上5000個線程,玩不轉:“CPU忙着切換線程上下文了,哪有時間幹正事”(《GO語言併發之道》)。

 

爲什麼多線程不行?

說起來,我曾經也是多線程的擁躉,在2003年設計的藍星際語音平臺時,按1通道對應1線程的方式。那時候幾十線正常,上百線就算規模較大的應用了。(注意:FreeSwitch 1.0在2008年纔出現)

我們先來說說多線程的好處。

我爲語音平臺設計了一種叫Koodoo的腳本語言,每個通道運行各自獨立的腳本,相當於在每個線程上跑應用,比如要跑一個IVR:

WaitRing();  // 堵塞等待一個來電

Play("welcome.wav");  // 播放歡迎詞,放完了才執行下一個語句

k = "";

Getkeys(k, 1, 20);  // 最多等20秒接收一個按鍵

if( k=="1" )

    Play("menu1.wav");

else if( k=="2" )

    Play("menu2.wav");

else

    Sleep(5);  // 延時5秒鐘

 

多線程的優勢體現出來了,一個通道一個線程,允許隨便堵塞,寫流程變得很簡單,因爲不用關心狀態機,程序員不用轉換編程的模式,寫多通道的應用就像寫單通道程序一樣。

除了開發應用帶來的便捷,一個通道一個線程讓系統的結構變得清晰,Bug更少,更穩定。

 

帶來的這種優勢並非沒有代價:操作系統線程的開銷太大了。以內存爲例,Windows下默認一個線程的堆棧是1M,就算什麼也不幹,跑5千個空線程就得佔用5G內存。

內存還不是主要問題,內存不足可以擴,主要問題在CPU:通道和線程同步增長,而邏輯CPU單元是有限的,一般4到8,很強的服務器32個,跑5千以上的線程,假設每個CPU上運行1千個線程,

則每個CPU需要在在上1千個線程之間來回切換,導致CPU的執行效率極低,很快所有的CPU負荷上升到100%,幾乎癱瘓,別的什麼也幹不了。

 

因爲Koodoo語言是我自己設計的,辦法來了。

 

解決之道:協程

大家知道GO語言有所謂“協程”,這個概念應該沿襲自Erlang,是解決大併發IO的一把鑰匙。

所謂協程,就是在語言級別將函數執行劃分成時間片,需要在語言級別實現調度器來分配這些協程的時間片。

在一個操作系統線程運行一個調度器,可以執行數千個協程,因爲協程是語言級別實現的,上下文開銷極小,因此將極大提升CPU的效率。

 

我來改造下Koodoo語言,也實現一個協程版本,這而且要做到源代碼兼容,開發者不需要做什麼,僅僅改一下配置,就可以得到性能的極大提升。

 

Koodoo語言的每一條語句,對應C++的一個對象,每個對象有個指針nextObj,指向下一條語句(對象),顯然我們的調度粒度是這一個個語句對象。我們很容易做一個調度器,在通道之間進行切換。

我們還要考慮堵塞問題,但很多IO類的操作是堵塞的,比如:等某個通道放完一個音再切換到下一個通道十幾秒過去了,這顯然不行。當然,Koodoo的IO類函數基本都有非堵塞版本,偷懶方法是要求開發者使用非堵塞版本,這違背了源代碼兼容的原則。另外有些語句沒辦法替換,比如Sleep(延時秒數)。

 

只能在語句對象上動手術了。

 

對於堵塞型的語句對象,增加1個成員變量runCount,用來記錄運行狀態,初值爲0,僞代碼如下:

語句對象::Run()

{

  if( runCount==0 ){

      此處執行啓動IO,比如開始放音

      runCount = 1;

  }

  else{

      if( IO已經完成 ){

         關閉IO

 

         runCount = 0;

      }   

  }

}

 

同時,改造一下得到下一個語句的函數:

Obj* 語句對象::GetNext()

{

  return(runCount ? this : nextObj);

}

如果IO沒有完成就還是執行本語句對象,執行完就正常跳到下一條。

因爲打開IO和判斷IO是否完成執行時間很短,可以看成是非堵塞的,用這種方法來進行調度,完全可行。

 

調度器的靈活配置

每個調度器運行在一個操作系統線程上。可以指定調度器(線程)的個數。

上述調度器的調度方式實際上是將CPU時間平均分配,這是默認配置。還可以對某些通道專門綁定到特定分配器,有一些執行特殊任務的通道,比如來電隊列的自動分配(ACD),調度器運行的協程更少,可以有更高的優先級。

 

 

對比實測:令人驚歎

測試環境:

MacBook Pro 15,2017版 MacOS Catalina 10.15.3

Paralles Desktop虛擬機下的windows10 64位家庭版,分配了2個處理器,6G內存。

運行結果:

傳統模式(1通道1線程),配置2000通道,CPU:100%,系統非常卡。5000通道就不用試了。

協程模式(2個調度器),配置2000通道,CPU:6-10%之間,系統順暢。

協程模式(2個調度器),配置5000通道,CPU:26-37%之間,系統順暢。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章