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;
}
}