閱讀115 返回首頁    go 阿裏雲 go 技術社區[雲棲]


使用 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可以通過selectepoll來監聽,有時候和程序的事件機製就結合在一起了。

<?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

  上一篇:go redis lua原理分析
  下一篇:go redis debug命令詳解