iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
Modern Web

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

第六章、Anser-Service:並行處理連線請求 - PHP 微服務入門與開發

  • 分享至 

  • xImage
  •  

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

在開始本章前我們得先調整一下範例微服務的組態設定檔案,請先定位到 Production Service 目錄下,你應該會看到一個名為 .rr.yml 的檔案,將他打開後關注第 11 行的區塊:

http:
  address: "0.0.0.0:8080"
  static:
    dir: "/app/public"
    forbid: [".htaccess", ".php"]
  pool:
    num_workers: 1
    # max_jobs: 64
    # debug: true

第 17 行顯示出目前伺服器僅有一個 worker 處理 HTTP 連線,這意味著在同一個時間內這個 Service 僅能處理一個連線請求。在此筆者建議你將 num_workers 設定為 CPU 核心數的兩倍,這會使往後的開發更加順暢。

更新完 .rr.yml 後,你需要下指令重新啟動伺服器。你可以使用 docker compose restart 或者是先 docker compose downdocker compose up -d

阻塞式連線處理

API 組合(Composition)模式

如同第二章提到的,開發人員可以透過 API 組合模式實現在單一請求中與多個端點(或資源)進行查詢。舉個例子,我們通常會期望傳入複數個商品 ID ,並在一個請求中查詢這些商品 ID 的詳細資訊,並一次回傳。

我們可能會寫出以下程式碼:

<?php

require_once './init.php';

use SDPMlab\Anser\Service\Action;
use Psr\Http\Message\ResponseInterface;

header("Content-Type: application/json");

$productIds = $_GET['products'] ?? [];

if (count($productIds) == 0) {
    echo json_encode([
        "data" => [],
        "message" => "No products, please add products id in query string. like: ?products[]=1&products[]=2"
    ]) . PHP_EOL;
    http_response_code(200);
    exit;
}

$data = [];

foreach ($productIds as $productId) {
    $action = (new Action(
        serviceName: "ProductionService",
        method: "GET",
        path: "/api/v1/products/{$productId}"
    ))->doneHandler(static function(
        ResponseInterface $response,
        Action $runtimeAction
    ) {
        $body = $response->getBody()->getContents();
        $data = json_decode($body, true);
        $runtimeAction->setMeaningData($data['data']);
    });
    $data[$productId] = $action->do()->getMeaningData();
}

echo json_encode([
    "data" => $data
]) . PHP_EOL;
  1. 使用 $_GET['products'] 獲取 URL 查詢字串中的 products 參數。例如:?products[]=1&products[]=2 會得到一個包含 1 和 2 的商品 ID 陣列。
  2. 對每個商品 ID 進行處理:
    • 對每個 productId,創建一個新的 Action 類別實體,指定要訪問的 API 服務名稱、方法和路徑。
    • 使用 doneHandler 設定一個匿名函式,將響應內容解析為 JSON 並存儲結果數據。
    • 最後,執行 do() 方法發送 API 請求,並將結果數據存儲在 $data 陣列中,使用 productId 作為 Key。
  3. 最後,將 $data 陣列編碼為 JSON 並返回給 Client。

讓我們試試看使用 Postman 實際執行這段程式碼:

{{main_service}}/multi_action.php?products[]=1&products[]=5&products[]=42&products[]=55

看起來還不賴,我們僅用了 335ms 就處理完這個連線,那再試試看把 4 個商品 ID 加碼為 8 個商品 ID:

{{main_service}}/multi_action.php?products[]=1&products[]=5&products[]=42&products[]=55&products[]=2&products[]=77&products[]=48&products[]=53

發現問題了嗎?隨著商品數量的提升,伺服器的處理時間也呈現線性增長。

這是因為我們的程式碼使用了阻塞式連線處理方法。在這種情境中,每當我們對某個商品 ID 發出 API 請求時,程式碼會停止執行並等待直到該請求完全返回響應。換句話說,這段程式碼是逐一處理每個 API 請求的,而不是並行地同時處理多個請求。

這意味著,當我們從 4 個商品 ID 增加到 8 個商品 ID 時,所需的時間近乎是原來的兩倍,因為每個請求都在等待前一個請求完成後才開始。這種阻塞式的行為在面對大量的資料或高流量時,會使系統效能大大降低。

要解決這個問題,我們需要使用非同步的方法來處理 API 請求。這樣,我們可以同時發出多個請求,並在所有請求都返回響應後再繼續處理。這種方法可以在面對大量的請求時,顯著減少總的等待時間。

並行連線處理

Anser 提供了 ConcurrentAction 類別,這個類別對並行連線 (Concurrent Connection)所需的方法進行了封裝,開發人員可以透過實體化 ConcurrentAction 類別後,將所有需要同時發出連線請求的 Action 加入至這個類別中。

來讓我們舉個例子,我們改寫一下上一個範例:

<?php

require_once './init.php';

use SDPMlab\Anser\Service\Action;
use Psr\Http\Message\ResponseInterface;
use SDPMlab\Anser\Service\ConcurrentAction;

header("Content-Type: application/json");

$productIds = $_GET['products'] ?? [];

if (count($productIds) == 0) {
    echo json_encode([
        "data" => [],
        "message" => "No products, please add products id in query string. like: ?products[]=1&products[]=2"
    ]) . PHP_EOL;
    http_response_code(200);
    exit;
}

$actions = [];
foreach ($productIds as $productId) {
    $actions[$productId] = (new Action(
        serviceName: "ProductionService",
        method: "GET",
        path: "/api/v1/products/{$productId}"
    ))->doneHandler(static function(
        ResponseInterface $response,
        Action $runtimeAction
    ) {
        $body = $response->getBody()->getContents();
        $data = json_decode($body, true);
        $runtimeAction->setMeaningData($data['data']);
    });
}
$concurrentAction = new ConcurrentAction();
$concurrentAction->setActions(actionList: $actions);

//上述程式碼等同於
// $concurrentAction = new ConcurrentAction();
// foreach ($productIds as $productId) {
//     $action = (new Action(
//         serviceName: "ProductionService",
//         method: "GET",
//         path: "/api/v1/products/{$productId}"
//     ))->doneHandler(static function(
//         ResponseInterface $response,
//         Action $runtimeAction
//     ) {
//         $body = $response->getBody()->getContents();
//         $data = json_decode($body, true);
//         $runtimeAction->setMeaningData($data['data']);
//     });
//     $concurrentAction->addAction(alias: $productId, action: $action);
// }

$concurrentAction->send();
$data = $concurrentAction->getActionsMeaningData();

echo json_encode([
    "data" => $data
]) . PHP_EOL;

在這個範例中,我們透過 ConcurrentAction 類別,優化了與多個 API 端點的同時查詢,以下進行詳細的說明:

  1. 首先建立一個名為 $actions 的空陣列,隨後遍歷每個商品 ID 並為每個 ID 建立一個新的 Action 類別實體。這些 Action 實體將被保存在 $actions 陣列中,用商品 ID 作為 Key。
  2. 實體化 ConcurrentAction 類別。此類別是為了同時發起多個連線請求而設計的。接著,我們使用 setActions 方法將整個 $actions 陣列加入至 ConcurrentAction 實體。此外,我們也示範了如何透過 addAction 方法單獨加入每一個 Action 實體。
  3. $concurrentAction->send(); 負責發起所有的連線請求。這些請求是同時進行的,不必等待前一個請求完成。
  4. 經由 $concurrentAction->getActionsMeaningData(); 取得所有的請求結果。

讓我們來實際執行看看著程式碼,首先從 4 個商品 ID開始:

{{main_service}}/concurrent_action.php?products[]=1&products[]=5&products[]=42&products[]=55

感覺還不錯吧,我們僅僅使用了 164ms 就處理完了,接將商品數量加大到 8 個試試:

{{main_service}}/concurrent_action.php?products[]=1&products[]=5&products[]=42&products[]=55&products[]=2&products[]=77&products[]=48&products[]=53

有發現不同嗎?這次我們僅用了 182ms 就處理完了 8 個商品的查詢請求,在幾乎沒有顯著增長請求處理時間的同時,我們還能夠取得更多的資料。

這是因為當我們使用 ConcurrentAction 進行並行請求時,每一個請求都是獨立而且幾乎同時發出的。而傳統的連續請求方式,每一個請求都必須等待前一個請求完成之後才能開始,這樣的方式會隨著請求的增加而使得總響應時間線性增長。

這裡的關鍵點在於「並行」。並行請求不意味著所有請求都會在同一個時刻完成,但它確保所有請求都幾乎在同一時間開始。所以,即使某些請求需要較長的時間,它也不會阻止或延遲其他請求的處理,也正式因為如此,我們才可以在相對短的時間內取得所有的回應。

另一個要考慮的因素是伺服器和網路的負載。當我們發出多個並行請求時,伺服器必須能夠同時處理所有這些請求,且網路必須能夠處理相應的流量。在我們的範例中,由於伺服器能夠快速地回應我們的請求,並且沒有其他瓶頸,所以我們看到的響應時間僅略有增加。這也是為什麼在文章的一開始,我們得先調整 Production Service 的伺服器組態設定檔案,使其能夠同時處理更多任務。

透過 ConcurrentAction 類別,開發人員不僅可以提高應用程式的響應速度,還可以更好地利用伺服器資源,進而提供更好的用戶體驗。

結語

透過這篇文章,讀者可以深入瞭解阻塞式連線和非同步的並行連線之間的差異,並學會如何透過 Anser 來優化 API 請求的處理方式。在現代的微服務架構中,面對高流量、大數據和多個連線請求是很常見的。這就需要我們的應用程式能夠快速、有效地處理這些請求,而不是被單一的請求所阻塞。

從範例中我們可以看出,儘管原始的阻塞式連線方式在小量的請求中能夠正常工作,但當請求的數量增加時,其效能會呈現出線性的下降。而透過並行連線的方式,即使請求的數量倍增,所需的時間並沒有明顯的增加,顯示出並行連線處理方式對於高流量的應用程式更加合適。


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

尚未有邦友留言

立即登入留言