この記事は New Relic Advent Calendar 2024 の 19 日目の記事です。
こんにちは。株式会社フィックスポイントの KI です。
この記事では、私が所属しているプロダクトデベロップメント部での New Relic の利用、特に分散トレースについて紹介します。
前半では分散トレースを試験環境に実際に導入し、その結果として監視体験がどのように変化したかを紹介します。
後半では、 Go 言語でのメッセージキューを経由するようなトレースの実装においての工夫や、 OpenTelemetry 導入時のおすすめの設定を紹介します。
はじめに
導入した製品のアーキテクチャ概要と、運用の課題
2024 年 11 月より、弊社製品 Kompira AlertHub の一部に分散トレースを導入しました。
Kompira AlertHub はマイクロサービスアーキテクチャを採用しており、複数のサービスによって構成されています。
Kompira AlertHub は非常に簡略化すると、以下のようなサービスです。
- 常にサービスの利用者からメッセージを受けつける(複数の監視ツールから上がってくるアラートなど)
- メッセージを Kompira AlertHub を構成する複数のサービスで処理する
- メッセージを処理した結果、特定の条件を満たす場合 Kompira AlertHub に登録されている通知先に Webhook や Email で通知をする
サービスは上記の「アーキテクチャ図 A」のように実装されています。
※実際にはもう少し複雑ですが、簡略化しています
この図で障害ポイントを考えたとき、下記のように障害を起こす可能性のあるポイントが複数あります。
- メッセージを受けつける Pod A の障害
- Pod A と Pod B をつなぐ Message Queue の障害
- 通知を実行する Pod B の障害
現在、外形監視によって「Pod A→ Message Queue → Pod B の処理が正常に終了しているか」といった監視をしています。
ですが、障害状態であることが判明した後にも、具体的にどの箇所が要因となって障害が発生しているのかの問題の切り分けが必要です。
「熟練のメンバーなら障害対応が可能でも、新規メンバーでは障害対応が難しい」などの課題があります。
New Relic のオブザーバビリティソリューション
前述の運用課題について、オブザーバビリティを実現することが解決になると考えました。
オブザーバビリティとは、以下のような能力です。
簡単に言うと、私たちが考えるソフトウェアシステムの「オブザーバビリティ」とは、 システムがどのような状態になったとしても、それがどんなに斬新で奇妙なものであっても、(中略)事前にデバッグの必要性を定義したり予測したりすることなく、 システムの状態データのあらゆるディメンションやそれらの組み合わせについてアドホックに調査し、よりデバッグが可能であるようにする必要があります。 もし、 新しいコードをデプロイする必要がなく 、どんな斬新で奇妙な状態でも理解できるなら、オブザーバビリティがあると言えます。
オブザーバビリティ・エンジニアリング 1.2
New Relic は公式サイトにも記載がある通り、オブザーバビリティプラットフォームです。
New Relicオブザーバビリティプラットフォーム | New Relic
New Relic をオブザーバビリティバックエンドとして利用するためには、サービスのデータを New Relic にエクスポートする必要がありますが、エクスポートする手段を New Relic APM か OpenTelemetry から選択できます。
私たちのサービスでは、以下の理由により OpenTelemetry (以下 OTel ) を選択しました。
- メッセージキューを利用している
- OTel を採用すると OTel の Carrier を実装することで柔軟かつ認知負荷の低い実装が可能
- 既に利用しているライブラリの OTel ラッパーが利用でき、既存実装の変更点の少ない計装が可能
- go-chi ライブラリに対する OTel ラッパー otelchi が存在する
- zerolog ライブラリに対する OTel ラッパー otelzerolog が存在する
📖 New Relic と OTel に関しての公式記事は こちら
OpenTelemetry のトレース計装の追加でどう変化したか
上述の通り Kompira AlertHub を構成するサービスについて OTel の導入を決め、まずは分散トレースの計装をサービスに追加することにしました。
トレースの計装はまだ本番環境への適用が完了しておらず、試験環境での変化ですが、下記のような変化が見られました。
サービスごとの処理時間が閲覧できるようになった
トレースデータがあると、複数のサービスを経由する一連の処理の中で、特定のサービスでの処理にかかった時間を簡単に求めることができます。
たとえば、あるトレースについて、サービスごとの処理時間は NRQL では以下のようなクエリで求めることができます。
SELECT (max(timestamp + duration.ms) - min(timestamp)) / 1000 as 'sec' FROM Span WHERE service.namespace = '{{ service_namespace }}' FACET trace.id, service.name LIMIT MAX
※タイムスタンプが 13 桁のエポックミリ秒で記録されていたため、 timestamp を 1000 で割って sec に変換しています
https://docs.newrelic.com/jp/docs/logs/ui-data/timestamp-support/
ヒストグラムで集計する場合は以下のようなクエリで求めることができます。
SELECT histogram(sec) FROM ( SELECT (max(timestamp + duration.ms) - min(timestamp)) / 1000 as 'sec' FROM Span WHERE service.namespace = '{{ service_namespace }}' FACET trace.id, service.name LIMIT MAX ) FACET service.name
サービスが行っている処理の理解がしやすくなった
今回、サービス内で以下のような箇所にのみトレースを計装しました。
- メッセージキューに enqueue / dequeue している関数
- http ハンドラ
これだけでも、以下のようなことがトレースデータを閲覧するだけで分かるようになりました。
🤔「この外部エンドポイントを叩くとどのサービスをどんな順番で処理がされてるんだろう…?」
→「トレースデータによって、どのサービスがどの順番で処理しているか分かる!」
→「Span をちゃんと計装したら、どの処理がどのファイルで実装されているかも分かるので、コードも追いやすい」
🤔「構成図ではこのサービス間は HTTP リクエストで連携しているけど、実際にどんなエンドポイントを叩いているんだろう…?」
→「http ハンドラにもトレースの計装を追加したことで、実際に叩いているエンドポイントが分かる!」
変化のまとめ
トレースの計装があることで
- 処理の全体図が分かる
- 実際の処理に紐づいた稼働状況の詳細が分かる
といった、新規メンバーがサービス理解をするのに非常に頼もしい変化がありました。
OpenTelemetry の計装時の工夫
このセクションでは、実際にサービスコードに OTel の計装を追加するにあたり、工夫した点や、おすすめの実装を紹介します。
- Go 言語でのメッセージキューを経由するようなトレースの実装においての工夫
- New Relic の APM & Services の利用が便利になる OTel Resource Attribute の紹介
Go 言語でのメッセージキューを経由するようなトレースの実装においての工夫
「アーキテクチャ図 A」のように、製品内でメッセージキューを利用しています。
メッセージキューを経由した場合でもトレースを維持するには、トレースコンテキストをメッセージに含める必要があります。
OTel において、トレースコンテキストの伝搬は Propagators API を利用して行いますが、現在この実装として TextMap Propagator が存在します。
The Propagators API currently defines one Propagator type:
- TextMapPropagator is a type that injects values into and extracts values from carriers as string key/value pairs. https://opentelemetry.io/docs/specs/otel/context/api-propagators/
つまり TextMap Propagator は何かというと、Map に対してトレースコンテキストを注入したり、抽出ができるものです。
TextMap Propagator を利用してキューのメッセージにトレースコンテキストを含めると、キューのメッセージでもトレースコンテキストの伝搬ができるようになります。
そこで製品に OTel トレースを導入するにあたり、以下のような Carrier を実装しました。
※ OpenTelemetry トレースを導入したサービスは Go で実装されているため、Go での実装です
メッセージキューとして Azure Service Bus Queue を利用しているため、関数名などに AZServicebus
を含んでいますが、 // type of azsb.Message.ApplicationProperties
というコメントの箇所をお使いのメッセージキューにあわせて変更いただければ他のメッセージキューでも利用いただけると思います。
package tracer import ( "strings" ) func getWithPrefix(prefix, key string, p map[string]any) string { value := p[prefix+key] str, ok := value.(string) if !ok { return "" } else { return str } } func keysWithPrefix(prefix string, p map[string]any) []string { var keys []string for k := range p { if strings.HasPrefix(k, prefix) { keys = append(keys, strings.TrimPrefix(k, prefix)) } } return keys } // NOTE: // AZServicebusMessageCarrier のメソッド経由で // map の値を操作すると強制的に prefix が付与されるので trace context propagation 以外の用途では利用しないこと type AZServicebusMessageCarrier struct { metadata map[string]any // type of azsb.Message.ApplicationProperties } func (c *AZServicebusMessageCarrier) Get(key string) string { return getWithPrefix("otel-", key, c.metadata) } func (c *AZServicebusMessageCarrier) Set(key string, value string) { c.metadata["otel-"+key] = value } func (c *AZServicebusMessageCarrier) Keys() []string { return keysWithPrefix("otel-", c.metadata) } // NOTE: // OpenTelemetry の Propagator(に含まれるCarrier) を Azure Service Bus のため独自定義する // NewAZServicebusMessageCarrier() の返り値を azsb.Message.ApplicationProperties にセットすることで // Azure Service Bus に trace context を埋め込む func NewAZServicebusMessageCarrier(p map[string]any) *AZServicebusMessageCarrier { return &AZServicebusMessageCarrier{metadata: p} }
この実装の工夫ポイントとして、この Carrier を利用した際、トレースコンテキストに otel-
prefix を付与するようにしたことが挙げられます。
Azure Service Bus Queue の ApplicationProperties には、トレースコンテキスト以外のメタデータを格納したいケースがあるかもしれません。
万が一でも他のメタデータを格納した際に衝突しないよう、prefix を付与しました。
補足:
carrier の計装については以下の記事にとても助けられました。ありがとうございます!
https://ymtdzzz.dev/post/opentelemetry-async-tracing-with-custom-propagator/
https://zenn.dev/google_cloud_jp/articles/20230626-pubsub-trace
New Relic の APM & Services の利用が便利になる OTel Resource Attribute の紹介
OTel Resource の Attribute は様々なものがありますが、Service Attribute の service.namespace
を指定しておくと、New Relic での体験が良かったので紹介です。
New Relic の OTel のテレメトリを表示する画面で APM & Services 画面があります。
ここには様々な環境のサービスが全て並ぶかと思います。
この状態では複数の環境に OTel 計装をデプロイした際に、目的のサービスを探すのが大変です。
ですが、 Attribute で service.namespace
を指定しておくと、画面上部の 「+」 から service.namespace
でのフィルタができ、特定の namespace のサービスのみ表示することが出来ます。
画面上部の「 Save view」より、フィルタ状態に名前をつけて保存することも可能です。
このように活用できるので、service.namespace
を指定しておくのをオススメします。
さいごに
分散トレースの導入にあたっては New Relic 社のテクニカルサポートも利用しました。
APM 画面の仕様の問い合わせや、 細かい疑問の解消など、お世話になりました。
ありがとうございました!