LINEのマイクロサービス環境における分散トレーシング

LINEとマイクロサービス

LINEのアプリは、トークをはじめとして、電話、ショップ、公式アカウントなど、多数のサービスで構成されていますが、これらのサービスはモノリシックな単一のシステムとして開発されているわけではありません。それぞれ独立したシステムとして開発・運用され、お互いにAPIを介してコミュニケーションする、いわゆるSOAやマイクロサービスと呼ばれるアーキテクチャになっています。

本エントリでは、大規模なマイクロサービス環境において、システムのトレーサビリティを向上させるためのLINEバックエンドの取り組みを紹介します。

マイクロサービスの課題

最近よく耳にするマイクロサービスですが、その利点は、システムを扱いやすい小さな部分にわけ、疎結合にすることで、全体としては巨大なシステムであってもそれぞれのサービスは独立してスピーディーに発展できる点にあると思います。

一方、リクエストが複数のサービスにまたがって処理されるため、リクエストの処理全体を追跡する能力(トレーサビリティ)が低下するというリスクが生じます。例えばあるリクエストのレイテンシの悪化を調査したいというような場合、そのリクエストが呼び出す全てのサービスを特定し、各々のサービスの担当チームに調査を依頼するなどの作業が発生するため、非常に煩雑なものになります。

また個々のサービスが最適化されていたとしても、サービスレイアウトが時間とともに複雑化することによって全体としてのパフォーマンスが低下することもありえます。トレーサビリティの低下によってその検出や改善は難しくなることが予想されます。

分散トレーシングシステム Zipkin

LINEバックエンドでは、前述のような課題意識から、Twitter社が開発したオープンソースの分散トレーシングシステムであるZipkinの導入に取り組んでいます。

Zipkinは、ネットワーク的に分散した一連のAPIコールのタイミング情報を収集し、可視化・分析するためのシステムです。Zipkinを導入したシステムでは、リクエスト単位で以下のようにAPIのコールパスを可視化することができます。

下のグラフでは、一番上の起点となるフロントエンドのリクエストから始まって、複数のサービスを連続して呼び出している様子が可視化されています。

このリクエストでは、Service Aに最も時間がかかっているものの、その大半はService Bからのレスポンス待ちに費やされており、Service Bをチューニングすることで改善できそうです。また、Service AとService Cに依存関係がなければ、呼び出しを並列化することによってレイテンシが改善できそうなことがわかります。

Zipkinで可視化できるのは同期的なRPCだけではありません。次のグラフは実際に我々のシステムで発生した状況を模したもので、MQを介して非同期的に実行されるプロデューサー・コンシューマー型の処理を可視化しています。

このグラフからは、 コンシューマー側の処理が途中(赤線で示した区間)で瞬断していることがわかります。これはMQに優先度の高いタスクが割り込んだことが原因だったのですが、こうして可視化されることによってその影響度合いをはっきりと確認することができました。

分散トレーシングの原理

リクエストから発生する一連のAPIのコールパスは、ツリー構造を形成します。Zipkinでは、このコールパス全体に一意の「TraceID」を、ツリーのノードにあたる各APIコールに一意の「SpanID」をそれぞれ発行します。また、ノード間の親子関係を把握するため、各SpanIDはその親のSpanIDと共に記録されます。

下の例では、リクエスト自体にTrace ID=1000が発行され、Server1、Server2への通信にはそれぞれSpanID=100、SpanID=200が発行されています。SpanID=200は、SpanID=100によって引き起こされたため、SpanID=200のParent SpanIDは100になっています。

これらのトレースデータ(Trace ID, SpanID, Parent SpanID)を末端のAPIコールまで連鎖的に引き継いでいき、各サーバ上で処理時間と共にZipkinに記録し、最終的にZipkin上で全てのTrace IDとそれに含まれるSpanID同士の親子関係のマッピングが作成されることによって、分散環境でのトレーシングが実現されています。

Zipkinサーバサイド

Zipkinのサーバサイドは、以下の4つのサーバから構成されます。

  • Scribeインターフェースでトレースデータを受信するzipkin-collector
  • トレースデータの検索機能を提供するzipkin-query
  • Web UIを提供するzipkin-web
  • トレースデータのストレージ

ストレージの選択

ZipkinのストレージはCassandra、HBase、Redis、RDB(MySQL等)などから選択できるようになっています。Twitter社は最近までCassandraをZipkinのバックエンドとして採用していたようですので(現在はManhattanに移行)、実績という面ではCassandraが最も有力な選択肢になるかと思います。

LINEではHBaseを採用しました。これは既に大規模なHBaseクラスタがあり、運用実績も蓄積していることと、将来的にMapReduceによる高度な分析をしたいというのが理由です。また、CassandraやHBaseでは、TTLを指定して古いデータを自動的に削除できることが運用コストの面で有利です。実際に我々のプロダクション環境では、トレースデータのTTLを2週間に設定しています。

トレースデータの送信

zipkin-collectorはScribe互換のサーバとして実装されているため、プロダクション環境で運用する際には、アプリケーションから各サーバのローカルのScribeを経由してトレースデータを送信することになるでしょう。

LINEでは、Scribeそのものではなく、既に運用済みのfluentdにScribeプラグインを追加する形で導入し、fluentd経由でトレースデータを送信していますが、今のところ問題無く稼働しています。今後は、トレースデータの増大に備えて中継サーバを用意して負荷分散をはかるなど、一般的なfluentdやScribeによる大規模ログ収集の運用と同様の対策が必要になってくると考えています。

Zipkinクライアントサイド

トレースデータをZipkinに集約するには、サービス間のRPCスタックにトレースデータの送信処理を組み込む必要があります。我々の導入時点では以下の選択肢がありました。

Finagle

Zipkinと同じくTwitter社が開発しているScala製のRPCフレームワークで、Zipkinへのトレースデータ送信機能が組み込まれています。

HTrace

Cloudera社が開発しているJavaベースのトレーシングライブラリで、特定の分散トレーシングシステムに依存しない汎用的な作りになっていますが、Zipkinへの送信機能も提供されています。

LINEバックエンドでは、ほとんどのサービスがJavaのアプリケーションサーバ上にThrift over HTTP(ThriftのトランスポートをHTTPで実装したもの)として実装されているため、既存のライブラリをそのまま使うのではなく、独自のライブラリを開発する必要がありました。それが次に紹介する「LINE Tracer」です。

LINE Tracer

LINE Tracerは、LINEバックエンドの標準的な構成のアプリケーションサーバで簡単にトレースデータ送信機能を組み込むことを目的として設計されました。LINE Tracerを組み込んだシステムの構成は次のようになります。

メインとなる部分はServlet Filterとして実装されており、HTTPヘッダに付与されたトレースデータ(TraceID, SpanID, Parent SpanID)を抽出し、後続のサービスに引き継ぐためにThreadLocalに格納します。次のサービスのAPIを呼び出す際は、この値を取得してリクエストのHTTPヘッダに付与することで、連鎖的なAPIコールをトレース可能にしています。サービス内で発生したトレースデータは、HTraceの機能を使ってローカルのfluentd経由でZipkinに送信されます。

また、サービスのAPIごとにトレースをON/OFFしたり、サンプリングレート(後述)を指定したいというニーズがあるため、APIを実装するメソッドに以下のようなアノテーションを付与することでトレースが有効になるようにし、アノテーションの属性で個別の設定をできるようにしています。

@Tracing
public void doSomething(String foo, String bar) throws FooException {
    // service implementation
}

このほか、APIのパラメータを自動的にトレース情報に付与する機能によって、コールパスの画面上で実際に指定されたパラメータを確認してデバッグに役立てたり、特定のパラメータで呼び出されたAPIコールを検索して調査するといったことが可能になりました。

AMQPへの適用

HTTPだけでなく、最近ではAMQPによる非同期的な通信を採用するケースが増えています。Zipkinでは、非同期通信であってもトレースデータの伝達さえできればトレースが可能です。むしろ非同期通信こそ、処理フローの全体像が見えにくくなるため、分散トレーシングの導入によって恩恵を受けやすいかもしれません。

ミドルウェアとしてRabbitMQを使った我々の最近のプロジェクトでは、送信データのヘッダにトレースデータを含め、キューのコンシューマー側にトレースデータ送信処理を仕込むことによって、大規模な非同期タスクが発生するコールパスをトレースすることができるようになりました。

トレーシングのサンプリングレート

分散トレーシング自体の負荷がサービスに影響を与えては本末転倒です。あるいは全リクエストのトレースデータを格納するほど巨大なZipkinクラスタを用意できないかもしれません。そこでFinagleやHTraceでは、トレーシングのON/OFFを確率(サンプリングレート)で指定できるようになっています。

我々の場合、トレーシングの影響を事前に見積もることが難しかったため、導入当初は非常に低いレートから始め、サービスへの影響をみながら徐々にレートを上げていく方法を採りました。またAPIごとに実行頻度が大きく異なるため、LINE Tracerの設定で、APIごとにレートを変更できるようにしています。現在、100%から0.001%までかなり幅がある状況です。基本的には全て100%に近づけていきたいのですが、時間帯によって超高頻度で実行されたりほとんど実行されなかったりするAPIの場合、固定のサンプリングレートではうまくいきません。このため、Zipkinのサーバサイドに実装されているアダプティブサンプリング(頻度によってレートを自動的に調節する技術)をLINE Tracerに組み込めないか検討しているところです。

時系列分析

Zipkinによるコールパスの可視化はそれだけでも便利なのですが、あくまで個々のリクエストを個別に可視化するため、あるサービスのレイテンシを継続的・統計的に分析したいといったことができません。しかしストレージには過去分のトレースデータが時系列で格納されているため、これを活用すれば可能なはずです。そこで、既存の社内統計システムからzipkin-queryサーバを介してトレースデータにアクセスし、独自に集計・可視化する機能を開発しました。

この分析機能では、個々のサービスのレイテンシの要約統計量を時間間隔毎に集計し、箱ひげ図として時系列で表示します。また、外れ値(Outlier)は個別にプロットされ、クリックするとZipkinのコールパス画面に遷移するようになっており、どんなリクエストの中で発生したのかを確認できるようになっています。

この機能はまだサービス単位の統計しかしていませんが、HBase上に蓄積されたコールパスのグラフデータを活用すれば、サービス間の依存関係をも考慮した、マイクロサービス環境ならではのよりインテリジェントな分析基盤が構築できるのではないかと期待しています。

最後に

巨大なモノリシックアーキテクチャを長期に開発運用していくことの難しさが明らかになるにつれ、今後ますます様々なシステムでマイクロサービスアーキテクチャの採用が進んでいくのではないでしょうか。

一方、マイクロサービスなりの注意点があるのも確かです。その一つとして、システムの分断により、全体最適化がおろそかになりやすいという面が挙げられます。この課題に対しLINEでは、Zipkinによる分散トレーシングの導入や、社内のThrift/HTTP環境に合わせた独自ライブラリの開発、さらに既存の統計システムへの統合などを通じて対策を試みています。今後も各サービスへの導入コストの軽減や、統計的な分析機能の強化をはかり、LINEの安定運用の基盤として引き続き取り組んでいこうと考えています。

LINEバックエンドでは、今回紹介した分散トレーシング以外にも、大規模なサービス群を安定的に開発・運用していくための取り組みを継続的に行っていますので、また別の機会に共有できれば幸いです。