iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0
Modern Web

30 天上手! PHP 微服務入門與開發系列 第 8

第八章、Anser-Service:服務重試與過濾器 - PHP 微服務入門與開發

  • 分享至 

  • xImage
  •  

在這個章節,我們會使用到 User Service 與 Main App,請參考第四章節所提到的內容建立你的本地開發環境。

延續前幾章,你可以將專案內的 init.php 加入下述程式碼,將 MainApp 也宣告進來:

//省略其他服務組態設定
ServiceList::addLocalService(
    name: "MainApp",
    address: "127.0.0.1",
    port: 80,
    isHttps: false
);

服務重試

當一個服務呼叫另一個服務時,有需多可能的原因會導致呼叫失敗,例如:網路延遲、服務超時,或大量的資源競爭等。此時,重試(Retry)機制就顯得尤為重要。若我們僅因為一次的請求失敗,就放棄了整個請求的機會,是一件很可惜的事情。在很多情況下,嘗試重新發送該請求也是一種開發成本較低的方案:

  • 服務失敗可能是短暫的,或許是短暫的網路不穩或資源偶遇瓶頸所導致。透過自動重試,系統可以在不需要外部干預的情況下,解決這些短暫的問題。
  • 微服務架構中的伺服器實體常常伴隨著動態的擴充或升級,在這種環境中,一個微服務的實體可能會突然消失或重新啟動,這種無法提供服務的間隔可能僅僅只是幾毫秒到幾秒間的空窗期。重試機制能夠確保在這些情況下,重新請求伺服器並獲取正確的執行結果。

Action 也支援 Retry 的設定,首先請在你的開發環境中建立一個名為 errorendpoint.php 的檔案並鍵入以下內容:

<?php
$errorLimit = $_GET["error_limit"] ?? 0;

$filePath = __DIR__ . "/error_count.txt";
if (!file_exists($filePath)) {
    file_put_contents($filePath, '0');
}
$count = (int)file_get_contents($filePath);
$message = "";

if ($count >= $errorLimit) {
    http_response_code(200);
    $message = "OK";
    $count = 0;
} else {
    http_response_code(500);
    $message = "ERROR";
    $count++;
}

file_put_contents($filePath, $count);
file_put_contents(__DIR__ . "/error_log.txt", date("Y-m-d H:i:s") . " " . $count . " " . http_response_code() . PHP_EOL, FILE_APPEND);

echo $message . PHP_EOL;

這是一個模擬 API 失敗與成功的端點,當你呼叫這個端點時,會隨著你所傳入的 error_limit 決定在第幾次呼叫時回傳 HTTP 200 的成功訊息。而上述測試用端點會將每次的請求資訊寫入一個 error_log.txt 檔案中。

接著,我們來建立本次測試的主要檔案:

<?php

require_once './init.php';

use SDPMlab\Anser\Service\Action;

$errorLimit = $_GET["error_limit"] ?? 0;

$action = (new Action(
    serviceName: "MainApp",
    method: "GET",
    path: "/errorendpoint.php"
))->setOptions([
    "query" => [
        "error_limit" => $errorLimit
    ]
])->setRetry(retry: (int)$errorLimit, retryDelay: 1.0);

$token = $action->do();

header("Content-Type: application/json");
echo json_encode([
    "status" => "OK"
]) . PHP_EOL;

這部份的程式碼將會呼叫剛才我們建立的測試用端點,並包含以下特性

  1. 設定 Action 物件的重試次數,而 setRetry() 允許你傳入下列參數:
    • int $retry:重試最大次數,本次設定為 error_limit
    • float $retryDelay:重試間隔秒數,本次設定為每次重試間隔 1 秒
  2. 執行該 Action,並根據其返回結果,將自動執行重試。

打開你的 Postman 試試看執行這個 PHP 檔案,我們將 Query Params 的 error_limit 設定為 3

你會發現,響應的處理時間耗時了 3 秒鐘,此時你的開發環境應該會多出一個檔案叫做 error_log.txt

沒錯,正如我們的預期,每間隔一秒鐘會重複呼叫相同的服務端點,直到到達 error_limit 的次數,再以 200 的狀態碼進行最後回傳。

如同上述的例子所示,透過簡單的設定,我們可以確保在面對臨時的服務中斷或其他非預期的錯誤時,請求仍然有機會被正確地處理。

過濾器

在 Anser 的微服務的架構中,每一個服務呼叫或資料交換都被視為一次「行動(Action)」。但這些動作,往往不只是單純的傳送請求和接收回應,還可能涉及一系列的前置和後置工作。這就是「過濾器」或「Filter」發揮其作用的地方。

首先,我們先從一個常見的場景著手:使用者驗證。在許多服務中,我們經常需要驗證使用者的身份,以確定他們是否有權限進行某些操作。而這個驗證過程,通常是透過 API key 或 token 來進行。

首先,請在你的開發環境中建立一個檔案 UserAuthFilters.php 並貼上以下內容:

<?php
namespace Filters;

require_once './vendor/autoload.php';

use SDPMlab\Anser\Service\FilterInterface;
use SDPMlab\Anser\Service\ActionInterface;

class UserAuthFilters implements FilterInterface
{
    public function beforeCallService(ActionInterface $action)
    {
        //get in request Header Authorization
        $apiKey = $_SERVER['HTTP_AUTHORIZATION'] ?? "";
        if (empty($apiKey)) {
            http_response_code(401);
            header("Content-Type: application/json");
            echo json_encode([
                "code" => 401,
                "status" => "error",
                "message" => "Unauthorized, The authorization header is missing."
            ]);
            exit;
        }
        
        $headersOptions = $action->getOption('headers') ?? [];
        $headersOptions["Authorization"] = $apiKey;
        $action->addOption("headers", $headersOptions);
    }

    public function afterCallService(ActionInterface $action)
    {
        //do nothing
    }
}

UserAuthFilters 實作了 SDPMlab\Anser\Service\FilterInterface ,在 Action 執行前你可以透過 beforeCallService() 取得 Action 的實體;之後,在 HTTP 200 響應且 doneHandler() 執行前,你可以透過 afterCallService() 處理尚未經過任何修改過的 Action。

而上述過濾器專門用於驗證使用者的 API key,當 Action 被執行前,它會首先檢查請求標頭中是否包含有效的 API key,如果缺少或無效,它將直接回傳一個錯誤訊息,而不進行後續的服務呼叫。

若本次的請求符合我們的需要,那麼就會透過即將被執行的 Action 實體,取出裡面的 Request Option 設定符合 API 需要的正確標頭,再將他設定回 Action 實體中。

接著,我們再建立一個新的檔案:

<?php

require_once './init.php';
require_once './UserAuthFilters.php';

use SDPMlab\Anser\Service\Action;
use SDPMlab\Anser\Exception\ActionException;
use Psr\Http\Message\ResponseInterface;
use Filters\UserAuthFilters;

$action = (new Action(
    serviceName: "UserService",
    method: "GET",
    path: "/api/v1/user"
))
->setFilter(className: UserAuthFilters::class)
->doneHandler(static function(
    ResponseInterface $response,
    Action $runtimeAction
){
    $body = $response->getBody()->getContents();
    $data = json_decode($body, true);
    $runtimeAction->setMeaningData([
        "code" => 200,
        "msg" => "success",
        "data" => $data['data']
    ]);
})->failHandler(function (
    ActionException $e
) {
    if($e->isClientError()){
        $msg = $e->getAction()->getResponse()->getBody()->getContents();
        $error = json_decode($msg, true)['error'];
        $e->getAction()->setMeaningData([
            "code" => $e->getAction()->getResponse()->getStatusCode(),
            "msg" => $error
        ]);
    }else if ($e->isServerError()){
        $e->getAction()->setMeaningData([
            "code" => 500,
            "msg" => "server error"
        ]);
    }else if($e->isConnectError()){
        $e->getAction()->setMeaningData([
            "code" => 500,
            "msg" => $e->getMessage()
        ]);
    }
});

header("Content-Type: application/json");
if($action->do()->isSuccess()){
    echo json_encode($action->getMeaningData()) . PHP_EOL;
    exit;
}

$error = $action->getMeaningData();
http_response_code($error['code']);
echo json_encode([
    "code" => $error['code'],
    "msg" => $error['msg']
]) . PHP_EOL;
exit;

關注上述程式碼第 15 行的位置,你可以透過 setFilter(string $className) 傳入完整的類別名稱字串(包含 Namespace 的類別名稱),將所需的 Filter 設定進 Action 之中。若你重複呼叫 setFilter(string $className) 就可以將多個 Filter 給設定進 Action ,而這些 Filter 將會依序執行。

當然,你也可以單獨使用 addBeforeFilter(string $className)addAfterFilter(string $className) 單獨將過濾器類別中的前濾器或後濾器單獨設定至 Action 中。

你可以打開 Postman 試試,首先我們先不傳入任何 API Key:

如同我們的預期,再未傳入任何 API Key 的情況下我們成功透過前濾器阻止了 Action 的執行。

接著,我們來試試看傳入 API Key 的結果:

一切都符合我們的預期,API Key 被正確傳遞給 User Service 了!

Filter 機制提供了一個靈活的工具,開發者能夠輕鬆地進行前置和後置的工作,無論是驗證或日誌記錄,透過這種機制,我們可以將業務邏輯和非業務邏輯(例如驗證)區分開來,使代碼更為模組化,並提高其可維護性和可讀性。

結語

本章,我們了解了服務重試和過濾器的概念與應用,Anser Action 在基於 Guzzle7 程式庫下,整合了一些常用的元件與設計模式,希望能滿足開發時所需的模組化與可用性等特性。期待透過這些工具和策略,能夠使開發人員確保服務能穩定運行且能快速應對變化。


上一篇
第七章、Anser-Service:服務溝通的正確與錯誤處理 - PHP 微服務入門與開發
下一篇
第九章、Anser-Service:服務抽象化 - PHP 微服務入門與開發
系列文
30 天上手! PHP 微服務入門與開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言