Kubernetes上のマイクロサービスを分散トレースする

![image.png](https://qiita-image-store.s3.amazonaws.com/0/5912/d3c871eb-27f4-fcc9-da0a-ef2d17dc486b.png)

# TL;DR;

- 分散トレーシングシステムの４要素を理解する
- 分散トレーシングシステムの「バックエンド」と「それ以外」は分けて選択してもいい
- 一例として、分散トレーシングシステムのバックエンドとしてDatadog APM、クライアントライブラリとしてOpenTracing Tracer、PropagationフォーマットとしてB3、SpanフォーマットとしてZipkin v1を採用すると楽
- アプリの設定例だけ知りたい方は[ここ](#go%E3%82%A2%E3%83%97%E3%83%AA%E3%81%A7%E3%81%AE%E5%AF%BE%E5%BF%9C%E4%BE%8B)から読んでください

# 何がうれしいの?

- Zipkinの運用保守
- 枯れてないトレーサのバグや機能不足という悩み
- ログ・メトリクス・トレースそれぞれ別のサービスに見に行く手間

などから解放されます。

# 分散トレースといったらZipkinなんでしょ?

そんなことはありません。
実際、Zipkinをデプロイせずに、Zipkin以外のサービスにZipkinトレースを集約・閲覧することができます。

CNCFがホストしていてZipkinとの互換性もある[Jaeger](https://github.com/jaegertracing)という分散トレーシングシステムもありますが、今回はそれも（ほとんど）使いません。

# どうするの?

この記事では「アプリからはZipkinにトレースを送るように見えているけど、実際にはDatadogに集約する」方法を紹介します。

具体的には、アプリケーションやミドルウェアからは既に枯れきった(と思う)Zipkin v1形式のトレースを送りつつ、それをDatadog APMに集約します。

# 構成

```
マイクロサービス1 --Zipkin v1 Span--> dd-zipkin-proxy --Datadog APM Span--> dd-agent ---> Datadog APM
↕ Zipkin B3 Propagation
マイクロサービス2
↕ Zipkin B3 Propagation
...
```

## なぜDatadog APMなのか

筆者が分散ロギングやモニタリング＆アラーティングのために既にDatadogを利用しているためです。
一言でいうと、アラートが発生したときにDatadogを見るだけで、ログ、トレース、メトリクスをすべて確認することができるため、障害原因の切り分けがしやすくなります。

参考までに、DatadogによるKubernetesクラスタの分散ロギングやモニタリング＆アラーティングについては、下記の記事にまとめてあります。

- [Kubernetes上のアプリケーションログを自動収集する](https://qiita.com/mumoshu/items/4d1d5e5ac0a0ce67d45f)
- [Kubernetesにデプロイしたアプリケーションのメトリクスを自動収集する](https://qiita.com/mumoshu/items/9fb7988ccbefa88b61cd)

## どうやってZipkinトレースをZipkinなしで集約するか

例えば、

- Google Stackdriver Tracingを使う場合、[stackdriver-zipkin](https://github.com/GoogleCloudPlatform/stackdriver-zipkin)
- Datadog APMを使う場合、[dd-zipkin-proxy](https://github.com/flachnetz/dd-zipkin-proxy)
    - の[fork](https://github.com/mumoshu/dd-zipkin-proxy)

をKubernetesクラスタにデプロイしておけば、それぞれのバックエンドとしてのZipkinの代わりにStackdriverやDatadog APMのようなSaaSを利用することができます。

今回はDatadog APMを利用しますが、できることや利用手順はStackdriverの場合でも大きくは変わりません。

# なぜdd-zipkin-proxyのようなものが必要なのか

## 分散トレーシングシステムの4要素

大雑把にいうと、Zipkinを始め多くの分散トレーシングシステムは以下の４要素から構成されています。

- アプリケーションに組み込む「トレーサ」というライブラリと、
- トレーサで計測・送信するSpanのフォーマット
    - 旧来の[/api/v1](https://github.com/openzipkin/zipkin-api/blob/master/zipkin-api.yaml)
        - 対応しているトレーサ多い
    - Zipkin v2の[span2 format](https://github.com/openzipkin/zipkin/issues/1499)または[/api/v2](https://github.com/openzipkin/zipkin-api/blob/master/zipkin2-api.yaml)
        - まだ対応しているトレーサは少ない
- それを見やすく集約してくれるバックエンド
    - ZipkinのWeb UI
        - それを提供するWebサービス、そのさらにバックエンドにあるデータストアも含む
- (マイクロ)サービス間でSpanを紐付けるためのPropagationフォーマット
    - [B3 propagation](https://github.com/openzipkin/b3-propagation)

仮にZipkinを採用するといった場合、この４つをセットで採用するのが一般的だとは思いますが、実際のところバックエンドだけ変えるということは可能です。

## Spanフォーマットを変換・翻訳するAdapterまたはProxy

前述のとおり、分散トレーシングシステムのバックエンドと、その分散システム用のトレーサ（クライアントライブラリ）、分散システム固有のSpanフォーマット、Propagationフォーマットは別物です。Spanフォーマットだけに注目すると、例えば、Datadog APMには固有の、ZipkinにはZipkin固有のSpanフォーマットがあります。したがって、Datadog APMにZipkinフォーマットのSpanを送っても、Datadogはそれを理解できません。

しかし、どの分散とレーシングシステムのSpanフォーマットも細部の違いこそあれ含まれる情報は大体同じです。そのため、トレーサと分散とレーシングシステムの間のどこかでフォーマットさえ変換してしまえば、ある分散トレーシングシステム用のライブラリから送信したトレースを、別の分散トレーシングシステムに蓄積したり表示することが可能になります。

ZipkinからDatadog APMにSpanを変換したい場合、dd-zipkin-proxyでそれが可能になります。

# AWS X-rayは？

AWSユーザの方は気になると思いますが、今のところAWS謹製の分散トレーシングサービスであるX-rayをZipkinの代わりに利用できるソリューションは存在しません。dd-zipkin-proxyやstackdriver-zipkinのようなものがX-ray向けにも存在すればよいのですが。

ただし、[OpenZipkinプロジェクトで既に検討は始まっています](https://github.com/openzipkin/zipkin/issues/1754)。また、[OpenZipkinプロジェクトで開発が進められているトレーサ"Brave"](https://github.com/openzipkin/brave/pull/510)には既にX-ray対応がマージされているようです。

# Zipkin v1ってまだ使われてるの?

使われています。

## Kubernetes界隈でのZipkin v1の事例

たとえばKubernetes界隈でリバースプロキシ、サービスメッシュ内のコンポーネントとしてのProxyとして定番になりつつあるEnvoyも、ごく最近まで(2017/12/2頃確認した限りでは少なくとも)Zipkinフォーマットにしか対応していませんでした。

サービスメッシュのIstioのControl-PlaneもZipkin v1フォーマットのみの対応です。

また、KubernetesでL7ノードバランサとしてよく使われるNginx Ingress ControllerはZipkinフォーマットに対応しています。

## OpenTracing界隈でのZipkin v1の事例

おそらく自社で開発したアプリの分散トレーシングを行いたい場合、何らかのトレーサライブラリを採用することになると思います。現時点では、様々な言語向けのトレーサライブラリがZipkin v1に対応しているか、Zipkin v1にしか対応していないか、そもそも特定言語と特定SaaSにしか対応していない、という構図になっています。

- [OpenZipkinプロジェクト公式のOpenTracing Tracer実装](https://github.com/openzipkin/zipkin-go-opentracing)は今のところZipkin v1のみの対応です
- [Golang向けのJaeger Tracer(OpenTracing TracerのJaeger実装)](https://github.com/jaegertracing/jaeger-client-go)はZipkin v1のみの対応です
- [OpenZipkinプロジェクト公式のJava向けトレーサ](https://github.com/openzipkin/brave)はZipkin v1/v2両対応です

というわけで、自社で幅広く使うようなKubernetesクラスタで共通的に利用できるような分散トレーシングシステムを構築しようと思うと、現時点ではZipkin v1のSpanフォーマットを採用し、それに対応したミドルウェアやライブラリを前提とするほうが開発効率は高いと思われます。

特にライブラリに関しては、ZipkinトレーサかOpenTracingトレーサ、Jaegerトレーサがおすすめです。

なぜJaeger?と思われるかもしれませんが、JaegerトレーサはOpenTracingトレーサの実装+トレースのサンプリングなどの便利機能を追加したもので、バックエンドはZipkin v1 Spanフォーマットを理解できる前提だからです。

## dd-zipkin-proxyではなく、トレーサを改良してDatadog APM対応してはどうか?

トレースをDatadogで見たいだけなら、jaegerのようなこなれたトレーサライブラリにDatadog APMトランスポートを実装してもいいのでは、と思われるかもしれません。だめということはないのですが、現状ではコスパが悪いと思います。

特にPolyglotなMicroservicesアーキテクチャを採用してしまっているような組織だと、利用している言語毎にトレーサのDatadog APM対応をしなければなりません。もちろん、メンテできればいいとは思うのですが・・。

また、おそらく皆さんがデプロイするミドルウェアやアプリの中にはZipkin v1フォーマットのトレース送信にしか対応していないものもあると思うので、いずれにしてもZipkin v1フォーマットには対応したくなると思います。

# Datadog APMがOpenTracing対応したけど?

https://www.datadoghq.com/blog/opentracing-datadog-cncf/

OpenTracingは現状Tracerの仕様しか規定していないので、「OpenTracingを採用したアプリやミドルウェアだからOpenTracingに対応したどのバックエンドにもトレースをマジカルに送れる」というわけではありません。

例えば、あなたのアプリケーションがOpenTracing Tracerでトレースをとっていれば、数行のコードでTracerの実装を切り替えることで、実装に対応してDatadog、Zipkin、Lightstep、Instanaのような様々なバックエンドにトレースを送ることができます。しかし、それは実装を切り替えられる場合に限ります。

例えば、ミドルウェアがOpenTracing Tracerに対応したとしても、Tracerの実装を動的にロードするようなプラグイン機能のようなものが実装されないかぎり、いちいち自分でミドルウェアのコードを変更してTracerの実装を差し替えて、ビルドして・・・という手間が発生します。現時点で、そこまでしてOpenTracing Tracerを利用したいケースはあるのでしょうか・・・？

トレースしたいものがアプリだけではないのであれば、特定のAPMベンダがOpenTracingに対応したとしても、私達の直近の技術選定には何の関係もありません。

# Datadog APMにZipkin v1スパンを集約するインフラのセットアップ手順

※既にインフラが用意されていて、スパンを送るだけという状態になっている方は[この節は飛ばしてください](#go%E3%82%A2%E3%83%97%E3%83%AA%E3%81%A7%E3%81%AE%E5%AF%BE%E5%BF%9C%E4%BE%8B)。

あまりに分散トレーシング界隈がカオスなため前置きが長くなりましたが、ここからが本題です。


## dd-agentのデプロイ

「[Datadog AgentをKubernetesにインストールするときのベスト・プラクティス - Qiita](https://qiita.com/mumoshu/items/4c19695e5c9e3edd098b)」の手順どおり、dd-agentのDaemonSetをhelmでデプロイします。

## dd-zipkin-proxyのデプロイ

### dd-agent/dd-zipkin-proxyデプロイの勘所

dd-agentは各ノードに１つずつデプロイされる前提で設計されています。

アプリケーションやミドルウェアからは、dd-agentを経由してメトリクスやスパンをDatadogへ送ります。そのとき、仮に１クラスタに1 dd-agentしかいなかったとすると、すべてのメトリクス、すべてのトレースがdd-agentがいる単一のホストから送られてくるように見えて、全く期待と違う結果になってしまいます。

### メトリクス収集に関する対応

メトリクスに関しては、前述の手順でdd-agentをDaemonSetとしてデプロイしておけばそれで問題ありません。

### スパン収集に関する対応

一方で、トレースのためのdd-zipkin-proxy(やdd-agent)はスパンの送信元ホストを「送り主のPodがいるホスト」ではなく「dd-zipkin-proxyがいるホスト」と解釈します。そうすると、アプリケーションに「自分が動いているPodと同じNodeで動いているdd-agent Podを発見したい」という要求が生まれます。

#### Node-Local Service

この文脈でのdd-agentのようなサービスのことは、Kubernetesでは(非公式ですが)Node-Local Serviceと呼びます。

参考: "I have node-local services I want to offer to pods, e.g. DataDog, how can I do that? · Issue #15169 · kubernetes/kubernetes"
https://github.com/kubernetes/kubernetes/issues/15169

残念ながら、このNode-Localサービスはまだ提案段階で、Kubernetesには実装されていません。何らかの形で「同じノードにいる特定のPod」への経路を確保する必要があります。

#### Node-Local Serviceを実現するためのMagic IP

例えば、「あるノードで動いているPodから`169.254.123.123:9411`にZipkin v1 Spanを送ると、そのノードで動いているdd-zipkin-proxyに送られる」というような状態にできれば、先程のノードローカルサービスが実現できたことになります。

この方式を「Magic IP」といいます。
実装としては、以下のようにします。

- 各Kubernetesノードでiptablesを実行することで、特定のIPアドレスへの通信をそのノードのdd-zipkin-proxy PodにDNAT
- アプリやミドルウェアのZipinSpan送信先を
`http://169.254.123.123:9411/api/v1/spans`のようにMagic IPを含むようにする

これで、どのノードのPodからSpanを送ったとしても、ちゃんとそのPodがいるノードのdd-agentを経由してDatadog APMへトレースを送ることができるようになります。

素朴な方法だと[各ノードにsshしてiptablesコマンドを実行する](https://qiita.com/mumoshu/items/079dbd17af8433ba3749)、ということになってしまうのですが、dd-zipkin-proxyのforkである[mumoshu/dd-zipkin-proxy](https://github.com/mumoshu/dd-zipkin-proxy)を使うと特権コンテナでiptablesを自動実行できます。権限的に問題無い場合はこちらの方法をおすすめします。

### mumoshu/dd-zipkin-proxyを利用する場合

helm installでできたdd-agentのdaemonset名が`datadog-datadog`だとした場合、以下のようにdaemonsetを変更して、dd-zipkin-proxyをSidecarコンテナとして追加します。

```
$ kubens kube-suystem
$ kubectl edit daemonset datadog-datadog
```

```
       - env:
        # DatadogのAPIキー(dd-agentと同じもの)
        - name: DDZK_KEY
          valueFrom:
            secretKeyRef:
              key: api-key
              name: datadog-datadog
        # iptablesルールの適用対象ネットワークインタフェース名
        # 近頃のcniに対応したKubernetsの場合、docker0とかflannel0ではなくこのcni0になるはず
        - name: DDZK_HOST_INTERFACE
          value: cni0
        - name: DDZK_HOST_IP
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: status.podIP
        # Magic IPを何にするか
        - name: DDZK_MAGIC_IP
          value: 169.254.123.123
        # Datadog APMに送るSpanに付与するenvタグの値。メトリクスやログのenvタグと合わせましょう
        - name: DDZK_ENV
          value: test
        # 最新のタグを以下で確認してください
        # https://github.com/mumoshu/dd-zipkin-proxy/blob/master/example/build-docker.sh#L7
        image: mumoshu/dd-zipkin-proxy-solo:v0.0.20
        imagePullPolicy: Always
        name: dd-zipkin-proxy
        args:
        # 重要: この--datadog-reportingがないとdd-agent/Datadog APMへの転送がされません
        - --datadog-reporting
        ports:
        # 9441はZipkin APIデフォルトのポート番号です。Zipkin本体もこのポートをLISTENします
        - containerPort: 9441
          hostPort: 9441
          name: zipkinport
          protocol: TCP
        resources: {}
        # このコンテナからiptablesを実行してMagic IPをセットアップさせるために必要な権限
        securityContext:
          capabilities:
            add:
            - NET_ADMIN
          privileged: true
```

# Datadog APMにZipkin v1スパンを送る手順

## Goアプリでの対応例

ZipkinフォーマットのSpanを送信する方法は利用するライブラリや言語によって異なりますが、一例としてGolang + jaeger-client-goのコードを紹介します。

[jaeger-client-goでZipkinトランスポートを使う方法](https://qiita.com/mumoshu/items/ec2059ee065d8f35f17d)

トレース送信先のホスト名は、上記で設定したMagic IPの`169.254.123.123`にしましょう。

## トレースの確認

例として、[IstioのBookinfoサンプルアプリ](https://istio.io/docs/guides/bookinfo.html)のトレースをとってみます。
Bookinfoはproductpage, reviews, detailsの３つのマイクロサービスから構成されるサービスです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/5912/3d7d4002-cbc1-18e3-cf78-aec319773178.png)

Bookinfoをデプロイすると、AWSの場合はELBが作成されます。ELBのエンドポイントにブラウザでアクセスしてみると、以下のようにbookinfoアプリが動いていることがわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/5912/18d9bb48-34c5-4e64-e483-f0f7bfc40519.png)

何度かページをリロードして、dd-zipkin-proxyのログをみると、以下のようにトレースの送信が完了したことがわかります。

```
$ stern datadog -c dd-zipkin-proxy
...
datadog-datadog-m2vdp dd-zipkin-proxy [2017-12-13T13:16:57Z]  INFO datadog: Sending 132 spans in traces 14 traces
...
```

30秒ほど待つとDatadog APMに反映されます。

[Service(マイクロサービス)詳細ページ](https://app.datadoghq.com/apm/service/)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/5912/776181d7-1925-f0eb-c196-40cc8fc0fed0.png)

今回はサンプルが少ないのであまり雰囲気が伝わらないかもしれませんが、

- 合計リクエスト数の時間推移
- 合計エラー数の時間推移
- トランザクションの平均レイテンシの時間推移
- レイテンシの分布
- リソース(=リクエストURLやアプリケーションのトランザクション名)別の統計値

などが確認できます。

[Trace(トレース)一覧ページ](https://app.datadoghq.com/apm/search)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/5912/d32f9ab4-acd1-e5bb-fa20-57eafc065ca3.png)

サービス詳細や一覧から個別のトレースを選択すると、以下のようにトレースに関わる全てのスパンがグラフィカルに確認できます。トレースのどこのスパンで時間がかかったか、またはその親子関係が一目瞭然ですね。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/5912/ac674542-0c50-71b4-0cf9-58081a6ef6d0.png)

# その他、Datadog APMでできること

- Datadog Logsから特定のログを出力したサービスのトレース詳細にリンク
    - ログとトレースを紐付けてアラートの原因分析をしたいときに役立ちます
- トレースのエラーレートやレイテンシがしきい値を超えたらアラート
- 遅いトランザクションが記録されたホストのメトリクスページにリンク
    - 遅さの原因がホストの状態によるものかどうかの切り分けに有効
- トレースに関する統計値のグラフを、メトリクスのグラフと並べてダッシュボード化
- など

# 今後の課題と注意点

- dd-zipkin-proxyの公式実装がないので、下手すると自分でメンテすることになってしまいます。
- また、Magic IP方式がまだ広く使われていないせいか、ノウハウがあまりない点も注意
- システムのMovingPartは一つ増えてしまいます
    - それをよしとするかも、この方法を採用するかのポイントになりそうです

# まとめ

分散とレーシングシステムの４要素を理解すると、分散トレーシングシステムの「バックエンド」と「それ以外」は分けて選択できることに気づけます。

今回は、分散トレーシングシステムのバックエンドとしてDatadog APM、クライアントライブラリとしてOpenTracing Tracer、PropagationフォーマットとしてB3、SpanフォーマットとしてZipkin v1を採用することで、

- Zipkinの運用保守
- 枯れてないトレーサのバグや機能不足という悩み
- ログ・メトリクス・トレースそれぞれ別のサービスに見に行く手間

などから解放されました。
同じような悩みのある方は、ぜひ試してみてください！