新型計(jì)劃任務(wù):以接口形式實(shí)現(xiàn)的計(jì)劃任務(wù)

2018-11-21 21:19 更新

1.31.1 這里所說的計(jì)劃任務(wù)

計(jì)劃任務(wù)主要負(fù)責(zé)處理一些耗時(shí)的操作,或者非用戶觸發(fā)的作業(yè)。

有些人會(huì)稱它為后臺(tái)任務(wù),或者推送作業(yè),又或者定時(shí)任務(wù)。這時(shí)則統(tǒng)稱為:計(jì)劃任務(wù)。

例如,當(dāng)你發(fā)布一條微信朋友圈后需要通知上百個(gè)好友時(shí);當(dāng)一條后臺(tái)的推薦資訊需要推送到每個(gè)用戶的客戶端時(shí);當(dāng)需要將本地的靜態(tài)資源如圖片同步到CDN時(shí)。
顯然這些動(dòng)則需要分鐘級(jí)別的操作,不應(yīng)該在客戶端調(diào)用接口時(shí)同步處理(但讓我驚訝的是現(xiàn)實(shí)真的有人會(huì)這么做?。只蛘叻怯脩粲|發(fā)而需要后臺(tái)處理(但更讓我驚訝的是竟然也有系統(tǒng)是在用戶請(qǐng)求時(shí)附帶進(jìn)行處理,而且還是國(guó)內(nèi)某個(gè)知名的會(huì)員中心?。?

這里不僅僅是提供實(shí)現(xiàn)計(jì)劃任務(wù)的約束和機(jī)制,更多的是引導(dǎo)大家更好地應(yīng)對(duì)此類問題。

1.31.2 計(jì)劃任務(wù)的關(guān)鍵環(huán)節(jié)

(1)觸發(fā)

首先,是何時(shí)何地由何用戶產(chǎn)生一條待執(zhí)行的計(jì)劃任務(wù),我們可以把這個(gè)場(chǎng)景點(diǎn)稱為一個(gè)觸發(fā)點(diǎn)。
通常的做法,我們會(huì)先紀(jì)錄下此觸發(fā)點(diǎn)的場(chǎng)景信息,并放入到一個(gè)隊(duì)列里面,以便等待計(jì)劃任務(wù)消費(fèi)。

(2)調(diào)度

其次,是通過何種機(jī)制進(jìn)行計(jì)劃任務(wù)的調(diào)度。
這里不僅有技術(shù)層面的問題,還有業(yè)務(wù)的問題,如每次批量處理多少,間隔多少,是否需要失敗重試等等?

(3)消費(fèi)

最后,則是具體的計(jì)劃任務(wù)執(zhí)行,以完成必要的操作,也稱為消費(fèi)。
很多傳統(tǒng)的做法,都是把這些操作和接口混在一起的,而這里,PhalApi則會(huì)以一種更為明朗的方式來實(shí)現(xiàn),從而自底而上,支持更多的調(diào)度方式和觸發(fā)機(jī)制。

1.31.3 傳統(tǒng)的計(jì)劃任務(wù)

a pic

如果以一圖而鱉之,上圖雖然簡(jiǎn)化,但可以很好地說明傳統(tǒng)計(jì)劃任務(wù)的結(jié)構(gòu)體系。
即:很多項(xiàng)目都是使用內(nèi)嵌的方式來包含計(jì)劃任務(wù),這樣明顯會(huì)把接口服務(wù)系統(tǒng)和后臺(tái)計(jì)劃任務(wù)混在一起,增加了系統(tǒng)間的耦合性。
雖然小項(xiàng)目可以忍受或者適合這種混合,但是出于長(zhǎng)遠(yuǎn)考慮,進(jìn)行有意識(shí)地分解還是很有好處的。

而且這種混合潛意識(shí)下又讓開發(fā)人員不加判斷就進(jìn)行調(diào)用,這會(huì)嚴(yán)重增加接口的反應(yīng)時(shí)間。
我曾目睹一個(gè)接口耗時(shí)了近36秒之久,在對(duì)這個(gè)舊系統(tǒng)的接口進(jìn)行一番排查后,原來是這個(gè)接口在發(fā)布后對(duì)上百個(gè)好友做了通知推送導(dǎo)致產(chǎn)生了上百條insert語句。

(1)傳統(tǒng)的調(diào)度方式

我們重點(diǎn)關(guān)注一下傳統(tǒng)計(jì)劃任務(wù)的調(diào)度方式,在過去,我們通常會(huì)有兩種方式:一種是啟動(dòng)死循環(huán)的進(jìn)程,另一種是啟動(dòng)一個(gè)crontab之類的定時(shí)任務(wù)。
當(dāng)然,上述的在接口請(qǐng)求時(shí)同步進(jìn)行調(diào)度也算一種方式,但不是正規(guī)的做法。

如果采用死循環(huán)的方式,我們還需要考慮代碼更新升級(jí)后,對(duì)腳本的重啟,以便載入新的代碼。如果是sh循環(huán)調(diào)用PHP腳本,則可以忽略。

1.31.4 新型的計(jì)劃任務(wù)

(1)以接口的形式提供計(jì)劃任務(wù)服務(wù)

PhalApi中最具特色的做法是,將計(jì)劃任務(wù)的執(zhí)行消費(fèi)實(shí)現(xiàn),以接口形式來提供。
這樣的好處在于,我們作為接口開發(fā)人員,可以以熟悉的方式來進(jìn)行計(jì)劃任務(wù)的開發(fā)。
但更大的得益在于,將計(jì)劃任務(wù)通過接口的形式提供后,我們會(huì)看到更為廣闊的使用場(chǎng)景:我們可以使用MQ隊(duì)列消費(fèi),可以同步請(qǐng)求也可以異步請(qǐng)求。

(2)系統(tǒng)架構(gòu)

我們所做的,不僅僅只是把原來混合型的代碼作簡(jiǎn)單分解,如下:a pic

而是以一種更為正統(tǒng)的做法,為此我們添加了一些必要的節(jié)點(diǎn)來設(shè)計(jì)此構(gòu)架。新的實(shí)現(xiàn)方式下的體系結(jié)構(gòu)如下:
a pic

節(jié)點(diǎn)說明

在上圖中,應(yīng)用節(jié)點(diǎn)還是我們的接口系統(tǒng);MQ隊(duì)列則是用于存放待消費(fèi)的場(chǎng)景信息,同其他的MQ一樣;計(jì)劃任務(wù)則可以分為兩部分,API接口實(shí)現(xiàn)和任務(wù)調(diào)度。
計(jì)劃任務(wù)這兩部分,物理部署上可以合在一起,也可以分開,這取決于應(yīng)用系統(tǒng)是采用分布式的做法,還是單一的服務(wù)器。

執(zhí)行流程

由上圖可以看出,一個(gè)完整的計(jì)劃任務(wù)流程為:

  • 1、應(yīng)用產(chǎn)生一條新的計(jì)劃任務(wù),并存放于MQ隊(duì)列
  • 2、計(jì)劃任務(wù)定時(shí)或者不停掃描新的計(jì)劃任務(wù);若有,則進(jìn)行調(diào)度
  • 3、計(jì)劃任務(wù)API完成需要的工作,并將結(jié)果返回調(diào)度器

(3)單個(gè)添加,批量處理

這里只支持單個(gè)MQ添加,而處理則是批量的,且每批處理的數(shù)據(jù)可指定配置。

(4)MQ共享

無論是分布式還是本地一體化,MQ隊(duì)列都應(yīng)該是可以共享訪問的,以便為應(yīng)用節(jié)點(diǎn)、計(jì)劃任務(wù)調(diào)度節(jié)點(diǎn)所訪問,如下圖所示:a pic

首選redis MQ

因?yàn)镸Q作為頻繁讀寫的媒介,應(yīng)該優(yōu)先使用高效緩存來提高系統(tǒng)的吞吐率以及增加并發(fā)的能力。此外,作為臨時(shí)一次性的數(shù)據(jù),使用高效緩存也是大有好處的(但我們也需要考慮到數(shù)據(jù)丟失的情況)。
而且,為了支持 單個(gè)添加,批量處理,第三方緩存應(yīng)該很好地支持隊(duì)列的操作。
所以,redis是一個(gè)不錯(cuò)的選擇。

如下,是redis簡(jiǎn)單的隊(duì)列操作:

$redis = new Redis();
$redis->connect('127.0.0.1', 6300);

$redis->lpush('test_key', 'www');
$redis->lpush('test_key', 'phalapi');
$redis->lpush('test_key', 'net');

echo $redis->lpop('test_key'), "\n";
echo $redis->lpop('test_key'), "\n";
echo $redis->lpop('test_key'), "\n";

數(shù)據(jù)庫(kù)MQ

如果考慮到redis擴(kuò)展不好安裝,或者應(yīng)用喜歡使用數(shù)據(jù)庫(kù)來存放MQ,也是可以的。只需要用SQL的一些基本的操作語句便可做到FIFO。

文件MQ

文件MQ也是一種方式,但很少使用。

(5)更豐富的調(diào)度方式

接口同步調(diào)度

雖然也是同步調(diào)度,但是我們將計(jì)劃任務(wù)隔離后,便于日后發(fā)現(xiàn)此同步的計(jì)劃任務(wù)影響到接口的響應(yīng)時(shí)間時(shí),可以及時(shí)輕松地切換到后臺(tái)異步處理的方式。

回歸傳統(tǒng)的調(diào)度

我們也可以沿用傳統(tǒng)的做法,即使用死循環(huán)的腳本調(diào)度,或者crontab類的定時(shí)任務(wù)。

MQ隊(duì)列消費(fèi)

既然我們以接口服務(wù)的形式提供計(jì)劃任務(wù)的操作,那么可以把同一接口的調(diào)度放置到同一隊(duì)列中進(jìn)行維護(hù)和消費(fèi)。

接口異步調(diào)度

當(dāng)計(jì)劃任務(wù)以接口服務(wù)提供后,我們可以使用另一種免MQ的做法,即使用接口的異步調(diào)度。如下:
a pic

這樣既可以避免死循環(huán)帶來的性能負(fù)載問題,也可以避免定時(shí)任務(wù)帶來的延時(shí)問題,可以說異步調(diào)度是一種折中完美的做法。
但這也可能是一種不負(fù)責(zé)任或者不安全的做法,因?yàn)槲覀儫o法跟進(jìn)異步計(jì)劃任務(wù)的結(jié)果。

本地調(diào)度和遠(yuǎn)程調(diào)度

本地調(diào)度是指在執(zhí)行過程中構(gòu)建模擬接口的調(diào)用而無須經(jīng)過網(wǎng)絡(luò)請(qǐng)求,遠(yuǎn)程調(diào)度則是通過遠(yuǎn)程接口請(qǐng)求來實(shí)現(xiàn)。
如果把本地調(diào)度和遠(yuǎn)程調(diào)度,跟同步/異步組合起來,我們可以得到以下三種有意義的組合:

  • 本地同步調(diào)度
  • 遠(yuǎn)程同步調(diào)度
  • 遠(yuǎn)程異步調(diào)度

(6)計(jì)劃任務(wù)的劃分

service即類型

明顯地,接口服務(wù)名稱service即可作為計(jì)劃任務(wù)劃分的依據(jù)。

不同的service作為不同的隊(duì)列,不同類型的計(jì)劃任務(wù);而相同的service則作為相同的隊(duì)列相同的計(jì)劃任務(wù)。

接口參數(shù)即參數(shù)

接口參數(shù)即可計(jì)劃任務(wù)執(zhí)行時(shí)所需要的上下文信息。

1.31.5 PhalApi中計(jì)劃任務(wù)的核心設(shè)計(jì)解讀

(1)橋接模式 - 數(shù)據(jù)與行為獨(dú)立變化

為了給計(jì)劃任務(wù)一個(gè)執(zhí)行的環(huán)境,我們提供了 計(jì)劃任務(wù)調(diào)度器 ,即:Task_Runner。
每個(gè)計(jì)劃任務(wù)需要調(diào)度的接口是不一樣的,即不同的接口服務(wù)決定不同的行為;每個(gè)行為需要的數(shù)據(jù)也不一樣,即不同的接口參數(shù)決定不同的數(shù)據(jù)。

自然而言的,Task_Runner按照橋接模式,其充當(dāng)?shù)慕巧缦拢?br />a pic

然后,我們就可以這樣各自實(shí)現(xiàn):a pic

(2)適配器模式 - 對(duì)象適配器和類適配器

在對(duì)MQ進(jìn)行實(shí)現(xiàn)時(shí),我們提供的Redis MQ隊(duì)列、文件MQ隊(duì)列和DB MQ隊(duì)列,都使用了適配器模式,以重用框架已有的功能。
其中,Redis MQ隊(duì)列和文件MQ隊(duì)列是屬于對(duì)象適配器,DB MQ隊(duì)列是類適配器。對(duì)于對(duì)象適配器,我們也提供了外部注入,以便客戶端在使用時(shí)可以輕松定制擴(kuò)展,當(dāng)然也可以使用默認(rèn)的緩存。

如下:a pic

這樣以后,我們可以這樣根據(jù)創(chuàng)建不同的MQ隊(duì)列:

//Redis MQ隊(duì)列
$mq = Task_MQ_Redis();
//或
$mq = Task_MQ_Redis(new PhalApi_Cache_Redis(array('host' => '127.0.0.1', 'port' => 6379)));

//文件MQ隊(duì)列
$mq = new Task_MQ_File();
//或
$mq = new Task_MQ_File(new PhalApi_Cache_File(array('path' => '/tmp/cache')));

//DB MQ隊(duì)列
$mq = new Task_MQ_DB();

//Array MQ隊(duì)列
$mq = new Task_MQ_Array();

(3)模板方法 - 本地和遠(yuǎn)程兩種調(diào)度策略

在完成底層的實(shí)現(xiàn)后,我們可以再來關(guān)注如何調(diào)度的問題,目前可以有本地調(diào)度和遠(yuǎn)程調(diào)度兩種方式。

  • 本地調(diào)度:是指本地模擬接口的請(qǐng)求,以實(shí)現(xiàn)接口的調(diào)度
  • 遠(yuǎn)程調(diào)度:是指通過計(jì)劃任務(wù)充當(dāng)接口客戶端,通過請(qǐng)求遠(yuǎn)程服務(wù)器的接口以完成接口的調(diào)度

為此,我們的設(shè)計(jì)演進(jìn)成了這樣:a pic

上圖多了兩個(gè)調(diào)度器的實(shí)現(xiàn)類,并且遠(yuǎn)程調(diào)度器會(huì)將遠(yuǎn)程的接口請(qǐng)求功能委托給連接器來完成。

(4)設(shè)計(jì)審視

好了!讓我們?cè)倩仡^審視這樣的設(shè)計(jì)。

首先,我們?cè)诟邔?,也就是?guī)約層得到了很好的約定。
不必過多地深入理解計(jì)劃任務(wù)內(nèi)部的實(shí)現(xiàn)細(xì)節(jié),我們也可以輕松得到這樣的概念流程:
計(jì)劃任務(wù)調(diào)度器(Task_Runner)從MQ隊(duì)列(Task_MQ)中不斷取出計(jì)劃任務(wù)接口服務(wù)(PhalApi_Api)進(jìn)行消費(fèi)。

再下一層,則是具體的實(shí)現(xiàn),即我們所說的實(shí)現(xiàn)層。
客戶可以根據(jù)自己的需要進(jìn)行選取使用,他們也可以擴(kuò)展他們需要的MQ。重要的是,他們需要自己實(shí)現(xiàn)計(jì)劃任務(wù)的接口服務(wù)。

根據(jù)愛因斯坦說的,要保持簡(jiǎn)單,但不要過于簡(jiǎn)單。
所以,為了更好地理解計(jì)劃任務(wù)的運(yùn)行過程,我們提供了簡(jiǎn)單的時(shí)序圖:
a pic

上圖主要體現(xiàn)了兩個(gè)操作流程:加入MQ和MQ消費(fèi)。
其中,注意這兩個(gè)流程是共享同一個(gè)MQ的,否則不能共享數(shù)據(jù)。同時(shí)調(diào)度是會(huì)進(jìn)行循環(huán)式的調(diào)度,并且窮極之。

(5)沒有引入工廠方法的原因

我們?cè)诳紤]是否需要提供工廠方法來創(chuàng)建計(jì)劃任務(wù)調(diào)度器,或者M(jìn)Q。
但我們發(fā)現(xiàn),設(shè)計(jì)是如此明了,不必要再引入工廠方法來增加使用的復(fù)雜性,因?yàn)榇嬖诮M合的情況。而且,對(duì)于后期客戶端進(jìn)行擴(kuò)展也不利。

當(dāng)我們需要啟動(dòng)一個(gè)計(jì)劃任務(wù)時(shí),可以這樣寫:

$mq = new Task_MQ_Redis();
$runner = new Task_Runner_Local($mq);

$runner->go('MyTask.DoSth');

上面簡(jiǎn)單的組合可以有:4種MQ * 2種調(diào)度 = 8種組合。

所以,我們最后決定不使用工廠方法,而是把這種自由組合的權(quán)利交給客戶端。

(6)失敗重試與并發(fā)問題

除了對(duì)計(jì)劃任務(wù)使用什么模式進(jìn)行探討外,我們還需要關(guān)注計(jì)劃任務(wù)其他運(yùn)行時(shí)的問題。

一個(gè)考慮的是失敗重試,這一點(diǎn)會(huì)發(fā)生在遠(yuǎn)程調(diào)度中,因?yàn)榻涌谡?qǐng)求可能會(huì)超時(shí)。這時(shí)我們采用的是失敗輪循重試。
即,把失敗的任務(wù)放到MQ的最后,等待下一批次的嘗試。連接器在進(jìn)行請(qǐng)求時(shí),也會(huì)進(jìn)行一定次數(shù)的超時(shí)重試。這里主要是為了預(yù)防接口服務(wù)器崩潰后的計(jì)劃任務(wù)丟失。

另一個(gè)則是并發(fā)的問題。這里并沒有過多地進(jìn)行加鎖策略。
而是把這種需要的實(shí)現(xiàn)移交給了客戶端。因?yàn)榧渔i會(huì)使得計(jì)劃任務(wù)更為復(fù)雜,而且有時(shí)不一定需要使用,如一個(gè)計(jì)劃任務(wù)只有一個(gè)進(jìn)程時(shí),也就是單個(gè)死循環(huán)的腳本進(jìn)程的情況。

(7)客戶端的使用

最后,客戶端的使用就很簡(jiǎn)單了:

$mq = new Task_MQ_Redis();
$taskLite = new Task_Lite();

$taskLite->add('MyTask.DoSth', array('id' => 888));

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)