「ABEMA」への「OpenTelemetry」導入事例を発表

宮﨑大芽氏:こんばんは。本日は、お集まりいただきありがとうございます。セッションが2つ続いてちょっとお疲れかもしれませんが、私のセッションで最後です。私からは、「ABEMAの安定稼働を支えたOpenTelemetryの導入事例」というタイトルで発表いたします。

はじめに、自己紹介をさせてください。宮﨑大芽と申します。私は2022年5月に中途入社しました。ABEMAのバックエンドエンジニアとしてサービス開発を行っています。

今回のテーマでもある「FIFA ワールドカップカタール 2022」に向けた開発期間中では、サービスの負荷対策や障害対策に関わっていました。現在は、プロダクト基盤チームで決済システムなどの開発に関わっています。趣味は、漫画やアニメです。最近上映された映画『BLUE GIANT』がすごくお薦めです。

当発表では、負荷対策中に導入した「OpenTelemetry」についてお話しいたします。また発表では「FIFA ワールドカップカタール 2022」を「ワールドカップ」と省略して呼ばせていただきます。それではよろしくお願いします。発表内容は次のようになっています。

OpenTelemetryを導入した背景とは?

はじめに、導入の背景をお話しいたします。ABEMAは、マイクロサービスアーキテクチャを適用しており、多数のマイクロサービスで構築されています。

ワールドカップの負荷試験では、各マイクロサービスごとの単体の負荷試験ではなく、サービス全体に対する複合的な負荷試験を行いました。ボトルネックの要因やキャパシティ増強のための各種係数を把握し、キャパシティ不足による機会損失を限りなくゼロに近づけたかったためです。

共通するデータを提供するサービスなど、サービスの共依存が存在しており、単体負荷試験で正しいリソース状況を測定することは困難でした。このことから複合的な負荷試験を採用しました。

負荷試験を開始した当初は、サービス全体に対して分散トレーシングが導入されていませんでした。そのため、ボトルネックを調査する際には、各サービスのメトリクス、プロファイラの情報、実際のソースコードなどから仮説を立てながら行う必要があり、ボトルネックの把握に時間がかかっていました。

そこで急遽、OpenTelemetryを導入し、各ゲートウェイ、マイクロサービス、ミドルウェアを横断した分散トレーシングを実現することになりました。

ベンダーに依存せずに計装ができる仕組みを持つ

OpenTelemetryの導入について説明する前に、OpenTelemetryの概要を簡単に説明いたします。

トレーシングを導入した方はわかるかと思うのですが、トレーシングを導入する際に、トレースしたい箇所のコードにSpanを作成する処理や、Attributesを付加する処理を追加する必要があります。これはInstrumentation、計装という作業です。

この計装に関して、基本的にベンダーの提供するSDKを用いることになるのですが、これがベンダーロックインにつながります。つまり、「AWS X-Ray」から「Cloud Trace」に乗り換えようとすると、計装のやり直しが必要になります。

そこで、OpenTracing、OpenCensusといったトレースの共通仕様を定めて、ベンダーに依存せずに計装ができる仕組みが有用です。1つに標準化するために、OpenTracingとOpenCensusを統合し、それらの後継となったのがOpenTelemetryです。

OpenTelemetryはトレース情報だけでなく、メトリクスやログといったテレメトリデータの計測・生成・収集・エクスポートに利用し、ソフトウェアのパフォーマンスと動作の分析に役立てることができます。

こちらの画像は、Cloud Traceの実際の画面です。分散トレーシングを導入することで、画像のようにサービスごとに処理とその実行時間を可視化することができます。

OpenTelemetryの導入方法

それでは、導入方法についてお話しします。導入方法の説明にあたって、まずはABEMAのアプリケーションの稼働している環境について紹介します。ABEMAは「Kubernetes」で稼働しており、「Anthos Service Mesh」が導入されています。

導入の全体観を概念図で紹介します。最終的にはロードバランサー、istio-proxy、アプリケーション、それぞれがSpanをCloud Traceに送信することで、Cloud Traceによるトレーシングが可能になります。OpenTelemetryは、今後のエコシステムの拡大を見据えて、アプリケーションに計装する手段として選定しました。

導入方法について説明します。Anthos Service Meshがトレーシングに対応しているため、この機能を有効にします。istio-proxyがSidecarとして起動しているPodでトレーシングが可能になります。Anthos Service Meshは、(スライドを示して)これらのTrace Contextのフォーマットに対応しています。

すでに稼働しているコードでトレーシングを行うためには数々の対応が必要で、段階的に導入を進めました。

計装にあたっては、(スライドを示して)これらのinstrumentation libraryが役に立ちました。ライブラリは、OpenTelemetryの公式のものや、ライブラリ開発元の公式のものです。

こちらの2つ(otelhttpとotelgrpc)について、サンプルコードを交えて紹介します。まずは「otelhttp」です。net/httpを使用したHTTPサーバーとクライアントに対して計装します。

まずはHTTPサーバーです。このようにミドルウェアを追加するだけで計装できます。ヘルスチェックのリクエストはノイズになるため、フィルターするところがポイントです。

続いて、HTTPクライアントです。こちらは、transportをwrapするだけで計装できます。

次に、otelgrpcです。gRPC-Goを使用したgRPCサーバーとクライアントに対して計装します。

まずはgRPCサーバーですが、interceptorを追加するだけで計装できます。こちらもヘルスチェックをフィルターするところがポイントです。

続いてgRPCクライアントですが、同様にinterceptorを追加するだけで計装できます。

Google Cloudのサービスを利用する際のクライアントライブラリでは、OpenCensusによってすでに計装されています。OpenCensus Bridgeを導入することで、OpenTelemetryに対応させることが可能になります。

トレーシングの設定を担うTracer Providerの初期化を行うコードについて紹介します。TextMapPropagatorを指定することで、プロセス間のTrace Contextを伝播させる方法を指定します。OpenCensus Bridgeを導入することで、前述のとおりGoogle Cloudクライアントライブラリに対応しています。

先ほどのコードの続きですが、これはCloud Traceにエクスポートするための実装です。万が一初期化に失敗した場合は、何もしないエクスポーターにフォールバックすることで、アプリケーションの予期せぬ起動失敗を防ぎます。私たちは「Wire」でDependency Injectionをすることが多いため、このように実装しています。

OpenCensus Bridgeを導入すると、Cloud Traceや「Cloud Profiler」へのAPIリクエストもトレースに含まれるようになります。これらはアプリケーションの稼働に必須ではなく、あくまでもパフォーマンス計測に必要なものなので、これらのトレースはノイズになります。

custom samplerを実装することで、Span名の先頭位置よりフィルターするように実装しました。また、OpenTelemetryで使用できるロギングライブラリが限定されているため、すでに導入されているライブラリを使用するには、ブリッジする必要がありました。

苦労した点はAnthos Service Meshとの連携

次は、実際に既存システムに導入する上での苦労について一部を紹介いたします。

検証環境で、Anthos Service Meshのトレーシングを有効にして確認を行いました。Zipkin B3 Single Headerには対応しておらず、検証中にはまったり、そもそもアプリケーションでコンテキストが伝播されていない箇所があり、トレースが途切れることが判明しました。

Anthos Service Mesh側でトレーシングを有効にするだけではなく、アプリケーション側でAnthos Service Meshが対応しているフォーマットのPropagatorを設定すること、コンテキストを正しく伝播させることが必要になります。

istio-proxyコンテナによって、Trace Contextが付与、伝播されます。istio-proxyが受信してアプリケーションに渡すリクエストに関してはTrace Contextが伝播されますが、アプリケーションが実装の中でコンテキストを伝播していないと、そこでトレースは途切れます。

アプリケーションがistio-proxyを介さず直接送信するリクエストはもちろんのこと、istio-proxyを経由して送信するリクエストであっても、アプリケーション側がリクエストにTrace Contextを含めていなければTrace Contextは伝播されません。

一部、コンテキストが渡っていないレガシーコードが残っており、向き合っていく必要がありました。

サンプルコードを交えて紹介します。これはそもそも、コンテキストが渡されていない関数呼び出しと、非同期処理のため上流のコンテキストを渡せない関数呼び出しの例です。非同期処理の場合に新しいコンテキストをそのまま渡してしまうと、Trace Contextが伝播されず、トレースが途切れてしまいます。

前者は、コンテキストを渡すかたちにシグネチャを修正して対応します。後者は、非同期処理に生成したコンテキストに上流のコンテキストが持っていたSpanをコピーすることでTrace Contextを伝播させることができます。

ほかにも、レガシーコードが古いgRPC-Goに依存していることで、otelgrpcはそのままでは導入できないという問題にも直面しました。これは愚直にクライアント実装を修正し、gRPC-Goのバージョンを上げる対応が必要でした。

分散トレーシング活用事例 APIのデュレーション遅延の原因特定

OpenTelemetryの導入によって実現した分散トレーシングが、ABEMAでどのように活用できたかをお話しします。

各種メトリクスと分散トレースを掛け合わせることによって、有効に活用することができました。負荷試験の際、データベースは遅くないにもかかわらず、APIのデュレーションが遅いことがありました。

分散トレーシング導入以前は、スライド上部の画像のメトリクスや、データベースのメトリクスを基に原因を探る必要があり、なかなか原因の特定に至りませんでした。分散トレーシング導入後は、スライド下部の画像のように、データベースのアクセス部分で遅くなっていることがわかりました。

この情報を基に、「サービスとデータベースの間に存在するistio-proxyコンテナがボトルネックになっているのでは?」と仮説を立て、データベースアクセスの際は、istio-proxyを経由しない対応を入れました。実際、この対応によって問題を解消することができました。このような詳細な情報を知ることができたのも、分散トレーシングを導入したからだと思います。

分散トレーシング活用事例 レイテンシ悪化の原因特定

次は、ワールドカップ試合中の監視時です。ワールドカップの試合中、サービスのレイテンシが悪化することがありました。画像は、ワールドカップの試合中に実際にレイテンシが悪化したサービスの分散トレース情報です。

画像のように、一瞬ではあるもののレイテンシが悪化した瞬間があったことがわかると思います。私たちは、レイテンシの悪化によりリクエストが滞留しサービスがOut Of Memoryとなることを懸念していました。

サービスによっては、試合配信に影響を与える可能性もあることから、ボトルネックの特定までをより早く行う必要がありました。画像下部の分散トレースの詳細情報を確認することで、ボトルネックの部分がわかります。

各サービスのメトリクスと分散トレース情報を掛け合わせることによって、処理の中で呼び出しているサービス、実際にレイテンシが悪化しているサービスを紐付けられ、素早い原因の特定ができました。

今後の展望

導入し活用もしていますが、まだ理想的な状態ではありません。今後の展望についてお話しいたします。

まず、「OpenTelemetry Collector」の導入が挙げられます。今は直接Cloud Traceに送信していますが、Collectorを経由させるアーキテクチャに変更することで、これらの恩恵が受けられます。

次に、「Grafana Tempo」の導入です。クラウド横断でトレーシングを実現する場合、どちらのクラウドのソリューションを採用するかという問題があります。また、大規模なサービスで利用する際には、ランニングコストの問題もあります。

現時点では、OSSであるGrafana Tempoの導入を検討しています。特定のクラウドベンダーに依存せず、トレース情報の保存期間を調整することで柔軟にコストコントロールできます。

まとめ

まとめです。負荷試験では、マイクロサービスのボトルネックの把握のためにOpenTelemetryを用いた分散トレーシングを実現しました。Anthos Service Mesh、OpenCensus Bridge、各種ライブラリを用いて導入を進めました。導入後は、負荷試験や監視などで活用することができ、「FIFA ワールドカップカタール 2022」を乗り切ることができました。

以上です。ご清聴ありがとうございました。

(次回へつづく)