Guzzleを用いたHTTP通信のパフォーマンス分析:百分位数統計によるボトルネックの特定

APIのパフォーマンス監視において、単純な平均応答時間(Average Response Time)だけに依存することは危険です。多くのユーザーで良好な数値が出ていても、ごく一部のユーザーのみが極端な遅延を経験している場合(ロングテール問題)、平均値ではこれを検知できません。Guzzle PHP HTTPクライアントを使用して、リクエストの詳細な統計情報を収集し、パーセンタイル(P50, P95, P99)を用いた分析手法により、隠れたボトルネックを特定する方法を解説します。

TransferStatsによるリクエスト計測の仕組み

GuzzleはTransferStatsクラスを通じて、リクエストに関する詳細なメトリクスを提供します。これを活用するには、クライアントインスタンス生成時にon_statsオプションを指定し、コールバック関数内でデータを収集します。

$client = new GuzzleHttp\Client();
$responseMetrics = [];

$client->request('GET', 'https://api.example.com/v1/data', [
    'on_stats' => function (GuzzleHttp\TransferStats $stats) use (&$responseMetrics) {
        $responseMetrics[] = [
            'duration' => $stats->getTransferTime(),
            'uri' => (string) $stats->getEffectiveUri(),
            'handler_data' => $stats->getHandlerStats(),
        ];
    }
]);

このコールバックは転送完了時に実行され、トータルの所要時間だけでなく、DNS解決やTCP接続などの内部的な処理時間も取得可能です。

パーセンタイル計算の実装

収集した生データを基に、P50(中央値)、P95、P99の値を計算するロジックを実装します。これにより、大多数のユーザー体験と、最悪のケースにおけるパフォーマンスを数値化できます。

function computeLatencyDistribution(array $durations, array $targets = [50, 95, 99]): array
{
    sort($durations);
    $count = count($durations);
    $distribution = [];

    foreach ($targets as $target) {
        // データ配列のインデックスを計算(1始まりを想定して調整)
        $index = (int) ceil(($target / 100) * $count) - 1;
        
        if (isset($durations[$index])) {
            // ミリ秒単位に変換して丸め処理
            $distribution['p' . $target] = round($durations[$index] * 1000, 2);
        }
    }

    return $distribution;
}

// 使用例
$results = computeLatencyDistribution($responseMetrics);
// 出力例: ['p50' => 120.00, 'p95' => 340.50, 'p99' => 890.10]

HandlerStatsを用いたボトルネックの特定

単なる合計時間だけでなく、リクエストのどのフェーズで時間がかかっているかを分析することで、適切な対策を講じることができます。getHandlerStats()メソッド(cURLハンドラー使用時)からは以下のような詳細が得られます。

  • namelookup_time: DNS解決にかかった時間
  • connect_time: TCP接続の確立にかかった時間
  • starttransfer_time: 最初のバイトが受信されるまでの時間
  • total_time: リクエスト全体の時間

例えば、namelookup_timeが全体の20%以上を占める場合、DNSサーバーの応答遅延やキャッシュの未使用が疑われます。また、connect_timeが長い場合は、TCP接続の再確立コストが影響しています。

接続プールと持続的接続(Keep-Alive)の活用

TCP接続のオーバーヘッドを削減するには、cURLのオプションを利用して接続を再利用する設定が有効です。

$client = new GuzzleHttp\Client([
    'curl' => [
        // 接続の再利用を許可(デフォルトtrueだが明示)
        CURLOPT_FORBID_REUSE => false,
        // 接続プールのキャッシュ数
        CURLOPT_MAXCONNECTS => 50,
    ],
]);

統合パフォーマンス解析クラス

以上の要素を統合し、実際のアプリケーションで利用可能な解析クラスの構築例です。

class ApiLatencyAnalyzer
{
    private array $samples = [];

    public function record(GuzzleHttp\TransferStats $stats): void
    {
        $this->samples[] = [
            'total_time' => $stats->getTransferTime(),
            'host' => $stats->getEffectiveUri()->getHost(),
            'dns_time' => $stats->getHandlerStat('namelookup_time') ?? 0,
            'connect_time' => $stats->getHandlerStat('connect_time') ?? 0,
        ];
    }

    public function getReport(): array
    {
        if (empty($this->samples)) {
            return [];
        }

        $durations = array_column($this->samples, 'total_time');
        $metrics = $this->computeLatencyDistribution($durations);

        return [
            'request_count' => count($this->samples),
            'percentiles' => $metrics,
            'max_latency_ms' => max($durations) * 1000,
            'min_latency_ms' => min($durations) * 1000,
            'avg_latency_ms' => array_sum($durations) / count($durations) * 1000,
        ];
    }

    private function computeLatencyDistribution(array $durations, array $targets = [50, 95, 99]): array
    {
        sort($durations);
        $count = count($durations);
        $result = [];

        foreach ($targets as $target) {
            $index = (int) ceil(($target / 100) * $count) - 1;
            if (isset($durations[$index])) {
                $result["p{$target}"] = round($durations[$index] * 1000, 2);
            }
        }
        return $result;
    }
}

タグ: PHP Guzzle HTTP Performance Percentile

7月3日 17:37 投稿