淺談雲巴實時通訊的程式設計模型

雲巴yunbaio發表於2016-09-28

淺談雲巴實時通訊的程式設計模型

概要

有人常問,雲巴實時通訊系統到底提供了一種怎樣的服務,與其他提供推送或 IM 服務的廠商有何本質區別。其實,從技術角度分析,雲巴與其它同類廠商都是面向開發者的通訊服務,巨集觀的程式設計模型都是大同小異,真正差異則聚焦於產品定位,業務模式,基礎技術水平等諸多細節上。本文暫不討論具體產品形態上的差異,著重從技術角度淺談實時通訊的程式設計模型。

什麼是實時通訊

「實時」(realtime) 一詞在語義層面上隱含著對時間的約束(real-time constraint),在工程上,我們習慣對「需要在一定時間內」 完成的操作稱為「實時操作」。通常,實時可細分為 「軟實時」(soft realtime),「準實時」(firm realtime)和 「硬實時」(hard realtime)。它們之間的差異,簡單來說,就是對無法在指定時間區間內(deadline)完成事務的容忍程度。維基百科上對這三者有如下解釋

  • Hard – missing a deadline is a total system failure.
  • Firm – infrequent deadline misses are tolerable, but may degrade the system's quality of service. The usefulness of a result is zero after its deadline.
  • Soft – the usefulness of a result degrades after its deadline, thereby degrading the system's quality of service.

假如我們把無法按時完成任務(missing a deadline)稱為異常事件,那麼硬實時系統無法容忍異常事件;準實時系統則可容忍極少量的異常事件,但超過一定數量後系統可用性為 0;軟實時系統可容忍異常事件,但是每發生一次異常事件,系統可用性降低。

綜上所述,我們可以舉例:

  • 火星上的無人探測器是硬實時系統,因為一次異常事件就極有可能導致探測器不可用,同理可類推核電站的監控系統,軍用無人機系統,遠端導彈的導航系統等一系列軍工產品;

  • 金融交易系統是準實時系統,此類系統可容忍極少數的交易故障,一旦故障次數增加,系統就會陷入崩潰狀態;

  • 簡訊 / 手機推送 / 電商購物 等都是軟實時系統。對於此類系統,使用者都可以容忍異常事件,但是太多的異常事件則會大幅降低系統可用程度,使用者體驗急劇下滑。

就目前來說,絕大多數網際網路產品(甚至可以說是 100%)都是軟實時系統。雲巴實時通訊系統的目標則是要做一個高可用的軟實時系統

一個最簡單的實時通訊程式設計模型

在軟體工程中,很多複雜的專案其實都可以用一個非常簡潔的模型來概括。正如愛因斯坦所說的:「一切都應該儘可能地簡單,但不要太簡單」(Everything should be made as simple as possible, but not simpler)。雖然這是描述物理世界的經驗之談,但同樣適用於計算機領域,將物理世界的關係投射到某種人為語言(物理公式/計算機程式語言),其規律其實都是共通的。

讓我們假設這麼一個簡單的場景:對 10 個客戶端傳送一條訊息

這個需求其實可以用偽碼錶示為:

for (i..10) {
    send_message(get_socket(i))
}

如果下圖所示:

enter image description here

在這個簡單的需求下,我們只需要讓這 10 個客戶端分別跟伺服器建立 TCP 連線(本文暫時只討論 TCP 協議),然後遍歷地傳送訊息即可。顯而易見,這是一個 O(N) 複雜度的邏輯。

基於這個簡單的模型,我們可以認為一條訊息從發出到接收,有以下幾個延時:

  • 網路延遲 ,一般是一個較為穩定的值,比如從北京到深圳,ping 延遲大約為 40 ms 左右;

  • 系統處理延遲,較之網路延遲,該值變化幅度較大,且可能因處理請求數的增加而急劇增大;

雲巴實時通訊系統以 200 ms 延遲作為總延遲標準,也就是說,假如網路鏈路是從北京到深圳,除去網路延遲的 40 ms,要想達到 200 ms 的通訊時間,系統延遲必須小於 160 ms。

可以想象,當客戶端數量達到一定數量級(比如百萬級別)時,以上系統模型的實時性將面臨極其嚴峻的考驗。

分而治之

在海量使用者下保持穩定的實時性,其實很多時候就只有一個手段:分而治之

圖 1 表示的是單機處理情況。當單機的處理能力,頻寬都無法應對客戶端數量急劇增加的時候,我們就必須將線路進行分割。而且圖 1 只體現了推送的意圖(單向),但通訊往往是一個雙向的概念,綜上,我們將 圖 1 改成下面的 圖 2

enter image description here

這樣每臺機器就可以處理符合其當前水位的連線。

在現實開發中,我們可能不僅僅滿足於一個如此簡單的訊息系統,我們可能想要有離線訊息,資料統計,資料快取,限流等一系列操作,所以我們還可以再優化一下架構:

  • 將整體架構劃分成業務邏輯層和資料儲存層;

  • 資料儲存層又可以根據儲存資料型別的不同來進一步劃分;

  • 前端可以單獨劃分一個網路接入層;

  • 資料包的流向可以用 MQ 來串聯;

這樣我們可以得到以下的圖 3:

enter image description here

在這個模型中,網路接入層和訊息業務邏輯層整體上應該是一個 stateless 的模組,可以較為輕鬆地做橫行擴充套件。儲存層作為一個有狀態的模組,想要做到橫行擴充套件是一件很不容易的事情。如果撇開這點來看,至此,這個模型理論上在應對海量使用者的場景下應該是有效的。

通訊協議和技術棧的選擇

做一個訊息系統,不可避免地要涉及到對通訊協議的選擇。我們在對通訊協議的選擇上,遵循以下幾個原則:

  • 協議儘可能精簡輕量,因為在系統設計之初我們就考慮了對物聯網的支援,省電,節約流量都是目標之一;

  • 通用性好,擴充套件性強,方便後期做特性開發;

  • 協議在業界被廣泛認可,且儘可能多的有不同語言的開源實現,以方便不同技術棧的客戶做整合;

綜上,我們沒有重新自定義一份通訊協議,而是選擇了基於長連線MQTT。從很多角度來看,MQTT 非常適合做訊息匯流排的通訊協議,而且協議棧也足夠輕巧和易於實現。雲巴實時訊息系統傳輸的訊息體積較小(一般小於 4 KB),比如控制訊號,普通聊天資訊等。就這點上,針對物聯網設計的 MQTT 有著天然的優勢。後面,在不斷地研究中我們又發現,MQTT 其實不僅僅適用於物聯網場景,在很多要求低延遲高穩定性的非物聯網場景也同樣適用(比如手機端 app 推送,IM,直播彈幕等)。

從前面幾個章節我們看到,雲巴訊息系統是一個典型的 IO 密集型系統。在出於開發效率和穩定的考慮下,我們選了 Erlang/OTP 作為主力開發語言。Erlang/OTP 作為一門小眾開發語言(無論是國內還是國際),在應付這類 IO 密集型系統上,有著得天獨厚的優勢(可參考 RabbitMQ 這個基於 Erlang/OTP 的著名開源專案):

  • 基於 actor 的程式建立模型,可以為每個資料包建立一個 Erlang 處理程式,充分利用多核;

  • OTP 的開發框架抽象了分散式開發的許多細節,使得開發者在很小的心智負擔下就能輕鬆快速地開發出功能原型;

  • Erlang/OTP 充分運用了容錯思想,應對異常不是防,而是容,很多時候我們寫出一些安全邏輯上有漏洞的程式碼,在 Erlang/OTP 上居然也能工作得好好的;

隨著不斷深入地使用 Erlang/OTP, 其效能問題也漸漸凸顯出來。我們發現,當客戶端請求量增加的時候,用 Erlang/OTP 寫出的模組輕而易舉地就可以將 CPU 跑滿,從而讓當前例項超負荷運轉。很多時候出於成本上的考量,我們無法選擇更多核數的機器來提升 Erlang 虛擬機器執行的效能(此點未明確驗證過),所以只好選擇適度增加服務處理例項來緩解壓力。

不過,通過對業務模組更細粒度的劃分,我們可以將一些核心的小模組用 C/C++ 語言改寫,在一定範圍的複雜度內,可以有效提升整體處理效能。這也是我們接下來優化核心系統的思路之一。

MQTT 的 Pub/Sub 模型與高可用 KV 儲存

MQTT 協議採用的是 Pub/Sub 的程式設計模型。其中有三個比較關鍵的動作:publishsubscribeunsubsribe。通過前面幾個章節的討論,我們又可以得到這麼一個場景:

假如存在一個訂閱量巨大的 topic(百萬級),如何在單次 publish 中保證實時性 ?

其實,解決思路跟之前的場景是一致的:分而治之。我們必須通過某種策略對 topic 進行分片,然後將分片分發到不同的 publish 模組上進行處理。在一定的演算法複雜度下,這個問題理論上是可以被有效解決的。於是,topic 的分片策略就成了高效能 publish 的關鍵。其實,如果想採用 MQTT 做海量訊息系統,訂閱關係的管理一定是無法繞開的大問題。它主要有以下幾個設計難點:

  • 如果採用 KV 方式儲存,如何設計資料結構 ?同上,我們要怎樣去設計一種高效的 topic 分片儲存策略;

  • 訂閱關係的管理是 MQTT 訊息系統的核心模組,假如這個儲存模組失效,就必定會導致訊息通訊失敗,從而讓客戶端收不到訊息,這就必須要求這個模組一定是高可用的,也就意味著我們必須構建一個高可用的 KV 儲存叢集,該叢集要能容忍一定程度的節點失效;

  • 冷熱 topic 要有淘汰機制,要有一定策略將不活躍的 topic 定期淘汰到磁碟以節約記憶體容量;

  • KV 儲存叢集要能高效地動態擴容;

在很長一段時間的實踐中,我們採用過好幾種 KV 儲存的叢集方案,踩了不少坑,最後還是決定自己造輪子來開發一個高可用的 KV 儲存模組。不過這又是一個很大的話題,我們將在後續部落格中具體闡述我們的做法。

缺陷與不足

在團隊發展初期,由於人力和時間等種種因素,我們把業務邏輯模組開發成了一個巨大的單體架構應用。在團隊規模較小的情況下,單體架構的應用確實較好維護和開發,但隨著新人的加入,單體架構則嚴重製約著特性開發和效能優化。從架構層面上來看,合理地劃分更細粒度的模組,在效能和可維護性上採用微服務(microservice)設計模式,成了我們未來優化系統的方向之一。

總結

軟體工程上有「沒有銀彈」(No Silver Bullet)這條金科玉律,使用者選擇雲服務商亦是如此,絕對沒有完美的第三方雲服務商,每一家都可能存在明顯的優點和缺陷。使用者必須從自己應用場景和痛點出發,選擇合適的後端服務。雲巴將會在自己產品的核心競爭力上持續發力,精打細磨,吸取行業內的高效實踐經驗,打造出更加優秀的高可用實時通訊系統。

相關文章