天天看點

TableStore Timeline:輕松建構千萬級IM和Feed流系統

移動網際網路時代,微信和微網誌已經成為這個時代的兩大支柱類社交應用。

這兩類應用,其中一個是IM産品,一個是Feed流産品,微信的朋友圈也屬于Feed流。

如果再細心去發現,會發現基本所有移動App都有Feed流的功能:消息廣場、個人關注、通知、新聞聚合和圖檔分享等等。各種各樣的Feed流産品占據了我們生活的方方面面。

IM和Feed流功能已經基本成為所有App标配,如何開發一個IM或者Feed流功能是很多架構師、工程師要面臨的問題。雖然是一個常見功能,但仍然是一個巨大的挑戰,要考慮的因素非常多,比如:

存儲系統裡面如何選型才能支援大量資料存儲,而且價格便宜。

同步系統使用何種架構、推送模型和系統才能保證高并發的同時延遲穩定?

為了解決上述問題,我們之前推出了三篇文章來闡述:

上述三篇文章推出後,使用者反響很好,在各個平台的傳播很廣,為很多使用者提供了設計一款IM和Feed流産品的架構思路,但是從這裡到完全實作一個可靠的IM、Feed流系統平台還有很長的路,比如:

如何才能更加簡單的弄懂消息存儲和同步模型?

如何将架構模型映射到存儲系統和推送系統中?

如何才能保證對存儲系統和推送系統的使用是最佳方式?

在《現代IM系統中消息推送和存儲架構的實作》中基于IM系統提出了Timeline模型,進一步會發現Timeline模型适用場景可以更廣泛:

有一方是消息生産者。

有一方是消息消費者。

生産者産生的一條消息可能會被一個或多個消費者消費。

消費者需要聚合來自多個生産者的消息在一個頁面展現。

IM和Feed流産品完全比對上述四個特征,是以Timeline模型可以完全适用于IM和Feed流場景中。

下面我們來看看如何在各個場景中使用Timeline:

單聊就是三個Timeline:

會話Timeline(存儲曆史消息)

使用者A的收件箱(A的同步庫Timeline)

使用者B的收件箱(B的同步庫Timeline)

群聊就是1 + N個Timeline:

。。。。。。

使用者N的收件箱(N的同步庫Timeline)

每個使用者隻有一個同步庫Timeline,就算使用者A在10個群裡面,那麼這個10個群的同步消息都是發送給使用者A的這一個同步庫Timeline中。

發一條朋友圈狀态就是1 + N個Timeline:

自己曆史消息Timeline(自己的存儲庫Timeline)

朋友A的收件箱(A的同步庫Timeline)

朋友N的收件箱(N的同步庫Timeline)

如果是微網誌,粉絲可能會達到上億級别,這時候會比朋友圈稍微複雜些:

大V發一條微網誌就是 1 + M個Timeline(M << N,N是粉絲數)。

粉絲A的收件箱(A的同步庫Timeline)

粉絲M的收件箱(M的同步庫Timeline)

從上面分析可以看出來,不管是IM,還是Feed流産品都可以将底層的存儲、同步邏輯抽象成一個對多個Timeline進行讀寫的模型。

有了Timeline概念模型後,從IM/Feed流應用映射到Timeline就比較容易了,但是從Timeline映射到存儲、同步系統仍然很複雜,主要展現在:

如何實作Timeline概念模型?

如何将Timeline模型轉換成對存儲系統、同步系統的讀寫接口?

如何設計存儲系統、同步系統的表結構?

如何選擇存儲系統、同步系統的讀寫方式?

如何評估存儲系統、同步系統的最大承載能力?

如何實作才能保證性能最佳?

如何才能吸取大型同類型系統架構設計的經驗教訓、

如何才能避免一些實作、使用上的隐患?

這些問題涉及的内容光,細節多,深度大,坑較多等,整體上很繁雜,這一部分在耗費了大量人力之後,結果可能并不理想。

針對上述問題,隻要存儲系統和推送系統确定後,剩餘的工作都是類似的,可以完全将經驗封裝起來成為一個LIB,将表結構設計,讀寫方式,隐患等等都解決好,然後供後來者使用,後來者可以不用再關心Timeline到底層存儲系統之間的事情了。

是以,我們基于JAVA語言實作了一個TableStore-Timeline LIB,簡稱Timeline LIB。

Timeline LIB的結構如下:

TableStore Timeline:輕松建構千萬級IM和Feed流系統

整個Timeline分為兩層,上層的Timeline層和下層的Store層。

Timeline層,提供最終的讀寫接口,使用者操作的也是Timeline的接口。

Store層,負責存儲系統的互動,目前Timeline LIB中提供了DistributeTimelineStore,基于Table Store,同時實作了分布式的存儲和同步。後續會繼續實作GlobalTimelineStore等。如果有使用者有其他系統需求,比如MySQL,Redis,可以通過實作IStore接口來新增MySQLStore和RedisStore。

也歡迎大家将自己實作的Store通過GitHub的PullRequest共享出來。

有了Timeline LIB之後,如果要實作一個IM或者Feed流,隻需要建立兩種類型Timeline(存儲類,同步類),然後調用Timeline的讀寫接口即可。

接下來,我們看下Timeline LIB的API。

Timeline LIB中面向最終使用者的是Timeline類,用于對每個Timeline做讀寫操作。

Timeline的接口主要分為三類:

寫:

store:同步寫入

storeAsync:異步寫入

batch:批量寫入

讀:

get:同步讀

getAsync:異步讀

範圍讀:

scan:同步範圍讀取

get接口一般用于讀取單條消息,在IM和Feed流中可使用的場景非常少,甚至可以不使用。

scan是讀取消息或Feed流消息的主要途徑,通過scan可以讀取到已經産生,但還未消費的消息。

語言:JAVA

在 Maven 工程中使用 Timeline LIB 隻需在 pom.xml 中加入相應依賴即可:

使用之前,需要先實作一個滿足自己業務特點的Message類,此Message類能表示業務中的一條完整消息。

需要實作IMessage的下列接口:

String getMessageID():

生成一個消息ID。

消息ID的作用主要是用于消息去重,最大使用場景是IM中的多人群。對于Timeline模型,消息ID隻需要在一段時間内(比如1小時或1天内)目前會話和收件人的消息中唯一即可。比如在IM中,隻需要在某個會話或者群裡面唯一即可,這時候其實更好的方式是由用戶端生成這個消息ID。最簡單的方式是用戶端循環使用0~10000之間的值和使用者ID拼接後作為消息ID即可滿足要求。

如果不想自己生成消息ID,可以直接繼承DistinctMessage,DistinctMessage類的消息ID = machineID + 程序ID + 循環遞增ID[0, Integer.MAX_VALUE]。

void setMessageID(String messageID):

在get和scan接口中設定消息ID。

IMessage newInstance():

使用預設構造函數,生成一個同類型執行個體。用于在Store中自動生成同類型執行個體對象。

byte[] serialize():

需要将消息内容完整序列号為位元組數組。這個接口會被store接口在寫入的時候調用。

void deserialize(byte[] input):

反序列化接口,這個接口會被store在讀取的時候調用。

在一個IM或Feed流産品中,一般會有兩個子系統,一個是存儲系統,一個是同步系統。

需要為這兩個系統各自生成一個Store對象。

存儲系統Store:存儲資料時間長,資料量大,成本很重要。如果不使用讀寫擴散相結合的方式,那麼存儲系統的store可以使用成本更低的混合存儲(SSD + SATA)。

同步系統Store:存儲受時間段,但是延時敏感,可以使用高性能存儲(SSD),讀取性能更穩定。

如果首次使用,需要調用store的create接口建立相應存儲表。

Store生成好後就可以構造最終的Timeline對象了,Timeline對象分為兩類,一類是存儲庫Timeline,一個是同步庫Timeline。

當在IM中釋出消息或者Feed流産品中釋出狀态時,就是對相應存儲庫Timeline和同步庫Timeline的消息寫入(store/storeAsync)。

當在IM或Feed流産品中讀取最新消息時,就是對相應同步庫Timeline的範圍讀取(scan)。

當在IM或Feed流産品中讀取曆史消息時,就是對相應存儲庫Timeline的範圍讀取(scan)。

如果是推拉結合的微網誌模式,則讀取最新消息時,就是對相應存儲庫Timeline和同步庫Timeline的同時範圍讀取(scan)。

Timeline LIB中會抛出TimelineException,TimelineException提供了兩種接口:getType()和getMessage(),getType()傳回此TimelineException的類型,包括了TET_ABORT,TET_RETRY,TET_INVALID_USE,TET_UNKNOWN:

TET_ABORT:目前隻有内部OutputStrem被非預期關閉後才會出現。

TET_RETRY:網絡不穩定或者底層存儲系統在負載均衡,可以繼續做重試。

TET_INVALID_USE:使用方式不對,建議直接報錯程序推出。

這一節會示範下如何使用Timeline LIB實作IM的群組功能。

多人群組,号碼是11789671。

群裡有使用者:user_A,user_B,user_C。

構造兩個store,一個用來存儲,一個用例同步。

user_A發一條群消息:“有人嗎”。

user_C讀取自己最新的同步消息

上面的示例示範了如何用Timeline LIB實作IM中的群組功能。其他的朋友圈,微網誌等也類似,這裡就不贅述了。

也歡迎大家共享其他場景的實作代碼。

我們使用阿裡雲ECS做了性能測試,效果較理想。

不同接口的寫入性能上,批量batch接口最快,其次是異步writeAsync,最後是同步write接口。

在阿裡雲共享型1核1G的ECS機器上,使用DistributeTimelineStore,Timeline LIB的storeAsync接口可以完成每秒1.2萬消息的寫入,如果使用batch批量接口,則可以完成每秒5.3萬消息的寫入。

如果使用一台8核的ECS,隻需要3秒鐘就可以完成100萬條消息的寫入。

由于DistributeTimelineStore使用了Table Store作為存儲和同步系統,Table Store是阿裡雲的一款服務化NoSQL服務,支援的TPS在理論上無上限,實際中僅受限于叢集大小,是以整個Timeline LIB的寫入能力和壓力器的CPU成正比。

下面的圖展示了不同機型上完成1000萬條消息寫入的延遲:

TableStore Timeline:輕松建構千萬級IM和Feed流系統

在一台8核ECS上隻需要27秒就可以完成1000萬寫入,由于寫入能力和CPU成線線性關系,如果用兩台16核的,則隻需要7秒就可以完成1000萬消息的寫入。

我們再來看一下scan讀取的性能,讀取20條1KB長度消息,LIB端延遲一直穩定在3.4ms,Table Store服務端延遲穩定在2ms。

這個量級和能力,可以撐得住目前所有的IM和Feed流産品的壓力。

Timeline LIB的想法來源于Table Store的真實場景需求,并且為了使用者可以更加簡單的使用,增加了主鍵列自增功能。

目前Timeline LIB的store層實作了DistributeTimelineStore類,DistributeTimelineStore可同時适用于存儲store和同步store。

DistributeTimelineStore是基于Table Store的,但是為了便于使用者使用其他系統,在Timeline LIB中将Store層獨立了出來。

如果使用者希望使用其他系統,比如MySQL作為存儲系統,可以實作IStore接口構造自己的Store類。我們也歡迎大家提供自己的各種Store層實作,最終希望為社交場景的架構師和開發者提供一套完整的易用性開發架構。

Store的擴充:

目前僅支援一種store:DistributeTimelineStore,雖然可以同時支援存儲和同步,但是功能還是比較簡單,接下來會支援更多的Store,比如全球多向同步的GlobalTimelineStore等。

更易用性的接口

目前Timeline接口比較偏向系統層,後續會提供更偏向業務層的接口,比如适用于IM的接口,适用于Feed流的接口等。

如果在使用過程中有任何問題或者建議,可以通過下列途徑聯系我們:

釘釘群回報:11789671。