有人常問,雲巴實時通信系統到底提供了一種怎樣的服務,與其他提供推送或 IM 服務的廠商有何本質區別。其實,從技術角度分析,雲巴與其它同類廠商都是面向開發者的通信服務,巨集觀的編程模型都是大同小異,真正差異則聚焦於產品定位,業務模式,基礎技術水平等諸多細節上。本文暫不討論具體產品形態上的差異,著重從技術... ...
概要
有人常問,雲巴實時通信系統到底提供了一種怎樣的服務,與其他提供推送或 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))
}
如果下圖所示:
在這個簡單的需求下,我們只需要讓這 10 個客戶端分別跟伺服器建立 TCP 連接(本文暫時只討論 TCP 協議),然後遍歷地發送消息即可。顯而易見,這是一個 O(N)
複雜度的邏輯。
基於這個簡單的模型,我們可以認為一條消息從發出到接收,有以下幾個延時:
-
網路延遲 ,一般是一個較為穩定的值,比如從北京到深圳,ping 延遲大約為 40 ms 左右;
-
系統處理延遲,較之網路延遲,該值變化幅度較大,且可能因處理請求數的增加而急劇增大;
雲巴實時通信系統以 200 ms 延遲作為總延遲標準,也就是說,假如網路鏈路是從北京到深圳,除去網路延遲的 40 ms,要想達到 200 ms 的通信時間,系統延遲必須小於 160 ms。
可以想象,當客戶端數量達到一定數量級(比如百萬級別)時,以上系統模型的實時性將面臨極其嚴峻的考驗。
分而治之
在海量用戶下保持穩定的實時性,其實很多時候就只有一個手段:分而治之。
圖 1 表示的是單機處理情況。當單機的處理能力,帶寬都無法應對客戶端數量急劇增加的時候,我們就必須將線路進行分割。而且圖 1 只體現了推送的意圖(單向),但通信往往是一個雙向的概念,綜上,我們將 圖 1 改成下麵的 圖 2:
這樣每台機器就可以處理符合其當前水位的連接。
在現實開發中,我們可能不僅僅滿足於一個如此簡單的消息系統,我們可能想要有離線消息,數據統計,數據緩存,限流等一系列操作,所以我們還可以再優化一下架構:
-
將整體架構劃分成業務邏輯層和數據存儲層;
-
數據存儲層又可以根據存儲數據類型的不同來進一步劃分;
-
前端可以單獨劃分一個網路接入層;
-
數據包的流向可以用 MQ 來串聯;
這樣我們可以得到以下的圖 3:
在這個模型中,網路接入層和消息業務邏輯層整體上應該是一個 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 的編程模型。其中有三個比較關鍵的動作:publish
,subscribe
和 unsubsribe
。通過前面幾個章節的討論,我們又可以得到這麼一個場景:
假如存在一個訂閱量巨大的 topic(百萬級),如何在單次 publish 中保證實時性 ?
其實,解決思路跟之前的場景是一致的:分而治之。我們必須通過某種策略對 topic 進行分片,然後將分片分發到不同的 publish 模塊上進行處理。在一定的演算法複雜度下,這個問題理論上是可以被有效解決的。於是,topic 的分片策略就成了高性能 publish 的關鍵。其實,如果想採用 MQTT 做海量消息系統,訂閱關係的管理一定是無法繞開的大問題。它主要有以下幾個設計難點:
-
如果採用 KV 方式存儲,如何設計數據結構 ?同上,我們要怎樣去設計一種高效的 topic 分片存儲策略;
-
訂閱關係的管理是 MQTT 消息系統的核心模塊,假如這個存儲模塊失效,就必定會導致消息通信失敗,從而讓客戶端收不到消息,這就必須要求這個模塊一定是高可用的,也就意味著我們必須構建一個高可用的 KV 存儲集群,該集群要能容忍一定程度的節點失效;
-
冷熱 topic 要有淘汰機制,要有一定策略將不活躍的 topic 定期淘汰到磁碟以節約記憶體容量;
-
KV 存儲集群要能高效地動態擴容;
在很長一段時間的實踐中,我們採用過好幾種 KV 存儲的集群方案,踩了不少坑,最後還是決定自己造輪子來開發一個高可用的 KV 存儲模塊。不過這又是一個很大的話題,我們將在後續博客中具體闡述我們的做法。
缺陷與不足
在團隊發展初期,由於人力和時間等種種因素,我們把業務邏輯模塊開發成了一個巨大的單體架構應用。在團隊規模較小的情況下,單體架構的應用確實較好維護和開發,但隨著新人的加入,單體架構則嚴重製約著特性開發和性能優化。從架構層面上來看,合理地劃分更細粒度的模塊,在性能和可維護性上採用微服務(microservice)設計模式,成了我們未來優化系統的方向之一。
總結
軟體工程上有「沒有銀彈」(No Silver Bullet)這條金科玉律,用戶選擇雲服務商亦是如此,絕對沒有完美的第三方雲服務商,每一家都可能存在明顯的優點和缺陷。用戶必須從自己應用場景和痛點出發,選擇合適的後端服務。雲巴將會在自己產品的核心競爭力上持續發力,精打細磨,吸取行業內的高效實踐經驗,打造出更加優秀的高可用實時通信系統。