IM系列3:如何保證IM實時消息的「時序性」與「一致性」?

1、前言

我們都知道,一個典型的分散式系統中,很多業務場景都需要考慮消息投遞的時序,例如:

IM中單聊消息投遞:保證發送方發送順序與接收方展現順序一致;

IM中群聊消息投遞:保證所有接收方展現順序一致;

電商充值支付消息:保證同一個用戶發起的請求在服務端執行序列一致。

實時消息時序和一致性是分散式系統架構設計中非常難的問題(尤其IM應用這種以消息為中心的應用形態),困難在哪?有什麼常見優化實踐?這就是本文要討論的內容。

2、IM開發乾貨系列文章

本文是系列文章中的第3篇,總目錄如下:

《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》

《IM消息送達保證機制實現(二):保證離線消息的可靠投遞》

《如何保證IM實時消息的「時序性」與「一致性」?》(本文)

《IM單聊和群聊中的在線狀態同步應該用「推」還是「拉」?》

《IM群聊消息如此複雜,如何保證不丟不重?》

《一種Android端IM智能心跳演算法的設計與實現探討(含樣例代碼)》

《移動端IM登錄時拉取數據如何作到省流量?》

《通俗易懂:基於集群的移動端IM接入層負載均衡方案分享》

《淺談移動端IM的多點登陸和消息漫遊原理》

3、憑什麼說保證即時消息的時序、一致性很困難?

為什麼分散式環境下,即時消息的時序難以保證,這邊簡要分析了幾點原因:

1時鐘不一致

分散式環境下,有多個客戶端、有web集群、service集群、db集群,他們都分布在不同的機器上,機器之間都是使用的本地時鐘,而沒有一個所謂的「全局時鐘」,所以不能用「本地時間」來完全決定消息的時序。

2多客戶端(發送方)

多伺服器不能用「本地時間」進行比較,假設只有一個接收方,能否用接收方本地時間表示時序呢?遺憾的是,由於多個客戶端的存在,即使是一台伺服器的本地時間,也無法表示「絕對時序」。

如上圖,絕對時序上,APP1先發出msg1,APP2後發出msg2,都發往伺服器web1,網路傳輸是不能保證msg1一定先於msg2到達的,所以即使以一台伺服器web1的時間為準,也不能精準描述msg1與msg2的絕對時序。

3服務集群(多接收方)

多發送方不能保證時序,假設只有一個發送方,能否用發送方的本地時間表示時序呢?遺憾的是,由於多個接收方的存在,無法用發送方的本地時間,表示「絕對時序」。

如上圖,絕對時序上,web1先發出msg1,後發出msg2,由於網路傳輸及多接收方的存在,無法保證msg1先被接收到先被處理,故也無法保證msg1與msg2的處理時序。

4網路傳輸與多線程

多發送方與多接收方都難以保證絕對時序,假設只有單一的發送方與單一的接收方,能否保證消息的絕對時序呢?結論是悲觀的,由於網路傳輸與多線程的存在,仍然不行。

如上圖,web1先發出msg1,後發出msg2,即使msg1先到達(網路傳輸其實還不能保證msg1先到達),由於多線程的存在,也不能保證msg1先被處理完。

5怎麼保證絕對時序

通過上面的分析,假設只有一個發送方,一個接收方,上下游連接只有一條連接池,通過阻塞的方式通訊,難道不能保證先發出的消息msg1先處理么?

答案是:可以,但吞吐量會非常低,而且單發送方單接收方單連接池的假設不太成立,高並發高可用的架構不會允許這樣的設計出現。

4、生產環境下的優化方法總結

1以客戶端或者服務端的時序為準

多客戶端、多服務端導致「時序」的標準難以界定,需要一個標尺來衡量時序的先後順序。

不過,我們可以根據業務場景,以客戶端或者服務端的時間為準,例如:

郵件展示順序:其實是以客戶端發送時間為準的,潛台詞是,發送方只要將郵件協議里的時間調整為1970年或者2970年,就可以在接收方收到郵件後一直「置頂」或者「置底」;

秒殺活動時間判斷:肯定得以伺服器的時間為準,不可能讓客戶端修改本地時間,就能夠提前秒殺。

2服務端能夠生成單調遞增的id

這個是毋庸置疑的,不展開討論,例如利用單點寫db的seq/auto_inc_id肯定能生成單調遞增的id,只是說性能及擴展性會成為潛在瓶頸。對於嚴格時序的業務場景,可以利用伺服器的單調遞增id來保證時序。

3大部分業務能接受誤差不大的趨勢遞增id

消息發送、帖子發布時間、甚至秒殺時間都沒有這麼精準時序的要求:

同1s內發布的聊天消息時序亂了;

同1s內發布的帖子排序不對;

用1s內發起的秒殺,由於伺服器多台之間時間有誤差,落到A伺服器的秒殺成功了,落到B伺服器的秒殺還沒開始,業務上也是可以接受的(用戶感知不到)。

所以,大部分業務,長時間趨勢遞增的時序就能夠滿足業務需求,非常短時間的時序誤差一定程度上能夠接受。

4利用單點序列化,可以保證多機相同時序

數據為了保證高可用,需要做到進行數據冗餘,同一份數據存儲在多個地方,怎麼保證這些數據的修改消息是一致的呢?

我們可以利用的就是「單點序列化」:

先在一台機器上序列化操作;

再將操作序列分發到所有的機器,以保證多機的操作序列是一致的,最終數據是一致的。

? 典型場景一:資料庫主從同步

資料庫的主從架構,上游分別發起了op1,op2,op3三個操作,主庫master來序列化所有的SQL寫操作op3,op1,op2,然後把相同的序列發送給從庫slave執行,以保證所有資料庫數據的一致性,就是利用「單點序列化」這個思路。

? 典型場景二:GFS中文件的一致性

GFS(Google File System)為了保證文件的可用性,一份文件要存儲多份,在多個上游對同一個文件進行寫操作時,也是由一個主chunk-server先序列化寫操作,再將序列化後的操作發送給其他chunk-server,來保證冗餘文件的數據一致性的。

5IM中單對單聊天,怎麼保證發送順序與接收順序一致

IM中單人聊天的需求,發送方A依次發出了msg1,msg2,msg3三個消息給接收方B,這三條消息能否保證顯示時序的一致性(發送與顯示的順序一致)?

答案是:

如果利用伺服器單點序列化時序,可能出現服務端收到消息的時序為msg3,msg1,msg2,與發出序列不一致;

業務上不需要全局消息一致,只需要對於同一個發送方A,ta發給B的消息時序一致就行,常見優化方案,在A往B發出的消息中,加上發送方A本地的一個絕對時序,來表示接收方B的展現時序。

msg1{seq:10, receiver:B,msg:content1 }

msg2{seq:20, receiver:B,msg:content2 }

msg3{seq:30, receiver:B,msg:content3 }

潛在問題:如果接收方B先收到msg3,msg3會先展現,後收到msg1和msg2後,會展現在msg3的前面。

無論如何,是按照接收方收到時序展現,還是按照服務端收到的時序展現,還是按照發送方發送時序展現,是pm需要思考的點,技術上都能夠實現(接收方按照發送時序展現是更合理的)。總之,需要一桿標尺來衡量這個時序。

6IM群聊消息,怎麼保證各接收方收到順序一致

IM群聊消息的需求,N個群友在一個群里聊,怎麼保證所有群友收到的消息顯示時序一致?

答案是:

不能再利用發送方的seq來保證時序,因為發送方不單點,時間也不一致;

可以利用伺服器的單點做序列化。

此時IM群聊的發送流程為:

sender1發出msg1,sender2發出msg2;

msg1和msg2經過接入集群,服務集群;

service層到底層拿一個唯一seq,來確定接收方展示時序;

service拿到msg2的seq是20,msg1的seq是30;

通過投遞服務講消息給多個群友,群友即使接收到msg1和msg2的時間不同,但可以統一按照seq來展現。

這個方法能實現,所有群友的消息展示時序相同。缺點是,這個生成全局遞增序列號的服務很容易成為系統瓶頸,還有沒有進一步的優化方法呢?

優化思路是:群消息其實也不用保證全局消息序列有序,而只要保證一個群內的消息有序即可,這樣的話,「id串列化」就成了一個很好的思路。

這個方案中,service層不再需要去一個統一的後端拿全局seq,而是在service連接池層面做細小的改造,保證一個群的消息落在同一個service上,這個service就可以用本地seq來序列化同一個群的所有消息,保證所有群友看到消息的時序是相同的。

7、本文小結

1)分散式環境下,消息的有序性是很難的,原因多種多樣:時鐘不一致,多發送方,多接收方,多線程,網路傳輸不確定性等;

2)要「有序」,先得有衡量「有序」的標尺,可以是客戶端標尺,可以是服務端標尺;

3)大部分業務能夠接受大範圍趨勢有序,小範圍誤差;絕對有序的業務,可以藉助伺服器絕對時序的能力;

4)單點序列化,是一種常見的保證多機時序統一的方法,典型場景有db主從一致,gfs多文件一致;

5)單對單聊天,只需保證發出的時序與接收的時序一致,可以利用客戶端seq;

6)群聊,只需保證所有接收方消息時序一致,需要利用服務端seq,方法有兩種,一種單點絕對時序,另一種id串列化。


推薦閱讀:

大國崛起:資料庫領域的中國力量
UCloud雲資料庫團隊誠招分散式資料庫研發
分散式時序資料庫 - LinDB
PhxPaxos架構設計、實現分析

TAG:分散式系統 | 分散式資料庫 | 即時通訊IM |