1. 程式人生 > >訊息“時序”與“一致性”為何這麼難

訊息“時序”與“一致性”為何這麼難

分散式系統中,很多業務場景都需要考慮訊息投遞的時序,例如:

(1)單聊訊息投遞,保證傳送方傳送順序與接收方展現順序一致

(2)群聊訊息投遞,保證所有接收方展現順序一致

(3)充值支付訊息,保證同一個使用者發起的請求在服務端執行序列一致

訊息時序是分散式系統架構設計中非常難的問題ta為什麼難,有什麼常見優化實踐,是本文要討論的問題。

一、為什麼時序難以保證,訊息一致性難?

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

【時鐘不一致】


分散式環境下,有多個客戶端、有web叢集、service叢集、db叢集,他們都分佈在不同的機器上,機器之間都是使用的本地時鐘,而沒有一個所謂的“全域性時鐘”,所以不能用“本地時間”來完全決定訊息的時序

【多客戶端(傳送方)】


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

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

【服務叢集(多接收方)】


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

如上圖,絕對時序上,

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

【網路傳輸與多執行緒】


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

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

【怎麼保證絕對時序】

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

msg1先處理麼?

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

二、優化實踐

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

多客戶端、多服務端導致“時序”的標準難以界定,需要一個標尺來衡量時序的先後順序,可以根據業務場景,以客戶端或者服務端的時間為準,例如:

1郵件展示順序,其實是以客戶端傳送時間為準的,潛臺詞是,傳送方只要將郵件協議裡的時間調整為1970年或者2970年,就可以在接收方收到郵件後一直“置頂”或者“置底”

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

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

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

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

訊息傳送、帖子釋出時間、甚至秒殺時間都沒有這麼精準時序的要求:

1)同1s內釋出的聊天訊息時序亂了

2)同1s內釋出的帖子排序不對

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

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

關於絕對遞增id,趨勢遞增id的生成架構,詳見文章《細聊分散式ID生成方法》,此處不展開。

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

資料為了保證高可用,需要做到進行資料冗餘,同一份資料儲存在多個地方,怎麼保證這些資料的修改訊息是一致的呢?利用的就是“單點序列化”

1先在一臺機器上序列化操作

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

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


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

典型場景二:GFS中檔案的一致性


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

【單對單聊天,怎麼保證傳送順序與接收順序一致】

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

回答:

1)如果利用伺服器單點序列化時序,可能出現服務端收到訊息的時序為msg3msg1msg2,與發出序列不一致

2)業務上不需要全域性訊息一致,只需要對於同一個傳送方Ata發給B的訊息時序一致就行,常見優化方案,AB發出的訊息中,加上傳送方A本地的一個絕對時序,來表示接收方B的展現時序

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

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

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


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

無論如何,是按照接收方收到時序展現,還是按照服務端收到的時序展現,還是按照發送方傳送時序展現,是pm需要思考的點,技術上都能夠實現(接收方按照發送時序展現是更合理的)。

總之,需要一杆標尺來衡量這個時序

【群聊訊息,怎麼保證各接收方收到順序一致】

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

回答:

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

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


此時群聊的傳送流程為:

1sender1發出msg1sender2發出msg2

2msg1msg2經過接入叢集,服務叢集

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

4service拿到msg2seq20msg1seq30

5)通過投遞服務講訊息給多個群友,群友即使接收到msg1msg2的時間不同,但可以統一按照seq來展現

這個方法能實現,所有群友的訊息展示時序相同。

缺點是,這個生成全域性遞增序列號的服務很容易成為系統瓶頸,還有沒有進一步的優化方法呢

思路群訊息其實也不用保證全域性訊息序列有序,而只要保證一個群內的訊息有序即可,這樣的話,id序列化”就成了一個很好的思路。


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

三、總結

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

2要“有序”,先得有衡量“有序”的標尺,可以是客戶端標尺,可以是服務端標尺

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

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

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

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

關注微信公眾號和今日頭條,精彩文章持續更新中。。。。。