async-helper,一個 PHP 的非同步程式助手

NjuTsiang發表於2019-02-16

async-helper

簡介

PHP 的非同步程式助手,藉助於 AMQP 實現非同步執行 PHP 的方法,將一些很耗時、追求高可用、需要重試機制的操作放到非同步程式中去執行,將你的 HTTP 服務從繁重的業務邏輯中解脫出來。以一個較低的成本將傳統 PHP 業務邏輯轉換成非阻塞、高可用、可擴充套件的非同步模式。

依賴

  • php 5.6+
  • ext-bcmath
  • ext-amqp 1.9.1+
  • ext-memcached 3.0.3+

安裝

通過 composer 安裝

composer require l669/async-helper

或直接下載專案原始碼

wget https://github.com/l669306630/async-helper/archive/master.zip

使用範例

業務邏輯:這裡定義了很多等待被呼叫的類和方法,在你的專案中這可能是資料模型、或是一個傳送郵件的類。

<?php
class SendMailHelper 
{
    /**
     * @param array $mail
     * @throws Exception
     */
    public static function request($mail)
    {
        // 在這裡傳送郵件,或是通過呼叫第三方提供的服務傳送郵件
        // 傳送失敗的時候你丟擲了異常,希望被程式捕獲,並按設定的規則進行重試
    }    
}

生產者:通常是 HTTP 服務,傳統的 PHP 專案或是一個命令列程式,接收到某個請求或指令後進行一系列的操作。

<?php 
use l669AsyncHelper;
class UserController
{
    public function register()
    {
        // 假設這是一個使用者註冊的請求,使用者提交了姓名、郵箱、驗證碼
        // 第一步、校驗使用者資訊
        // 第二步、例項化非同步助手,這時候會連線 AMQP
        $async_helper = new AsyncHelper([
            `host` => `127.0.0.1`,
            `port` => `5672`,
            `user` => `root`,
            `pass` => `123456`,
            `vhost` => `/`
        ]);
        // 第三步、儲存使用者資訊到資料庫
        $mail = [
            `from` => `service@yourdomain.com`, 
            `to` => `username@163.com`, 
            `subject` => `恭喜你註冊成功`,
            `body` => `請點選郵件中的連結完成驗證....`
        ];
        // 第四步、通過非同步助手傳送郵件
        $async_helper->run(`\SendMailHelper`, `request`, [$mail]);
        
        // 這是同步的模式去傳送郵件,如果郵件服務響應遲緩或異常,就會直接影響該請求的響應時間,甚至丟失這封重要郵件
        // SendMailHelper::request($mail);
    }
}

消費者:PHP 的非同步程式,監聽訊息佇列,執行你指定的方法。並且該消費者程式是可擴充套件的高可用的服務,這一切都得益於 AMQP,這是系統解耦、佈局微服務的最佳方案。

consume.php

<?php
require_once(`vendor/autoload.php`);
require_once(`SendMailHelper.php`);

use l669AsyncHelper;
use l669CacheHelper;

$cache_helper = new CacheHelper(`127.0.0.1`, 11211);
while(true){
    try{
        $async_helper = new AsyncHelper([
            `host` => `127.0.0.1`,
            `port` => `5672`,
            `user` => `root`,
            `pass` => `123456`,
            `vhost` => `/`,
            `cacheHelper` => $cache_helper
        ]);
        $async_helper->consume();
    }catch(Exception $e){
        // 可以在這裡記錄一些日誌
        sleep(2);
    }
}
# 在命令列下啟動消費者程式,推薦使用 supervisor 來管理程式
php consume.php

支援事務:需要一次提交執行多個非同步方法,事務可以確保完成性。

// 接著上面的示例來說,這裡省略了一些重複的程式碼,下同
$async_helper->beginTransaction();
try{
    $async_helper->run(`\SendMailHelper`, `request`, [$mail1]);
    $async_helper->run(`\SendMailHelper`, `request`, [$mail2]);
    $async_helper->run(`\SendMailHelper`, `request`, [$mail3]);
    $async_helper->commit();
}catch(Exception $e){
    $async_helper->rollback();
}

阻塞式重試:當非同步程式執行一個方法,方法內部丟擲異常時進行重試,一些必須遵循執行順序的業務就要採用阻塞式的重試,通過指定重試最大阻塞時長來控制。

use l669CacheHelper;
use l669AsyncHelper;
$async_helper = new AsyncHelper([
    `host` => `127.0.0.1`,
    `port` => `5672`,
    `user` => `root`,
    `pass` => `123456`,
    `vhost` => `/`,
    `cacheHelper` => new CacheHelper(`127.0.0.1`, 11211),
    `retryMode` => AsyncHelper::RETRY_MODE_REJECT,  // 阻塞式重試
    `maxDuration` => 600                            // 最長重試 10 分鐘
]);
$send_mail_helper = new SendMailHelper();
$mail = new stdClass();
$mail->from = `service@yourdomain.com`;
$mail->to = `username@163.com`;
$mail->subject = `恭喜你註冊成功`;
$mail->body = `請點選郵件中的連結完成驗證....`;
$async_helper->run($send_mail_helper, `request`, [$mail]);

// 如果方法中需要丟擲異常來結束程式,又不希望被非同步程式重試,可以丟擲以下幾種錯誤碼,程式捕獲到這些異常後會放棄重試:
// l669AsyncException::PARAMS_ERROR
// l669AsyncException::METHOD_DOES_NOT_EXIST
// l669AsyncException::KNOWN_ERROR

非阻塞式重試:當非同步執行的方法內部丟擲異常,async-helper 會將該方法重新放進佇列的尾部,先執行新進入佇列的方法,回頭再重試剛才執行失敗的方法,通過指定最大重試次數來控制。

use l669CacheHelper;
use l669AsyncHelper;
$async_helper = new AsyncHelper([
    `host` => `127.0.0.1`,
    `port` => `5672`,
    `user` => `root`,
    `pass` => `123456`,
    `vhost` => `new`,
    `cacheHelper` => new CacheHelper(`127.0.0.1`, 11211),
    `queueName` => `emails.vip`,                    // 給付費的大爺走 VIP 佇列
    `retryMode` => AsyncHelper::RETRY_MODE_TTL,     // 非阻塞式重試
    `maxRetries` => 10                              // 最多重試 10 次
]);
$mail = new stdClass();
$mail->from = `service@yourdomain.com`;
$mail->to = `username@163.com`;
$mail->subject = `恭喜你註冊成功`;
$mail->body = `請點選郵件中的連結完成驗證....`;
$async_helper->run(`\SendMailHelper`, `request`, [$mail]);

應用和解惑

  • 我們採用的是開源的 RabbitMQ 來為我們提供的 AMQP 服務。
  • 你的專案部署在擁有很多伺服器節點的叢集上,每個節點的程式都需要寫日誌檔案,現在的問題就是要收集所有節點上面的日誌到一個地方,方便我們及時發現問題或是做一些統計。所有節點都可以使用 async-helper 非同步呼叫一個寫日誌的方法,而執行這個寫日誌的方法的程式只需要在一臺機器上啟動就可以了,這樣所有節點的日誌就都實時掌握在手裡了。
  • 做過微信公眾號開發的都知道,騰訊微信可以將使用者的訊息推送到我們的伺服器,如果我們在 5s 內未及時響應,騰訊微信會重試 3 次,其實這就是訊息佇列的應用,使用 async-helper 可以輕鬆的做和這一樣的事情。
  • 得益於 RabbitMQ,你可以輕鬆的橫向擴充套件你的消費者程式的能力,因為 RabbitMQ 天生就支援叢集部署,你可以輕鬆的啟動多個消費者程式,或是將消費者程式分佈到多臺機器上。
  • 如果 RabbitMQ 服務不可用怎麼辦呢?部署 RabbitMQ 高可用服務是容易的,對外提供單一 IP,這個 IP 是個負載均衡,背後是 RabbitMQ 叢集,負載均衡承擔對後端叢集節點的健康檢查。
  • async-helper 能否承受高併發請求?async-helper 生產者使用的是短連線,也就說在你的 HTTP 還沒有響應瀏覽器的時候 async-helper 就已經結束了工作,你連線 RabbitMQ 的時間是百分之百小於 HTTP 請求的時間的,換言之,只要 RabbitMQ 承受併發的能力超過你的 HTTP 服務的承受併發的能力,RabbitMQ 就永遠不會崩,通過橫向擴充套件 RabbitMQ 很容易做到的。

和傳統 PHP 相比

  • 對任何 PHP 方法通過反射進行非同步執行;
  • 高可用,執行方法進入訊息佇列,可持久化,即使伺服器當機,執行任務也不丟失;
  • 高可用,對異常可以進行不限次數和時間的重試,重試次數和時間可配置;
  • 支援對多個非同步方法包含在事務中執行,支援回滾事務;
  • 方法的引數型別支援除資源型別(resource)和回撥函式(callable)外的任意型別的引數;
  • 得益於 AMQP,非同步方法可以承受高併發、高負載,支援叢集部署、橫向擴充套件;
  • 低延時,實測延時時間 0.016 ~ 0.021s;
  • 適用於:日常資料庫操作、日誌收集、金融交易、訊息推送、傳送郵件和簡訊、資料匯入匯出、計算大量資料生成報表;

附錄

相關文章