系統設計 - 實時協作應用的設計

文 | 少個分號 (轉載請註明出處)

關注公衆號:DDD 和微服務

微信號:shaogefenhao

同名知乎:少個分號

互聯網時代越來越多的實時協作軟件出現,例如在線點餐、文檔編輯、在線繪圖等。

今天來聊聊這些場景一般如何實現的。

場景和問題

實時協作軟件一般用於多個人同時操作(也包括一個人多個會話)。例如 Google Doc 可以支持同時編輯文檔,並將多人編輯的結果合併到一起展示,而且能相互看到其它人的操作。

  1. Web 平臺如何建立長連接?

  2. 如何進行一致性處理,讓多人編輯的結果儘量不衝突?

  3. 如何支持離線操作?

  4. 如何擴容?

  5. 有那些框架?

Web 平臺如何建立長連接?

建立長連接的技術從遠古互聯網技術發展開始就有很多方案(有些方案現在很多人都不會再聽到了)。

建立 Web 長連接我們一般可以:

  1. 定時重試:比如每隔 60 秒發起一次 HTTP 請求,這是最差的一種方案。

  2. HTTP 長連接(Comet):前端發起一個 HTTP,後端掛起,在沒有信息回填之前掛起請求,前端如果超時,自動重連再發送一個 HTTP 請求。

  3. WebSocket:通過 HTTP 握手,然後建立(Upgrade 過程)一個 TCP 連接。代表框架 Socket.IO,可以支持使用 WebSocket 和 Comet 建立連接。

  4. MQTT 協議,或者其它消息協議:提供一些消息持久化、QoS(根據服務質量,選擇重傳的策略) 的支持。

  5. Flash、Silverlight 等瀏覽器富應用運行平臺。這也是古老的互聯網技術,這兩個平臺目前都已經宣佈停止維護,優先推薦使用 H5 相關技術。

  6. 瀏覽器插件。某些廠商對於專業應用有更高的要求,所以會實現自己的協議,根據原生 Socket 進行連接,並通過插件的方式和瀏覽器集成,爲 Web 提供更基礎的 API。

當前主流的方案一般是 WebSocket。

如何進行一致性處理?

相比網絡連接的問題,如何讓多人編輯的結果儘量不衝突這個問題更難。

不同的場景、數據類型可以採用的策略並不相同,下面以文章開頭的幾個例子說明如何實現一致性。

點餐的場景下如何處理一致性?一般將業務操作原子化 + 冪等,把數據更新修改爲加、減操作,使用最終一次性讓服務器決策結果(發生衝突時,一般是先到先做,後到丟棄策略),並將事件分發到參與方。

在線繪圖場景如何實現一致性?抽象圖的數據結構,使用節點和邊對圖進行結構化處理,以節點和邊爲原子單位,利用數據庫的能力高速更新。發生衝突時,可以使用後到覆蓋的策略,後到的更新會覆蓋前一次的更新。可以使用一些非關係型數據庫的 upset 能力,將插入和更新合併處理。

PS:對於圖數據庫的選擇,可以使用 Orientdb、MongoDB、Neo4J 等數據庫來實現。

在線文檔編輯場景如何實現一致性?可以使用 OT 算法(另外一種算法叫做 CRDT,CRDT 可以看做 OT 算法的拓展),OT 算法使用偏移量作爲更新依據,可以快速合併協同者的數據內容。

某種程度上來說,文檔結構和圖的結構非常類似,因此可以使用非關係型數據庫的特點儘可能地提高性能。

不管什麼一致性策略,時間久了都會不一致,在遊戲場景和一些協作算法中會使用一個叫影子跟隨的策略定期抹掉各個客戶端的差異。其原理是定期或者網絡連接中斷後,重新和服務器對齊版本,把服務器的最新版本往客戶端拉取。

如何支持離線操作?

離線操作在實時協作系統是一個非常費力不討好的特性,因爲總是會導致用戶數據丟失,而且非常難以探查問題。

以 MQTT over Socket 爲例,如果要實現實時協作,可以考慮如下方案。

  1. 先把離線過程中的變化記錄下,網絡連接後發送到後端,並廣播出去。這個方案會記錄很多過時的操作數據,比如上週協作了一次,中間其他成員協作,今天線上會播放整週的數據,而且還會存在服務器上,會產生大量的衝突。

  2. 本地操作不記錄變化,網絡連接後只發送一個快照版本,服務器根據某種策略接收這個快照版本的數據或者部分數據。

一般優先推薦使用方案 2,這樣在版本比較時更可控,甚至可以允許用戶確認兩個版本的差異,並根據某種策略合併。

另外需要注意,離線後重新上線需要使用類似影子跟隨的方式,獲取服務器最新的版本。(因爲服務器總是要保存一份最新版本數據)。

如何擴容?

實時協作系統不具備天然的水平擴容能力,需要設計相關機制進行擴容。

擴容方式一般有兩種:

  1. 將用戶隔離到小規模範圍內的協作範圍中。

  2. 將用戶放到一個大池子,對事件交換機制進行擴容。

我把他們叫做集中式擴容和分散式擴容。

集中式擴容有點像喫大鍋飯,對用戶羣不區分,而是對事件分發的服務器進行水平擴容。

下面是 Socket.IO 提供的解決方案:

反過來看分散式擴容,如果我們根據一些業務策略將用戶 stick 到具體的服務器上,讓這些用戶在服務內部完成事件交換,即使這些服務器在一定程度上是有狀態的。

分散式擴容更像是遊戲服務器模式,避免將消息廣播到太大範圍,因爲我們在業務上總是能找到消息廣播的主題,讓廣播的範圍儘可能小,這樣系統崩潰時影響的用戶範圍比較小。

有那些框架可以幫助簡化實時協作的工作?

從零開始實現一套實時協作系統比較困難,而且需要長期探索一些技術選型的問題。

這裏分享一些常見的技術方案和框架作爲參考。

從需求匹配性上來說,Convergence 就是爲了實時應用設計的,它有很多 Demo,包括在線協同編程、文檔協同編輯、協同繪圖等。藉助 Akka 的分佈式能力,Convergence 也支持水平拓展的集羣部署。

Convergence 的缺點是它是一個完整的應用,而不是一個庫,且是 Scala 編寫的,需要學習相關的語言特性。

而 Socket.IO 做的事情就少很多,Socket.IO 只提供網絡連接、事件分發等任務,默認情況通過 Websocket 進行通信,在瀏覽器版本過低時也支持 HTTP 長連接,也就是 COMET 技術。

Socket.IO 提供了 Java、Nodejs 等平臺和語言的實現,所以更容易集成到自己的應用中來。

默認情況下 Socket.IO 在內存中實現消息轉發,也可以接入集中的數據源或者事件 Adapter 來完成消息廣播的能力。

常用的有:

圖片來源:https://socket.io/docs/v4/adapter/

實時協作應用設計上的注意事項

有一些實踐上的坑不得不提一下。

用戶數據丟失幾乎是必然的。

這一點主要還是 CAP 定理約束,在 系統設計 | 分佈式事務場景、概念和方案整理(含概念圖) 中我們討論過這個話題,而在實時協作應用中這種問題變得尤爲突出。

用戶離線後即使開啓高的 QoS 保證消息最終發送到用戶,但是當用戶最終收到消息後,現場可能已經發生變化,所以這甚至帶來一些副作用。

不要過於依賴歷史消息。

爲了保證用戶的數據安全,我們可能會開啓 QoS 保證,很多通信框架都會通過確認的方式保證送達事件。

高的 QoS 不僅影響性能而且對一致性幫助不大,因爲和上面提到的類似,歷史消息對最終結果意義不大。

本地實現 Undo、Redo 操作

我們有時候會設置 Undo、Redo 操作來實現撤回和重做,但是一定不要將這兩個事件廣播出去,而是應該在本地完成後,作爲新的操作事件發送出去。

例如 Undo 在本地表現爲刪除一段文本,那麼發送到服務器的消息應該是刪除一段文本。

合理設置心跳檢測機制

通信框架都會使用心跳來檢測離線,默認情況下部分框架的心跳設置非常長,可能不能滿足某些場景需要,可以設置更短的心跳。

但是,心跳一般是網絡協議的職責,儘量不要在應用中再實現一次。

隨處可見的非阻塞式編程。

我們可以採用一些非阻塞式的框架或者庫提高響應式編程的體驗,例如 RxJava、RxJS 等。

編程語言和平臺影響很大

有一些語言的併發能力天生就很強,比如 elixir、Erlang/OTP、Scala、Nodejs 等。這些編程語言或者平臺往往都是非阻塞式的,而 Java 一般來說主要應用於業務系統,對於需要高併發的場景來說,有能力的話嘗試其它語言可以收穫非常多(我曾經將 MQTT 服務器切換爲 EMQX 後獲得性能上的飛躍提升,不過其構建在 Erlang/OTP 之上,非常遺憾的是,完全沒有能力對其源碼進行拓展)。

參考資料

[1] The free and open source engine for real-time collaboration https://convergence.io/

[2] Socket.IO 文檔 https://socket.io/docs/v4/adapter/

[3] Building Real-time Applications with Phoenix & Elixir https://www.educative.io/courses/building-real-time-applications-phoenix-elixir

[4] Building real-time collaboration applications: OT vs CRDT https://www.tiny.cloud/blohttps://raw.githubusercontent.com/linksgo2011/shaogefenhao-v2/master/src/posts/architecture//real-time-collaboration-ot-vs-crdt/

[5] Comet (programming) https://en.wikipedia.org/wiki/Comet_(programming)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MAzBEotOw-9Q4ArjMri8Qw