一种低延迟的超时中心实现方式
簡介:?在很多產(chǎn)品中都存在生命周期相關(guān)的設(shè)計,時間節(jié)點(diǎn)到了之后需要做對應(yīng)的事情。超時中心(TimeOutCenter,TOC)負(fù)責(zé)存儲和調(diào)度生命周期節(jié)點(diǎn)上面的超時任務(wù),當(dāng)超時任務(wù)設(shè)置的超時時間到期后,超時中心需要立即調(diào)度處理這些超時任務(wù)。對于一些需要低延遲的超時場景,超時中心調(diào)度延遲會給產(chǎn)品帶來不可估量的影響。
作者 | 默達(dá)
來源 | 阿里技術(shù)公眾號
一 背景
在很多產(chǎn)品中都存在生命周期相關(guān)的設(shè)計,時間節(jié)點(diǎn)到了之后需要做對應(yīng)的事情。
超時中心(TimeOutCenter,TOC)負(fù)責(zé)存儲和調(diào)度生命周期節(jié)點(diǎn)上面的超時任務(wù),當(dāng)超時任務(wù)設(shè)置的超時時間到期后,超時中心需要立即調(diào)度處理這些超時任務(wù)。對于一些需要低延遲的超時場景,超時中心調(diào)度延遲會給產(chǎn)品帶來不可估量的影響。
因此本文提出一種低延遲的超時中心實(shí)現(xiàn)方式,首先介紹傳統(tǒng)的超時中心的實(shí)現(xiàn)方案,以及傳統(tǒng)方案中的缺點(diǎn),然后介紹低延遲的方案,說明如何解決傳統(tǒng)方案中的延遲問題。
二 傳統(tǒng)高延遲方案
1 整體框架
傳統(tǒng)的超時中心整體框架如下所示,任務(wù)輸入后存儲在超時任務(wù)庫中,定時器觸發(fā)運(yùn)行數(shù)據(jù)庫掃描器,數(shù)據(jù)庫掃描器從超時任務(wù)庫中掃描已經(jīng)到達(dá)超時時間的任務(wù),已經(jīng)到達(dá)超時時間的任務(wù)存儲在機(jī)器的內(nèi)存隊列中,等待交給業(yè)務(wù)處理器進(jìn)行處理,業(yè)務(wù)處理器處理完成后更新任務(wù)狀態(tài)。
在大數(shù)據(jù)時代,超時任務(wù)數(shù)量肯定是很大的,傳統(tǒng)的超時中心通過分庫分表支持存儲海量的超時任務(wù),定時器觸發(fā)也需要做相應(yīng)的改變,需要充分利用集群的能力,下面分別從超時任務(wù)庫和定時器觸發(fā)兩方面詳細(xì)介紹。
2 任務(wù)庫設(shè)計
任務(wù)庫數(shù)據(jù)模型如下所示,采用分庫分表存儲,一般可設(shè)計為8個庫1024個表,具體可以根據(jù)業(yè)務(wù)需求調(diào)整。biz_id為分表鍵,job_id為全局唯一的任務(wù)ID,status為超時任務(wù)的狀態(tài),action_time為任務(wù)的執(zhí)行時間,attribute存儲額外的數(shù)據(jù)。只有當(dāng)action_time小于當(dāng)前時間且status為待處理時,任務(wù)才能被掃描器加載到內(nèi)存隊列。任務(wù)被處理完成后,任務(wù)的狀態(tài)被更新成已處理。
job_id bigint unsigned 超時任務(wù)的ID,全局唯一 gmt_create datetime 創(chuàng)建時間 gmt_modified datetime 修改時間 biz_id bigint unsigned 業(yè)務(wù)id,一般為關(guān)聯(lián)的主訂單或子訂單id biz_type bigint unsigned 業(yè)務(wù)類型 status tinyint 超時任務(wù)狀態(tài)(0待處理,2已處理,3取消) action_time datetime 超時任務(wù)執(zhí)行時間 attribute varchar 額外數(shù)據(jù)3 定時調(diào)度設(shè)計
定時調(diào)度流程圖如下所示,定時器每間隔10秒觸發(fā)一次調(diào)度,從集群configserver中獲取集群ip列表并為當(dāng)前機(jī)器編號,然后給所有ip分配表。分配表時需要考慮好幾件事:一張表只屬于一臺機(jī)器,不會出現(xiàn)重復(fù)掃描;機(jī)器上線下線需要重新分配表。當(dāng)前機(jī)器從所分配的表中掃描出所有狀態(tài)為待處理的超時任務(wù),遍歷掃描出的待處理超時任務(wù)。對于每個超時任務(wù),當(dāng)內(nèi)存隊列不存在該任務(wù)且內(nèi)存隊列未滿時,超時任務(wù)才加入內(nèi)存隊列,否則循環(huán)檢查等待。
4 缺點(diǎn)
- 需要定時器定時調(diào)度,定時器調(diào)度間隔時間加長了超時任務(wù)處理的延遲時間;
- 數(shù)據(jù)庫掃描器為避免重復(fù)掃描數(shù)據(jù),一張表只能屬于一臺機(jī)器,任務(wù)庫分表的數(shù)量就是任務(wù)處理的并發(fā)度,并發(fā)度受限制;
- 當(dāng)單表數(shù)據(jù)量龐大時,即使從單張表中掃描所有待處理的超時任務(wù)也需要花費(fèi)很長的時間;
- 本方案總體處理步驟為:先掃描出所有超時任務(wù),再對單個超時任務(wù)進(jìn)行處理;超時任務(wù)處理延遲時間需要加上超時任務(wù)掃描時間;
- 本方案處理超時任務(wù)的最小延遲為定時器的定時間隔時間,在任務(wù)數(shù)量龐大的情況下,本方案可能存在較大延遲。
三 低延遲方案
1 整體框架
任務(wù)輸入后分為兩個步驟。第一個步驟是將任務(wù)存儲到任務(wù)庫,本方案的任務(wù)庫模型設(shè)計和上面方案中的任務(wù)庫模型設(shè)計一樣;第二步驟是任務(wù)定時,將任務(wù)的jobId和actionTime以一定方式設(shè)置到Redis集群中,當(dāng)定時任務(wù)的超時時間到了之后,從Redis集群pop超時任務(wù)的jobId,根據(jù)jobId從任務(wù)庫中查詢詳細(xì)的任務(wù)信息交給業(yè)務(wù)處理器進(jìn)行處理,最后更新任務(wù)庫中任務(wù)的狀態(tài)。
本方案與上述方案最大的不同點(diǎn)就是超時任務(wù)的獲取部分,上述方案采用定時調(diào)度掃描任務(wù)庫,本方案采用基于Redis的任務(wù)定時系統(tǒng),接下來將具體講解任務(wù)定時的設(shè)計。
2 Redis存儲設(shè)計
Topic的設(shè)計
Topic的定義有三部分組成,topic表示主題名稱,slotAmount表示消息存儲劃分的槽數(shù)量,topicType表示消息的類型。主題名稱是一個Topic的唯一標(biāo)示,相同主題名稱Topic的slotAmount和topicType一定是一樣的。消息存儲采用Redis的Sorted Set結(jié)構(gòu),為了支持大量消息的堆積,需要把消息分散存儲到很多個槽中,slotAmount表示該Topic消息存儲共使用的槽數(shù)量,槽數(shù)量一定需要是2的n次冪。在消息存儲的時候,采用對指定數(shù)據(jù)或者消息體哈希求余得到槽位置。
StoreQueue的設(shè)計
上圖中topic劃分了8個槽位,編號0-7。計算消息體對應(yīng)的CRC32值,CRC32值對槽數(shù)量進(jìn)行取模得到槽序號,SlotKey設(shè)計為#{topic}_#{index}(也即Redis的鍵),其中#{}表示占位符。
StoreQueue結(jié)構(gòu)采用Redis的Sorted Set,Redis的Sorted Set中的數(shù)據(jù)按照分?jǐn)?shù)排序,實(shí)現(xiàn)定時消息的關(guān)鍵就在于如何利用分?jǐn)?shù)、如何添加消息到Sorted Set、如何從Sorted Set中彈出消息。定時消息將時間戳作為分?jǐn)?shù),消費(fèi)時每次彈出分?jǐn)?shù)大于當(dāng)前時間戳的一個消息。
PrepareQueue的設(shè)計
為了保障每條消息至少消費(fèi)一次,消費(fèi)者不是直接pop有序集合中的元素,而是將元素從StoreQueue移動到PrepareQueue并返回消息給消費(fèi)者,等消費(fèi)成功后再從PrepareQueue從刪除,或者消費(fèi)失敗后從PreapreQueue重新移動到StoreQueue,這便是根據(jù)二階段提交的思想實(shí)現(xiàn)的二階段消費(fèi)。
在后面將會詳細(xì)介紹二階段消費(fèi)的實(shí)現(xiàn)思路,這里重點(diǎn)介紹下PrepareQueue的存儲設(shè)計。StoreQueue中每一個Slot對應(yīng)PrepareQueue中的Slot,PrepareQueue的SlotKey設(shè)計為prepare_{#{topic}#{index}}。PrepareQueue采用Sorted Set作為存儲,消息移動到PrepareQueue時刻對應(yīng)的(秒級時間戳*1000+重試次數(shù))作為分?jǐn)?shù),字符串存儲的是消息體內(nèi)容。這里分?jǐn)?shù)的設(shè)計與重試次數(shù)的設(shè)計密切相關(guān),所以在重試次數(shù)設(shè)計章節(jié)詳細(xì)介紹。
PrepareQueue的SlotKey設(shè)計中需要注意的一點(diǎn),由于消息從StoreQueue移動到PrepareQueue是通過Lua腳本操作的,因此需要保證Lua腳本操作的Slot在同一個Redis節(jié)點(diǎn)上,如何保證PrepareQueue的SlotKey和對應(yīng)的StoreQueue的SlotKey被hash到同一個Redis槽中呢。Redis的hash tag功能可以指定SlotKey中只有某一部分參與計算hash,這一部分采用{}包括,因此PrepareQueue的SlotKey中采用{}包括了StoreQueue的SlotKey。
DeadQueue的設(shè)計
消息重試消費(fèi)16次后,消息將進(jìn)入DeadQueue。DeadQueue的SlotKey設(shè)計為prepare{#{topic}#{index}},這里同樣采用hash tag功能保證DeadQueue的SlotKey與對應(yīng)StoreQueue的SlotKey存儲在同一Redis節(jié)點(diǎn)。
定時消息生產(chǎn)
生產(chǎn)者的任務(wù)就是將消息添加到StoreQueue中。首先,需要計算出消息添加到Redis的SlotKey,如果發(fā)送方指定了消息的slotBasis(否則采用content代替),則計算slotBasis的CRC32值,CRC32值對槽數(shù)量進(jìn)行取模得到槽序號,SlotKey設(shè)計為#{topic}_#{index},其中#{}表示占位符。發(fā)送定時消息時需要設(shè)置actionTime,actionTime必須大于當(dāng)前時間,表示消費(fèi)時間戳,當(dāng)前時間大于該消費(fèi)時間戳的時候,消息才會被消費(fèi)。因此在存儲該類型消息的時候,采用actionTime作為分?jǐn)?shù),采用命令zadd添加到Redis。
超時消息消費(fèi)
每臺機(jī)器將啟動多個Woker進(jìn)行超時消息消費(fèi),Woker即表示線程,定時消息被存儲到Redis的多個Slot中,因此需要zookeeper維護(hù)集群中Woker與slot的關(guān)系,一個Slot只分配給一個Woker進(jìn)行消費(fèi),一個Woker可以消費(fèi)多個Slot。Woker與Slot的關(guān)系在每臺機(jī)器啟動與停止時重新分配,超時消息消費(fèi)集群監(jiān)聽了zookeeper節(jié)點(diǎn)的變化。
Woker與Slot關(guān)系確定后,Woker則循環(huán)不斷地從Redis拉取訂閱的Slot中的超時消息。在StoreQueue存儲設(shè)計中說明了定時消息存儲時采用Sorted Set結(jié)構(gòu),采用定時時間actionTime作為分?jǐn)?shù),因此定時消息按照時間大小存儲在Sorted Set中。因此在拉取超時消息進(jìn)行只需采用Redis命令ZRANGEBYSCORE彈出分?jǐn)?shù)小于當(dāng)前時間戳的一條消息。
為了保證系統(tǒng)的可用性,還需要考慮保證定時消息至少被消費(fèi)一次以及消費(fèi)的重試次數(shù),下面將具體介紹如何保證至少消費(fèi)一次和消費(fèi)重試次數(shù)控制。
?
?
至少消費(fèi)一次
至少消費(fèi)一次的問題比較類似銀行轉(zhuǎn)賬問題,A向B賬戶轉(zhuǎn)賬100元,如何保障A賬戶扣減100同時B賬戶增加100,因此我們可以想到二階段提交的思想。第一個準(zhǔn)備階段,A、B分別進(jìn)行資源凍結(jié)并持久化undo和redo日志,A、B分別告訴協(xié)調(diào)者已經(jīng)準(zhǔn)備好;第二個提交階段,協(xié)調(diào)者告訴A、B進(jìn)行提交,A、B分別提交事務(wù)。本方案基于二階段提交的思想來實(shí)現(xiàn)至少消費(fèi)一次。
Redis存儲設(shè)計中PrepareQueue的作用就是用來凍結(jié)資源并記錄事務(wù)日志,消費(fèi)者端即是參與者也是協(xié)調(diào)者。第一個準(zhǔn)備階段,消費(fèi)者端通過執(zhí)行Lua腳本從StoreQueue中Pop消息并存儲到PrepareQueue,同時消息傳輸?shù)较M(fèi)者端,消費(fèi)者端消費(fèi)該消息;第二個提交階段,消費(fèi)者端根據(jù)消費(fèi)結(jié)果是否成功協(xié)調(diào)消息隊列服務(wù)是提交還是回滾,如果消費(fèi)成功則提交事務(wù),該消息從PrepareQueue中刪除,如果消費(fèi)失敗則回滾事務(wù),消費(fèi)者端將該消息從PrepareQueue移動到StoreQueue,如果因為各種異常導(dǎo)致PrepareQueue中消息滯留超時,超時后將自動執(zhí)行回滾操作。二階段消費(fèi)的流程圖如下所示。
?
?
消費(fèi)重試次數(shù)控制
采用二階段消費(fèi)方式,需要將消息在StoreQueue和PrepareQueue之間移動,如何實(shí)現(xiàn)重試次數(shù)控制呢,其關(guān)鍵在StoreQueue和PrepareQueue的分?jǐn)?shù)設(shè)計。
PrepareQueue的分?jǐn)?shù)需要與時間相關(guān),正常情況下,消費(fèi)者不管消費(fèi)失敗還是消費(fèi)成功,都會從PrepareQueue刪除消息,當(dāng)消費(fèi)者系統(tǒng)發(fā)生異常或者宕機(jī)的時候,消息就無法從PrepareQueue中刪除,我們也不知道消費(fèi)者是否消費(fèi)成功,為保障消息至少被消費(fèi)一次,我們需要做到超時回滾,因此分?jǐn)?shù)需要與消費(fèi)時間相關(guān)。當(dāng)PrepareQueue中的消息發(fā)生超時的時候,將消息從PrepareQueue移動到StoreQueue。
因此PrepareQueue的分?jǐn)?shù)設(shè)計為:秒級時間戳*1000+重試次數(shù)。定時消息首次存儲到StoreQueue中的分?jǐn)?shù)表示消費(fèi)時間戳,如果消息消費(fèi)失敗,消息從PrepareQueue回滾到StoreQueue,定時消息存儲時的分?jǐn)?shù)都表示剩余重試次數(shù),剩余重試次數(shù)從16次不斷降低最后為0,消息進(jìn)入死信隊列。消息在StoreQueue和PrepareQueue之間移動流程如下:
5 優(yōu)點(diǎn)
- 消費(fèi)低延遲:采用基于Redis的定時方案直接從Redis中pop超時任務(wù),避免掃描任務(wù)庫,大大減少了延遲時間。
- 可控并發(fā)度:并發(fā)度取決于消息存儲的Slot數(shù)量以及集群Worker數(shù)量,這兩個數(shù)量都可以根據(jù)業(yè)務(wù)需要進(jìn)行調(diào)控,傳統(tǒng)方案中并發(fā)度為分庫分表的數(shù)量。
- 高性能:Redis單機(jī)的QPS可以達(dá)到10w,Redis集群的QPS可以達(dá)到更高的水平,本方案沒有復(fù)雜查詢,消費(fèi)過程中從Redis拉取超時消息的時間復(fù)雜度為O(1)。
- 高可用:至少消費(fèi)一次保障了定時消息一定被消費(fèi),重試次數(shù)控制保證消費(fèi)不被阻塞。
原文鏈接
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的一种低延迟的超时中心实现方式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 排查指南 | 当 mPaaS 小程序提示
- 下一篇: 为了让盲人也能追剧,优酷做了哪些努力?