定時器在大型web項目中的應用和實現

在大規模分佈式系統中,每一個業務均可能是集羣,每一個業務機都會產生定時任務,不一樣的業務會有不一樣的任務管理需求,統一的任務調度和管理變得很是有必要。java

  1. 定時如何準確,大量的定時被同時觸發怎麼辦?node

  2. 定時結束的時候,怎麼通知業務機去處理呢?git

  3. 某臺業務機下線了怎麼辦?github

  4. 如何提供任務更新、刪除功能?web

基本模型以下圖:
clipboard.pngredis

定時器在社會中有着普遍的應用,好比天天叫你起牀的鬧鐘。在軟件項目中,定時器也被應用到了各方各面,本文將從 web 項目入手,講述定時器,本文的例子都以 node 爲例。express

爲何要用定時器?

沒有什麼比機器更加準時!在我接觸單片機的時候,已經開始感嘆,爲何機器時間能夠作到這麼準!編程

好比文章的定時發佈、商品的準點開始搶購、活動定時上下架,確定不會是一個又一個管理員在後臺幫你點擊按鈕,完成操做!系統的準時能夠定位到毫秒級,雖然每一個用戶可能和服務器的時間不一致,秒級的差異仍是在可接受範圍的,可是在某些領域也會有不少精細到毫秒級的定時任務需求,好比航空航天、定時炸彈等等。服務器

定時器總類

定時器有兩種 intervaltimeout, 對應重複任務和一次性任務。在個人理解裏,interval 任務只是在 timeout 的時候再次註冊了本任務。併發

// 重複性任務
var timer = setInterval(function(){
 // do something
}, milliseconds)
// 一次性任務
var timer = setTimeout(function(){
 // do something
}, milliseconds)

unix crontab 能解決問題嗎?

crontab 並不能精確到秒,crontab 的最小粒度是分,即當第一位是「*/1」時,即最小單位是每分鐘執行,(不排除大家有奇淫技巧能夠作到秒級控制的)。unix 自己支持強大的定時任務管理 crontab,定時的格式也是強大得使人驚歎。

* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ |
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, optional)

1)Cron 表達式的格式:秒 分 時 日 月 周 年 (可選)。

字段名 容許的值 容許的特殊字符
秒 0-59 , - * /
分 0-59 , - * /
小時 0-23 , - * /
日 1-31 , - * ? / L W C
月 1-12 or JAN-DEC , - * /
周幾 1-7 or SUN-SAT , - * ? / L C #
年 (可選字段) empty, 1970-2099 , - * /

「?」字符:表示不肯定的值

「,」字符:指定數個值

「-」字符:指定一個值的範圍

「/」字符:指定一個值的增長幅度。n/m 表示從 n 開始,每次增長 m

「L」字符:用在日表示一個月中的最後一天,用在周表示該月最後一個星期 X

「W」字符:指定離給定日期最近的工做日 (週一到週五)

「#」字符:表示該月第幾個周 X。6#3 表示該月第 3 個週五

Cron 表達式範例:

每隔 5 秒執行一次:/5 * ?

每隔 1 分鐘執行一次:0 /1 ?

天天 23 點執行一次:0 0 23 ?

天天凌晨 1 點執行一次:0 0 1 ?

每個月 1 號凌晨 1 點執行一次:0 0 1 1 * ?

每個月最後一天 23 點執行一次:0 0 23 L * ?

每週星期天凌晨 1 點實行一次:0 0 1 ? * L

在 26 分、29 分、33 分執行一次:0 26,29,33 * ?

天天的 0 點、13 點、18 點、21 點都執行一次:0 0 0,13,18,21 ?

每種開發語言都提供了 crontab 的相關封裝,讓開發者調用起來駕輕就熟。以 node 爲例:

require('crontab').load(function(err, crontab) {
 // create with string expression
 var job = crontab.create('ls -la', '0 7 * * 1,2,3,4,5');
});

你在 github 搜索 crontab 能搜到主流語言的實現。

有個問題,定時器不許時!

setInterval 的回調函數並非到時後當即執行,而是等系統計算資源空閒下來後纔會執行。而下一次觸發時間則是在 setInterval 回調函數執行完畢以後纔開始計時,因此若是 setInterval 內執行的計算過於耗時,或者有其餘耗時任務在執行,setInterval 的計時會愈來愈不許, 延遲很厲害。crontab 也是一樣的原理。

var startTime = new Date().getTime();
var count = 0;
//耗時任務
setInterval(function(){
 var i = 0;
 while(i++ < 100000000);
}, 0);
setInterval(function(){
 count++;
 console.log(new Date().getTime() - (startTime + count * 1000));
}, 1000);

結果

126
176
163
112
109
107
203
189
170

固然,不排除大家有奇淫技巧能夠作到秒級控制的。

成千上萬定時任務時怎麼管理?

Crontab 存在任務上限(其實我也不知道上限是多少,知道的麻煩告訴我),任務的同步、備份管理都比較麻煩,也會有比較多的併發問題須要處理。在分佈式系統中,單獨去部署一個定時任務機器也是可行的。不過任務調度、定時結束通知客戶端也須要蠻多工做量的。

unix 的 crontab 再也不是咱們的第一選擇,每種編程可能都有定時任務管理的相關框架。好比 java 的 Quartz,Python 的 APScheduler。nodejs 的 node-schedule。可是這些東西是否能真的知足你的需求呢?

So,咱們須要一個定時任務管理平臺。

思路和實現

目標

  1. 業務方能夠定義定時時間、時間結束的觸發任務

  2. 業務方能夠更新或者刪除已經發布的定時任務

  3. 定時任務管理平臺統一接收和調度任務

主要解決兩個問題:

  1. 設置準確的定時時間

  2. 時間結束觸發客戶端,不能重複消費

redis 在 2.8.X 版本能夠開啓了鍵空間通知,更多相關請移步 Redis Keyspace Notifications。(默認不開啓,3.x 版本好像就失效了。),redis 支持的不少鍵空間事件,好比:DEL,RENAME,EXPIRE等等,redis 自己能夠定義某個鍵的過時時間,ttl key

這個值正好用來設置爲定時任務的時間。更多相關請移步 Redis Keyspace Notifications。若是客戶端訂閱了某種規則的鍵通知,好比過時,那麼在某個鍵過時的時候就會收到一個通知,這個事件就是定時結束,能夠告訴業務機能夠開啓任務了。

可若是有多個 redis 客戶端訂閱了某個鍵的過時時間,那麼任務仍是會被觸發不少次。 由於每一個客戶端
都是平等的,你能訂閱,我一樣能夠訂閱。解決辦法就是 生產者和消費者模式。同一個過時消息只能被消費一次。

重點來了

把全部的定時任務按照定時開啓的時間倒序排列,存入 sorted Sets , 把時間設置爲 score。這樣就會造成一個按照時間排好序的集合,能夠按照時間前後依次取出全部的任務,須要新增和修改任務,也是能夠經過 redis 的命令實現的。

定時管理服務器每 1000ms 去取 sorted sets 頂部的數據,若是獲取到的 task 離觸發小於 1s,那麼就能夠執行 pop() 操做,表示這個任務開始被調度執行,由於 redis 的 pop() 是原子性的,同一個 task 永遠只會被消費一次。這樣就解決了 redis 鍵空間通知會被重複消費的問題。

僞代碼以下:

var taskSorts = new Sets(task1, task2, task3); // 在 redis 中創建按時間排序的集合

// 每隔一秒執行一下操做,
var newOne = taskSorts.zrank(-1); // 獲取到最快發生的任務
if(newOne.time < 1000){ // 若是知足消費條件
 newOne = taskSorts.pop(); // 消費該任務,重複此循環,繼續消費下一個任務
 setTimeout(function(){
 // dosomething
 }, newOne.time)
}

任務觸發

  1. 任務的提交和觸發都應該在業務方完成。定時任務管理平臺只是幫助管理和調度任務。在定義的任務裏面定義好任務執行的回調參數和接口。

  2. 客戶端定義任務的時候,同時註冊好定時結束的回調接口,或者應該在項目啓動的時候,就註冊好全部回調的接口。由於同一個業務的 A 機器提交了任務,觸發的時候可能 A 機器下線了,只能定時任務平臺只能去觸發業務 A 的 B 機器了。

  3. 引入跨服務遠程調用。業務和定時任務管理平臺可能不在同一個機器,可能分佈在不一樣的 ip。聽起來很複雜,實際上跨語言的調用調用方式有不少,好比 REST API、消息隊列、RPC。個人團隊選擇了 Thrift(Facebook 開源的,跨語言的,如今共享給了 Apache 基金)。以上的方式均可以實現任務只被觸發了一次,遠程通知給客戶端(任務註冊方)。

成品 -- nodejs 的實現 cron-redis

https://github.com/MZMonster/cron-redis
主要依賴 bull 實現了任務隊列的管理功能實現的定時任務管理工具。

demo:

// 就這樣定義,3 秒鐘以後,hello 函數將被執行。
function hello (x, y){
 console.log(new Date());
 console.log(x + ' + '+ y +' = %s', x+y);
}

// 我是一個任務
var task1 = {
 method: hello.name, // 任務回調的函數
 params: [2, 3], // 任務執行的參數
 rule: moment().add(3, 's').toDate() // 任務執行間隔,支持 crontab 格式
}

queue.register(hello)
queue.publish(task1);

若是你要求不高,unix 自帶的 crontab 也足夠你折騰了。使用 redis 來實現定時也是一種極好的思路,cron-redis 值得你去試一試。

該庫只是一個定時任務的庫,實際上能夠經過以上的思路實現微服務————定時任務管理平臺。經過 cron-redis 組合遠程服務調用 thrift、服務的註冊發現工具 zookeeper,定時任務管理平臺分分鐘就被搭建了(等我下一篇文章吧,分分鐘搭建微服務)。

相關文章
相關標籤/搜索