老板喊你設計一個高效的定時任務系統(tǒng)!
原創(chuàng)【51CTO.com原創(chuàng)稿件】今天想跟大家一起探討一個聽起來很簡單的話題:定時任務機制。
圖片來自 Pexels
無非就是一個計時器,到了指定時間就開始跑唄。too young,要是這么簡單我還說啥呢,干不就完了。
那如果是幾千上萬個定時任務,你的計時器該如何設計呢?如果是 A 任務執(zhí)行完后再執(zhí)行 B 任務你會怎么調度呢?
如果是幾十臺機器同時要處理一些任務,你又該如何設計呢?帶著這些看似不簡單的問題我們開始時間之旅。
操作系統(tǒng)的時間系統(tǒng)
應用程序部署在操作系統(tǒng)上,定時任務依賴操作系統(tǒng)的時鐘。鑒于大部分的服務器都部署在 Linux 上,我們就只討論 Linux 的時間系統(tǒng),Windows 服務器別打我。
大部分 PC 機中有兩個時鐘源,他們分別叫做 RTC(Real Time Clock,實時時鐘) 和 OS(操作系統(tǒng))時鐘。
RTC
RTC(Real Time Clock,實時時鐘)也叫做 CMOS 時鐘,它是 PC 主機板上的一塊芯片(或者叫做時鐘電路),它靠電池供電,即使系統(tǒng)斷電也可以維持日期和時間。
由于獨立于操作系統(tǒng)所以也被稱為硬件時鐘,它為整個計算機提供一個計時標準,是最原始最底層的時鐘數(shù)據(jù)。
OS 時鐘
OS 時鐘產生于 PC 主板上的定時/計數(shù)芯片(8253/8254),由操作系統(tǒng)控制這個芯片的工作,OS 時鐘的基本單位就是該芯片的計數(shù)周期。
在開機時操作系統(tǒng)取得 RTC 中的時間數(shù)據(jù)來初始化 OS 時鐘,然后通過計數(shù)芯片的向下計數(shù)形成了 OS 時鐘,所以 OS 時鐘并不是本質意義上的時鐘,它更應該被稱為一個計數(shù)器。
OS 時鐘只在開機時才有效,而且完全由操作系統(tǒng)控制,所以也被稱為軟時鐘或系統(tǒng)時鐘。
時鐘中斷
Linux 的 OS 時鐘的物理產生原因是可編程定時/計數(shù)器產生的輸出脈沖,這個脈沖送入 CPU,就可以引發(fā)一個中斷請求信號,我們就把它叫做時鐘中斷。
Linux 中用全局變量 jiffies 表示系統(tǒng)自啟動以來的時鐘滴答數(shù)目。每個時鐘滴答,時鐘中斷得到執(zhí)行。
時鐘中斷執(zhí)行的頻率很高:100 次/秒(Linux 設計者將一個時鐘滴答(tick)定義為 10ms),時鐘中斷的主要工作是處理和時間有關的所有信息、決定是否執(zhí)行調度程序。
和時間有關的所有信息包括系統(tǒng)時間、進程的時間片、延時、使用 CPU 的時間、各種定時器,進程更新后的時間片為進程調度提供依據(jù),然后在時鐘中斷返回時決定是否要執(zhí)行調度程序。
在單處理器系統(tǒng)中,每個 tick 只發(fā)生一次時鐘中斷。在對應的中斷處理程序中完成更新系統(tǒng)時間、統(tǒng)計、定時器、等全部功能。
而在多處理器系統(tǒng)下,時鐘中斷實際上是分成兩個部分:
- 全局時鐘中斷,系統(tǒng)中每個 tick 只發(fā)生一次。對應的中斷處理程序用于更新系統(tǒng)時間和統(tǒng)計系統(tǒng)負載。
- 本地時鐘中斷,系統(tǒng)中每個 tick 在每個 CPU 上發(fā)生一次。對應的中斷處理程序用于統(tǒng)計對應 CPU 和運行于該CPU上的進程的時間,以及觸發(fā)對應 CPU 上的定時器。
于是,在多處理器系統(tǒng)下,每個 tick,每個 CPU 要處理一次本地時鐘中斷;另外,其中一個 CPU 還要處理一次全局時鐘中斷。
時鐘中斷的應用
更新系統(tǒng)時間:在 Linux 內核中,全局變量 jiffies_64 用于記錄系統(tǒng)啟動以來所經歷的 tick 數(shù)。
每次進入時鐘中斷處理程序(多處理器系統(tǒng)下對應的是全局時鐘中斷)都會更新 jiffies_64 的值,正常情況下,每次總是給 jiffies_64 加 1。
而時鐘中斷存在丟失的可能。內核中的某些臨界區(qū)是不能被中斷的,所以進入臨界區(qū)前需要屏蔽中斷。
當中斷屏蔽取消的時候,硬件只能告訴內核是否曾經發(fā)生了時鐘中斷、卻不知道已經發(fā)生過多少次。
于是,在極端情況下,中斷屏蔽時間可能超過 1 個 tick,從而導致時鐘中斷丟失。
如果計算機上的時鐘振蕩器有很高的精度,Linux 內核可以讀振蕩器中的計數(shù)器,通過比較上一次讀的值與當前值,以確定中斷是否丟失;如果發(fā)現(xiàn)中斷丟失,則本次中斷處理程序會給 jiffies_64 增加相應的計數(shù)。
但是如果振蕩器硬件不允許(不提供計數(shù)器、或者計數(shù)器不允許讀、或者精度不夠),內核也沒法知道時鐘中斷是否丟失了。
內核中的全局變量 xtime 用于記錄當前時間(自 1970-01-01 起經歷的秒數(shù)、本秒中經歷的納秒數(shù))。xtime 的初始值就是內核啟動時從 RTC 讀出的。
在時鐘中斷處理程序更新 jiffies_64 的值后,便更新 xtime 的值。通過比較 jiffies_64 的當前值與上一次的值(上面說到,差值可能大于 1),決定 xtime 應該更新多少。
系統(tǒng)調用 gettimeofday(對應庫函數(shù) time 和 gettimeofday)就是用來讀 xtime 變量的,從而讓用戶程序獲取系統(tǒng)時間。
實現(xiàn)定時器:既然已知每個 tick 是 10ms,用 tick 來做定時任務統(tǒng)計再好不過。無論是內核還是應用系統(tǒng)其實都有大量的定時任務需求,這些定時任務類型不一,但是都是依賴于 tick。
已知的操作系統(tǒng)實現(xiàn)定時任務的方式有哪些呢?
①維護一個帶過期時間的任務鏈表
簡單且行之有效的方式。在一個全局鏈表中維護一個定時任務鏈。每次 tick 中斷來臨,遍歷該鏈表找到 expire 到期的任務。
如果將任務以 expire 排序,每次只用找到鏈頭的元素即可,時間復雜度為 O(1)。
這種方式對于早期的 Linux 系統(tǒng)來說沒有問題,隨著現(xiàn)在的系統(tǒng)復雜度漸漸變化,它無法支撐如今的網(wǎng)絡流量暴增時代的需求。
②時間輪(Timing-Wheel)算法
時間輪很容易理解,上圖有 n 個 bucket,每一個 bucket 表示一秒,當前 bucket 表示當前這一秒到來之后要觸發(fā)的事件。
每個 bucket 會對應著一個鏈表,鏈表中存儲的就是當前時刻到來要處理的事件。
那這里有個問題來了,如果有個定時任務需要在 16 小時后執(zhí)行,換算成秒就是 57600s,難道我們的時間輪也要這么多個 bucket 嗎。幾萬個對內存也是一種損耗。
為了減少 bucket 的數(shù)量,時間輪算法提供了一個擴展算法,即 Hierarchy 時間輪。
Hierarchy 很好理解,層級制度。既然一個時間輪可能會導致 bucket 過多,那么為什么不能多弄幾個輪子來代替時分秒呢?
基于時、分、秒各自實現(xiàn)一個 wheel,每個 wheel 維護一個自己的 cursor,在 Hour 數(shù)組中,每個 bucket 代表一個小時。
Minute 數(shù)組中每一個 bucket 代表 1 分鐘,Second 數(shù)組中每個 bucket 代表 1 秒。
采用分層時間輪,我們只需要 24+60+60=144 個 bucket 就可以表示所有的時間。
完全模擬到時鐘的用法,Second wheel 每轉完 60 個 bucket ,要聯(lián)動 Minute wheel 轉動一格,同理 Minite wheel 轉動 60 個 bucket 也要聯(lián)動 Hours wheel 轉動一格。
③維護一個基于小根堆算法的定時任務
小根堆的性質是滿足除了根節(jié)點以外的每個節(jié)點都不小于其父節(jié)點的堆?;谶@種性質從根節(jié)點開始遍歷每個節(jié)點能保證獲取到一個最小優(yōu)先級的隊列。
那么應用到定時器中,每次只用獲取當前最小堆的 root 節(jié)點看是否到期即可。最小堆的插入時間復雜度為 O(lgn),獲取頭結點時間復雜度為 O(1)。
開箱即用的定時器
單機版定時器
①cron/crontab
cron 是 Linux 中的一個定時任務機制。cron 表示一個在后臺運行的守護進程,crontab 是一個設置 cron 的工具,所有的定時任務都寫在 crontab 文件中。
cron 調度的是 /etc/crontab 文件中的內容。crontab 的命令構成為時間+動作,其時間有分、時、日、月、周五種。
這里要注意,最小單位為分鐘,默認是不到秒的級別,大家也給出了各種精確到秒的方案,有興趣的可以搜索一下。
/etc/crontab 文件中的每一行都代表一項任務,它的格式是:
- minute hour day month dayofweek user-name command
- * minute — 分鐘,從 0 到 59 之間的任何整數(shù)
- * hour — 小時,從 0 到 23 之間的任何整數(shù)
- * day — 日期,從 1 到 31 之間的任何整數(shù)(如果指定了月份,必須是該月份的有效日期)
- * month — 月份,從 1 到 12 之間的任何整數(shù)(或使用月份的英文簡寫如 jan、feb 等等)
- * dayofweek — 星期,從 0 到 7 之間的任何整數(shù),這里的 0 或 7 代表星期日(或使用星期的英文簡寫如 sun、mon 等等)
- * user-name - 用戶,腳本以什么用戶執(zhí)行
- * command — 要執(zhí)行的命令(命令可以是 ls /proc >> /tmp/proc 之類的命令,也可以是執(zhí)行你自行編寫的腳本的命令。)
②JDK 提供的定時器:Timer
Timer 的思路很簡單,基于最小堆的方案創(chuàng)建一個 TaskQueue 來盛 TimerTask。
Timer 中有一個 TimerThread 線程,該線程是 Timer 中唯一負責任務輪詢和任務執(zhí)行的線程。
這就意味著如果一個任務耗時很久,久到已經超過了下個任務的開始執(zhí)行時間,那么就意味下一個任務會延遲執(zhí)行。
另外 Timer 線程是不會捕獲異常的,如果某個 TimerTask 執(zhí)行過程中發(fā)生了異常而被終止,那么后面的任務將不會被執(zhí)行。所以要做好異常處理防止出現(xiàn)異常影響任務繼續(xù)。
因為有阻塞和異常終止的缺點,JDK 又封裝了另一個定時器的實現(xiàn)方式,這次保證不會阻塞。
因為它是線程池實現(xiàn)方式的一種:ScheduledExecutorService。ScheduledExecutorService 內部將任務封裝之后交給了 DelayQueue。
DelayQueue 是一個依靠 AQS 隊列同步器所實現(xiàn)的無界延遲阻塞隊列,內部通過 PriorityQueue 來實現(xiàn),本質還是還是一個堆,所以插入的時間復雜度也是 O(lgn)。
③Netty 封裝的時間輪:HashedWheelTimer
上面簡要描述了操作系統(tǒng)中的時間輪實現(xiàn),在著名框架 Netty 中也封裝了一個自己的時間輪實現(xiàn):HashedWheelTimer 類。
因為 Netty 中需要管理大量的 I/O 超時事件,基于時間輪的方案有助于節(jié)省資源。
Netty 中采用一個輪子的方案,一個格子代表的時間是 100ms,默認有 512 個格子。
來看看 HashedWheelTimer 的構造函數(shù)參數(shù):
- HashedWheelTimer(
- ThreadFactory threadFactory, //類似于Clock中的updater, 負責創(chuàng)建Worker線程.
- long tickDuration, //時間刻度之間的時長(默認100ms), 通俗的說, 就是多久tick++一次.
- TimeUnit unit, //tickDuration的單位.
- int ticksPerWheel //類似于Clock中的wheel的長度(默認512).
- );
另外為了不無休止的增加 bucket,這里采用了輪(round)的概念,一輪所花費的時間:round time=ticksPerWheel*tickDuration。
如果 bucket 只有 512 個, 而當前休眠時間長于一輪,那么就增加相應的輪次來表示當前休眠時長。
HashedWheelTimer 中有一些主要的成員:
- HashedWheelTimer 類本身,主要負責啟動 Worker 線程、添加任務等。
- Worker:內部負責添加任務,累加 tick,執(zhí)行任務等。
- HashedWheelTimeout:任務的包裝類,鏈表結構,負責保存 deadline,輪數(shù)等。
- HashedWheelBucket:wheel 數(shù)組元素,負責存放 HashedWheelTimeout 鏈表。
Worker 線程是 HashedWheelTimer 的核心,主要負責每當已過 tickDuration 時間就累加一次 tick。
同時也負責執(zhí)行到期的 timeout 任務和添加 timeout 任務到指定的 wheel 中。
當添加 Timeout 任務的時候,會根據(jù)設置的時間來計算出需要等待的時間長度,根據(jù)時間長度進而算出要經過多少次 tick,然后根據(jù) tick 的次數(shù)來算出經過多少輪最終得出 task 在 wheel 中的位置。
對于這種時間輪一般是怎么遍歷判斷任務到期呢?每個 ticket 到來,都要去遍歷每一個 bucket ,以此來判斷是否有 bucket 到期。
所以這種方式就要求 bucket 盡量不要太多,如果太多每次遍歷都需要很長的時間。另外就是每次都會遍歷,必然會有很多空轉,也是一種資源的浪費。
④Kafka 中的時間輪:TimingWheel
Netty 中的時間輪實現(xiàn)采用了單輪+round 的模式,在 Kafka 中采用了多輪的模式。
上面說過多輪模式下如果按照時分秒來表達,每個輪所需的 bucket 都非常的少,遍歷輪的時候就會很快。
但是多輪也會帶來另一個問題就是輪的維護:比如有個定時任務是 1*60*60+50=36050s,這時候就需要分鐘和秒輪同時維護這個任務。
當這個任務繼續(xù)走,只剩下 59s 的時候,分鐘輪就無需在維護它的信息,只剩下秒輪來維護,這里出現(xiàn)了降輪的概念 。
單機定時機制對比
以上簡單描述了各個實現(xiàn)方案,簡單對比可以得出:
Timer 的實現(xiàn)方案毋庸置疑是最差的。阻塞,異常退出這兩條“罪名”無疑讓現(xiàn)代程序員無法承受因為出錯被老板罵的鍋。
ScheduledExecutorService 使用線程池的方式來異步的執(zhí)行任務,當任務量巨大的時候,如果設置了優(yōu)先數(shù)量的可執(zhí)行線程,無疑還是會阻塞任務,好在可執(zhí)行線程多。
而 HashedWheelTimer 是面向 bucket 設計,如果采用多輪的方式可以不受任務量限制,任務量非常大的時候,維護數(shù)組的成本遠遠要低于維護堆的成本。
但是如果是任務量很少的情況,時間輪依舊需要全盤掃描,出現(xiàn)空轉的狀態(tài),這種空載無疑也是浪費資源的提現(xiàn)。
所以面向使用場景編程的話:
- 如果當前待運行的定時任務屬于耗時長一點,任務量也不是那么大的時候,可以采用 ScheduledExecutorService 的方式來實現(xiàn)。
- 如果任務量比較大,任務耗時短,無疑使用 HashedWheelTimer 對內存更加友好。
定時任務系統(tǒng)
前面從操作系統(tǒng)時鐘源開始,說到時鐘中斷產生了時鐘滴答,所有的定時任務都依賴于此。
軟件層面,通過各種有效的算法在節(jié)約資源的前提下通過監(jiān)聽時鐘滴答來實現(xiàn)任務。
還記得開篇提到我們本篇文章的意圖是什么嗎,要設計一個高效的定時任務系統(tǒng)。
既然談到了設計,是不是要先出一版產品需求文檔呢。這個真的可以有,我們先提提需求再聊聊方案。
你要的需求設計
定時任務系統(tǒng)的核心功能是什么?既然是第一版,我們不要那些花里胡哨,錦上添花的功能,從本質出發(fā)。
我理解應該有三個核心模塊:
任務錄入:提供錄入定時任務的入口,支持最基本的定時任務機制:cron 表達式,自定義執(zhí)行時間等等方式。
任務調度:通過合適的調度算法從任務庫中觸發(fā)到期的任務以期執(zhí)行,當然調度系統(tǒng)最好不要直接參數(shù)執(zhí)行,做好自己的事即可。
任務執(zhí)行:調度系統(tǒng)已經觸發(fā)了任務,那么可以由專門的執(zhí)行系統(tǒng)來負責任務執(zhí)行,執(zhí)行不會阻塞任務調度,縱然執(zhí)行有阻塞也是在執(zhí)行系統(tǒng)中阻塞,保持調度的可用性。
以上 3 個模塊就能滿足基本的任務系統(tǒng)需求,接下來聊聊實現(xiàn)方案。
技術實現(xiàn)方案
①錄入模塊實現(xiàn)
一般執(zhí)行定時任務的場景是:每隔多久執(zhí)行一次操作,這種在業(yè)務系統(tǒng)中最常見的就是使用 cron 表達式來代替,所以錄入模塊要做到可以解析 cron 表達式即可。
這種錄入模式主要是針對后臺手動錄入任務的場景,對于開發(fā)人員來說最優(yōu)解就是能用代碼實現(xiàn)就不去切換鼠標(有同學說能點點鼠標誰還去砌磚)。
所以還需要提供可執(zhí)行 jar 包用于業(yè)務系統(tǒng)集成,方便開發(fā)人員通過編碼的方式將任務錄入到系統(tǒng)。
總結一下錄入任務的兩種途徑:
- 提供業(yè)務系統(tǒng)可集成 jar 包,由開發(fā)人員編碼錄入任務。
- 提供管理后臺界面,提供可配置方式錄入任務。
對于業(yè)務代碼植入式的任務業(yè)務服務器啟動的時候會通過 jar 包把任務推送過來,對于后臺錄入的任務那就需要入庫保存。
②調度模塊實現(xiàn)
在拿到錄入模塊的定時任務配置信息之后接下來要做的事情:將 cron 表達式變?yōu)橐粋€個可執(zhí)行的時間點。
比如在 Spring 中就已經提供解析 cron 的功能:CronSequenceGenerator 類可以幫我們執(zhí)行此操作。
有了可執(zhí)行時間點之后要做的事情就是管理它,讓它調度起來。上面我們討論過的各種調度算法此時可以派上用場。
如果任務密度不是很大,多為固定的定期執(zhí)行任務,小根堆算法就可以勝任;如果任務密集,很多短期快速執(zhí)行的任務,可以采用時間輪的方式提高效率。
另外,比如有個任務是 5 分鐘執(zhí)行一次,那么你一次要解析出來多少個可執(zhí)行的時間點?一天,一周,一個月?
這樣肯定是有問題的,目前的實現(xiàn)方案是任務首次啟動的時候給出第一次執(zhí)行的時間,每次執(zhí)行的時候去計算下次任務開始的時間。
這里有一個點:Java 相關的框架現(xiàn)在實現(xiàn)的方案都是當前任務執(zhí)行完成之后再計算下次任務開始執(zhí)行的時間。
如果任務是 5 分鐘一次,當前時間是:10:00,第一個任務完成需要 6 分鐘,那么第二個任務開始的時間就是:
我們預期是每隔 5 分鐘執(zhí)行一次,事實上除了第一次是按照預期的準點執(zhí)行以外,后面都會在絕對時間上有延期。
到這里我們解決了兩個問題:
- 解析時間表達式為時間點,如何確認周期性任務的下一個可執(zhí)行時間點。
- 將可執(zhí)行時間點送入調度器中,讓時間流動起來。
③任務執(zhí)行模塊
任務錄入,任務調度我們都完成了,執(zhí)行模塊才是最后的重頭戲。這里我們再細化一下,任務錄入不能說只是把任務所屬的表達式載入系統(tǒng)就完事,要把任務對象化,達到招手即用的狀態(tài)。
這里我們把每個任務都封裝為一個對象 Job,所有的 Job 都在內存中加載,調度器定義為 Scheduler,把每個可執(zhí)行時間封裝為 Trigger 對象。
Trigger 用于定義調度任務的事件規(guī)則,唯一關聯(lián)一個 Job 并標識當前 Job 的執(zhí)行狀態(tài)。
上圖就是我們的極簡版定時任務系統(tǒng)核心功能,怎么樣,麻雀雖小,五臟俱全。該有的功能一樣不少,不該有的功能一個都沒有。
到這里為止我們已經輸出了極簡版定時任務調度系統(tǒng)的核心設計和實現(xiàn)方案,依據(jù)這個方案你可以實現(xiàn)定時任務調度系統(tǒng)的單機版核心功能。
我們先不提加需求的問題,先來個高可用的問題,上面的方案是將任務加載到一臺機器的內存中定時執(zhí)行,那么如果要實現(xiàn)高可用,多臺機器的情況任務如何防止多次執(zhí)行呢?
很顯然上面的方案肯定是行不通了,下面我們開始擴容。
高可用
回答高可用的問題先說目前的思路:單機純內存抗所有任務。要做高可用必然會大于等于 2 臺機器。
那么兩臺機器都執(zhí)行任務必然會重復運行,該用什么方案在多機環(huán)境中可以統(tǒng)一管理,統(tǒng)一調度,統(tǒng)一運行任務呢?
方案一:傳統(tǒng)方案-數(shù)據(jù)庫(獨占鎖)
任務觸發(fā)的關鍵在于 Trigger 觸發(fā)器,我們只用管住 Trigger 的手讓它別亂動 task 就好,基于數(shù)據(jù)庫操作的話,保證任一時刻某個 Trigger 只會被觸發(fā)一次即可。這里可以使用行級鎖來實現(xiàn)。
某臺機器執(zhí)行到這個 Trigger 的時候向數(shù)據(jù)庫插入一條 Trigger 記錄并持有該鎖,那么其余機器即使遇到了這個任務也不能執(zhí)行。
方案二:分布式組件特性支持(分布式鎖)
一般來說數(shù)據(jù)庫肯定是值得信任的,但是面對實施要求高,任務執(zhí)行頻繁的場景的時候,數(shù)據(jù)庫又是不敢信任的,數(shù)據(jù)庫有一定的并發(fā)瓶頸。
要保證同一時刻的唯一性,除了數(shù)據(jù)庫的鎖特性以外,分布式組件肯定也支持,比如 Zookeeper,ETCD 等等。
可以利用 ZK 的臨時節(jié)點性質,同一個任務注冊一個唯一的節(jié)點,哪個機器搶到這個節(jié)點誰就來執(zhí)行任務即可。
產品加需求
基礎功能我們已經完成,高可用也做到了,上線一段時間,產品覺得的整點幺蛾子啊,不然 KPI 咋整。
①新增功能
基于事件分發(fā)的任務機制:可能有一些任務是基于特定的條件觸發(fā),這種任務在分布式環(huán)境下一般自己實現(xiàn)分布式鎖來實現(xiàn),那么任務系統(tǒng)既然提供分布式特性也可以實現(xiàn)分布式鎖的功能。
所以對于這一類任務完全可以交給任務系統(tǒng)來做,把它當成一次性觸發(fā)的任務。
②新增特性
任務終止:如果某個任務因為業(yè)務需求不再執(zhí)行,那么是否可以不發(fā)布的條件下終止該任務呢?這個時候任務終止的功能就很重要,產品經理暗暗自喜,老板加雞腿。
任務依賴:B 任務依賴 A 任務的結果才能執(zhí)行,所以要提供任務之間的級聯(lián)操作。
任務分片:如果我們有 3 臺執(zhí)行任務的機器,有 10 個每 5s 執(zhí)行一次的定時任務,恰恰每個任務都打到第一臺機器。它累如黃牛的時候另外兩臺還在曬太陽這豈不是資源的浪費嘛。
為了避免任務集中到某一臺機和提高資源利用率,我們需要一種將任務均衡分配到當前所有可執(zhí)行機器的能力,這就是所謂的分片機制。
常用的分片算法有如下:
平均分配算法:
- 如果有 3 個任務實例,分成 9 片,每個實例對應到的分片就是:1=[1,2,3],2=[4,5,6],3=[7,8,9]。
- 如果有 3 個任務實例,分成 8 片,每個實例對應到的分片就是:1=[0,1,6],2=[2,3,7],3=[4,5]。
- 如果有 3 個任務實例,分成 10 片,每個實例對應到的分片就是:1=[0,1,2,9],2=[3,4,5],3=[6,7,8]。
根據(jù)作業(yè)名 hash 值決定根據(jù) IP 升序/降序算法:
- 如果有 3 個任務實例分別為 1,2,3,作業(yè)名稱對應的 hash 值如果為奇數(shù)就按照 IP 升序尋找機器執(zhí)行,作業(yè)名稱對應的 hash 值如果為偶數(shù)就按照 IP 降序尋找機器執(zhí)行。這種算法最多要求最多只有兩個分片,即只有兩臺機器參與執(zhí)行。
輪詢算法:
- 輪詢的原理就很簡單,基于可執(zhí)行機器依次執(zhí)行。
任務日志:日志功能肯定不可少,檢測任務執(zhí)行成功與否,任務執(zhí)行記錄、時長,統(tǒng)計任務系統(tǒng)每日任務量等等。
③新增容錯機制
容錯機制:任務執(zhí)行失敗,可能是任務本身邏輯問題,也可能是外部條件,所以可以設置一些容錯機制,給它一次重試的機會。
故障轉移:集群中如果某一臺機器發(fā)生了故障,它如果還在注冊中心注冊,那么任務會被該機器執(zhí)行,很顯然如果僅有失敗重試策略,那么這個任務永遠都不會執(zhí)行成功。
首先需要心跳檢測機制,檢測活動機器是否健康;其次需要在重試失敗之后做任務轉移操作,防止多次失敗仍在同一臺機器吊死。
手動觸發(fā):如果萬不得已遇到任務沒有執(zhí)行到的情況 ,那么是否要提供手動觸發(fā)的機制呢?我想產品經理這種人精肯定不想背鍋,所以你還是做吧!
后話
做完上面的功能之后,產品經理躺在他的折疊床上打著呼嚕鼻子不時的還冒幾個泡安心的睡著了。
程序員小哥整苦逼的構思這些功能該如何實現(xiàn),是給 3 天還是給 3 個月,一場人月神話即將上演。
目前圈子里比較流行的定時任務系統(tǒng)有 Quartz,XXLJob,Elastic Job 等,實現(xiàn)方式不會脫離上文描述的范圍。
這些都是程序員自己沒事?lián)v鼓的實用型系統(tǒng),有需求就有產出,有方向就有動力。
工作之余大家也可以自己思考目前在寫的東西是否可以抽象為大層次的一個功能,簡單說,你是否也能整出個中臺來。
在這個萬物皆中臺的時代,大家不遺余力的照虎畫瓢,雖說可能畫出個四不像,起碼對于寫代碼的人,抽象能力是得到了鍛煉。
作者:楊越
簡介:目前就職廣州歡聚時代,專注音視頻服務端技術,對音視頻編解碼技術有深入研究。日常主要研究怎么造輪子和維護已經造過的輪子,深耕直播類 APP 多年,對垂直直播玩法和應用有廣泛的應用經驗,學習技術不局限于技術,歡迎大家一起交流。
編輯:陶家龍
征稿:有投稿、尋求報道意向技術人請聯(lián)絡 editor@51cto.com
【51CTO原創(chuàng)稿件,合作站點轉載請注明原文作者和出處為51CTO.com】