0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

如何快速實現(xiàn)一個定時器

jf_78858299 ? 來源:云社區(qū) ? 作者:ruoyuliu ? 2023-04-21 14:40 ? 次閱讀

導語

定時器Timer)是一種在業(yè)務開發(fā)中常用的組件,主要用在執(zhí)行延時通知任務上。本文以筆者在工作中的實踐作為基礎,介紹如何使用平時部門最常用的組件快速實現(xiàn)一個業(yè)務常用的分布式定時器服務。同時介紹了過程中遇到問題的一些解決方案,希望能夠給類似場景提供一些解決思路。文章作者:劉若愚,騰訊WXG后臺開發(fā)工程師。

一、什么是定時器

定時器(Timer)是一種在指定時間開始執(zhí)行某一任務的工具(也有周期性反復執(zhí)行某一任務的Timer,我們這里暫不討論)。它常常與延遲隊列這一概念關聯(lián)。那么在什么場景下我才需要使用定時器呢?

我們先看看以下業(yè)務場景:

  • 當訂單一直處于未支付狀態(tài)時,如何及時的關閉訂單,并退還庫存?
  • 如何定期檢查處于退款狀態(tài)的訂單是否已經退款成功?
  • 新創(chuàng)建店鋪,N天內沒有上傳商品,系統(tǒng)如何知道該信息,并發(fā)送激活短信?

為了解決以上問題,最簡單直接的辦法就是定時去掃表。每個業(yè)務都要維護一個自己的掃表邏輯。當業(yè)務越來越多時,我們會發(fā)現(xiàn)掃表部分的邏輯會非常類似。我們可以考慮將這部分邏輯從具體的業(yè)務邏輯里面抽出來,變成一個公共的部分。這個時候定時器就出場了。

二、定時器的本質

一個定時器本質上是這樣的一個數(shù)據(jù)結構:deadline越近的任務擁有越高優(yōu)先級,提供以下幾種基本操作:

  1. Add 新增任務
  2. Delete 刪除任務
  3. Run 執(zhí)行到期的任務/到期通知對應業(yè)務處理
  4. Update 更新到期時間 (可選)

Run通常有兩種工作方式:1.輪詢,每隔一個時間片就去查找哪些任務已經到期;2.睡眠/喚醒,不停地查找deadline最近的任務,如到期則執(zhí)行;否則sleep直到其到期。在sleep期間,如果有任務被Add或Delete,則deadline最近的任務有可能改變,線程會被喚醒并重新進行1的邏輯。

它的設計目標通常包含以下幾點要求:

  1. 支持任務提交(消息發(fā)布)、任務刪除、任務通知(消息訂閱)等基本功能。
  2. 消息傳輸可靠性:消息進入延遲隊列以后,保證至少被消費一次(到期通知保證At-least-once ,追求Exactly-once)。
  3. 數(shù)據(jù)可靠性:數(shù)據(jù)需要持久化,防止丟失。
  4. 高可用性:至少得支持多實例部署。掛掉一個實例后,還有后備實例繼續(xù)提供服務,可橫向擴展。
  5. 實時性:盡最大努力準時交付信息,允許存在一定的時間誤差,誤差范圍可控。

三、數(shù)據(jù)結構

下面我們談談定時器的數(shù)據(jù)結構。定時器通常與延遲隊列密不可分,延時隊列是什么?顧名思義它是一種帶有延遲功能的消息隊列。而延遲隊列底層通??梢圆捎靡韵聨追N數(shù)據(jù)結構之一來實現(xiàn):

  1. 有序鏈表,這個最直觀,最好理解。
  2. 堆,應用實例如Java JDK中的DelayQueue、Go內置的定時器等。
  3. 時間輪/多級時間輪,應用實例如Linux內核定時器、Netty工具類HashedWheelTimer、Kafka內部定時器等。

這里重點介紹一下時間輪(TimeWheel)。一個時間輪是一個環(huán)形結構,可以想象成時鐘,分為很多格子,一個格子代表一段時間(越短Timer精度越高),并用一個List保存在該格子上到期的所有任務,同時一個指針隨著時間流逝一格一格轉動,并執(zhí)行對應List中所有到期的任務。任務通過取模決定應該放入哪個格子。示意圖如下所示:

圖片

時間輪

如果任務的時間跨度很大,數(shù)量也多,傳統(tǒng)的單輪時間輪會造成任務的round很大,單個格子的任務List很長,并會維持很長一段時間。這時可將Wheel按時間粒度分級(與水表的思想很像),示意圖如下所示:

圖片

多級時間輪

時間輪是一種比較優(yōu)雅的實現(xiàn)方式,且如果采用多級時間輪時其效率也是比較高的。

四、業(yè)界實現(xiàn)方案

業(yè)界對于定時器/延時隊列的工程實踐,則通常基于以下幾種方案來實現(xiàn):

  1. 基于Redis ZSet實現(xiàn)。
  2. 采用某些自帶延時選項的隊列實現(xiàn),如RabbitMQ、Beanstalkd、騰訊TDMQ等。
  3. 基于Timing-Wheel時間輪算法實現(xiàn)。

五、方案詳述

介紹完定時器的背景知識,接下來看下我們系統(tǒng)的實現(xiàn)。我們先看一下需求背景。在我們組的實際業(yè)務中,有延遲任務的需求。一種典型的應用場景是:商戶發(fā)起扣費請求后,立刻為用戶下發(fā)扣費前通知,24小時后完成扣費;或者發(fā)券給用戶,3天后通知用戶券過期?;谶@種需求背景,我們引出了定時器的開發(fā)需求。

我們首先調研了公司內外的定時器實現(xiàn),避免重復造輪子。調研了諸如例如公司外部的Quartz、有贊的延時隊列等,以及公司內部的PCG tikker、TDMQ等,以及微信支付內部包括營銷、代扣、支付分等團隊的一些實現(xiàn)方案。最后從可用性、可靠性、易用性、時效性以及代碼風格、運維代價等角度考慮,我們決定參考前人的一些優(yōu)秀的技術方案,并根據(jù)我們團隊的技術積累和組件情況,設計和實現(xiàn)一套定時器方案。

首先要確定定時器的存儲數(shù)據(jù)結構。這里借鑒了時間輪的思想,基于微信團隊最常用的存儲組件tablekv進行任務的持久化存儲。使用到tablekv的原因是它天然支持按uin分表,分表數(shù)可以做到千萬級別以上;其次其單表支持的記錄數(shù)非常高,讀寫效率也很高,還可以如mysql一樣按指定的條件篩選任務。

我們的目標是實現(xiàn)秒級時間戳精度,任務到期只需要單次通知業(yè)務方。故我們方案主要的思路是基于tablekv 按任務執(zhí)行時間分表 ,也就是使用使用方指定的start_time(時間戳)作為分表的uin,也即是時間輪bucket。為什么不使用多輪時間輪?主要是因為首先kv支持單表上億數(shù)據(jù), 其二kv分表數(shù)可以非常多,例如我們使用1000萬個分表需要約115天的間隔才會被哈希分配到同一分表內。故暫時不需要使用到多輪時間輪。

最終我們采用的分表數(shù)為1000w,uin=時間戳mod分表數(shù)。這里有一個注意點,通過mod分表數(shù)進行Key收斂, 是為了避免時間戳遞增導致的key無限擴張的問題。示例圖如下所示:

圖片

kv時間輪

任務持久化存儲之后,我們采用一個Daemon程序執(zhí)行定期掃表任務,將到期的任務取出,最后將請求中帶的業(yè)務信息(biz_data添加任務時帶來,定時器透傳,不關注其具體內容)回調通知業(yè)務方。這么一看流程還是很簡單的。

這里掃描的流程類似上面講的時間輪算法,會有一個指針(我們在這里不妨稱之為time_pointer)不斷向后移動,保證不會漏掉任何一個bucket的任務。這里我們采用的是commkv(可以簡單理解為可以按照key-value形式讀寫的kv,其底層仍是基于tablekv實現(xiàn))存儲CurrentTime,也就是當前處理到的時間戳。每次輪詢時Daemon都會通過GetByKey接口獲取到CurrentTime,若大于當前機器時間,則sleep一段時間。若小于等于當前機器時間,則取出tablekv中以CurrentTime為uin的分表的TaskList進行處理。本次輪詢結束,則CurrentTime加一,再通過SetByKey設置回commkv。這個部分的工作模式我們可以簡稱為Scheduler。

Scheduler拿到任務后只需要回調通知業(yè)務方即可。如果采用同步通知業(yè)務方的方式,由于業(yè)務方的超時情況是不可控的,則一個任務的投遞時間可能會較長,導致拖慢這個時間點的任務整體通知進度。故而這里自然而然想到采用異步解耦的方式。即將任務發(fā)布至事件中心(微信內部的高可用、高可靠的消息平臺,支持事務和非事務消息。由于一個任務的投遞到事件中心的時間僅為幾十ms,理論上任務量級不大時1s內都可以處理完。此時time_pointer會緊跟當前時間戳。當大量任務需要處理時,需要采用多線程/多協(xié)程的方式并發(fā)處理,保證任務的準時交付。broker訂閱事件中心的消息,接受到消息后由broker回調通知業(yè)務方,故broker也充當了Notifier的角色。整體架構圖如下所示:

圖片

*架構圖

主要模塊包括:

任務掃描Daemon :充當Scheduler的角色。掃描所有到期任務,投遞到事件中心,讓它通知broker,由broker的Notifier通知業(yè)務方。

定時器broker :集業(yè)務接入、Notifier兩者功能于一身。

任務狀態(tài)圖如下所示,只有兩種狀態(tài)。當任務插入kv成功時即為pending狀態(tài),當任務成功被取出并通知業(yè)務方成功時即為finish狀態(tài)。

圖片

狀態(tài)圖

六、實現(xiàn)細節(jié)與難點思考

下面就上面的方案涉及的幾個技術細節(jié)進行進一步的解釋。

1. 業(yè)務隔離

通過biz_type定義不同的業(yè)務類型,不同的biz_type可以定義不同的優(yōu)先級(目前暫未支持),任務中保存biz_type信息。

業(yè)務信息(主鍵為biz_type)采用境外配置中心進行配置管理。方便新業(yè)務的接入和配置變更。業(yè)務接入時,需要在配置中添加諸如回調通知信息、回調重試次數(shù)限制、回調限頻等參數(shù)。業(yè)務隔離的目的在于使各個接入業(yè)務不受其他業(yè)務的影響,這一點由于目前我們的定時器用于支持本團隊內部業(yè)務的特點,僅采取對不同的業(yè)務執(zhí)行不同業(yè)務限頻規(guī)則的策略,并未做太多優(yōu)化工作,就不詳述了。

2. 時間輪空轉問題

由于1000w分表,肯定是大部分Bucket為空,時間輪的指針推進存在低效問題。聯(lián)想到在飯店排號時,常有店員來登記現(xiàn)場尚存的號碼,就是因為可以跳過一些號碼,加快叫號進度。同理,為了減少這種“空推進”,Kafka引入了DelayQueue,以bucket為單位入隊,每當有bucket到期,即queue.poll能拿到結果時,才進行時間的“推進”,減少了線程空轉的開銷。在這里類似的,我們也可以做一個優(yōu)化,維護一個有序隊列,保存表不為空的時間戳。大家可以思考一下如何實現(xiàn),具體方案不再詳述。

3. 限頻

由于定時器需要寫kv,還需要回調通知業(yè)務方。因此需要考慮對調用下游服務做限頻,保證下游服務不會雪崩。這是一個分布式限頻的問題。這里使用到的是微信支付的限頻組件。保證1.任務插入時不超過定時器管理員配置的頻率。2.Notifier回調通知業(yè)務方時不超過業(yè)務方申請接入時配置的頻率。這里保證了1.kv和事件中心不會壓力太大。2.下游業(yè)務方不會受到超過其處理能力的請求量的沖擊。

4. 分布式單實例容災

出于容災的目的,我們希望Daemon具有容災能力。換言之若有Daemon實例異常掛起或退出,其他機器的實例進程可以繼續(xù)執(zhí)行任務。但同時我們又希望同一時刻只需要一個實例運行,即“分布式單實例”。所以我們完整的需求可以歸納為 “分布式單實例容災部署” 。

實現(xiàn)這一目標,方式有很多種,例如:

  • 接入“調度中心”,由調度中心來負責調度各個機器;
  • 各節(jié)點在執(zhí)行任務前先分布式搶鎖,只有成功占用鎖資源的節(jié)點才能執(zhí)行任務;
  • 各節(jié)點通過通信選出“master"來執(zhí)行邏輯,并通過心跳包持續(xù)通信,若“master”掉線,則備機取代成為master繼續(xù)執(zhí)行。

主要從開發(fā)成本,運維支撐兩方面來考慮,選取了基于chubby分布式鎖的方案來實現(xiàn)單實例容災部署。這也使得我們真正執(zhí)行業(yè)務邏輯的機器具有隨機性。

5. 可靠交付

這是一個核心問題,如何保證任務的通知滿足At-least-once的要求?

我們系統(tǒng)主要通過以下兩種方式來保證。

1.任務達到時即存入tablekv持久化存儲,任務成功通知業(yè)務方才設置過期(保留一段時間后刪除),故而所有任務都是落地數(shù)據(jù),保證事后可以對賬。

2.引入可靠事件中心。在這里使用的是事件中心的普通消息,而非事務消息。實質是當做一個高可用性的消息隊列。

這里引入消息隊列的意義在于:

  • 將任務調度和任務執(zhí)行解耦(調度服務并不需要關心任務執(zhí)行結果)。
  • 異步化,保證調度服務的高效執(zhí)行,調度服務的執(zhí)行是以ms為單位。
  • 借助消息隊列實現(xiàn)任務的可靠消費。

事件中心相比普通的消息隊列還具有哪些優(yōu)點呢?

  • 某些消息隊列可能丟消息(由其實現(xiàn)機制決定),而事件中心本身底層的分布式架構,使得事件中心保證極高的可用性和可靠性,基本可以忽略丟消息的情況。
  • 事件中心支持按照配置的不同事件梯度進行多次重試(回調時間可以配置)。
  • 事件中心可以根據(jù)自定義業(yè)務ID進行消息去重。

事件中心的引入,基本保證了任務從Scheduler到Notifier的可靠性。

當然,最為完備的方式,是增加另一個異步Daemon作為兜底策略,掃出所有超時還未交付的任務進行投遞。這里思路較為簡單,不再詳述。

6. 及時交付

若同一時間點有大量任務需要處理,如果采用串行發(fā)布至事件中心,則仍可能導致任務的回調通知不及時。這里自然而然想到采用多線程/多協(xié)程的方式并發(fā)處理。在本系統(tǒng)中,我們使用到了微信的BatchTask庫,BatchTask是這樣一個庫,它把每一個需要并發(fā)執(zhí)行的RPC任務封裝成一個函數(shù)閉包(返回值+執(zhí)行函數(shù)+參數(shù)),然后調度協(xié)程(BatchTask的底層協(xié)程為libco)去執(zhí)行這些任務。對于已有的同步函數(shù),可以很方便的通過BatchTask的Api去實現(xiàn)任務的批量執(zhí)行。Daemon將發(fā)布事件的任務提交到BatchTask創(chuàng)建的線程池+協(xié)程池(線程和協(xié)程數(shù)可以根據(jù)參數(shù)調整)中,充分利用流水線和并發(fā),可以將任務List處理的整體時延大大縮短,盡最大努力及時通知業(yè)務方。

7. 任務過期刪除

從節(jié)省存儲資源考慮,任務通知業(yè)務成功后應當刪除。但刪除應該是一個異步的過程,因為還需要保留一段時間方便查詢日志等。這種情況,通常的實現(xiàn)方式是啟動一個Daemon異步刪除已完成的任務。我們系統(tǒng)中,是利用了tablekv的自動刪除機制,回調通知業(yè)務完成后,除了設置任務狀態(tài)為完成外,同時通過tablekv的update接口設置kv的過期時間為1個月,避免了異步Daemon掃表刪除任務,簡化了實現(xiàn)。

8. 其他風險項

1.由于time_pointer的CurrentTime初始值置為首次運行的Daemon實例的機器時間,而每次輪詢時都會對比當前Daemon實例的機器時間與CurrentTime的差別,故機器時間出錯可能會影響任務的正常調度。這里考慮到現(xiàn)網機器均有時間校正腳本在跑,這個問題基本可以忽略。

2.本系統(tǒng)的架構對事件中心構成了強依賴。定時器的可用性和可靠性依賴于事件中心的可用性和可靠性。雖然目前事件中心的可用性和可靠性都非常高,但如果要考慮所有異常情況,則事件中心的短暫不可用、或者對于訂閱者消息出隊的延遲和堆積,都是需要正視的問題。一個解決方案是使用MQ做雙鏈路的消息投遞,解決對于事件中心單點依賴的問題。

結語

這里的定時器服務目前僅用于支持境外的定時器需求,調用量級尚不大,已可滿足業(yè)務基本要求。如果要支撐更高的任務量級,還需要做更多的思考和優(yōu)化。隨時歡迎大家和我交流探討。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 定時器
    +關注

    關注

    23

    文章

    3228

    瀏覽量

    114202
  • 分布式
    +關注

    關注

    1

    文章

    843

    瀏覽量

    74428
  • 組件
    +關注

    關注

    1

    文章

    500

    瀏覽量

    17777
收藏 人收藏

    評論

    相關推薦

    延時定時器的缺陷—如何快速復位555定時器?

    如圖是555定時器快速復位電路。該電路用于快速電容器放電,其中的555定時器為TLC555
    發(fā)表于 03-12 14:25 ?421次閱讀

    如何使用定時器實現(xiàn)定時器中斷

    簡介本例程主要講解如何使用定時器實現(xiàn)定時器中斷,每秒打印串數(shù)據(jù)STM32CubeMx基本配
    發(fā)表于 08-13 08:55

    如何在Dragonboard 410c上實現(xiàn)秒表定時器

    本篇將通過渠道程序啟動系統(tǒng)定時器,這個定時器以1S為間隔不斷的條用
    發(fā)表于 02-10 10:43 ?987次閱讀

    定時器原理以及定時器實現(xiàn)的方式

    定時器原理定時器實現(xiàn)的方式有以下幾種: 基于排序鏈表方式: 通過排序鏈表來保存定時器,由于鏈表是排序好的,所以獲取最小(最早到期)的
    的頭像 發(fā)表于 08-14 11:15 ?6678次閱讀

    STM32基于cubeMX實現(xiàn)定時器點燈

    概述STM32的常見的定時器資源: 系統(tǒng)嘀嗒定時器SysTick、看門狗定時器WatchDog、實時時鐘RTC、基本定時器、通用定時器、高級
    發(fā)表于 11-23 18:21 ?19次下載
    STM32基于cubeMX<b class='flag-5'>實現(xiàn)</b><b class='flag-5'>定時器</b>點燈

    STM32定時器-基本定時器

    ,分為基本定時器,通用定時器和高級定時器?;?b class='flag-5'>定時器 TIM6 和 TIM7 是 16 位的
    發(fā)表于 11-23 18:21 ?31次下載
    STM32<b class='flag-5'>定時器</b>-基本<b class='flag-5'>定時器</b>

    通過TIM輸出比較做一個定時器

    TIM是定時器模塊的簡稱。TIM的核心是16位的自由定時器(TCNT)。有8完整的16位的捕捉/比較(IC/OC)通道。 模塊運行時,
    發(fā)表于 11-26 20:21 ?11次下載
    通過TIM輸出比較做<b class='flag-5'>一個</b><b class='flag-5'>定時器</b>

    STM32定時器學習---基本定時器

    STM32F1系列的產品,除了互聯(lián)網產品外,工作8,3種定時器,其中種就是基本定時器。那么STM32單片機的基本定時器如何操作以及編程呢
    發(fā)表于 12-02 14:06 ?27次下載
    STM32<b class='flag-5'>定時器</b>學習---基本<b class='flag-5'>定時器</b>

    SysTick 定時器

    11.1關于 SysTick 定時器SysTick定時器(又名系統(tǒng)滴答定時器)是存在于Cortex-M3的
    發(fā)表于 12-05 14:51 ?9次下載
    SysTick <b class='flag-5'>定時器</b>

    31章-定時器

    基本定時器TIMSTM32F1 系列中,除了互聯(lián)型的產品,共有8 定時器,分為基本定時器,通用定時器和高級
    發(fā)表于 01-17 09:39 ?3次下載
    31章-<b class='flag-5'>定時器</b>

    什么是軟件定時器?軟件定時器實現(xiàn)原理

    軟件定時器是用程序模擬出來的定時器,可以由硬件定時器模擬出成千上萬軟件
    的頭像 發(fā)表于 05-23 17:05 ?2613次閱讀

    分享廚房定時器電路

    廚房計時是我們在廚房中使用的小工具,以幫助我們烹飪食物。我們使用的大多數(shù)廚房定時器都是機械定時器,容易磨損。然而,上述數(shù)字廚房定時器電路比機械定時
    發(fā)表于 06-18 11:05 ?1232次閱讀
    分享<b class='flag-5'>一</b><b class='flag-5'>個</b>廚房<b class='flag-5'>定時器</b>電路

    STM32如何使用定時器實現(xiàn)微秒(us)級延時?

    如何使用定時器實現(xiàn)微秒級延時的步驟: 步驟 1:配置定時器 首先,需要選擇適合的定時器。大多
    的頭像 發(fā)表于 11-06 11:05 ?5693次閱讀

    定時器設計實現(xiàn)

    返回ITimer類型的共享指針。其中ITimer類中定義了start和stop方法,用于啟動或停止當前定時器。 TimerManager還有內部類TimerMessageQueue用于實現(xiàn)
    的頭像 發(fā)表于 11-08 16:50 ?540次閱讀

    如何實現(xiàn)軟件定時器

    在Linux,uC/OS,F(xiàn)reeRTOS等操作系統(tǒng)中,都帶有軟件定時器,原理大同小異。典型的實現(xiàn)方法是:通過硬件定時器產生固定的時鐘節(jié)
    的頭像 發(fā)表于 04-29 11:00 ?534次閱讀