使用 PHP 和 Phalcon 作 daemon 進程
某些情況下,我們除了提供web界麵給用戶,還需要運行一些後台任務。這些任務可能是由用戶觸發的(比如用戶提交了一個請求,而這種請求很特殊,例如從github克隆一個項目並執行構建,至少需要幾分鍾才能執行完成,這種情況不適合阻塞的方式讓瀏覽器等待結果返回);也可能是一些常規性的係統任務(比如將日誌進行歸檔,轉移到統一的地方進行備份)。前者一般是引入消息隊列,用戶的請求隻是增加了一條待構建的消息到消息隊列,然後有一個專門的訂閱者讀取消息,調度分發執行這個任務。後者最簡單的方式便是crontab,但缺點是每個機器需要單獨進行設置,不易維護;當然也可以通過一個統一的調度器,分發任務到多個任務節點的方式來執行。
這裏隻說第一種情況,業務背景是許可範圍內按照用戶的配置生成移動端APP。對於Android,項目構建可以使用Ant或者Gradle,這樣可以通過命令行調用,在程序中就可以fork一個進程,設置相應的參數來執行了。
對於不同的Android APP,package name需要是不同的;除此之外,對於用戶的一些配置,也會體現到最終待構建的項目文件中。因此,隻能生成一套APP的模板代碼,按照用戶的輸入,修改java代碼和資源文件,最後再執行構建產生apk。這個構建過程可能需要幾分鍾。
這裏我們采用的方案是有一個單獨的daemon進程,讀取消息隊列,得到前端寫入的待構建的消息,fork進程執行Ant,完成構建後(成功或失敗)將狀態寫入數據庫,前端采用定期查詢的方式以便獲知任務是否結束。這樣daemon進程的邏輯相對簡單,構建的過程都是發生在其他進程空間的,不會對daemon進程產生影響。
代碼采用了類似 Phalcon 示例 中Multiple
的形式。
啟動程序
命令行的入口文件,參考 @guweigang 的falcon,通過一些參數指定運行哪個模塊,哪個action,以及傳遞給action的參數。這裏通過getopt
解析命令行參數,-d
決定了是否變成daemon進程。
<?php
/**
* Usage:
* php console.php -d -m module-name -t task-name -a action-name -p parameters
*
* If you has multiple parameters to pass to an action, use it like this:
* -p first -p second
*
*/
error_reporting(E_ALL);
ini_set("memory_limit", "4G");
$HELP_TEXT = <<<EOT
appcreator console
usage: php console.php [OPTION]... [PARAMS]...
-h, --help show help message
-d, --daemon running as daemon
-m, --module specify module
-t, --task specify task
-a, --action specify action
-p, --param parameter passed to action
:)
EOT;
try {
$opts = "dm:t:a:p:h";
$longopts = array(
"module:",
"task:",
"action:",
"param:",
"help",
"daemon",
);
$options = getopt($opts, $longopts);
if (isset($options['h']) || isset($options['help'])) {
print($HELP_TEXT);
exit(0);
}
if (isset($options['d']) || isset($options['daemon'])) {
$pid = pcntl_fork();
if ($pid == -1) {
error_log('fork process failed');
exit(1);
} elseif ($pid) {
// if in parent process, exit
exit(0);
}
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
// TODO
$STDIN = fopen('/dev/null', 'r');
$STDOUT = fopen('/dev/null', 'w');
$STDERR = fopen('/dev/null', 'w');
posix_setsid();
}
$args = array();
// 處理傳遞給action的參數
foreach(array('p', 'param') as $p) {
if (isset($options[$p])) {
if (is_array($options[$p])) {
$args = array_merge($options[$p]);
} else {
$args[] = $options[$p];
}
}
}
if (!isset($options['a']) && !isset($options['action'])) {
error_log('Please specify the action you want to run with -a or --action');
exit(1);
}
/*
* set cli parameters
*/
$args['module'] = (isset($options['m']) || isset($options['module'])) ? (
isset($options['m']) ? $options['m'] : $options['module']) : 'appcreator';
$args['task'] = (isset($options['t']) || isset($options['task'])) ? (
isset($options['t']) ? $options['t'] : $options['task']) : 'package';
$args['action'] = isset($options['a']) ? $options['a'] : $options['action'];
/*
* 這裏實例化\Phalcon\CLI\Console並加載服務和模塊
*/
$application = new \Phalcon\CLI\Console();
/* load services and modules */
...
/* Finally handle args */
$application->handle($args);
} catch (Exception $e) {
error_log('[AppCreator]' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
echo $e->getMessage();
echo "\n\nBacktrace:" . $e->getTraceAsString()."\n";
}
循環讀取消息,執行任務
這裏是真正的daemon進程執行的代碼。目前的邏輯比較簡單,就是定期獲取前端寫入的待構建的消息,創建進程執行一次構建任務。
<?php
public function daemonAction()
{
// change to working dir
chdir(WORKING_DIR);
// install signal handlers
$this->_setupSignals();
$nextWakeUp = 0;
// 每隔時間檢查一次當前需不需要生成APP
while(1) {
$this->roused = FALSE;
if ($nextWakeUp <= time()) {
$nextWakeUp = time() + self::SLEEP_SECONDS;
}
sleep($nextWakeUp - time());
pcntl_signal_dispatch();
if ($this->roused) {
continue;
}
$limit = self::MAX_PROCESS - count(self::$childs);
// exceeds process limit
if ($limit <= 0) {
continue;
}
// find todo tasks
$tasks = ...;
foreach ($tasks as $task) {
$pid = $this->_runTask($task);
}
}
}
通過子進程執行任務
_runTask()
其實就是調用一個新的PHP進程,執行特定的action。我們在這個action再去fork進程執行Ant
或者Gradle
。
<?php
private function _runTask($task)
{
$pid = pcntl_fork();
if ($pid == -1) {
return FALSE;
} else if ($pid) {
return $pid;
} else {
pcntl_exec($_SERVER['_'], $this->_getTaskCommand($task));
// fork succeed but exec failed,
// need to be _exit(). actually exit() will cause problem
exit(1);
}
}
signal處理
這裏daemon進程處理的signal比較簡單,主要就是需要退出和需要wait子進程結束的信息,以便OS清理其留在進程表中的信息。但是pcntl_signal_dispatch()
有個問題,有可能兩個接近同時退出的子進程,會導致可能當時隻發了一個SIGCHLD的信號,另一個信號下一次dispatch才會發出,所以隻能通過循環來wait。
signal handler需要注意一些問題,這篇文章有很詳盡的介紹,非常值得一讀。也可以看看Nginx中是怎麼做的。signal是古老UNIX的產物,signal handler是異步觸發的,這導致如果signal handler不是可重入的話,很可能會出現問題,雖然一般情況可能不會發生。更現代的處理方式是signalfd(類似的東西還有eventfd,timerfd),這個fd可以通過select
,epoll
來監聽,有時候和程序的事件機製就結合在一起了。
<?php
private function _setupSignals()
{
pcntl_signal(SIGTERM, array($this, "signalHandler"));
pcntl_signal(SIGINT, array($this, "signalHandler"));
pcntl_signal(SIGCHLD, array($this, "signalHandler"));
}
protected function signalHandler($signo)
{
$this->roused = TRUE;
switch($signo) {
case SIGTERM:
case SIGINT:
exit(1);
case SIGCHLD:
while(1) {
$pid = pcntl_wait($status, WNOHANG);
if ($pid == 0) {
break;
}
if ($pid > 0) {
// logs
} else {
break;
}
if (!pcntl_wifexited($status)) {
// error happened
} else if (($code = pcntl_wexitstatus($status)) != 0) {
// error happened
} else {
// normal exit
}
}
break;
default:
break;
}
}
最後更新:2017-04-01 13:37:10