簡單的搭建一個高並發低時延系統


首先聲明一點:這里的“高並發”是相對的,相對於硬件而言,而不是絕對的高並發。后者需要分布式來實現,這里不做討論。本文關注的是單機的高並發。

最近在做一個語音通信系統,要求在線用戶2W,並發1K路通話。硬件是兩台服務器,酷睿多核,4G內存,千兆網卡(我用過的最好的硬件,負擔這些應該問題不大)。

系統的另一個指標是呼叫時延和語音時延。這是這個系統的關鍵。最終我們的系統拿到用戶現場測試的時候,效果可能有點太好,對方測試不大相信。其實降低時延只要幾個地方把握好了,應該問題不大的。這里總結一下。

 

1、 整體結構:

整體上采用控制與承載相分離的結構。控制部分負責流程的控制部分,包括流程的建立,處理,語音資源的管理等,是系統核心部分。承載部分主要負責語音處理,包括語音編解碼,加解密,轉發錄音等。這樣的好處是:1)降低系統的整體復雜度。2)提高系統的可擴展性。特別是如果用戶數上去,這種結構更好擴展。

這在通信中其實就是一個典型的軟交換結構。兩台服務器,一台負責控制,一台負責承載。控制和承載之間通過網絡通信。

控制程序是一個進程,可以管理多個承載程序。

2、 流程:

要降低時延,關鍵的一點是功能實現流程的設計。要減少不必要的環節和網元間的交互。數據能夠一次通知就不要兩次交互。必要的時候,為了時延,可以犧牲一點協議的標准性,使用私有協議完成(至少從目前看沒有問題,這個系統是一個端對端封閉的系統。)。

3、開發語言:

控制層面使用的python來實現的。控制部分流程邏輯復雜,而python很擅長描述邏輯。本來有點擔心python的運行效率,其實沒有必要:整個系統的壓力在承載,而不在控制部分,控制部分不會有太多的壓力;另外,cpu夠強悍,時延的瓶頸在I/O。況且,python也重用了我們之前用C實現的協議編解碼庫。

承載部分用C來實現。

4、 利用多核

利用多進程來利用多核。在承載服務器上,並行跑了兩個進程,每個分別處理500路通話。也許線程切換成本更低,但是編程復雜度高。對於線程,我只用最簡單的模型。

控制部分沒有多進程,似乎利用不能這個服務器的多核,不過目前來看,還不需要,因為現在就能很好的滿足需求。

5、 網絡通信

承載服務器的壓力中很大一部分來自於網絡通信。按照我們的功能,1000路語音並發,意味着沒20毫秒至少要處理1000個語音包(最惡劣的情況是2000個語音包,包括收發)。

Libevent開源,號稱輕量級高性能,而且應用也廣泛,也許是個不錯的選擇。不過在我看來還是有些龐大,很多特性(跨平台,多種通信模型)我都用不上。

更為關鍵的一點事,linux的epoll接口足夠簡單,而且非常好用。接口中提供一個參數可以設置用戶數據,這樣我可以把一些數據包括函數指針放進去,從而很方便的構造一個事件驅動的網絡模塊。它能夠保證代碼足夠簡單。

6、 文件讀寫

整個系統涉及到文件讀寫的主要有兩塊:錄音及日志。我們常用的文件操作接口都是阻塞式,進程(線程)會被掛起,等待讀寫完畢,然后在繼續執行。我們都知道,對磁盤的操作要慢很多,所以這個地方是請求時延的一個瓶頸。

異步IO可以解決這個問題。參考資料:http://www.ibm.com/developerworks/cn/linux/l-async/。不過網上看到有人說AIO接口有bug。時間不多,沒有時間深入研究,還是保守的放棄了這個思路。新技術有風險,使用需謹慎。

Libeio也應該是一個選擇。參考資料:http://rdc.taobao.com/blog/cs/?p=1524,它是用線程池來模擬異步IO。問題是,我們的程序主要是寫文件,而且一般不需要知道結果,在這種情況下使用libeio的必要性有多大?

我們最終的方案是參考libeio,直接為承載進程申請了一個線程來負責寫文件。主線程負責語音編解碼及轉發,完全非阻塞,以保證低延遲。涉及的文件寫操作,通過接口發送給另外一個線程調用阻塞IO接口來實現。線程間接口很簡單,一塊要寫的內容加一個路徑名。

7、 數據庫操作

我們的數據庫使用的是mysql。和文件讀寫一樣,數據庫操作也是請求時延的一個瓶頸。在整個流程中,我們會多次的讀寫數據。我們的做法是:系統啟動后,將運行時用到的數據全部讀到內存,后面直接查看內存。好在數據不大,這個工作也簡單。如果涉及到數據的新增修改刪除。則另外一個線程完成相關操作,再通知主線程更新內存。

最終的結果是,主線程是完全非阻塞的,涉及到阻塞的操作,全部移到另外一個線程中。兩個線程不共享任何全局數據,只通過FIFO交互。

這個地方redis也許是可以考慮的一種選擇,它的數據保存在內存,讀寫效率也非常好。不過相對來說還是有點復雜,而且還是nosql,我們的開發人員並不熟悉。“最小驚訝原則”不但適用於程序接口,也適用於系統。

 

經過所有的這些考慮和優化,可以基本達到目標,而且足夠簡單。

 

如何榨干服務器:

經過上面的一些優化,基本上可以滿足用戶的需求了。但我知道,還沒有完全的利用服務器的能力(包括CPU,IO)。要進一步榨干服務器的能力,可以在承載服務器上將每個進程的處理能力擴大一倍,每個進程處理1000路。也可以考慮再多跑幾個進程。

控制服務器沒有充分的利用多核,可以考慮在控制服務器也運行兩個承載程序。

這樣下來,初步估計硬件不提高的情況下,注冊用戶數至少能夠提高到6W,並發呼叫數目至少能夠提高到3K。

提高絕對容量和並發:

業務特點不同,通信行業高並發的解決方案和互聯網行業可能會有一些區別。可以想一下我們使用的電話系統,就是一個實例。它通過分布在全國各地的一個個用戶歸屬的局端,再配合強大的路由能力,以及端到端之間非常標准的協議來解決。

采用類似的方案也可以提高本系統的容量和並發,不過,目前系統的容量已經可以滿足我們公司市場幾年內的需求,沒有進一步提升的必要,還是保持簡單的好。

 

總結:

很多時候,使用開源軟件都是一個非常不錯的主意,可以避免“重復發明輪子”。而且,它還有一定的誘惑:它可以為你的履歷加分。

但是有的時候,你需要的可能並不是“輪子”,想想為一個滑板安裝一個汽車輪是什么效果。

 

什么樣的方案才是好的方案?

1、 滿足現在的需求及未來50%的需求。當一個可預見的需求發生概率超過一半時,為它考慮可擴展時必要的。否則會過度設計而沖擊簡單性。

2、 保持簡單。

 

最后,再次提一下KISS原則,keep it simple and stupid !


免責聲明!

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



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