Go 語言 延時任務系統(tǒng)

2023-03-22 15:04 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch6-cloud/ch6-03-delay-job.html


6.3 延時任務系統(tǒng)

我們在做系統(tǒng)時,很多時候是處理實時的任務,請求來了馬上就處理,然后立刻給用戶以反饋。但有時也會遇到非實時的任務,比如確定的時間點發(fā)布重要公告?;蛘咝枰谟脩糇隽艘患虑榈?X 分鐘 / Y 小時后,對其特定動作,比如通知、發(fā)券等等。

如果業(yè)務規(guī)模比較小,有時我們也可以通過數(shù)據(jù)庫配合輪詢來對這種任務進行簡單處理,但上了規(guī)模的公司,自然會尋找更為普適的解決方案來解決這一類問題。

一般有兩種思路來解決這個問題:

  1. 實現(xiàn)一套類似 crontab 的分布式定時任務管理系統(tǒng)。
  2. 實現(xiàn)一個支持定時發(fā)送消息的消息隊列。

兩種思路進而衍生出了一些不同的系統(tǒng),但其本質是差不多的。都是需要實現(xiàn)一個定時器(timer)。在單機的場景下定時器其實并不少見,例如我們在和網絡庫打交道的時候經常會調用 SetReadDeadline() 函數(shù),就是在本地創(chuàng)建了一個定時器,在到達指定的時間后,我們會收到定時器的通知,告訴我們時間已到。這時候如果讀取還沒有完成的話,就可以認為發(fā)生了網絡問題,從而中斷讀取。

下面我們從定時器開始,探究延時任務系統(tǒng)的實現(xiàn)。

6.3.1 定時器的實現(xiàn)

定時器(timer)的實現(xiàn)在工業(yè)界已經是有解的問題了。常見的就是時間堆和時間輪。

6.3.1.1 時間堆

最常見的時間堆一般用小頂堆實現(xiàn),小頂堆其實就是一種特殊的二叉樹,見 圖 6-4


圖 6-4 二叉堆結構

小頂堆的好處是什么呢?對于定時器來說,如果堆頂元素比當前的時間還要大,那么說明堆內所有元素都比當前時間大。進而說明這個時刻我們還沒有必要對時間堆進行任何處理。定時檢查的時間復雜度是 O(1)

當我們發(fā)現(xiàn)堆頂?shù)脑匦∮诋斍皶r間時,那么說明可能已經有一批事件已經開始過期了,這時進行正常的彈出和堆調整操作就好。每一次堆調整的時間復雜度都是 O(LgN)。

Go 自身的內置定時器就是用時間堆來實現(xiàn)的,不過并沒有使用二叉堆,而是使用了扁平一些的四叉堆。在最近的版本中,還加了一些優(yōu)化,我們先不說優(yōu)化,先來看看四叉的小頂堆長什么樣:


圖 6-5 四叉堆結構

小頂堆的性質,父節(jié)點比其 4 個子節(jié)點都小,子節(jié)點之間沒有特別的大小關系要求。

四叉堆中元素超時和堆調整與二叉堆沒有什么本質區(qū)別。

6.3.1.2 時間輪


圖 6-6 時間輪

用時間輪來實現(xiàn)定時器時,我們需要定義每一個格子的 “刻度”,可以將時間輪想像成一個時鐘,中心有秒針順時針轉動。每次轉動到一個刻度時,我們就需要去查看該刻度掛載的任務列表是否有已經到期的任務。

從結構上來講,時間輪和哈希表很相似,如果我們把哈希算法定義為:觸發(fā)時間 % 時間輪元素大小。那么這就是一個簡單的哈希表。在哈希沖突時,采用鏈表掛載哈希沖突的定時器。

除了這種單層時間輪,業(yè)界也有一些時間輪采用多層實現(xiàn),這里就不再贅述了。

6.3.2 任務分發(fā)

有了基本的定時器實現(xiàn)方案,如果我們開發(fā)的是單機系統(tǒng),那么就可以擼起袖子開干了,不過本章我們討論的是分布式,距離 “分布式” 還稍微有一些距離。

我們還需要把這些 “定時” 或是“延時”(本質也是定時)任務分發(fā)出去。下面是一種思路:


圖 6-7 分布式任務分發(fā)

每一個實例每隔一小時,會去數(shù)據(jù)庫里把下一個小時需要處理的定時任務撈出來,撈取的時候只要取那些 task_id % shard_count = shard_id 的那些任務即可。

當這些定時任務被觸發(fā)之后需要通知用戶側,有兩種思路:

  1. 將任務被觸發(fā)的信息封裝為一條消息,發(fā)往消息隊列,由用戶側對消息隊列進行監(jiān)聽。
  2. 對用戶預先配置的回調函數(shù)進行調用。

兩種方案各有優(yōu)缺點,如果采用 1,那么如果消息隊列出故障會導致整個系統(tǒng)不可用,當然,現(xiàn)在的消息隊列一般也會有自身的高可用方案,大多數(shù)時候我們不用擔心這個問題。其次一般業(yè)務流程中間走消息隊列的話會導致延時增加,定時任務若必須在觸發(fā)后的幾十毫秒到幾百毫秒內完成,那么采用消息隊列就會有一定的風險。如果采用 2,會加重定時任務系統(tǒng)的負擔。我們知道,單機的定時器執(zhí)行時最害怕的就是回調函數(shù)執(zhí)行時間過長,這樣會阻塞后續(xù)的任務執(zhí)行。在分布式場景下,這種憂慮依然是適用的。一個不負責任的業(yè)務回調可能就會直接拖垮整個定時任務系統(tǒng)。所以我們還要考慮在回調的基礎上增加經過測試的超時時間設置,并且對由用戶填入的超時時間做慎重的審核。

6.3.3 數(shù)據(jù)再平衡和冪等考量

當我們的任務執(zhí)行集群有機器故障時,需要對任務進行重新分配。按照之前的求模策略,對這臺機器還沒有處理的任務進行重新分配就比較麻煩了。如果是實際運行的線上系統(tǒng),還要在故障時的任務平衡方面花更多的心思。

下面給出一種思路:

我們可以參考 Elasticsearch 的數(shù)據(jù)分布設計,每份任務數(shù)據(jù)都有多個副本,這里假設兩副本,如 圖 6-8 所示:


圖 6-8 任務數(shù)據(jù)分布

一份數(shù)據(jù)雖然有兩個持有者,但持有者持有的副本會進行區(qū)分,比如持有的是主副本還是非主副本,主副本在圖中為摸黑部分,非主副本為正常線條。

一個任務只會在持有主副本的節(jié)點上被執(zhí)行。

當有機器故障時,任務數(shù)據(jù)需要進行數(shù)據(jù)再平衡的工作,比如節(jié)點 1 掛了,見 圖 6-9。


圖 6-9 故障時數(shù)據(jù)分布

節(jié)點 1 的數(shù)據(jù)會被遷移到節(jié)點 2 和節(jié)點 3 上。

當然,也可以用稍微復雜一些的思路,比如對集群中的節(jié)點進行角色劃分,由協(xié)調節(jié)點來做這種故障時的任務重新分配工作,考慮到高可用,協(xié)調節(jié)點可能也需要有 1 至 2 個備用節(jié)點以防不測。

之前提到我們會用消息隊列觸發(fā)對用戶的通知,在使用消息隊列時,很多隊列是不支持 exactly once 的語義的,這種情況下我們需要讓用戶自己來負責消息的去重或者消費的冪等處理。



以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號