延遲隊(duì)列:一種帶有 延遲功能 的消息隊(duì)列
- 延時(shí) → 未來一個(gè)不確定的時(shí)間
- mq → 消費(fèi)行為具有順序性
這樣解釋,整個(gè)設(shè)計(jì)就清楚了苛谷。你的目的是 延時(shí),承載容器是 mq独悴。
背景
列舉一下我日常業(yè)務(wù)中可能存在的場(chǎng)景:
- 建立延時(shí)日程赫蛇,需要提醒老師上課
- 延時(shí)推送 → 推送老師需要的公告以及作業(yè)
為了解決以上問題,最簡(jiǎn)單直接的辦法就是定時(shí)去掃表:
服務(wù)啟動(dòng)時(shí)悟耘,開啟一個(gè)異步協(xié)程 → 定時(shí)掃描 msg table织狐,到了事件觸發(fā)事件,調(diào)用對(duì)應(yīng)的 handler
幾個(gè)缺點(diǎn):
- 每一個(gè)需要定時(shí)/延時(shí)任務(wù)的服務(wù)旺嬉,都需要一個(gè) msg table 做額外存儲(chǔ) → 存儲(chǔ)與業(yè)務(wù)耦合
- 定時(shí)掃描 → 時(shí)間不好控制厨埋,可能會(huì)錯(cuò)過觸發(fā)時(shí)間
- 對(duì) msg table instance 是一個(gè)負(fù)擔(dān)。反復(fù)有一個(gè)服務(wù)不斷對(duì)數(shù)據(jù)庫(kù)產(chǎn)生持續(xù)不斷的壓力
最大問題其實(shí)是什么雨效?
調(diào)度模型基本統(tǒng)一废赞,不要做重復(fù)的業(yè)務(wù)邏輯
我們可以考慮將邏輯從具體的業(yè)務(wù)邏輯里面抽出來,變成一個(gè)公共的部分据悔。
而這個(gè)調(diào)度模型耘沼,就是 延時(shí)隊(duì)列 。
其實(shí)說白了:
延時(shí)隊(duì)列模型群嗤,就是將未來執(zhí)行的事件提前存儲(chǔ)好,然后不斷掃描這個(gè)存儲(chǔ)浸赫,觸發(fā)執(zhí)行時(shí)間則執(zhí)行對(duì)應(yīng)的任務(wù)邏輯。
那么開源界是否已有現(xiàn)成的方案呢既峡?答案是肯定的。Beanstalk (https://github.com/beanstalkd/beanstalkd) 它基本上已經(jīng)滿足以上需求
設(shè)計(jì)目的
- 消費(fèi)行為 at least
- 高可用
- 實(shí)時(shí)性
- 支持消息刪除
依次說說上述這些目的的設(shè)計(jì)方向:
消費(fèi)行為
這個(gè)概念取自 mq 校仑。mq 中提供了消費(fèi)投遞的幾個(gè)方向:
-
at most once
→ 至多一次传惠,消息可能會(huì)丟,但不會(huì)重復(fù) -
at least once
→ 至少一次羊瘩,消息肯定不會(huì)丟失盼砍,但可能重復(fù) -
exactly once
→ 有且只有一次,消息不丟失不重復(fù)浇坐,且只消費(fèi)一次。
exactly once
盡可能是 producer + consumer 兩端都保證擒贸。當(dāng) producer 沒辦法保證是觉渴,那 consumer 需要在消費(fèi)前做一個(gè)去重,達(dá)到消費(fèi)過一次不會(huì)重復(fù)消費(fèi)蜕猫,這個(gè)在延遲隊(duì)列內(nèi)部直接保證哎迄。
最簡(jiǎn)單:使用 redis 的 setNX 達(dá)到 job id 的唯一消費(fèi)
高可用
支持多實(shí)例部署。掛掉一個(gè)實(shí)例后漱挚,還有后備實(shí)例繼續(xù)提供服務(wù)。
這個(gè)對(duì)外提供的 API 使用 cluster 模型蹬屹,內(nèi)部將多個(gè) node 封裝起來,多個(gè) node 之間冗余存儲(chǔ)贩耐。
為什么不使用 kafka厦取?
考慮過類似基于 kafka/rocketmq 等消息隊(duì)列作為存儲(chǔ)的方案,最后從存儲(chǔ)設(shè)計(jì)模型放棄了這類選擇虾攻。
舉個(gè)例子,假設(shè)以 Kafka 這種消息隊(duì)列存儲(chǔ)來實(shí)現(xiàn)延時(shí)功能奇钞,每個(gè)隊(duì)列的時(shí)間都需要?jiǎng)?chuàng)建一個(gè)單獨(dú)的 topic(如: Q1-1s, Q1-2s..)漂坏。這種設(shè)計(jì)在延時(shí)時(shí)間比較固定的場(chǎng)景下問題不太大,但如果是延時(shí)時(shí)間變化比較大會(huì)導(dǎo)致 topic 數(shù)目過多纠亚,會(huì)把磁盤從順序讀寫會(huì)變成隨機(jī)讀寫從導(dǎo)致性能衰減筋夏,同時(shí)也會(huì)帶來其他類似重啟或者恢復(fù)時(shí)間過長(zhǎng)的問題图呢。
- topic 過多 → 存儲(chǔ)壓力
- topic 存儲(chǔ)的是現(xiàn)實(shí)時(shí)間,在調(diào)度時(shí)對(duì)不同時(shí)間 (topic) 的讀取蛤织,順序讀 → 隨機(jī)讀
- 同理,寫入的時(shí)候順序?qū)?→ 隨機(jī)寫
架構(gòu)設(shè)計(jì)
API 設(shè)計(jì)
producer
producer.At(msg []byte, at time.Time)
producer.Delay(body []byte, delay time.Duration)
producer.Revoke(ids string)
consumer
consumer.Consume(consume handler)
使用延時(shí)隊(duì)列后,服務(wù)整體結(jié)構(gòu)如下摊鸡,以及隊(duì)列中 job 的狀態(tài)變遷:
- service →
producer.At(msg []byte, at time.Time)
→ 插入延時(shí)job到 tube 中 - 定時(shí)觸發(fā) → job 狀態(tài)更新為 ready
- consumer 獲取到 ready job → 取出 job免猾,開始消費(fèi);并更改狀態(tài)為 reserved
- 執(zhí)行傳入 consumer 中的 handler 邏輯處理函數(shù)
生產(chǎn)實(shí)踐
主要介紹一下在日常開發(fā)猎提,我們使用到延時(shí)隊(duì)列的哪些具體功能。
生產(chǎn)端
- 開發(fā)中生產(chǎn)延時(shí)任務(wù)疙教,只需確定任務(wù)執(zhí)行時(shí)間
- 傳入 At()
producer.At(msg []byte, at time.Time)
- 內(nèi)部會(huì)自行計(jì)算時(shí)間差值,插入 tube
- 傳入 At()
-
如果出現(xiàn)任務(wù)時(shí)間的修改贞谓,以及任務(wù)內(nèi)容的修改
- 在生產(chǎn)時(shí)可能需要額外建立一個(gè) logic_id → job_id 的關(guān)系表
- 查詢到 job_id →
producer.Revoke(ids string)
经宏,對(duì)其刪除,然后重新插入
消費(fèi)端
首先烁兰,框架層面保證了消費(fèi)行為的 exactly once
,但是上層業(yè)務(wù)邏輯消費(fèi)失敗或者是出現(xiàn)網(wǎng)絡(luò)問題广辰,亦或者是各種各樣的問題主之,導(dǎo)致消費(fèi)失敗,兜底交給業(yè)務(wù)開發(fā)做槽奕。這樣做的原因:
- 框架以及基礎(chǔ)組件只保證 job 狀態(tài)的流轉(zhuǎn)正確性
- 框架消費(fèi)端只保證消費(fèi)行為的統(tǒng)一
- 延時(shí)任務(wù)在不同業(yè)務(wù)中行為不統(tǒng)一
- 強(qiáng)調(diào)任務(wù)的必達(dá)性,則消費(fèi)失敗時(shí)需要不斷重試直到任務(wù)成功
- 強(qiáng)調(diào)任務(wù)的準(zhǔn)時(shí)性所森,則消費(fèi)失敗時(shí)夯接,對(duì)業(yè)務(wù)不敏感則可以選擇丟棄
這里描述一下框架消費(fèi)端是怎么保證消費(fèi)行為的統(tǒng)一:
分為 cluster 和 node。cluster:
https://github.com/tal-tech/go-queue/blob/master/dq/consumer.go#L45
- cluster 內(nèi)部將 consume handler 做了一層再封裝
- 對(duì) consume body 做hash晴弃,并使用此 hash 作為 redis 去重的key
- 如果存在逊拍,則不做處理,丟棄
node:
https://github.com/tal-tech/go-queue/blob/master/dq/consumernode.go#L36
- 消費(fèi) node 獲取到 ready job旗国;先執(zhí)行 Reserve(TTR)注整,預(yù)訂此job度硝,將執(zhí)行該job進(jìn)行邏輯處理
- 在 node 中 delete(job)寿冕;然后再進(jìn)行消費(fèi)
- 如果失敗,則上拋給業(yè)務(wù)層驼唱,做相應(yīng)的兜底重試
所以對(duì)于消費(fèi)端,開發(fā)者需要自己實(shí)現(xiàn)消費(fèi)的冪等性辨赐。
項(xiàng)目地址
go-queue
是基于 go-zero
實(shí)現(xiàn)的京办,go-zero
在 github 上 Used by
有300+,開源一年獲得11k+ stars.
go-zero: https://github.com/zeromicro/go-zero
go-stash: https://github.com/tal-tech/go-queue
歡迎使用并 star 支持我們不恭!
微信交流群
關(guān)注『微服務(wù)實(shí)踐』公眾號(hào)并點(diǎn)擊 交流群 獲取社區(qū)群二維碼财饥。