爆速でGo!

GoGo!

Envoyによる分散トレーシングの実現と、未来

Click here for English version

この記事はMicroservices Advent Calendarの9日目の記事です。前回は@moomooyaさんのマイクロサービス化にあたって、gRPCを導入しようとしている話でした。
この記事のタイトルは、先日のGoCon2018 Autumnにて発表された「OpenCensusによるAPMの実現と、未来」というセッション名がかっこよかったので、表現を拝借しました。

分散トレーシングの必要性

マイクロサービスのような分散アーキテクチャは複数のサービスにまたがって処理されているため、サービス間の通信を追跡することが難しくなります。そのため、

  • 障害発生時の原因究明が難しくなる
  • パフォーマンス低下の原因究明が難しくなる

そこで、分散トレーシングツールで可視化して上記の問題を解決する必要があります。

用語

分散トレーシングを学ぶにあたって、以下2つの用語は必ず覚える必要があります。

用語 意味
スパン 1つのサービス内の処理
トレース 1つのリクエストに含まれるスパンの集合体

何故Envoyなのか

Envoyを使わずとも、ZipkinJaeger等のトレース可視化プロバイダが提供するツールがあれば、分散トレーシングを実現することは可能です。
しかし実現のためには、トレース毎の識別子を生成してそれを伝搬していき、最終的にコレクターというコンポーネントに送ることが必須です。そのために各サービスに言語毎のクライアントライブラリを導入して、専用の実装をする必要があります。
Polyglotが前提にある分散アーキテクチャであるマイクロサービスアーキテクチャにおいて、これは大きなコストを生む原因になります。

そこで、インバウンドとアウトバウンド双方のトラフィックを前提に作られているEnvoyをサイドカーとして配置しておけば、この問題を解決してネットワークとアプリケーションを分離することが出来ます。
このような、全サービス間通信をサイドカープロキシを経由させることによってマイクロサービス固有の課題の解決を行うアーキテクチャサービスメッシュと呼びます。
ただ、スパン間の親子関係等のコンテキストを適切に伝搬させるためには、アプリ内で特定の識別子を伝搬させる実装をする必要があります。これに関しては後述します。

Envoyのトレーシングアーキテクチャ

利用するトレースプロバイダにもよりますが、多くの場合以下のようなコンポーネントで構成されたアーキテクチャとなります。プロバイダによってStorageを提供してくれるものもあれば、自前で運用する必要があるものもあります。

f:id:NakaWatch:20181215162055p:plain

分散トレーシングでは通常、トレース内で一意の識別子を伝搬させることでスパンを関連付けていきます。
この一連の流れの中で、Envoyの仕事は大きく分けて3つあります。トレースの生成とトレースの伝搬、そしてコレクターへの送信です。

生成ドキュメント

tracingオブジェクトを設定することで、トレースを生成することが可能になります。

伝搬ドキュメント
x-request-idという識別子を伝播して、呼び出されたサービス間のロギングを相関させていきます。
そして前述したように、スパン間の親子関係を明確にするためには別の識別子の伝搬も必要です。そしてEnvoyはこの識別子をHTTPリクエストヘッダーに乗せて通信します。そのため、特定のヘッダーから識別子を取り出して次に通信するサービスへのリクエストヘッダーに追加する必要があります。つまり、少なからずコードの修正は必要になるということです。
Envoyにはインバウンドトラフィックとアウトバウンドトラフィックを関連付ける手段がないので、これはアプリケーション開発者の責任だということです。
また、これはトレース可視化プロバイダ毎に異なる識別子が必要になるため、自分が使うプロバイダのドキュメントをよく読む必要があります。

送信
執筆時点(2018/12/9)で対応しているトレース可視化プロバイダは以下です。プラガブルになるよう設計されておりドライバを実装するだけで良いので、新たなプロバイダをサポートすることも難しくありません。

ドキュメントに従って、トレースを送信するコレクタークラスタを指定します。例えばZipkinの場合は以下のように定義します。

static_resources:
  clusters:
  - name: zipkin
    connect_timeout: 1s
    type: static
    lb_policy: round_robin
    hosts:
    - socket_address:
        address: 10.5.0.2
        port_value: 9411
tracing:
  http:
    name: envoy.zipkin
    config:
      collector_cluster: zipkin
      collector_endpoint: "/api/v1/spans"

触って理解する

Envoyは公式のサンドボックスがいくつか用意されています。今回はこの中のjaeger-tracingを触りながら、分散トレーシングの一連の流れの理解を深めていきたいと思います。

起動

$ git clone https://github.com/envoyproxy/envoy.git
$ cd envoy/examples/jaeger-tracing
$ docker-compose up -d

front-proxy, service1, service2 という3つのコンテナが起動します。

リクエスト送信

$ curl -v http://localhost:8000/trace/1
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8000 (#0)
> GET /trace/1 HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
< content-length: 89
< server: envoy
< date: Sat, 08 Dec 2018 06:10:24 GMT
< x-envoy-upstream-service-time: 27
<
Hello from behind Envoy (service 1)! hostname: 580a7e50d809 resolvedhostname: 172.18.0.5
* Connection #0 to host localhost left intact

front-proxyservice1service2 という順に通信が行われます。

可視化
ブラウザでhttp://localhost:16686にアクセスして下さい。
複数のスパンが一つのトレースとなって表示されているのが確認できます。

f:id:NakaWatch:20181208155100p:plain

サービスの実装を変更
サービスのサンプル実装はexamples/front-proxy/service.pyにあります。
注目すべき実装は以下の部分で、ここで受け取った識別子をヘッダーに乗せて伝搬させています。

    if int(os.environ['SERVICE_NAME']) == 1 :
        for header in TRACE_HEADERS_TO_PROPAGATE:
            if header in request.headers:
                 # ここをコメントアウト
#               headers[header] = request.headers[header]
        ret = requests.get("http://localhost:9000/trace/2", headers=headers)

Zipkin互換のあるプロバイダの場合、この識別子についてはB3 Propagationというリポジトリにて詳述されています。
試しにこの処理をコメントアウトして、再度リクエストを送ってみましょう。

f:id:NakaWatch:20181208154918p:plain

スパンがそれぞれ別のトレースと認識されてしまっているのが分かります。 スパン間で識別子を伝搬させないと正しくトレースを取得出来ない事が確認できました。

未来

全体的なアーキテクチャに始まり最終的にコードを触ることで、大まかな仕組みを理解して頂けたでしょうか?
先日(2018/11/28)、EnvoyはCNCFの卒業プロジェクトに認定されました。 その時のブログ内でも繰り返して述べられている通り、Envoyの目標は、アプリケーション開発者からネットワークを抽象化してビジネスロジックに集中できるようにすることです。
これはEnvoyに限らず、クラウドネイティブが主流になった現代では多くの人が、アプリケーション開発者が創作活動に集中できる環境を望んでいます。
2016年9月にLyftがEnvoyをOSSとして公開した時、本人達の想像を超えるほど反響がありました。これは、クラウドネイティブを推進するにあたってネットワークプロキシが非常に重要なポジションを占めていることを表しています。今後アプリケーションからネットワークを抽象化するようなプロジェクトがどんどん立ち上がるとしても、内部では常にネットワークプロキシがアプリケーションに寄り添っているはずです。そのため、プロキシへの理解を深めておくことはクラウドネイティブ時代において非常に大事だと思います。 筆者も微力ながら議論に参加していき、アプリケーション開発者にとっての最高の環境作りに貢献していきたいと思います。

*間違いを見つけたり情報が古くなっていたりしたらTwitter等でご指摘頂けると幸いです。