基於Swoole和Redis實現的並發隊列處理系統

來源:互聯網
上載者:User

背景

由於PHP不支援多線程,但是作為一個完善的系統,有很多操作都是需要非同步完成的。為了完成這些非同步作業,我們做了一個基於Redis隊列任務系統。

大家知道,一個訊息佇列處理系統主要分為兩大部分:消費者和生產者。

在我們的系統中,主系統作為生產者,任務系統作為消費者。

具體的工作流程如下:1、主系統將需要需要處理的任務名稱+任務參數push到隊列中。2、任務系統即時的對任務隊列進行pop,pop出來一個任務就fork一個子進程,由子進程完成具體的任務邏輯。

具體代碼如下:

/** * 啟動守護進程 */public function runAction() {    Tools::log_message('ERROR', 'daemon/run' . ' | action: restart', 'daemon-');    while (true) {        $this->fork_process();    }    exit;}/** * 建立子進程 */private function fork_process() {    $ppid = getmypid();    $pid = pcntl_fork();    if ($pid == 0) {//子進程        $pid = posix_getpid();        //echo "* Process {$pid} was created \n\n";        $this->mq_process();        exit;    } else {//主進程        $pid = pcntl_wait($status, WUNTRACED); //取得子進程結束狀態        if (pcntl_wifexited($status)) {            //echo "\n\n* Sub process: {$pid} exited with {$status}";            //Tools::log_message('INFO', 'daemon/run succ' . '|status:' . $status . '|pid:' . $ppid . '|childpid:' . $pid );        } else {            Tools::log_message('ERROR', 'daemon/run fail' . '|status:' . $status . '|pid:' . $ppid . '|childpid:' . $pid, 'daemon-');        }    }}/** * 業務任務隊列處理 */private function mq_process() {    $data_pop = $this->masterRedis->rPop($this->redis_list_key);    $data = json_decode($data_pop, 1);    if (!$data) {        return FALSE;    }    $worker = '_task_' . $data['worker'];    $class_name = isset($data['class']) ? $data['class'] : 'TaskproModel';    $params = $data['params'];    $class = new $class_name();    $class->$worker($params);    return TRUE;}

這是一個簡單的任務處理系統。

通過這個任務系統協助我們實現了非同步,到目前為止已經穩定運行了將近一年。

但很可惜,它是一個單進程的系統。它是一直在不斷的fork,如果有任務就處理,沒有任務就跳過。

這樣很穩定。

但問題有兩個:一是不斷地fork、pop會浪費伺服器資源,二是不支援並發!

第一個問題還好,但第二個問題就很嚴重。

當主系統 同時 拋過來大量的任務時,任務的處理時間就會無限的拉長。

新的設計

為瞭解決並發的問題,我們計劃做一個更加高效強壯的隊裡處理系統。

因為在PHP7之前不支援多線程,所以我們採用多進程。

從網上找了不少資料,大多所謂的多進程都是N個進程同時在後台運行。

顯然這是不合適的。

我的預想是:每pop出一個任務就fork一個任務,任務執行完成後子進程結束。

遇到的問題

1、如何控制最大進程數

這個問題很簡單,那就是每fork一個子進程就自增一次。而當子進程執行完成就自減一次。

自增沒有問題,我們就在主進程中操作就完了。那麼該如何自減呢?

可能你會說,當然是在子進程中啊。但這裡你需要注意:當fork的時候是從主進程複製了一份資源給子進程,這就意味著你無法在子進程中操作主進程中的計數器!

所以,這裡就需要瞭解一個知識點:訊號。

具體的可以自行Google,這裡直接看代碼。

// install signal handler for dead kidspcntl_signal(SIGCHLD, array($this, "sig_handler"));

這就安裝了一個訊號處理器。當然還缺少一點。

declare(ticks = 1);

declare是一個控制結構語句,具體的用法也請去Google。

這句代碼的意思就是每執行一條低級語句就調用一次訊號處理器。

這樣,每當子進程結束的時候就會調用訊號處理器,我們就可以在訊號處理器中進行自減。

2、如何解決進程殘留

在多進程開發中,如果處理不當就會導致進程殘留。

為瞭解決進程殘留,必須得將子進程回收。

那麼如何對子進程進行回收就是一個技術點了。

在pcntl的demo中,包括很多博文中都是說在主進程中回收子進程。

但我們是基於Redis的brpop的,而brpop是阻塞的。

這就導致一個問題:當執行N個任務之後,任務系統閒置時候主進程是阻塞的,而在發生阻塞的時候子進程還在執行,所以就無法完成最後幾個子進程的進程回收。。。

這裡本來一直很糾結,但當我將訊號處理器搞定之後就也很簡單了。

進程回收也放到訊號處理器中去。

新系統的評估

pcntl是一個進程處理的擴充,但很可惜它對多進程的支援非常乏力。

所以這裡採用Swoole擴充中的Process。

具體代碼如下:

declare(ticks = 1);class JobDaemonController extends Yaf_Controller_Abstract{    use Trait_Redis;    private $maxProcesses = 800;    private $child;    private $masterRedis;    private $redis_task_wing = 'task:wing'; //待處理隊列    public function init(){        // install signal handler for dead kids        pcntl_signal(SIGCHLD, array($this, "sig_handler"));        set_time_limit(0);        ini_set('default_socket_timeout', -1); //隊列處理不逾時,解決redis報錯:read error on connection    }    private function redis_client(){        $rds = new Redis();        $rds->connect('redis.master.host',6379);        return $rds;    }    public function process(swoole_process $worker){// 第一個處理        $GLOBALS['worker'] = $worker;        swoole_event_add($worker->pipe, function($pipe) {            $worker = $GLOBALS['worker'];            $recv = $worker->read();            //send data to master            sleep(rand(1, 3));            echo "From Master: $recv\n";            $worker->exit(0);        });        exit;    }    public function testAction(){        for ($i = 0; $i < 10000; $i++){            $data = [                'abc' => $i,                'timestamp' => time().rand(100,999)            ];            $this->masterRedis->lpush($this->redis_task_wing, json_encode($data));        }        exit;    }    public function runAction(){        while (1){//            echo "\t now we de have $this->child child processes\n";            if ($this->child < $this->maxProcesses){                $rds = $this->redis_client();                $data_pop = $rds->brpop($this->redis_task_wing, 3);//無任務時,阻塞等待                if (!$data_pop){                    continue;                }                echo "\t Starting new child | now we de have $this->child child processes\n";                $this->child++;                $process = new swoole_process([$this, 'process']);                $process->write(json_encode($data_pop));                $pid = $process->start();            }        }    }    private function sig_handler($signo) {//        echo "Recive: $signo \r\n";        switch ($signo) {            case SIGCHLD:                while($ret = swoole_process::wait(false)) {//                    echo "PID={$ret['pid']}\n";                    $this->child--;                }        }    }}

最終,經過測試,單核1G的伺服器執行1到3秒的任務可以做到800的並發。

ps:歡迎各位大神與我交流,不知能否做到更好~

我是閆大伯,一隻奮戰了兩個周末的野生程式猿

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.