本文原題爲「一套高可用羣聊消息系統實現」,由做者「於雨氏」受權整理和發佈,內容有些許改動,做者博客地址:alexstocks.github.io。應做者要求,如需轉載,請聯繫做者得到受權。php
要實現一整套能用於大用戶量、高併發場景下的IM羣聊,技術難度遠超IM系統中的其它功能,緣由在於:IM羣聊消息的實時寫擴散特性帶來了一系列技術難題。html
舉個例子:如一個2000人羣裏,一條普通消息的發出問題,將瞬間寫擴散爲2000條消息的接收問題,如何保證這些消息的及時、有序、高效地送達,涉及到的技術問題點實在太多,更別說個別場景下萬人大羣裏的炸羣消息難題了更別說個別場景下萬人大羣裏的炸羣消息難題了。前端
這也是爲何通常中大型IM系統中,都會將羣聊單獨拎出來考慮架構的設計,單獨有針對性地進行架構優化,從而下降整個系統的設計難度。node
本文將分享的是一套生產環境下的IM羣聊消息系統的高可用、易伸縮、高併發架構設計實踐,屬於原創第一手資料,內容較專業,適合有必定IM架構經驗的後端程序員閱讀。git
推薦:若有興趣,本文做者的另外一篇《一套原創分佈式即時通信(IM)系統理論架構方案》,也適合正在進行IM系統架構設計研究的同窗閱讀。程序員
學習交流:github
- 即時通信開發交流3羣:185926912[推薦]算法
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》數據庫
(本文同步發佈於:http://www.52im.net/thread-2015-1-1.html)後端
《IM羣聊消息到底是存1份(即擴散讀)仍是存多份(即擴散寫)?》
所謂的羣聊消息系統,就是一種多對多羣體聊天方式,譬如直播房間內的聊天室對應的服務器端就是一個羣聊消息系統。
2017年9月初,咱們初步實現了一套極簡的羣聊消息系統,其大體架構以下:
系統名詞解釋:
1)Client : 消息發佈者【或者叫作服務端羣聊消息系統調用者】,publisher;
2)Proxy : 系統代理,對外統一接口,收集Client發來的消息轉發給Broker;
3)Broker :系統消息轉發Server,Broker 會根據 Gateway Message 組織一個 RoomGatewayList【key爲RoomID,value爲 Gateway IP:Port 地址列表】,而後把 Proxy 發來的消息轉發到 Room 中全部成員登陸的全部 Gateway;
4)Router :用戶登陸消息轉發者,把Gateway轉發來的用戶登入登出消息轉發給全部的Broker;
5)Gateway :全部服務端的入口,接收合法客戶端的鏈接,並把客戶端的登陸登出消息經過Router轉發給全部的Broker;
6)Room Message : Room聊天消息;
7)Gateway Message : Room內某成員 登陸 或者 登出 某Gateway消息,包含用戶UIN/RoomID/Gateway地址{IP:Port}等消息。
當一個 Room 中多個 Client 鏈接一個 Gateway 的時候,Broker只會根據 RoomID 把房間內的消息轉發一次給這個Gateway,由Gateway再把消息複製多份分別發送給鏈接這個 Gateway 的 Room 中的全部用戶的客戶端。
這套系統有以下特色:
1)系統只轉發房間內的聊天消息,每一個節點收到後當即轉發出去,不存儲任何房間內的聊天消息,不考慮消息丟失以及消息重複的問題;
2)系統固定地由一個Proxy、三個Broker和一個Router構成;
3)Proxy接收後端發送來的房間消息,而後按照必定的負載均衡算法把消息發往某個Broker,Broker則把消息發送到全部與Room有關係的接口機Gateway;
4)Router接收Gateway轉發來的某個Room內某成員在這個Gateway的登出或者登陸消息,而後把消息發送到全部Broker;
5)Broker收到Router轉發來的Gateway消息後,更新(添加或者刪除)與某Room相關的Gateway集合記錄;
6)整個系統的通訊鏈路採用UDP通訊方式。
從以上特色,整個消息系統足夠簡單,沒有考慮擴縮容問題,當系統負載到達極限的時候,就從新再部署一套系統以應對後端client的消息壓力。
這種處理方式本質是把系統的擴容能力甩鍋給了後端Client以及前端Gateway:每次擴容一個系統,全部Client須要在本地配置文件中添加一個Proxy地址而後所有重啓,全部Gateway則須要再本地配置文件添加一個Router地址而後所有重啓。
這種「幸福我一人,辛苦千萬家」的擴容應對方式,必然致使公司內部這套系統的使用者怨聲載道,下一階段的升級就是必然的了。
大道之行也,天下爲公,不一樣的系統有不一樣的構架,相同的系統總有相似的實現。相似於數據庫的分庫分表【關於分庫分表,目前看到的最好的文章是《一種支持自由規劃無須數據遷移和修改路由代碼的Replicaing擴容方案》】,其擴展實現核心思想是分Partition分Replica,但各Replica之間還區分leader(leader-follower,只有leader可接受寫請求)和non-leader(全部replica都可接收寫請求)兩種機制。
從數據角度來看,這套系統接收兩種消息:Room Message(房間聊天消息)和Gateway Message(用戶登陸消息)。兩種消息的交匯之地就是Broker,因此應對擴展的緊要地方就是Broker,Broker的每一個Partition採用non-leader機制,各replica都可接收Gateway Message消息寫請求和Room Message轉發請求。
首先,當Room Message量加大時能夠對Proxy進行水平擴展,多部署Proxy便可因應Room Message的流量。
其次,當Gateway Message量加大時能夠對Router進行水平擴展,多部署Router便可因應Gateway Message的流量。
最後,兩種消息的交匯之地Broker如何擴展呢?能夠把若干Broker Replica組成一個Partition,由於Gateway Message是在一個Partition內廣播的,全部Broker Replica都會有相同的RoomGatewayList 數據,所以當Gateway Message增長時擴容Partition便可。當Room Message量增長時,水平擴容Partition內的Broker Replica便可,由於Room Message只會發送到Partition內某個Replica上。
從我的經驗來看,Room ID的增加以及Room內成員的增長量在一段時間內能夠認爲是直線增長,而Room Message可能會以指數級增加,因此若設計得當則Partition擴容的機率很小,而Partition內Replica水平增加的機率幾乎是100%。
無論是Partition級別的水平擴容仍是Partition Replica級別的水平擴容,不可能像系統極簡版本那樣每次擴容後都須要Client或者Gateway去更新配置文件而後重啓,因應之道就是可用zookeeper充當角色的Registriy。經過這個zookeeper註冊中心,相關角色擴容的時候在Registry註冊後,與之相關的其餘模塊獲得通知便可獲取其地址等信息。採用zookeeper做爲Registry的時候,因此程序實現的時候採用實時watch和定時輪詢的策略保證數據可靠性,由於一旦網絡有任何的抖動,zk就會認爲客戶端已經宕機把連接關閉。
分析完畢,與之相對的架構圖以下:
如下各分章節將描述各個模塊詳細流程。
Client詳細流程以下:
1)從配置文件加載Registry地址;
2)從Registy上Proxy註冊路徑/pubsub/proxy下獲取全部的Proxy,依據各個Proxy ID大小順序遞增組成一個ProxyArray;
3)啓動一個線程實時關注Registry路徑/pubsub/proxy,以獲取Proxy的動態變化,及時更新ProxyArray;
4)啓動一個線程定時輪詢獲取Registry路徑/pubsub/proxy下各個Proxy實例,做爲關注策略的補充,以期本地ProxyArray內各個Proxy成員與Registry上的各個Proxy保持一致;定時給各個Proxy發送心跳,異步獲取心跳回包;定時清除ProxyArray中心跳超時的Proxy成員;
5)發送消息的時候採用snowflake算法給每一個消息分配一個MessageID,而後採用相關負載均衡算法把消息轉發給某個Proxy。
Proxy詳細流程以下:
1)讀取配置文件,獲取Registry地址;
2)把自身信息註冊到Registry路徑/pubsub/proxy下,把Registry返回的ReplicaID做爲自身ID;
3)從Registry路徑/pubsub/broker/partition(x)下獲取每一個Broker Partition的各個replica;
4)從Registry路徑/pubsub/broker/partition_num獲取當前有效的Broker Partition Number;
5)啓動一個線程關注Registry上的Broker路徑/pubsub/broker,以實時獲取如下信息:
{Broker Partition Number}
- 新的Broker Partition(此時發生了擴容);
- Broker Partition內新的broker replica(Partition內發生了replica擴容);
- Broker Parition內某replica掛掉的信息;
6)定時向各個Broker Partition replica發送心跳,異步等待Broker返回的心跳響應包,以探測其活性,以保證不向超時的replica轉發Room Message;
7)啓動一個線程定時讀取Registry上的Broker路徑/pubsub/broker下各個子節點的值,以定時輪詢的策略觀察Broker Partition Number變更,以及各Partition的變更狀況,做爲實時策略的補充;同時定時檢查心跳包超時的Broker,從有效的BrokerList中刪除;
8)依據規則【BrokerPartitionID = RoomID % BrokerPartitionNum, BrokerReplicaID = RoomID % BrokerPartitionReplicaNum】向某個Partition的replica轉發Room Message,收到Client的Heatbeat包時要及時給予響應。
之因此把Room Message和Heartbeat Message放在一個線程處理,是爲了防止進程假死這種狀況。
當/pubsub/broker/partition_num的值發生改變的時候(譬如值改成4),意味着Router Partition進行了擴展,Proxy要及時獲取新Partition路徑(如/pubsub/broker/Partition2和/pubsub/broker/Partition3)下的實例,並關注這些路徑,獲取新Partition下的實例。
之因此Proxy在獲取Registry下全部當前的Broker實例信息後再註冊自身信息,是由於此時它才具備轉發消息的資格。
Proxy轉發某個Room消息時候,只發送給處於Running狀態的Broker。爲Broker Partition內全部replica依據Registry給其分配的replicaID進行遞增排序,組成一個Broker Partition Replica Array,規則中BrokerPartitionReplicaNum爲Array的size,而BrokerReplicaID爲replica在Array中的下標。
收到的 Room Message 須要作三部工做:收取 Room Message、消息協議轉換和向 Broker 發送消息。
初始系統這三步流程若是均放在一個線程內處理,proxy 的總體吞吐率只有 50 000 Msg/s。
最後的實現方式是按照消息處理的三個步驟以 pipeline 方式作以下流程處理:
1)啓動 1 個消息接收線程和 N【N == Broker Parition 數目】個多寫一讀形式的無鎖隊列【稱之爲消息協議轉換隊列】,消息接收線程分別啓動一個 epoll 循環流程收取消息,而後把消息以相應的 hash 算法【隊列ID = UIN % N】寫入對應的消息協議轉換隊列;
2)啓動 N 個線程 和 N * 3 個一寫一讀的無鎖隊列【稱之爲消息發送隊列】,每一個消息協議專家線程從消息協議轉換隊列接收到消息並進行協議轉換後,根據相應的 hash 算法【隊列ID = UIN % 3N】寫入消息發送隊列;
3)啓動 3N 個消息發送線程,分別建立與之對應的 Broker 的鏈接,每一個線程單獨從對應的某個消息發送隊列接收消息而後發送出去。
通過以上流水線改造後,Proxy 的總體吞吐率可達 200 000 Msg/s。
關於 pipeline 自身的解釋,本文不作詳述,能夠參考下圖:
每一個 Room 的人數不均,最簡便的解決方法就是給不一樣人數量級的 Room 各搭建一套消息系統,不用修改任何代碼。
然所謂需求推進架構改進,在系統迭代升級過程當中遇到了這樣一個需求:業務方有一個全國 Room,用於給全部在線用戶進行消息推送。針對這個需求,不可能爲了一個這樣的 Room 單獨搭建一套系統,何況這個 Room 的消息量不多。
若是把這個 Room 的消息直接發送給現有系統,它有可能影響其餘 Room 的消息發送:消息系統是一個寫放大的系統,全國 Room 內有系統全部的在線用戶,每次發送都會卡頓其餘 Room 的消息發送。
最終的解決方案是:使用相似於分區的方法,把這樣的大 Room 映射爲 64 個虛擬 Room【稱之爲 VRoom】。在 Room 號段分配業務線的配合下,給消息系統專門保留了一個號段,用於這種大 Room 的切分,在 Proxy 層依據一個 hash 方法 【 VRoomID = UserID % 64】 把每一個 User 分配到相應的 VRoom,其餘模塊代碼不用修改即完成了大 Room 消息的路由。
Broker詳細流程以下:
1)Broker加載配置,獲取自身所在Partition的ID(假設爲3);
2)向Registry路徑/pubsub/broker/partition3註冊,設置其狀態爲Init,註冊中心返回的ID做爲自身的ID(replicaID);
3)接收Router轉發來的Gateway Message,放入GatewayMessageQueue;
4)從Database加載數據,把自身所在的Broker Partition所應該負責的 RoomGatewayList 數據加載進來;
5)異步處理GatewayMessageQueue內的Gateway Message,只處理知足規則【PartitionID == RoomID % PartitionNum】的消息,把數據存入本地路由信息緩存;
6)修改Registry路徑/pubsub/broker/partition3下自身節點的狀態爲Running;
7)啓動線程實時關注Registry路徑/pubsub/broker/partition_num的值;
8)啓動線程定時查詢Registry路徑/pubsub/broker/partition_num的值;
9)當Registry路徑/pubsub/broker/partition_num的值發生改變的時候,依據規則【PartitionID == RoomID % PartitionNum】清洗本地路由信息緩存中每條數據;
10)接收Proxy發來的Room Message,依據RoomID從路由信息緩存中查找Room有成員登錄的全部Gateway,把消息轉發給這些Gateway。
注意Broker之因此先註冊而後再加載Database中的數據,是爲了在加載數據的時候同時接收Router轉發來的Gateway Message,可是在數據加載完前這些受到的數據先被緩存起來,待全部 RoomGatewayList 數據加載完後就把這些數據重放一遍;
Broker之因此區分狀態,是爲了在加載完畢 RoomGatewayList 數據前不對Proxy提供轉發消息的服務,同時也方便Broker Partition應對的消息量增大時進行水平擴展。
當Broker發生Partition擴展的時候,新的Partition個數必須是2的冪,只有新Partition內全部Broker Replica都加載實例完畢,再更改/pubsub/broker/partition_num的值。
老的Broker也要watch路徑/pubsub/broker/partition_num的值,當這個值增長的時候,它也須要清洗本地的路由信息緩存。
Broker的擴容過程猶如細胞分裂,造成中的兩個細胞有着徹底相同的數據,分裂完成後【Registry路徑/pubsub/broker/partition_num的值翻倍】則須要清洗垃圾信息。這種方法稱爲翻倍法。
Router詳細流程以下:
1)Router加載配置,Registry地址;
2)把自身信息註冊到Registry路徑/pubsub/router下,把Registry返回的ReplicaID做爲自身ID;
3)從Registry路徑/pubsub/broker/partition(x)下獲取每一個Broker Partition的各個replica;
4)從Registry路徑/pubsub/broker/partition_num獲取當前有效的Broker Partition Number;
5)啓動一個線程關注Registry上的Broker路徑/pubsub/broker,以實時獲取如下信息:
{Broker Partition Number}
- 新的Broker Partition(此時發生了擴容);
- Broker Partition內新的broker replica(Partition內發生了replica擴容);
- Broker Parition內某replica掛掉的信息;
6)定時向各個Broker Partition replica發送心跳,異步等待Broker返回的心跳響應包,以探測其活性,以保證不向超時的replica轉發Gateway Message;
7)啓動一個線程定時讀取Registry上的Broker路徑/pubsub/broker下各個子節點的值,以定時輪詢的策略觀察Broker Partition Number變更,以及各Partition的變更狀況,做爲實時策略的補充;同時定時檢查心跳包超時的Broker,從有效的BrokerList中刪除;
8)從Database全量加載路由 RoomGatewayList 數據放入本地緩存;
9)收取Gateway發來的心跳消息,及時返回ack包;
10)收取Gateway轉發來的Gateway Message,按照必定規則【BrokerPartitionID % BrokerPartitionNum = RoomID % BrokerPartitionNum】轉發給某個Broker Partition下全部Broker Replica,保證Partition下全部replica擁有一樣的路由 RoomGatewayList 數據,再把Message內數據存入本地緩存,當檢測到數據不重複的時候把數據異步寫入Database。
Gateway詳細流程以下:
1)讀取配置文件,加載Registry地址;
2)從Registry路徑/pubsub/router/下獲取全部router replica,依據各Replica的ID遞增排序組成replica數組RouterArray;
3)啓動一個線程實時關注Registry路徑/pubsub/router,以獲取Router的動態變化,及時更新RouterArray;
4)啓動一個線程定時輪詢獲取Registry路徑/pubsub/router下各個Router實例,做爲關注策略的補充,以期本地RouterArray及時更新;定時給各個Router發送心跳,異步獲取心跳回包;定時清除RouterArray中心跳超時的Router成員;
5)當有Room內某成員客戶端鏈接上來或者Room內全部成員都不鏈接當前Gateway節點時,依據規則【RouterArrayIndex = RoomID % RouterNum】向某個Router發送Gateway Message;
6)收到Broker轉發來的Room Message時,根據MessageID進行去重,若是不重複則把消息發送到鏈接到當前Gateway的Room內全部客戶端,同時把MessageID緩存起來以用於去重判斷。
Gateway本地有一個基於共享內存的LRU Cache,存儲最近一段時間發送的消息的MessageID。
系統具備了可擴展性僅僅是系統可用的初步,整個系統要保證最低粒度的SLA(0.99),就必須在兩個維度對系統的可靠性就行感知:消息延遲和系統內部組件的高可用。
準確的消息延遲的統計,通用的作法能夠基於日誌系統對系統全部消息或者以必定機率抽樣後進行統計,但限於人力目前沒有這樣作。
目前使用了一個方法:經過一種構造一組僞用戶ID,定時地把消息發送給proxy,每條消息通過一層就把在這層的進入時間和發出時間以及組件自身的一些信息填入消息,這組僞用戶的消息最終會被髮送到一個僞Gateway端,僞Gateway對這些消息的信息進行歸併統計後,便可計算出當前系統的平均消息延遲時間。
經過全部消息的平均延遲能夠評估系統的總體性能。同時,由於系統消息路由的哈希方式已知,當固定時間內僞Gateway沒有收到消息時,就把消息當作發送失敗,當某條鏈路失敗必定次數後就能夠產生告警了。
上面的方法同時可以檢測某個鏈路是否出問題,可是鏈路具體出問題的點沒法判斷,且實時性沒法保證。
爲了保證各個組件的高可用,系統引入了另外一種評估方法:每一個層次都給後端組件發送心跳包,經過心跳包的延遲和成功率判斷其下一級組件的當前的可用狀態。
譬如proxy定時給每一個Partition內每一個broker發送心跳,能夠依據心跳的成功率來快速判斷broker是否處於「假死」狀態(最近業務就遇到過broker進程還活着,可是對任何收到的消息都不處理的狀況)。
同時依靠心跳包的延遲還能夠判斷broker的處理能力,基於此延遲值可在同一Partition內多broker端進行負載均衡。
公司內部內部原有一個走tcp通道的羣聊消息系統,可是通過元旦一次大事故(幾乎全線崩潰)後,相關業務的一些重要消息改走這套基於UDP的羣聊消息系統了。這些消息如服務端下達給客戶端的遊戲動做指令,是不容許丟失的,但其特色是相對於聊天消息來講量很是小(單人1秒最多一個),因此須要在目前UDP鏈路傳遞消息的基礎之上再構建一個可靠消息鏈路。
國內某IM大廠的消息系統也是以UDP鏈路爲基礎的(見《爲何QQ用的是UDP協議而不是TCP協議?》),他們的作法是消息重試加ack構建了可靠消息穩定傳輸鏈路。可是這種作法會下降系統的吞吐率,因此須要獨闢蹊徑。
UDP通訊的本質就是假裝的IP通訊,TCP自身的穩定性無非是重傳、去重和ack,因此不考慮消息順序性的狀況下能夠經過重傳與去重來保證消息的可靠性。
基於目前系統的可靠消息傳輸流程以下:
1)Client給每一個命令消息依據snowflake算法配置一個ID,複製三份,當即發送給不一樣的Proxy;
2)Proxy收到命令消息之後隨機發送給一個Broker;
3)Broker收到後傳輸給Gateway;
4)Gateway接收到命令消息後根據消息ID進行重複判斷,若是重複則丟棄,不然就發送給APP,並緩存之。
正常的消息在羣聊消息系統中傳輸時,Proxy會根據消息的Room ID傳遞給固定的Broker,以保證消息的有序性。
當線上須要部署多套羣聊消息系統的時候,Gateway須要把一樣的Room Message複製多份轉發給多套羣聊消息系統,會增大Gateway壓力,能夠把Router單獨獨立部署,而後把Room Message向全部的羣聊消息系統轉發。
Router系統原有流程是:Gateway按照Room ID把消息轉發給某個Router,而後Router把消息轉發給下游Broker實例。新部署一套羣聊消息系統的時候,新系統Broker的schema須要經過一套約定機制通知Router,使得Router自身邏輯過於複雜。
重構後的Router架構參照上圖,也採用分Partition分Replica設計,Partition內部各Replica之間採用non-leader機制;各Router Replica不會主動把Gateway Message內容push給各Broker,而是各Broker主動經過心跳包形式向Router Partition內某個Replica註冊,然後此Replica纔會把消息轉發到這個Broker上。
相似於Broker,Router Partition也以2倍擴容方式進行Partition水平擴展,並經過必定機制保證擴容或者Partition內部各個實例中止運行或者新啓動時,盡力保證數據的一致性。
Router Replica收到Gateway Message後,replica先把Gateway Message轉發給Partition內各個peer replica,而後再轉發給各個訂閱者。Router轉發消息的同時異步把消息數據寫入Database。
獨立Router架構下,下面小節將分別詳述Gateway、Router和Broker三個相關模塊的詳細流程。
Gateway詳細流程以下:
1)從Registry路徑/pubsub/router/partition(x)下獲取每一個Partition的各個replica;
2)從Registry路徑/pubsub/router/partition_num獲取當前有效的Router Partition Number;
3)啓動一個線程關注Registry上的Router路徑/pubsub/router,以實時獲取如下信息:{Router Partition Number} -> 新的Router Partition(此時發生了擴容); Partition內新的replica(Partition內發生了replica擴容); Parition內某replica掛掉的信息;
4)定時向各個Partition replica發送心跳,異步等待Router返回的心跳響應包,以探測其活性,以保證不向超時的replica轉發Gateway Message;
5)啓動一個線程定時讀取Registry上的Router路徑/pubsub/router下各個子節點的值,以定時輪詢的策略觀察Router Partition Number變更,以及各Partition的變更狀況,做爲實時策略的補充;同時定時檢查心跳包超時的Router,從有效的BrokerList中刪除;
6 依據規則向某個Partition的replica轉發Gateway Message。
第六步的規則決定了Gateway Message的目的Partition和replica,規則內容有:
若是某Router Partition ID知足condition(RoomID % RouterPartitionNumber == RouterPartitionID % RouterPartitionNumber),則把消息轉發到此Partition;
這裏之因此不採用直接hash方式(RouterPartitionID = RoomID % RouterPartitionNumber)獲取Router Partition,是考慮到當Router進行2倍擴容的時候當全部新的Partition的全部Replica都啓動完畢且數據一致時纔會修改Registry路徑/pubsub/router/partitionnum的值,按照規則的計算公式才能保證新Partition的各個Replica在啓動過程當中就能夠獲得Gateway Message,也即此時每一個Gateway Message會被髮送到兩個Router Partition。 當Router擴容完畢,修改Registry路徑/pubsub/router/partitionnum的值後,此時新集羣進入穩按期,每一個Gateway Message只會被髮送固定的一個Partition,condition(RoomID % RouterPartitionNumber == RouterPartitionID % RouterPartitionNumber)等效於condition(RouterPartitionID = RoomID % RouterPartitionNumber)。
若是Router Partition內某replia知足condition(replicaPartitionID = RoomID % RouterPartitionReplicaNumber),則把消息轉發到此replica。
replica向Registry註冊的時候獲得的ID稱之爲replicaID,Router Parition內全部replica按照replicaID遞增排序組成replica數組RouterPartitionReplicaArray,replicaPartitionID即爲replica在數組中的下標。
Gateway Message數據一致性:
Gateway向Router發送的Router Message內容有兩種:某user在當前Gateway上進入某Room 和 某user在當前Gateway上退出某Room,數據項分別是UIN(用戶ID)、Room ID、Gateway Addr和User Action(Login or Logout。
因爲全部消息都是走UDP鏈路進行轉發,則這些消息的順序就有可能亂序。Gateway能夠統一給其發出的全部消息分配一個全局遞增的ID【下文稱爲GatewayMsgID,Gateway Message ID】以保證消息的惟一性和全局有序性。
Gateway向Registry註冊臨時有序節點時,Registry會給Gateway分配一個ID,Gateway能夠用這個ID做爲自身的Instance ID【假設這個ID上限是65535】。
GatewayMsgID字長是64bit,其格式以下:
//63 -------------------------- 48 47 -------------- 38 37 ------------ 0
//| 16bit Gateway Instance ID | 10bit Reserve | 38bit自增碼 |
Router系統部署以前,先設置Registry路徑/pubsub/router/partition_num的值爲1。
Router詳細流程以下:
1)Router加載配置,獲取自身所在Partition的ID(假設爲3);
2)向Registry路徑/pubsub/router/partition3註冊,設置其狀態爲Init,註冊中心返回的ID做爲自身的ID(replicaID);
3)註冊完畢會收到Gateway發來的Gateway Message以及Broker發來的心跳消息(HeartBeat Message),先緩存到消息隊列MessageQueue;
4)從Registry路徑/pubsub/router/partition3下獲取自身所在的Partition內的各個replica;
5)從Registry路徑/pubsub/router/partition_num獲取當前有效的Router Partition Number;
6)啓動一個線程關注Registry路徑/pubsub/router,以實時獲取如下信息:{Router Partition Number} -> Partition內新的replica(Partition內發生了replica擴容); Parition內某replica掛掉的信息;
7)從Database加載數據;
8)啓動一個線程異步處理MessageQueue內的Gateway Message,把Gateway Message轉發給同Partition內其餘peer replica,而後依據規則【RoomID % BrokerPartitionNumber == BrokerReplicaPartitionID % BrokerPartitionNumber】轉發給BrokerList內每一個Broker;處理Broker發來的心跳包,把Broker的信息存入本地BrokerList,而後給Broker發送回包;
9)修改Registry路徑/pubsub/router/partition3下節點的狀態爲Running;
10)啓動一個線程定時讀取Registry路徑/pubsub/router下各個子路徑的值,以定時輪詢的策略觀察Router各Partition的變更狀況,做爲實時策略的補充;檢查超時的Broker,把其從BrokerList中剔除;
11)當RouterPartitionNum倍增時,Router依據規則【RoomID % BrokerPartitionNumber == BrokerReplicaPartitionID % BrokerPartitionNumber】清洗自身路由信息緩存中數據;
12)Router本地存儲每一個Gateway的最大GatewayMsgID,收到小於GatewayMsgID的Gateway Message能夠丟棄不處理,不然就更新GatewayMsgID並根據上面邏輯進行處理。
之因此把Gateway Message和Heartbeat Message放在一個線程處理,是爲了防止進程假死這種狀況。
Broker也採用了分Partition分Replica機制,因此向Broker轉發Gateway Message時候路由規則,與Gateway向Router轉發消息的路由規則相同。
另外啓動一個工具,當水平擴展後新啓動的Partition內全部Replica的狀態都是Running的時候,修改Registry路徑/pubsub/router/partition_num的值爲全部Partition的數目。
Broker詳細流程以下:
1)Broker加載配置,獲取自身所在Partition的ID(假設爲3);
2)向Registry路徑/pubsub/broker/partition3註冊,設置其狀態爲Init,註冊中心返回的ID做爲自身的ID(replicaID);
3)從Registry路徑/pubsub/router/partition_num獲取當前有效的Router Partition Number;
4)從Registry路徑/pubsub/router/partition(x)下獲取每一個Router Partition的各個replica;
5)啓動一個線程關注Registry路徑/pubsub/router,以實時獲取如下信息:{Router Partition Number} -> 新的Router Partition(此時發生了擴容); Partition內新的replica(Partition內發生了replica擴容); Parition內某replica掛掉的信息;
6)依據規則【RouterPartitionID % BrokerPartitionNum == BrokerPartitionID % BrokerPartitionNum,RouterReplicaID = BrokerReplicaID % BrokerPartitionNum】選定目標Router Partition下某個Router replica,向其發送心跳消息,包含BrokerPartitionNum、BrokerPartitionID、BrokerHostAddr和精確到秒級的Timestamp,並異步等待全部Router replica的回覆,全部Router轉發來的Gateway Message放入GatewayMessageQueue;
7)依據規則【BrokerPartitionID == RoomID % BrokerParitionNum】從Database加載數據;
8)依據規則【BrokerPartitionID % BrokerParitionNum == RoomID % BrokerParitionNum】異步處理GatewayMessageQueue內的Gateway Message,只留下合乎規則的消息的數據;
9)修改Registry路徑/pubsub/broker/partition3下自身節點的狀態爲Running;
10)啓動一個線程定時讀取Registry路徑/pubsub/router下各個子路徑的值,以定時輪詢的策略觀察Router各Partition的變更狀況,做爲實時策略的補充;定時檢查超時的Router,某Router超時後更換其所在的Partition內其餘Router替換之,定時發送心跳包;
11)當Registry路徑/pubsub/broker/partition_num的值BrokerPartitionNum發生改變的時候,依據規則【PartitionID == RoomID % PartitionNum】清洗本地路由信息緩存中每條數據;
12)接收Proxy發來的Room Message,依據RoomID從路由信息緩存中查找Room有成員登錄的全部Gateway,把消息轉發給這些Gateway;
13)Broker本地存儲每一個Gateway的最大GatewayMsgID,收到小於GatewayMsgID的Gateway Message能夠丟棄不處理,不然更新GatewayMsgID並根據上面邏輯進行處理。
BrokerPartitionNumber能夠小於或者等於或者大於RouterPartitionNumber,兩個數應該均是2的冪,兩個集羣能夠分別進行擴展,互不影響。譬如BrokerPartitionNumber=4而RouterPartitionNumber=2,則Broker Partition 3只須要向Router Partition 1的某個follower發送心跳消息便可;若BrokerPartitionNumber=4而RouterPartitionNumber=8,則Broker Partition 3須要向Router Partition 3的某個follower發送心跳消息的同時,還須要向Router Partition 7的某個follower發送心跳,以獲取全量的Gateway Message。
Broker須要關注/pubsub/router/partitionnum和/pubsub/broker/partitionnum的值的變化,當router或者broker進行parition水平擴展的時候,Broker須要及時從新構建與Router之間的對應關係,及時變更發送心跳的Router Replica對象【RouterPartitionID = BrokerReplicaID % RouterPartitionNum,RouterPartitionID爲Router Replica在PartitionRouterReplicaArray數組的下標】。
當Router Partition內replica死掉或者發送心跳包的replica對象死掉(不管是註冊中心通知仍是心跳包超時),broker要及時變更發送心跳的Router replica對象。
另外,Gateway使用UDP通訊方式向Router發送Gateway Message,如若這個Message丟失則此Gateway上該Room內全部成員一段時間內(當有新的成員在當前Gateway上加入Room 時會產生新的Gateway Message)都沒法再接收消息,爲了保證消息的可靠性,可使用這樣一個約束解決問題:在此Gateway上登陸的某Room內的人數少於3時,Gateway會把Gateway Message複製兩份非連續(如以10ms爲時間間隔)重複發送給某個Partition leader。因Gateway Message消息處理的冪等性,重複Gateway Message並不會致使Room Message發送錯誤,只在極少機率的狀況下會致使Gateway收到消息的時候Room內已經沒有成員在此Gateway登陸,此時Gateway會把消息丟棄不做處理。
傳遞實時消息羣聊消息系統的Broker向特定Gateway轉發Room Message的時候,會帶上Room內在此Gateway上登陸的用戶列表,Gateway根據這個用戶列表下發消息時若是檢測到此用戶已經下線,在放棄向此用戶轉發消息的同時,還應該把此用戶已經下線的消息發送給Router,當Router把這個消息轉發給Broker後,Broker把此用戶從用戶列表中剔除。經過這種負反饋機制保證用戶狀態更新的及時性。
前期的系統只考慮了用戶在線狀況下實時消息的傳遞,當用戶離線時其消息便沒法獲取。
若系統考慮用戶離線消息傳遞,須要考慮以下因素:
1)消息固化:保證用戶上線時收到其離線期間的消息;
2)消息有序:離線消息和在線消息都在一個消息系統傳遞,給每一個消息分配一個ID以區分消息前後順序,消息順序越靠後則ID愈大。
離線消息的存儲和傳輸,須要考慮用戶的狀態以及每條消息的發送狀態,整個消息核心鏈路流程會有大的重構。
新消息架構以下圖:
系統名詞解釋:
1)Pi : 消息ID存儲模塊,存儲每一個人未發送的消息ID有序遞增集合;
2)Xiu : 消息存儲KV模塊,存儲每一個人的消息,給每一個消息分配ID,以ID爲key,以消息內爲value;
3)Gateway Message(HB) : 用戶登陸登出消息,包括APP保活定時心跳(Hearbeat)消息。
系統內部代號貔貅(貔貅者,雄貔雌貅),源自上面兩個新模塊。
這個版本架構流程的核心思想爲「消息ID與消息內容分離,消息與用戶狀態分離」。消息發送流程涉及到模塊 Client/Proxy/Pi/Xiu,消息推送流程則涉及到模塊 Pi/Xiu/Broker/Router/Gateway。
下面小節先細述Pi和Xiu的接口,而後再詳述發送和推送流程。
Xiu模塊功能名稱是Message Storage,用戶緩存和固化消息,並給消息分配ID。Xiu 集羣採用分 Partition 分 Replica 機制,Partition 初始數目須是2的倍數,集羣擴容時採用翻倍法。
8.2.1 存儲消息
存儲消息請求的參數列表爲{SnowflakeID,UIN, Message},其流程以下:
1)接收客戶端發來的消息,獲取消息接收人ID(UIN)和客戶端給消息分配的 SnowflakeID;
2)檢查 UIN % Xiu_Partition_Num == Xiu_Partition_ID % Xiu_Partition_Num 添加是否成立【即接收人的消息是否應當由當前Xiu負責】,不成立則返回錯誤並退出;
3)檢查 SnowflakeID 對應的消息是否已經被存儲過,若已經存儲過則返回其對應的消息ID而後退出;
4)給消息分配一個 MsgID:
每一個Xiu有本身惟一的 Xiu_Partition_ID,以及一個初始值爲 0 的 Partition_Msg_ID。MsgID = 1B[ Xiu_Partition_ID ] + 1B[ Message Type ] + 6B[ ++ Partition_Msg_ID ]。每次分配的時候 Partition_Msg_ID 都自增長一。
5)以 MsgID 爲 key 把消息存入基於共享內存的 Hashtable,並存入消息的 CRC32 hash值和插入時間,把 MsgID 存入一個 LRU list 中:
LRU List 自身並不存入共享內存中,當進程重啓時,能夠根據Hashtable中的數據重構出這個List。把消息存入 Hashtable 中時,若是 Hashtable full,則依據 LRU List 對Hashtable 中的消息進行淘汰。
6)把MsgID返回給客戶端;
7)把MsgID異步通知給消息固化線程,消息固化線程根據MsgID從Hashtable中讀取消息並根據CRC32 hash值判斷消息內容是否完整,完整則把消息存入本地RocksDB中。
8.2.2讀取消息
讀取消息請求的參數列表爲{UIN, MsgIDList},其流程爲:
1)獲取請求的 MsgIDList,判斷每一個MsgID MsgID{Xiu_Partition_ID} == Xiu_Partition_ID 條件是否成立,不成立則返回錯誤並退出;
2)從 Hashtable 中獲取每一個 MsgID 對應的消息;
3)若是 Hashtable 中不存在,則從 RocksDB 中讀取 MsgID 對應的消息;
4)讀取完畢則把全部獲取的消息返回給客戶端。
8.2.3主從數據同步
目前從簡,暫定Xiu的副本只有一個。
Xiu節點啓動的時候根據自身配置文件中分配的 Xiu_Partition_ID 到Registry路徑 /pubsub/xiu/partition_id 下進行註冊一個臨時有序節點,註冊成功則Registry會返回Xiu的節點 ID。
Xiu節點獲取 /pubsub/xiu/partition_id 下的全部節點的ID和地址信息,依據 節點ID最小者爲leader 的原則,便可斷定本身的角色。只有leader可接受讀寫數據請求。
數據同步流程以下:
1)follower定時向leader發送心跳信息,心跳信息包含本地最新消息的ID;
2)leader啓動一個數據同步線程處理follower的心跳信息,leader的數據同步線程從LRU list中查找 follower_latest_msg_id 以後的N條消息的ID,若獲取到則讀取消息並同步給follower,獲取不到則回覆其與leader之間消息差距太大;
3)follower從leader獲取到最新一批消息,則存儲之;
4)follower若獲取leader的消息差距太大響應,則請求leader的agent把RocksDB的固化數據全量同步過來,整理完畢後再次啓動與leader之間的數據同步流程。
follower會關注Registry路徑 /pubsub/xiu/partition_id 下全部全部節點的變化狀況,若是leader掛掉則及時轉換身份並接受客戶端請求。若是follower 與 leader 之間的心跳超時,則follower刪掉 leader 的 Registry 路徑節點,及時進行身份轉換處理客戶端請求。
當leader重啓或者follower轉換爲leader的時候,須要把 Partition_Msg_ID 進行一個大數值增值(譬如增長1000)以防止可能的消息ID亂序狀況。
8.2.4集羣擴容
Xiu 集羣擴容採用翻倍法,擴容時新 Partition 的節點啓動後工做流程以下:
1)向Registry的路徑 /pubsub/xiu/partition_id 下本身的 node 的 state 爲 running,同時註冊本身的對外服務地址信息;
2)另外啓動一個工具,當水平擴展後全部新啓動的 Partition 內全部 Replica 的狀態都是 Running 的時候,修改 Registry 路徑 /pubsub/xiu/partition_num 的值爲擴容後 Partition 的數目。按照開頭的例子,即由2升級爲4。
之因此 Xiu 不用像 Broker 和 Router 那樣啓動的時候向老的 Partition 同步數據,是由於每一個 Xiu 分配的 MsgID 中已經帶有 Xiu 的 PartitionID 信息,即便集羣擴容這個 ID 也不變,根據這個ID也能夠定位到其所在的Partition,而不是藉助 hash 方法。
Pi 模塊功能名稱是 Message ID Storage,存儲每一個用戶的 MsgID List。Xiu 集羣也採用分 Partition 分 Replica 機制,Partition 初始數目須是2的倍數,集羣擴容時採用翻倍法。
8.3.1存儲消息ID
MsgID 存儲的請求參數列表爲{UIN,MsgID},Pi 工做流程以下:
1)判斷條件 UIN % Pi_Partition_Num == Pi_Partition_ID % Pi_Partition_Num 是否成立,若不成立則返回error退出;
2)把 MsgID 插入UIN的 MsgIDList 中,保持 MsgIDList 中全部 MsgID 不重複有序遞增,把請求內容寫入本地log,給請求者返回成功響應。
Pi有專門的日誌記錄線程,給每一個日誌操做分配一個 LogID,每一個 Log 文件記錄必定量的寫操做,當文件 size 超過配置的上限後刪除之。
8.3.2讀取消息ID列表
讀取請求參數列表爲{UIN, StartMsgID, MsgIDNum, ExpireFlag},其意義爲獲取用戶 UIN 自起始ID爲 StartMsgID 起(不包括 StartMsgID )的數目爲 MsgIDNum 的消息ID列表,ExpireFlag意思是 全部小於等於 StartMsgID 的消息ID是否刪除。
流程以下:
1)判斷條件 UIN % Pi_Partition_Num == Pi_Partition_ID % Pi_Partition_Num 是否成立,若不成立則返回error退出;
2)獲取 (StartID, StartMsgID + MsgIDNum] 範圍內的全部 MsgID,把結果返回給客戶端;
3)若是 ExpireFlag 有效,則刪除MsgIDList內全部在 [0, StartMsgID] 範圍內的MsgID,把請求內容寫入本地log。
8.3.3主從數據同步
同 Xiu 模塊,暫定 Pi 的同 Parition 副本只有一個。
Pi 節點啓動的時候根據自身配置文件中分配的 Pi_Partition_ID 到Registry路徑 /pubsub/pi/partition_id 下進行註冊一個臨時有序節點,註冊成功則 Registry 會返回 Pi 的節點 ID。
Pi 節點獲取 /pubsub/pi/partition_id 下的全部節點的ID和地址信息,依據 節點ID最小者爲leader 的原則,便可斷定本身的角色。只有 leader 可接受讀寫數據請求。
數據同步流程以下:
1)follower 定時向 leader 發送心跳信息,心跳信息包含本地最新 LogID;
2)leader 啓動一個數據同步線程處理 follower 的心跳信息,根據 follower 彙報的 logID 把此 LogID;
3)follower 從 leader 獲取到最新一批 Log,先存儲而後重放。
follower 會關注Registry路徑 /pubsub/pi/partition_id 下全部節點的變化狀況,若是 leader 掛掉則及時轉換身份並接受客戶端請求。若是follower 與 leader 之間的心跳超時,則follower刪掉 leader 的 Registry 路徑節點,及時進行身份轉換處理客戶端請求。
8.3.4集羣擴容
Pi 集羣擴容採用翻倍法。則節點啓動後工做流程以下:
1)向 Registry 註冊,獲取 Registry 路徑 /pubsub/xiu/partition_num 的值 PartitionNumber;
2)若是發現本身 PartitionID 知足條件 PartitionID >= PartitionNumber 時,則意味着當前 Partition 是擴容後的新集羣,更新 Registry 中本身狀態爲start;
3)讀取 Registry 路徑 /pubsub/xiu 下全部 Parition 的 leader,根據條件 自身PartitionID % PartitionNumber == PartitionID % PartitionNumber 尋找對應的老 Partition 的 leader,稱之爲 parent_leader;
4)緩存收到 Proxy 轉發來的用戶請求;
5)向 parent_leader 獲取log;
6)向 parent_leader 同步內存數據;
7)重放 parent_leader 的log;
8)更新 Registry 中本身的狀態爲 Running;
9)重放用戶請求;
10)當 Registry 路徑 /pubsub/xiu/partition_num 的值 PartitionNumber 知足條件 PartitionID >= PartitionNumber 時,意味着擴容完成,處理用戶請求時要給用戶返回響應。
Proxy 會把讀寫請求參照條件 UIN % Pi\_Partition\_Num == Pi\_Partition\_ID % Pi\_Partition\_Num 向相關 partition 的 leader 轉發用戶請求。假設原來 PartitionNumber 值爲2,擴容後值爲4,則原來轉發給 partition0 的寫請求如今需同時轉發給 partition0 和 partition2,原來轉發給 partition1 的寫請求如今需同時轉發給 partition1 和 partition3。
另外啓動一個工具,當水平擴展後全部新啓動的 Partition 內全部 Replica 的狀態都是 Running 的時候,修改Registry路徑/pubsub/xiu/partition_num的值爲擴容後 Partition 的數目。
消息自 PiXiu 的外部客戶端(Client,服務端全部使用 PiXiu 提供的服務者統稱爲客戶端)按照必定負載均衡規則發送到 Proxy,而後存入 Xiu 中,把 MsgID 存入 Pi 中。
其詳細流程以下:
1)Client 依據 snowflake 算法給消息分配 SnowflakeID,依據 ProxyID = UIN % ProxyNum 規則把消息發往某個 Proxy;
2)Proxy 收到消息後轉發到 Xiu;
3)Proxy 收到 Xiu 返回的響應後,把響應轉發給 Client;
4)若是 Proxy 收到 Xiu 返回的響應帶有 MsgID,則發起 Pi 寫流程,把 MsgID 同步到 Pi 中;
5)若是 Proxy 收到 Xiu 返回的響應帶有 MsgID,則給 Broker 發送一個 Notify,告知其某 UIN 的最新 MsgID。
轉發消息的主體是Broker,原來的在線消息轉發流程是它收到 Proxy 轉發來的 Message,而後根據用戶是否在線而後轉發給 Gateway。
PiXiu架構下 Broker 會收到如下類型消息:
1)用戶登陸消息;
2)用戶心跳消息;
3)用戶登出消息;
4)Notify 消息;
5)Ack 消息。
Broker流程受這五種消息驅動,下面分別詳述其收到這五種消息時的處理流程。
用戶登陸消息流程以下:
1)檢查用戶的當前狀態,若爲 OffLine 則把其狀態值爲在線 OnLine;
2)檢查用戶的待發送消息隊列是否爲空,不爲空則退出;
3)向 Pi 模塊發送獲取 N 條消息 ID 的請求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false},設置用戶狀態爲 GettingMsgIDList 並等待迴應;
4)根據 Pi 返回的消息 ID 隊列,向 Xiu 發起獲取消息請求 {UIN: uin, MsgIDList: msg ID List},設置用戶狀態爲 GettingMsgList 並等待迴應;
5)Xiu 返回消息列表後,設置狀態爲 SendingMsg,並向 Gateway 轉發消息。
能夠把用戶心跳消息當作用戶登陸消息處理。
Gateway的用戶登出消息產生有三種狀況:
1)用戶主動退出;
2)用戶心跳超時;
3)給用戶轉發消息時發生網絡錯誤。
用戶登出消息處理流程以下:
1)檢查用戶狀態,若是爲 OffLine,則退出;
2)用戶狀態不爲 OffLine 且檢查用戶已經發送出去的消息列表的最後一條消息的 ID(LastMsgID),向 Pi 發送獲取 MsgID 請求{UIN: uin, StartMsgID: LastMsgID, MsgIDNum: 0, ExpireFlag: True},待 Pi 返回響應後退出。
處理 Proxy 發來的 Notify 消息處理流程以下:
1)若是用戶狀態爲 OffLine,則退出;
2)更新用戶的最新消息 ID(LatestMsgID),若是用戶發送消息隊列不爲空則退出;
3)向 Pi 模塊發送獲取 N 條消息 ID 的請求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false},設置用戶狀態爲 GettingMsgIDList 並等待迴應;
4)根據 Pi 返回的消息 ID 隊列,向 Xiu 發起獲取消息請求 {UIN: uin, MsgIDList: msg ID List},設置用戶狀態爲 GettingMsgList 並等待迴應;
5)Xiu 返回消息列表後,設置狀態爲 SendingMsg,並向 Gateway 轉發消息。
所謂 Ack 消息,就是 Broker 經 Gateway 把消息轉發給 App 後,App 給Broker的消息回覆,告知Broker其最近成功收到消息的 MsgID。
Ack 消息處理流程以下:
1)若是用戶狀態爲 OffLine,則退出;
2)更新 LatestAckMsgID 的值;
3)若是用戶發送消息隊列不爲空,則發送下一個消息後退出;
4)若是 LatestAckMsgID >= LatestMsgID,則退出;
5)向 Pi 模塊發送獲取 N 條消息 ID 的請求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false},設置用戶狀態爲 GettingMsgIDList 並等待迴應;
6)根據 Pi 返回的消息 ID 隊列,向 Xiu 發起獲取消息請求 {UIN: uin, MsgIDList: msg ID List},設置用戶狀態爲 GettingMsgList 並等待迴應;
7)Xiu 返回消息列表後,設置狀態爲 SendingMsg,並向 Gateway 轉發消息。
整體上,PiXiu 轉發消息流程採用拉取(pull)轉發模型,以上面五種消息爲驅動進行狀態轉換,並做出相應的動做行爲。
這套羣聊消息系統尚有如下task list需完善:
1)消息以UDP鏈路傳遞,不可靠【2018/01/29解決之】;
2)目前的負載均衡算法採用了極簡的RoundRobin算法,能夠根據成功率和延遲添加基於權重的負載均衡算法實現;
3)只考慮傳遞,沒有考慮消息的去重,能夠根據消息ID實現這個功能【2018/01/29解決之】;
4)各個模塊之間沒有考慮心跳方案,整個系統的穩定性依賴於Registry【2018/01/17解決之】;
5)離線消息處理【2018/03/03解決之】;
6)區分消息優先級。
此記。
參考文檔:《一種支持自由規劃無須數據遷移和修改路由代碼的Replicaing擴容方案》
於雨氏,2017/12/31,初做此文於豐臺金箱堂。
於雨氏,2018/01/16,於海淀添加「系統穩定性」一節。
於雨氏,2018/01/29,於海淀添加「消息可靠性」一節。
於雨氏,2018/02/11,於海淀添加「Router」一節,並從新格式化全文。
於雨氏,2018/03/05,於海淀添加「PiXiu」一節。
於雨氏,2018/03/14,於海淀添加負反饋機制、根據Gateway Message ID保證Gateway Message數據一致性 和 Gateway用戶退出消息產生機制 等三個細節。
於雨氏,2018/08/05,於海淀添加 「pipeline」 一節。
於雨氏,2018/08/28,於海淀添加 「大房間消息處理」 一節。
《一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》
《IM開發基礎知識補課(二):如何設計大量圖片文件的服務端存儲架構?》
《IM開發基礎知識補課(三):快速理解服務端數據庫讀寫分離原理及實踐建議》
《IM開發基礎知識補課(四):正確理解HTTP短鏈接中的Cookie、Session和Token》
《WhatsApp技術實踐分享:32人工程團隊創造的技術神話》
《王者榮耀2億用戶量的背後:產品定位、技術架構、網絡方案等》
《IM系統的MQ消息中間件選型:Kafka仍是RabbitMQ?》
《騰訊資深架構師乾貨總結:一文讀懂大型分佈式系統設計的方方面面》
《子彈短信光鮮的背後:網易雲信首席架構師分享億級IM平臺的技術實踐》
《知乎技術分享:從單機到2000萬QPS併發的Redis高性能緩存實踐之路》
《IM開發基礎知識補課(五):通俗易懂,正確理解並用好MQ消息隊列》
《微信技術分享:微信的海量IM聊天消息序列號生成實踐(算法原理篇)》
《微信技術分享:微信的海量IM聊天消息序列號生成實踐(容災方案篇)》
《新手入門:零基礎理解大型分佈式架構的演進歷史、技術原理、最佳實踐》
>> 更多同類文章 ……
(本文同步發佈於:http://www.52im.net/thread-2015-1-1.html)