文/升哲科技劉鵬
摘要:本文主要描述升哲科技在打造物聯智慧城市平臺過程中關于如何實現延時隊列服務的技術選型經驗、延時隊列服務的架構設計以及延時隊列的底層細節實現原理。
背景
升哲科技是一家物聯網與人工智能領域的國家高新技術企業、獨角獸企業。
要打造物聯智慧城市平臺,在業務中涉及到各種延時任務的需求,例如設備定時空氣開關,定時更新設備狀態,定時提醒等等,基于這些需求,需要一個可靠、實時、海量的延時隊列服務作為基礎設施。
那么延時隊列是什么呢?延時隊列不同于消息隊列按照先入先出(FIFO)的順序來消費,而是根據消息指定時間延時消費。延時隊列的使用在我們日常應用也非常多,比如:
· 在電商平臺購物,在30分鐘內沒有支付自動取消訂單;
· 待處理的工單超過1天未處理,二次發送提醒。
以上場景往往都需要延時隊列實現。
早期延時隊列的實現采用了數據庫掃表方式,服務定期查詢到期的任務,再通過Kafka來中轉消息。當任務量小,延時精度要求低時掃表方式還能應對,然而隨著業務增長、任務數量不斷增多,延時時間精度要求也變高,掃表的方式已經無法滿足我們的業務,于是我們開始探索新的技術方案來支撐百萬級任務的延時隊列。
延時隊列的設計目標
1.高可用:多副本部署,保證服務不出現單點故障;
2.可擴展:可隨著業務量增長來擴容,同時生產消費的請求延時也要低;
3.兼容舊接口,保證舊的服務不需要做任何修改;
4.消息傳遞可靠,至少保證一次送達。
技術選型
在開源社區已經存在一些解決方案:
方案 | 描述 |
Beanstalkd | Beanstalkd C語言實現,我們團隊主要采用Golang和Java,二次開發有難度,beanstalkd不支持集群部署,高可用無法保證。 |
RabbitMQ延時隊列 | RabbitMQ提供了延時隊列插件,需要單獨開啟插件使用,其原理是通過死信隊列實現。 |
NSQ | NSQ開源延時隊列,NSQ支持延時隊列。 |
DelayQueue延時隊列 | JDK中提供了一組實現延時隊列的API,位于Java.util.concurrent包下DelayQueue。 |
時間輪算法 | 時間輪是一個算法,在 Netty、Akka、Quartz、ZooKeeper、Kafka等組件中都有使用,適合做統一調度器。 |
Redis Sorted Set 利用它的score屬性,啟用一個線程輪詢,根據score獲取超時的數據,然后觸發超時操作。 |
考慮到運維難度和可擴展性,最終我們選擇了開源項目Lmstfy作為基礎來進行二次開發,選擇Lmstfy的原因如下:
● 無狀態服務,使用Redis來持久化,Redis的高可用方案已經非常成熟,在公/私有云都有Paas服務可使用;
● 支持擴容,可以配置多個Redis集群;
● 提供Java/Go/Rust/PHP客戶端,監控面板完善;
● 采用Golang開發,高并發性能優秀,也方便后續二次開發。
整體架構設計
1.Delayer:無狀態服務,提供給業務服務調用,兼容舊接口,在Delayer這一層直接操作Redis實現了任務刪除和更新任務等等功能;
2.Lmstfy:無狀態服務,提供延時隊列基礎服務,底層實現采用;
3.Redis Sentinel集群:保證Redis發生故障時自動主備切換。
![pYYBAGMIPSWAadr5AAFsTrVOjrQ938.png](https://file.elecfans.com/web2/M00/65/48/pYYBAGMIPSWAadr5AAFsTrVOjrQ938.png)
基礎概念
● namespace -用于隔離業務,也可以通過配置namespace綁定不同的Redis集群;
● queue -隊列,用區分同一業務不同消息類型;
● job -業務定義的業務,主要包含以下幾個屬性:
○ id:任務 ID,全局唯一;
○ delay:任務延時下發時間,單位是秒;
○ tries:任務最大重試次數,tries = N表示任務會最多下發 N次;
○ ttr(time to run):任務預期執行時間,超過 ttr則認為任務消費失敗,觸發任務自動重試。
數據存儲
Lmstfy的 Redis存儲由四部分組成:
● Timer:使用ZSET結構來存儲延時任務,Score即任務的到期時間來排序;
● Ready queue - 使用LIST結構,存儲已經到期的延時任務,實現FIFO消費;
● Deadletter-使用LIST結構,消費失敗(重試次數到達上限)的任務,可以手動重新放回到隊列;
● Job pool– string類型,存儲消息meta信息;
● Job mapping - string -存儲應用自定義id和job的關聯關系。
創建任務
創建任務會生成一個Job ID, Job ID包括寫入時間戳、隨機數和延時時長,然后將任務的meta信息寫入Redis,Key為 j/{namespace}/queue/{id},當任務延時時間(delay)= 0,(實時消息隊列我們使用Kafka)表示不需要延時則直接寫到 Ready Queue(List),當延時時間(delay) = n(n > 0),表示需要延時,將延時加上當前系統時間作為絕對時間戳寫到 Timer(sorted set),Timer的實現是利用 ZSET根據絕對時間戳進行排序,再由一個goroutine定期輪詢將到期的任務通過 redis lua script來將數據轉移到 Ready Queue(List)中。
任務消費
支持延時的任務隊列本質上是兩個數據結構的結合: Ready Queue(LIST)和 Sorted Set。
Sorted Set用來實現延時的部分,將任務按照到期時間戳升序存儲,隨后定期將到期的任務遷移至 Ready Queue(LIST)。
任務的具體內容只會存儲一份在 Job pool里面,其他的如 Ready Queue只是存儲Job id,這樣可以節省內存空間。
任務更新和刪除
Lmstfy本身不支持刪除和更新,我們在Delayer層中在創建任務同時在Redis中創建了一個Mapping Key,客戶端可以自定一個ID關聯到Job id,Delayer提供了刪除和更新(先刪除再創建)API,我們業務還需要支持多次執行的功能,在處理Job Ack時根據任務參數重新插入隊列,結合我們二次開發整體結構如下:
![pYYBAGMIPVCAa4BwAADfigE8rpw668.png](https://file.elecfans.com/web2/M00/65/48/pYYBAGMIPVCAa4BwAADfigE8rpw668.png)
性能表現
通過本地限定1核CPU壓測生產消息數據如下:
200萬任務量占內存600MB+,其中包括mapping key導致key數量翻倍。
以下是單核CPU的環境下壓測結果,任務創建可高達1500TPS:
![pYYBAGMIPXqARGSKAABKeHHOOaY266.png](https://file.elecfans.com/web2/M00/65/48/pYYBAGMIPXqARGSKAABKeHHOOaY266.png)
延時任務到期時間比較分散的情況下,消費表現如下接800TPS:
![poYBAGMIPYuAbM6lAABarCzSqO4714.png](https://file.elecfans.com/web2/M00/64/AD/poYBAGMIPYuAbM6lAABarCzSqO4714.png)
總結
封裝lmstfy的方案已足夠支撐當前的使用場景,但還是有一些不足之處,比如:
● 在Delayer中操作Redis中的任務,無法保證原子性;
● 任務創建和消費另外會多一次網絡請求,產生不必要的開銷;
● 無法支持循環任務;
● Lmstfy采用HTTP協議,無法發揮更好性能。
未來,我們計劃融合兩個服務,完善任務CRUD功能,減少網絡開銷,并采用GRPC來替換HTTP協議通訊。
-
大數據
+關注
關注
64文章
8908瀏覽量
137801 -
智慧城市
+關注
關注
21文章
4276瀏覽量
97741
發布評論請先 登錄
相關推薦
評論