Hyperf/Crontab 元件原始碼解析

zonghay發表於2020-06-02

前置閱讀:Hyperf/Crontab使用文件

前置閱讀:Hyperf/Process自定義程式使用文件

前置閱讀:Hyperf事件機制

寫在開頭

之前做專案用到了Hyperf/Crontab元件來進行秒級的資料清洗,最近又在做定時任務的拆分,於是就打算過一遍元件原始碼加深理解,順便構思一下如何在此基礎上搭建Hyperf/Crontab的任務排程功能。Crontab本質上是一個隨Server啟動的自定義程式,所以接下來我們將從啟動和執行兩個階段來進行介紹。

定時器的啟動

由於Crontab是一個隨著Server啟動的程式,分析他的生命週期肯定會設計到框架的啟動。但本文不是主要介紹Hyperf框架啟動原始碼的,所以我們會簡單的過一下涉及到自定義程式啟動的框架啟動程式碼。

當我們使用命令 php bin/hyperf.php start 啟動hyperf時


$application = $container->get(\Hyperf\Contract\ApplicationInterface::class);

在入口檔案 hyperf.php 框架會例項化一個 Hyperf\Framework\ApplicationFactory 例項,同時會掃描一遍所有帶 @Command 註解或者定義了 commands 配置的地方,例項化一個Symfony\Component\Console\Application物件 (這是一個Symfony的Consoul命令類用於定義命令觸發執行的任務) 。將所有被定義為command的物件類註冊到$application物件中,最後在入口檔案執行


$application->run();

執行所有的Command命令,其中包括 Hyperf\Server\Command\StartServer 服務Server啟動類。在這個類裡面定義瞭如果接收到的命令引數包含 start 那麼就會執行他的邏輯,即根據 config/autoload/server.php 的配置例項化Hyperf\Server\Server類,在這個過程中會觸發BeforeMainServerStart事件,到這裡我們即將進入自定義程式啟動的核心階段。


$serverProcesses = $serverConfig['processes'] ?? [];

$processes = $this->config->get('processes', []);

$annotationProcesses = $this->getAnnotationProcesses();

// Retrieve the processes have been registered.

$processes = array_merge($serverProcesses, $processes, ProcessManager::all(), array_keys($annotationProcesses));

foreach ($processes as $process) {

...

if ($instance instanceof ProcessInterface) {

$instance->isEnable() && $instance->bind($server);

}

}

BootProcessListener監聽到BeforeMainServerStart事件的觸發會拿到所有 @Process 和 定義在processes配置檔案的程式類,執行他們的 isEnablebind 方法。

定時器的執行

在上面我們對自定義程式是如何隨框架啟動的進行了簡單的介紹,接下來我們對本文的主角Crontab自定義程式進行解析。

Hyperf文件介紹在使用Crontab之前需要在 config/autoload/processes.php 內註冊一下 Hyperf\Crontab\Process\CrontabDispatcherProcess 自定義程式。那麼我們就直接來看一下這個process類裡面做了什麼事情。

如果你對 Hyperf/Process 元件使用熟悉的話會知道,一個process類主要執行的邏輯都在 handle() 方法

內。


public function handle(): void

{

$this->event->dispatch(new CrontabDispatcherStarted());

while (true) {

$this->sleep();

$crontabs = $this->scheduler->schedule();

while (! $crontabs->isEmpty()) {

$crontab = $crontabs->dequeue();

$this->strategy->dispatch($crontab);

}

}

}

在handle內首先觸發來一個 CrontabDispatcherStarted 事件,目前這個事件無人監聽。接下來就是一段時間的協程阻塞,阻塞時間第一次為距離下一次整分鐘的秒數,其餘的都是60s。關於為什麼要使用 \Swoole\Coroutine::sleep 而不是直接 sleep() ,是因為自定義程式預設是一個協程的Server。

接下來


$crontabs = $this->scheduler->schedule();

返回一個當前這一分鐘該執行的SplQueue佇列,佇列中的是Crontab物件


object(Hyperf\Crontab\Crontab)#46164 (10) {

["name":protected]=>

string(4) "Foo4"

["type":protected]=>

string(8) "callback"

["rule":protected]=>

string(11) "* * * * * *"

["singleton":protected]=>

bool(false)

["mutexPool":protected]=>

string(7) "default"

["mutexExpires":protected]=>

int(3600)

["onOneServer":protected]=>

bool(true)

["callback":protected]=>

array(2) {

[DEBUG] Event Hyperf\Framework\Event\OnPipeMessage handled by Hyperf\Crontab\Listener\OnPipeMessageListener listener.

[0]=>

string(16) "App\Task\FooTask"

[1]=>

string(7) "execute"

}

["memo":protected]=>

NULL

["executeTime":protected]=>

object(Carbon\Carbon)#46106 (3) {

["date"]=>

string(26) "2020-06-02 14:15:57.000000"

["timezone_type"]=>

int(3)

["timezone"]=>

string(13) "Asia/Shanghai"

}

}

這個物件中記錄了我們目前比較關注的兩個關鍵資訊:

1. 執行callback

2. 執行時間

關於這個crontab物件是如何生成的,這個我們在後面會介紹到。

在我們拿到crontab物件後,我們會把物件傳送給在 config/autoload/dependencies.php 定義好的 StrategyInterface 的實現類來進行dispatch,預設指定的是Worker 程式執行策略。


$server = $this->serverFactory->getServer()->getServer();

if ($server instanceof Server && $crontab->getExecuteTime() instanceof Carbon) {

$workerId = $this->getNextWorkerId($server);

$server->sendMessage(new PipeMessage(

'callback',

[Executor::class, 'execute'],

$crontab

), $workerId);

}

除Coroutine策略外的dispatch方法是相同的,都是程式間輪訓的向WorkID通過 sendMessage 傳送 PipeMessage 物件,同時 sendMessage 方法會觸發 OnPipeMessage 事件。該事件被 OnPipeMessageListener 監聽,會根據 PipeMessage 執行對應的callback函式,即 Executor->execute’ 。在該方法中會根據corntab物件的屬性定義 Swoole\Timer::after ,根據corontab的executeTime屬性定義多少秒後執行callback。


$callback && Timer::after($diff > 0 ? $diff * 1000 : 1, $callback);

這樣基本就完成了我們一個秒級定時任務的執行。

現在我們回到上面那個關於crontab物件是什麼時候生成的問題。我們說過Cornrab本質上就是一個自定義程式,那根據Hyperf/Process的使用說明,所有的自定義程式都繼承了 Hyperf\Process\AbstractProcess ,這個類在啟動SwooleProcess時會觸發 BeforeProcessHandle 事件,在這個事件中會掃描所有的crontab配置和註解,將這些註解進行解析生成crontab物件,儲存在crontabs屬性中。


public function register(Crontab $crontab): bool

{

if (! $this->isValidCrontab($crontab)) {

return false;

}

$this->crontabs[$crontab->getName()] = $crontab;

return true;

}

以上大致就是一個crontab定時任務的執行流程,當然裡面還有很多執行細節和Contab個性化的定義引數由於篇幅有限我們還沒來得及介紹,感興趣的同學可以私下進行閱讀,下面我也附上來Hyperf/Crontab的整體執行流程類之間的關係圖,方便大家對照著閱讀原始碼。

寫在最後

Hyperf 是基於 Swoole 4.4+ 實現的高效能、高靈活性的 PHP 協程框架,內建協程伺服器及大量常用的元件,效能較傳統基於 PHP-FPM 的框架有質的提升,提供超高效能的同時,也保持著極其靈活的可擴充套件性,標準元件均基於 PSR 標準 實現,基於強大的依賴注入設計,保證了絕大部分元件或類都是 可替換可複用 的。

框架元件庫除了常見的協程版的 MySQL 客戶端、Redis 客戶端,還為您準備了協程版的 Eloquent ORM、WebSocket 服務端及客戶端、JSON RPC 服務端及客戶端、GRPC 服務端及客戶端、Zipkin/Jaeger (OpenTracing) 客戶端、Guzzle HTTP 客戶端、Elasticsearch 客戶端、Consul 客戶端、ETCD 客戶端、AMQP 元件、Apollo 配置中心、阿里雲 ACM 應用配置管理、ETCD 配置中心、基於令牌桶演算法的限流器、通用連線池、熔斷器、Swagger 文件生成、Swoole Tracker、Blade 和 Smarty 檢視引擎、Snowflake 全域性ID生成器 等元件,省去了自己實現對應協程版本的麻煩。

Hyperf 還提供了 基於 PSR-11 的依賴注入容器、註解、AOP 面向切面程式設計、基於 PSR-15 的中介軟體、自定義程式、基於 PSR-14 的事件管理器、Redis/RabbitMQ 訊息佇列、自動模型快取、基於 PSR-16 的快取、Crontab 秒級定時任務、Translation 國際化、Validation 驗證器 等非常便捷的功能,滿足豐富的技術場景和業務場景,開箱即用。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章