高可用延遲佇列設計與實現
延遲佇列:一種帶有 延遲功能 的訊息佇列
- 延時 → 未來一個不確定的時間
- mq → 消費行為具有順序性
這樣解釋,整個設計就清楚了。你的目的是 延時,承載容器是 mq。
背景
列舉一下我日常業務中可能存在的場景:
- 建立延時日程,需要提醒老師上課
- 延時推送 → 推送老師需要的公告以及作業
為了解決以上問題,最簡單直接的辦法就是定時去掃表:
服務啟動時,開啟一個非同步協程 → 定時掃描 msg table,到了事件觸發事件,呼叫對應的 handler
幾個缺點:
- 每一個需要定時/延時任務的服務,都需要一個 msg table 做額外儲存 → 儲存與業務耦合
- 定時掃描 → 時間不好控制,可能會錯過觸發時間
- 對 msg table instance 是一個負擔。反覆有一個服務不斷對資料庫產生持續不斷的壓力
最大問題其實是什麼?
排程模型基本統一,不要做重複的業務邏輯
我們可以考慮將邏輯從具體的業務邏輯裡面抽出來,變成一個公共的部分。
而這個排程模型,就是 延時佇列 。
其實說白了:
延時佇列模型,就是將未來執行的事件提前儲存好,然後不斷掃描這個儲存,觸發執行時間則執行對應的任務邏輯。
那麼開源界是否已有現成的方案呢?答案是肯定的。Beanstalk (https://github.com/beanstalkd/beanstalkd) 它基本上已經滿足以上需求
設計目的
- 消費行為 at least
- 高可用
- 實時性
- 支援訊息刪除
一次說說上述這些目的的設計方向:
消費行為
這個概念取自 mq 。mq 中提供了消費投遞的幾個方向:
-
at most once
→ 至多一次,訊息可能會丟,但不會重複 -
at least once
→ 至少一次,訊息肯定不會丟失,但可能重複 -
exactly once
→ 有且只有一次,訊息不丟失不重複,且只消費一次。
exactly once
儘可能是 producer + consumer 兩端都保證。當 producer 沒辦法保證是,那 consumer 需要在消費前做一個去重,達到消費過一次不會重複消費,這個在延遲佇列內部直接保證。
最簡單:使用 redis 的 setNX 達到 job id 的唯一消費
高可用
支援多例項部署。掛掉一個例項後,還有後備例項繼續提供服務。
這個對外提供的 API 使用 cluster 模型,內部將多個 node 封裝起來,多個 node 之間冗餘儲存。
為什麼不使用 kafka?
考慮過類似基於 kafka/rocketmq 等訊息佇列作為儲存的方案,最後從儲存設計模型放棄了這類選擇。
舉個例子,假設以 Kafka 這種訊息佇列儲存來實現延時功能,每個佇列的時間都需要建立一個單獨的 topic(如: Q1-1s, Q1-2s..)。這種設計在延時時間比較固定的場景下問題不太大,但如果是延時時間變化比較大會導致 topic 數目過多,會把磁碟從順序讀寫會變成隨機讀寫從導致效能衰減,同時也會帶來其他類似重啟或者恢復時間過長的問題。
- topic 過多 → 儲存壓力
- topic 儲存的是現實時間,在排程時對不同時間 (topic) 的讀取,順序讀 → 隨機讀
- 同理,寫入的時候順序寫 → 隨機寫
架構設計
API 設計
producer
producer.At(msg []byte, at time.Time)
producer.Delay(body []byte, delay time.Duration)
producer.Revoke(ids string)
consumer
consumer.Consume(consume handler)
使用延時佇列後,服務整體結構如下,以及佇列中 job 的狀態變遷:
- service →
producer.At(msg []byte, at time.Time)
→ 插入延時 job 到 tube 中 - 定時觸發 → job 狀態更新為 ready
- consumer 獲取到 ready job → 取出 job,開始消費;並更改狀態為 reserved
- 執行傳入 consumer 中的 handler 邏輯處理函式
生產實踐
主要介紹一下在日常開發,我們使用到延時佇列的哪些具體功能。
生產端
- 開發中生產延時任務,只需確定任務執行時間
- 傳入 At()
producer.At(msg []byte, at time.Time)
- 內部會自行計算時間差值,插入 tube
- 傳入 At()
-
如果出現任務時間的修改,以及任務內容的修改
- 在生產時可能需要額外建立一個 logic_id → job_id 的關係表
- 查詢到 job_id →
producer.Revoke(ids string)
,對其刪除,然後重新插入
消費端
首先,框架層面保證了消費行為的 exactly once
,但是上層業務邏輯消費失敗或者是出現網路問題,亦或者是各種各樣的問題,導致消費失敗,兜底交給業務開發做。這樣做的原因:
- 框架以及基礎元件只保證 job 狀態的流轉正確性
- 框架消費端只保證消費行為的統一
- 延時任務在不同業務中行為不統一
- 強調任務的必達性,則消費失敗時需要不斷重試直到任務成功
- 強調任務的準時性,則消費失敗時,對業務不敏感則可以選擇丟棄
這裡描述一下框架消費端是怎麼保證消費行為的統一:
分為 cluster 和 node。cluster:
https://github.com/tal-tech/go-queue/blob/master/dq/consumer.go#L45
- cluster 內部將 consume handler 做了一層再封裝
- 對 consume body 做 hash,並使用此 hash 作為 redis 去重的 key
- 如果存在,則不做處理,丟棄
node:
https://github.com/tal-tech/go-queue/blob/master/dq/consumernode.go#L36
- 消費 node 獲取到 ready job;先執行 Reserve(TTR),預訂此 job,將執行該 job 進行邏輯處理
- 在 node 中 delete(job);然後再進行消費
- 如果失敗,則上拋給業務層,做相應的兜底重試
所以對於消費端,開發者需要自己實現消費的冪等性。
專案地址
go-queue
是基於 go-zero
實現的,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 支援我們!
微信交流群
關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 實現簡單延遲佇列和分散式延遲佇列佇列分散式
- RabbitMQ實現延遲佇列MQ佇列
- RabbitMQ 實現延遲佇列MQ佇列
- php+redis實現延遲佇列PHPRedis佇列
- 如何用RabbitMQ實現延遲佇列MQ佇列
- RabbitMQ、RocketMQ、Kafka延遲佇列實現MQKafka佇列
- Golang 實現 RabbitMQ 的延遲佇列GolangMQ佇列
- 使用RabbitMq原生實現延遲佇列MQ佇列
- RabbitMQ實戰《延遲佇列》MQ佇列
- Laravel 延遲佇列Laravel佇列
- redis 延遲佇列Redis佇列
- 你知道Redis可以實現延遲佇列嗎?Redis佇列
- 延遲阻塞佇列 DelayQueue佇列
- hyperf redis延遲佇列Redis佇列
- Delayed Message 外掛實現 RabbitMQ 延遲佇列MQ佇列
- 如何才能讓Spring Boot與RabbitMQ結合實現延遲佇列Spring BootMQ佇列
- 基於訊息佇列(RabbitMQ)實現延遲任務佇列MQ
- RabbitMQ 學習筆記 -- 12 死信佇列 DLX + TTL 方式實現延遲佇列MQ筆記佇列
- [Redis]延遲訊息佇列Redis佇列
- 你真的知道怎麼實現一個延遲佇列嗎?佇列
- 詳細介紹Spring Boot + RabbitMQ實現延遲佇列Spring BootMQ佇列
- 一張圖帶你理解和實現RabbitMQ的延遲佇列功能MQ佇列
- Spring Boot(十四)RabbitMQ延遲佇列Spring BootMQ佇列
- Node.js結合RabbitMQ延遲佇列實現定時任務Node.jsMQ佇列
- 百行程式碼實現基於Redis的可靠延遲佇列行程Redis佇列
- 使用 RabbitMQ 實現延時佇列MQ佇列
- RabbitMQ從零到叢集高可用(.NetCore5.0) - 死信佇列,延時佇列MQNetCore佇列
- redis應用系列三:延遲訊息佇列正確實現姿勢Redis佇列
- Laravel 在事件監聽中實現佇列的方法以及指定加入的佇列名稱和佇列延遲時間Laravel事件佇列
- RabbitMQ 高可用之映象佇列MQ佇列
- RabbitMQ 延遲佇列實現訂單支付結果非同步階梯性通知MQ佇列非同步
- 基於Dynomite的分散式延遲佇列MIT分散式佇列
- RabbitMQ高階之訊息限流與延時佇列MQ佇列
- 如何設計和實現高可用MySQLMySql
- 【RabbitMQ】一文帶你搞定RabbitMQ延遲佇列MQ佇列
- RabbitMQ使用 prefetch_count優化佇列的消費,使用死信佇列和延遲佇列實現訊息的定時重試,golang版本MQ優化佇列Golang
- 如何設計和實現高可用的MySQLMySql
- Timestone:Netflix 的高吞吐量、低延遲優先佇列系統佇列