OpenTelemetry & Google Cloud Monitoringによるリアルタイム通信サーバのレイテンシ計測 Now in REALITY Tech #107
こんにちは!DevOpsチームの小田 大輔です。最近は日本料理を作ったり、TVの録画サーバの構築にハマっています。
さてREALITYのDevOpsチームは、メンバー2人で一手に開発生産性向上や、費用維持・削減、サービスをスケーラブルに保つ取り組みを行なっています。今回は、その中でも最近行ったNode.jsなリアルタイム通信システムのレイテンシ計測の取り組みについてご紹介します。
REALITYのリアルタイム通信サーバ
REALITYでは配信のストリーミングサーバ、コメントサーバ、配信中のゲームの通信を捌くサーバなど、さまざまなリアルタイム通信サーバが肩を並べています。そしてその全てがNode.js & WebSocketで実装されています。
上記以外の、いわゆるAPIサーバ(Go言語)については今までたびたび可観測性の向上やパフォーマンス改善に力を入れてきました。
参照 https://techcon.gree.jp/2021/session/Session-11
しかしリアルタイム通信サーバは、その辺りの可観測性改善はあまり力を入れていない状態でした。まずは全体像を知るため、可用性指標から測ることにしました。
2種類のレイテンシデータを実装
まずは、以下の2つのレイテンシを可用性指標として計測することにしました。
1. クライアントがWebSocket接続を行なってから、レスポンスを返すまで
2. クライアントがWebSocketのmessage payloadを送信してから、それが他のユーザに反映されるまで
まずは都度カスタムメトリクスを送信する方式で実装(ボツ案)
「1. クライアントがWebSocket接続を行なってから、レスポンスを返すまで」のメトリクスについては、まずクライアントがWebSocketコネクションを確立した時点でのunix timestampを保存しておきます。そしてpayloadを返し始めた時点でもう一度unix timestampを取得し、差分をレイテンシとしてカスタムメトリクスを書き込みます。「2.」も原理的には同様です。
今回は、下記のようにGoogle が公式で公開しているnpmの @google-cloud/monitoring パッケージを用いて、 Google CloudのCloud Monitoring APIに対してテレメトリ(計測データ)を直接書き込みます。
import monitoring from '@google-cloud/monitoring'
const logLatencyMetrics = async (name: string, duration: number) => {
const dataPoint = {
interval: {
endTime: { seconds: Date.now() / 1000 },
},
value: { int64Value: duration },
}
const request = {
name: projectPath,
timeSeries: [
{
metric: {
type: 'custom.googleapis.com/comment/trace',
labels: { name: name },
},
resource: {
type: 'global',
labels: { project_id: "PROJECT_ID" },
},
points: [dataPoint],
unit: "ms",
}
],
}
await client.createTimeSeries(request)
}
}
また負荷観点でサンプリングレートを設定し、一定割合でのみメトリクスを吐き出すようにしました。
以上のように実装し、実際に計測してみたところ、サンプル率が少ないせいで画像のように中央値の上下が激しくなってしまい、あまり使い物にならない指標になってしまいました。だからと言ってサンプリングレートを増やすと、この実装の場合ネットワークIOが増えてしまうので、根本的に実装方法を変える必要がありそうです。
OpenTelemetryを使った計測方法に改善
OpenTelemetryとは、オープンソースのオブザーバビリティフレームワークです。クラウドにおいてサーバのテレメトリを生成・収集・エクスポートするためのAPI仕様が定められていたり、SDKが提供されています。現在CNCF(Cloud Native Computing Foundation)のincubatingプロジェクトとして採択されており、聞いたこともある人が多いのではないでしょうか。ちなみに、Google Cloudでカスタムメトリクスを使う場合もOpenTelemetryの利用が推奨されています。
初期実装版の時点では、Node.jsのバージョンが古すぎるせいでOpenTelemetry SDKの安定版を使うことができなかったので、対応にあたってそれぞれのサーバのNode.jsバージョンを更新しました。
ということで、導入の準備が整ったので改良版として opentelemetry-js を使って計測していきます。このSDKはさまざまな機能がありますが、一例として挙げると、レイテンシ計測を行いつつヒストグラムとしてインメモリに蓄積し、一定時間ごとにCloudMonitoring APIにテレメトリ(計測データ)を吐き出す、ということができます。これによって、APIアクセスの頻度も減りますし、ネットワークIOも最小限で済むので100%の範囲でレイテンシ計測することが可能です。
下記に実装の一部を載せてみました。opentelemetry-js にExporterとしてgoogle-cloud/opentelemetry-cloud-monitoring-exporter をアタッチして、Cloud Monitoring Agentがメトリクスを収集できるようにします。また、開発環境でのみデバッガーを有効にしています。
import { MeterProvider, PeriodicExportingMetricReader, View, ExponentialHistogramAggregation, InstrumentType } from "@opentelemetry/sdk-metrics"
import { Histogram, diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api"
import { Resource } from "@opentelemetry/resources"
import { MetricExporter } from "@google-cloud/opentelemetry-cloud-monitoring-exporter";
import { GcpDetectorSync } from "@google-cloud/opentelemetry-resource-util";
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_NAMESPACE, SEMRESATTRS_SERVICE_INSTANCE_ID, SEMRESATTRS_K8S_POD_NAME } from '@opentelemetry/semantic-conventions'
...
class OTel {
private histogram: Histogram;
constructor() {
if (!isProd) {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
}
const metricReader = new PeriodicExportingMetricReader({
exportIntervalMillis: 60_000,
exporter: new MetricExporter(),
});
const meterProvider = new MeterProvider({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: "サーバの名前",
[SEMRESATTRS_SERVICE_NAMESPACE]: "default",
[SEMRESATTRS_K8S_POD_NAME]: "Pod名",
}).merge(new GcpDetectorSync().detect()),
readers: [metricReader],
views: [new View({
aggregation: new ExponentialHistogramAggregation(),
instrumentName: 'latency',
instrumentType: InstrumentType.HISTOGRAM,
})],
});
this.histogram = meterProvider.getMeter("main").createHistogram("latency", {
unit: "ms",
});
}
recordLatency = (name: string, latencyMS: number) => {
this.histogram.record(latencyMS, { name });
}
}
ハマった・工夫したポイント
時系列データの不整合エラー
k8s.pod_name にインスタンスpod名を指定して、podごとにuniqueな時系列データとして集計できるようにしました。こうしないと、複数Podから同時にメトリクスを生成した場合に画像のようにCloudMonitoring側で時系列不整合エラーとして弾かれるようです。
参考 https://github.com/micrometer-metrics/micrometer/issues/1335#issuecomment-950203451
OpenTelemetry SDKによるデバッグ
以下のようなデバッガー設定を、開発環境でのみ有効化するようにしました。これにより、画像のように一定時間おきにメトリクスが書き出されていることが確認できます。また、何かエラーで書き込みに失敗した場合はエラーも出力してくれます。
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api"
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
Cloud Monitoringでの書き込み結果確認
「monitoring.googleapis.com/collection/write_request_point_count」というメトリクスで 画像1 のようにCloud Monitoring APIへのメトリクス書き込みの結果ごとのレートを確認することができます(2024年4月現在ベータ版)。正常に収集されると「OK」としてカウントされます。
OpenTelemetry SDKにより生成できたテレメトリのグラフ
最終的にこのように、散発的ではない正常なグラフを得ることができました。これにより、我々はまた一つアプリケーションのヤバさを測るための指標を増やすことに成功しました。
まとめ
以上、Node.jsなアプリケーションでOpenTelemetryを使ってレイテンシ計測を実現した事例をご紹介しました。Node.jsによるSDK利用事例はまだ少ない状況ですが、ドキュメントと内部実装を両方読みながら試行錯誤を重ねた末、正しくメトリクスを計測することができました。
自分は主にサーバやインフラ面という、表からは見えづらい部分を担っていますが、これからも快適な体験を提供できるよう取り組んでいくので、よろしくお願いします。