こんにちは!ホワイトプラスのコアシステム開発グループでエンジニアをやっている古賀です。
以前、PHPでOpenTelemetryを使ってトレースを取得する方法を紹介しました。
今回はメトリクス編ということで、OpenTelemetryでメトリクス(リクエストレイテンシ)を収集し、OpenTelemetry Collectorを介してGoogle Cloudに送信する方法を紹介します。
前提
PHP:8.1
open-telemetry/opentelemetry:1.0.0
メトリクスとは
メトリクスはデータの数値表現で、CPU使用率やリクエストレイテンシなどが挙げられます。メトリクスを取得して監視やアラート、傾向分析に利用します。
メトリクスのデータモデルには複数のタイプがありますが、本稿ではリクエストレイテンシを可視化するためにHistogramを用います。
Histogram
ヒストグラムは、測定範囲全体を一連の間隔 (バケットと呼ばれる) に分割し、各バケットに含まれる測定値の数をカウントすることで集計します。
これだけだとイメージが湧きづらいため、具体的なカウント方法を見ていきます。
あるアプリケーションのリクエストレイテンシは、遅い時で3sくらいかかっているとします。 プロダクトオーナーは、いつ、どれだけのリクエストが、どれくらい時間がかかっているか、を把握したいと考えており、その一歩としてリクエストレイテンシ(以下レイテンシ)を観察します。
0秒から4秒の間で1秒間隔のレイテンシ分布を見たいというプロダクトオーナーの要望を元に、0秒以上1秒未満、1秒以上2秒未満、2秒以上3秒未満、3秒以上4秒未満の4つのバケットを用意します。
例えば、1:00から1:01の間に測定された7個のレイテンシが「0.7秒、0.9秒、1.1秒、1.3秒、1.5秒、2.1秒、3.7秒」だとすると、各バケットに含まれる測定値は次表のようにカウントされます。
1:01以降も測定を続けることで、以下のように1分間隔でバケットカウントが得られます。
上記のデータを元にヒートマップ(Cloud Monitoringの例)で可視化してみます。
X 軸が時間、Y 軸がバケット、色はバケットカウントを表しており、色が明るいほどバケットカウントが多いことを示し、色が暗いほどバケットカウントが少ないことを示しています。
この例では、最小バケットカウント(0)を黒、最大バケットカウント(10)を黄色で表し、赤とオレンジはその中間値を表します。
このように、ヒストグラム形式のデータをヒートマップで可視化することで、傾向を見て取りやすくなります。
OpenTelemetry のインストール
インストール方法は前回の記事と同じため割愛します。
セットアップ
セットアップで必要なことは大きく分けて2つあり、メトリクスに関する設定を担う MeterProvider
の作成と登録です。
MeterProviderの作成
MeterProviderをある程度デフォルトの設定で使う場合は、MeterProviderBuilder
をこんな感じに呼び出すことにより数行で作成できます。
しかし、色々とユーザ側で設定したい場合は以下のようにMeterProviderのコンストラクタを呼び出して作成します。
今回は ExportingReader
・ResourceInfo
・CriteriaViewRegistry
をユーザ側で設定し、それ以外は MeterProviderBuilder::build()で設定しているものを使用します。
<?php function createMeterProvider(): MeterProviderInterface { $reader = new ExportingReader( new MetricExporter( PsrTransportFactory::discover()->create( 'http://otelcol:4318/v1/metrics', ContentTypes::JSON ), Temporality::DELTA, ) ); $resource = ResourceInfo::create( Attributes::create( [ ResourceAttributes::SERVICE_NAME => 'test-application', ResourceAttributes::SERVICE_VERSION => '1.0', 'project_id' => 'sample-project', 'location' => 'asia-northeast1', 'cluster' => (string)random_int(1, 100), 'namespace' => (string)random_int(1, 100), 'job' => random_int(1, 100), 'instance' => random_int(1, 100), ] ) ); $views = (new CriteriaViewRegistry()); $views->register( // 条件文をスキップ new class implements SelectionCriteriaInterface { public function accepts( Instrument $instrument, InstrumentationScopeInterface $instrumentationScope ): bool { return true; } }, // バケット定義 ViewTemplate::create() ->withName('request_latency') ->withAggregation(new ExplicitBucketHistogramAggregation([0, 1, 2, 3, 4])), ); return new MeterProvider( null, $resource, ClockFactory::getDefault(), Attributes::factory(), new InstrumentationScopeFactory(Attributes::factory()), [$reader], $views, new WithSampledTraceExemplarFilter(), new NoopStalenessHandlerFactory(), ); }
ExportingReader
- Exporterに送信するデータを渡すクラス。
- OpenTelemetry CollectorにJSON形式で送信するためのMetricExporterを使用しています。
ResourceInfo
- Resourceを表現するクラス。Resourceはテレメトリデータを生成するものに関する情報をキーバリュー形式で格納したコレクションです。
- 今回テレメトリデータを生成するのはPHPアプリケーションになるため、アプリケーションに関する情報としてサービス名(
test-application
)やサービスのバージョン(1.0
)を設定しています。 - また、メトリクスをGoogle Cloud Managed Prometheusに送信する際、データの競合が発生して送信エラーになるのを防ぐため、{project_id, location, cluster, namespace, job, instance}の組み合わせで一意になるように
random_int
を使って値を指定しています。
CriteriaViewRegistry
- メトリクスをカスタマイズする機能にViewというものがあります。
CriteriaViewRegistry
は条件とViewを受け取り、条件に合致する場合にViewを使ってメトリクスをカスタマイズします。 - ここでは、条件文をスキップし(常に
true
を返す)、ヒストグラムのバケットを定義するために[0, 1, 2, 3, 4]
を設定しています。
- メトリクスをカスタマイズする機能にViewというものがあります。
MeterProviderの登録
次に、先ほど作成したMeterProviderを登録します。
Sdk::builder()
から返される SdkBuilder
は、MeterProviderを含む各種Providerを登録します。
登録すると、任意の場所でMeterProviderを取得できます。
<?php $meterProvider = createMeterProvider(); Sdk::builder() ->setMeterProvider($meterProvider) ->setAutoShutdown(true) ->buildAndRegisterGlobal();
setAutoShutdown(true)
を呼ぶことでプログラムの終了時にProviderのshutdown
メソッドを実行することができます。
MeterProviderのshutdownメソッドでは終了処理を行なっており、収集したメトリクスを送信しています。
つまり、プログラムが終了するとメトリクスが送信されます。
メトリクスの記録
メトリクスを記録するには、まずMeterProviderからMeterを生成します。
<?php $meter = Globals::meterProvider()->getMeter('php_meter_demo');
次に、MeterはInstrumentと呼ばれるものを生成します。
Instrumentはメトリクスの記録を担う、言わば計測器です。
どのメトリクスタイプを使うかで生成するInstrumentが変わり、ここではcreateHistogram()
を呼んでヒストグラム用のInstrumentを生成しています。もし、カウンターであればcreateCounter()
を呼びます。
<?php $histogram = $meter->createHistogram('request_latency', 's', 'Random number');
最後に、Instrumentを使って値を記録します。
<?php $histogram->record($requestDuration);
ここまでのまとめ
OpenTelemetryをセットアップしてからメトリクスを記録するまでのコードを以下にまとめます。
sleep関数に$_GET['sleep']
を渡すことで、サンプルコードのリクエストレイテンシをコントロールできるようにしています。
<?php declare(strict_types=1); use OpenTelemetry\API\Globals; use OpenTelemetry\Contrib\Otlp\ContentTypes; use OpenTelemetry\Contrib\Otlp\MetricExporter; use OpenTelemetry\SDK\Common\Attribute\Attributes; use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory; use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory; use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface; use OpenTelemetry\SDK\Common\Time\ClockFactory; use OpenTelemetry\SDK\Metrics\Aggregation\ExplicitBucketHistogramAggregation; use OpenTelemetry\SDK\Metrics\Data\Temporality; use OpenTelemetry\SDK\Metrics\Exemplar\ExemplarFilter\WithSampledTraceExemplarFilter; use OpenTelemetry\SDK\Metrics\Instrument; use OpenTelemetry\SDK\Metrics\MeterProvider; use OpenTelemetry\SDK\Metrics\MeterProviderInterface; use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader; use OpenTelemetry\SDK\Metrics\StalenessHandler\NoopStalenessHandlerFactory; use OpenTelemetry\SDK\Metrics\View\CriteriaViewRegistry; use OpenTelemetry\SDK\Metrics\View\SelectionCriteriaInterface; use OpenTelemetry\SDK\Metrics\View\ViewTemplate; use OpenTelemetry\SDK\Resource\ResourceInfo; use OpenTelemetry\SDK\Sdk; use OpenTelemetry\SemConv\ResourceAttributes; require_once __DIR__ . '/../vendor/autoload.php'; $startTimeSec = microtime(true); $meterProvider = createMeterProvider(); Sdk::builder() ->setMeterProvider($meterProvider) ->setAutoShutdown(true) ->buildAndRegisterGlobal(); // リクエスト処理にかかる時間を指定 sleep((int)$_GET['sleep']); $meter = Globals::meterProvider()->getMeter('php_meter_demo'); $histogram = $meter->createHistogram('request_latency', 's', 'Random number'); $endTimeSec = microtime(true); $requestDuration = $endTimeSec - $startTimeSec; $histogram->record($requestDuration); function createMeterProvider(): MeterProviderInterface { $reader = new ExportingReader( new MetricExporter( PsrTransportFactory::discover()->create( 'http://otelcol:4318/v1/metrics', ContentTypes::JSON ), Temporality::DELTA, ) ); $resource = ResourceInfo::create( Attributes::create( [ ResourceAttributes::SERVICE_NAME => 'test-application', ResourceAttributes::SERVICE_VERSION => '1.0', 'project_id' => 'sample-project', 'location' => 'asia-northeast1', 'cluster' => '1', 'namespace' => 'abc', 'job' => random_int(1, 100), 'instance' => random_int(1, 100), ] ) ); $views = (new CriteriaViewRegistry()); $views->register( // 条件文をスキップ new class implements SelectionCriteriaInterface { public function accepts( Instrument $instrument, InstrumentationScopeInterface $instrumentationScope ): bool { return true; } }, // バケット定義を設定 ViewTemplate::create() ->withName('request_latency') ->withAggregation(new ExplicitBucketHistogramAggregation([0, 1, 2, 3, 4])), ); return new MeterProvider( null, $resource, ClockFactory::getDefault(), Attributes::factory(), new InstrumentationScopeFactory(Attributes::factory()), [$reader], $views, new WithSampledTraceExemplarFilter(), new NoopStalenessHandlerFactory(), ); }
OpenTelemetry Collector の構築
PHPでは収集したメトリクスをGoogle Cloud MonitoringやGoogle Managed Prometheus に直接送信するためのExporterが記事執筆時点では提供されていません。 そのため、OpenTelemetry Collector(以下 Collector)を介して送信します。
Collectorの構築方法はTraceを取得する時に実施した方法と同様のため、今回使用するCollectorの設定ファイルのみ説明します。
receivers: otlp: protocols: http: exporters: googlemanagedprometheus: service: pipelines: metrics: receivers: [otlp] exporters: [googlemanagedprometheus]
receiversはTraceの時と同じく、OTLP形式のデータをHTTP経由で取り込みます。
exportersには、Google Managed Prometheusに送信するように設定します。
serviceのpipelinesにmetricsを追加し、receiversとexportersで定義したものを指定します。
出力結果の確認
構築したCollectorコンテナを立ち上げ、http://localhost:9010/metrics.php
を叩くと作成したPHPファイルが実行されるように準備します。
そして、ベンチマークツールのheyを使って以下スクリプトを実行します。これで各バケットのカウントが予め想定した値になるようにリクエストを発行します。-c
は同時実行数、-n
はリクエスト数を表しています。
#!/bin/sh # バケット[0,1)にカウントされるリクエストを3件発行 hey -c 3 -n 3 http://localhost:9010/metrics.php?sleep=0 # バケット[1,2)にカウントされるリクエストを20件発行 hey -c 5 -n 20 http://localhost:9010/metrics.php?sleep=1 # バケット[2,3)にカウントされるリクエストを3件発行 hey -c 3 -n 3 http://localhost:9010/metrics.php?sleep=2 # バケット[3,4)にカウントされるリクエストを40件発行 hey -c 5 -n 40 http://localhost:9010/metrics.php?sleep=3
このスクリプトを1分間隔で実行すると、Cloud Monitoringに収集したメトリクスが表示され、ヒートマップ形式で確認できます。
ヒートマップの見方は上述したものと同じく、色が明るいほど高いバケットカウントを示します。 最もリクエスト数が多いバケット[3,4)が白色、その次に多い[1,2)が明るい紫色、最も少ない[0,1)と[2,3)が暗い紫色で表現されています。
まとめ
PHPでOpenTelmetryを使ってメトリクスを収集し、Cloud Monitoring上で可視化する方法を見てきました。このようにメトリクスを活用して、信頼性を担保していきたいですね。
ホワイトプラスでは、ビジョンやバリューに共感していただけるエンジニアを募集しています!
ネットクリーニングの「リネット」など「生活領域×テクノロジー」で事業を展開している会社です。どんな会社か気になった方はオウンドメディア「ホワプラSTYLE」をぜひご覧ください。
オンラインでカジュアル面談もできますので、ぜひお気軽にお問い合わせください。