Dyno-queues 分散式延遲佇列 之 基本功能
阿新 • • 發佈:2021-02-17
# Dyno-queues 分散式延遲佇列 之 基本功能
[toc]
## 0x00 摘要
本系列我們會以設計分散式延遲佇列時重點考慮的模組為主線,穿插灌輸一些訊息佇列的特性實現方法,通過分析Dyno-queues 分散式延遲佇列的原始碼來具體看看設計實現一個分散式延遲佇列的方方面面。
## 0x01 Dyno-queues分散式延遲佇列
Dyno-queues 是 Netflix 實現的基於 Dynomite 和 Redis 構建的佇列。
Dynomite是一種通用的實現,可以與許多不同的 key-value 儲存引擎一起使用。目前它提供了對Redis序列化協議(RESP)和Memcached寫協議的支援。
### 1.1 設計目標
具體設計目標依據業務系統不同而不同。
Dyno-queues 的業務背景是:在 Netflix 的平臺上執行著許多的業務流程,這些流程的任務是通過非同步編排進行驅動,現在要實現一個分散式延遲佇列,這個延遲佇列具有如下特點:
- 分散式;
- 不用外部的鎖機制;
- 高併發;
- 至少一次語義交付;
- 不遵循嚴格的FIFO;
- 延遲佇列(訊息在將來某個時間之前不會從佇列中取出);
- 優先順序;
### 1.2 選型思路
Netflix 選擇 Dynomite,是因為:
- 其具有效能,多資料中心複製和高可用性的特點;
- Dynomite提供分片和可插拔的資料儲存引擎,允許在資料需求增加垂直和水平擴充套件;
Netflix選擇Redis作為構建佇列的儲存引擎是因為:
- Redis架構通過提供構建佇列所需的資料結構很好地支援了佇列設計,同時Redis的效能也非常優秀,具備低延遲的特性;
- Dynomite在Redis之上提供了高可用性、對等複製以及一致性等特性,用於構建分散式叢集佇列;
## 0x02 總體設計
### 2.1 系統假設
**查詢模型**:基於Key-Value模型,而不是SQL,即關係模型。儲存物件比較小。
**ACID屬性**:傳統的關係資料庫中,用ACID(A原子性、C一致性、I 隔離性、D永續性)來保證事務,在保證ACID的前提下往往有很差的可用性。Dynamo用弱一致性C來達到高可用,不提供資料隔離 I,只允許單Key更新 。
### 2.2 高可用
其實所有的高可用,是可以依賴於RPC和儲存的高可用來實現的。
- 先來看RPC的高可用,比如美團的基於MTThrift的RPC框架,阿里的Dubbo等,其本身就具有服務自動發現,負載均衡等功能。
- 而訊息佇列的高可用,只要保證broker接受訊息和確認訊息的介面是冪等的,並且consumer的幾臺機器處理訊息是冪等的,這樣就把訊息佇列的可用性,轉交給RPC框架來處理了。
Netflix 選擇 Dynomite,是因為:
- 其具有高效能,多資料中心複製和高可用性的特點;
- Dynomite 提供分片和可插拔的資料儲存引擎,允許在資料需求增加垂直和水平擴充套件;
所以 Dyno-queues 的高可用就自動解決了。
### 2.3 冪等
怎麼保證冪等呢?最簡單的方式莫過於共享儲存。broker多機器共享一個DB或者一個分散式檔案/kv系統,則處理訊息自然是冪等的。就算有單點故障,其他節點可以立刻頂上。
對於不共享儲存的佇列,如Kafka使用分割槽加主備模式,就略微麻煩一些。需要保證每一個分割槽內的高可用性,也就是每一個分割槽至少要有一個主備且需要做資料的同步。
Dynomite 使用 redis 叢集這個共享儲存 做了冪等保證。
### 2.4 承載訊息堆積
訊息到達服務端後,如果不經過任何處理就到接收者,broker就失去了它的意義。為了滿足我們錯峰/流控/最終可達等一系列需求,把訊息儲存下來,然後選擇時機投遞就顯得是順理成章的了。
這個儲存可以做成很多方式。比如儲存在記憶體裡,儲存在分散式KV裡,儲存在磁盤裡,儲存在資料庫裡等等。但歸結起來,主要有持久化和非持久化兩種。
持久化的形式能更大程度地保證訊息的可靠性(如斷電等不可抗外力),並且理論上能承載更大限度的訊息堆積(外存的空間遠大於記憶體)。
但並不是每種訊息都需要持久化儲存。很多訊息對於投遞效能的要求大於可靠性的要求,且數量極大(如日誌)。這時候,訊息不落地直接暫存記憶體,嘗試幾次failover,最終投遞出去也未嘗不可。
Dynomite 使用 redis 叢集這個共享儲存 在一定程度上緩解了訊息堆積問題。
### 2.5 儲存子系統
我們來看看如果需要資料落地的情況下各種儲存子系統的選擇。理論上,從速度來看,檔案系統 > 分散式KV(持久化)> 分散式檔案系統 > 資料庫,而可靠性卻截然相反。還是要從支援的業務場景出發作出最合理的選擇。
如果你們的訊息佇列是用來支援支付/交易等對可靠性要求非常高,但對效能和量的要求沒有這麼高,而且沒有時間精力專門做檔案儲存系統的研究,DB是最好的選擇。
但是DB受制於IOPS,如果要求單broker 5位數以上的QPS效能,基於檔案的儲存是比較好的解決方案。整體上可以採用資料檔案 + 索引檔案的方式處理。
分散式KV(如MongoDB,HBase)等,或者持久化的Redis,由於其程式設計介面較友好,效能也比較可觀,如果在可靠性要求不是那麼高的場景,也不失為一個不錯的選擇。
因為 場景是 可靠性要求不那麼高,所以 Dynomite 使用 redis 叢集這個儲存子系統 也是可以的。
### 2.6 消費關係解析
下一個重要的事情就是解析傳送接收關係,進行正確的訊息投遞了。拋開現象看本質,傳送接收關係無外乎是單播與廣播的區別。所謂單播,就是點到點;而廣播,是一點對多點。
一般比較通用的設計是支援組間廣播,不同的組註冊不同的訂閱。組內的不同機器,如果註冊一個相同的ID,則單播;如果註冊不同的ID(如IP地址+埠),則廣播。
至於廣播關係的維護,一般由於訊息佇列本身都是叢集,所以都維護在公共儲存上,如 config server、zookeeper等。維護廣播關係所要做的事情基本是一致的:
- 傳送關係的維護。
- 傳送關係變更時的通知。
本文後續會介紹如何維護髮送關係。
### 2.7 資料分片
資料分片的邏輯既可以實現在客戶端,也可以實現在 `Proxy` 層,取決於你的架構如何設計。
傳統的資料庫中介軟體大多將分片邏輯實現在客戶端,通過改寫物理 `SQL` 訪問不同的 `MySQL` 庫;而在 `NewSQL` 資料庫倡導的計算儲存分離架構中,通常將分片邏輯實現在計算層,即 `Proxy` 層,通過無狀態的計算節點轉發使用者請求到正確的儲存節點。
在 Dynomite 之中,佇列根據可用區域進行分片,將資料推送到佇列時,通過輪訓機制確定分片,這種機制可以確保所有分片的資料是平衡的,每個分片都代表Redis中的有序集合,有序集中的 key 是 queueName 和 AVAILABILITY _ZONE 的組合。
```java
public class RoundRobinStrategy implements ShardingStrategy {
private final AtomicInteger nextShardIndex = new AtomicInteger(0);
/**
* Get shard based on round robin strategy.
* @param allShards
*/
@Override
public String getNextSh