👣

Opentelemetry, Jaeger による k8s クラスタ Tracing

2023/11/03に公開

概要

Observability の 3 本柱として以下の要素があります。

  • Logging
  • Metrics
  • Tracing

以前の記事では Logging, Metrics を取り扱いました。
今回は 3 つめの Tracing に注目し、k8s クラスタ上で動作するコンポーネントの tracing data を収集する方法を実装します。

Tracing は Logging や Metrics と比べるとそこまで浸透していない印象はありますが (あくまで主観)、現代において主流となっているマイクロサービスアーキテクチャの運用において重要となっています。
Elasticsearch の説明で述べられているように、既存のシステムで Tracing を導入することは以下のようなメリットがあります。

https://www.elastic.co/jp/blog/distributed-tracing-opentracing-and-elastic-apm

レイテンシの追跡
1人のユーザーのリクエストまたはトランザクションは、異なるランタイム環境のさまざまなサービスを経由します。特定のリクエストに対する各サービスのレイテンシを把握することは、システム全体としての総合的なパフォーマンス特性を理解し、改善の可能性に関する貴重なインサイトを得るために不可欠です。

根本原因の分析
根本原因の分析は、マイクロサービスの大規模なエコシステム上に構築されているアプリケーションにとって、さらに大きな課題です。どのような問題がどのサービスで、どのようなタイミングで発生するか分かりません。分散トレーシングは、そのようなシステムにおける問題をデバッグする際にきわめて重要です。

OSS プロダクトで Tracing 収集基盤を構築する場合、 Opentelemetry collector (以下 otel collector) と Jaeger を使って収集、可視化を行う構成が主流であるため、今回はこれらのコンポーネントを k8s クラスタにインストールします。

Otel collector

クラスタ上のアプリケーションから tracing データを収集・加工するには otel collector を使用します。

https://opentelemetry.io/docs/collector/

ドキュメントにいくつか記載されている方法のうち、今回は Operator を使用する方法 で collector をインストールします。

はじめに今回の作業でインストールされる一連のコンポーネントを分離するため tracing namespace を作成しておきます。

kubectl create namespace tracing

ドキュメントでは Github release で公開されている otel operator マニフェストをクラスタに適用する方法となっています。ただしデフォルトでは opentelemetry-operator-system という namespace を作成しそこに CRD 等を定義する設定になっています。インストール先を tracing namespace に変更するには、いったんマニフェストをローカルでダウンロードし、エディタ等で namespace: opentelemetry-operator-system となっている箇所を tracing に置換して apply します。

# ローカルにダウンロード
wget https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml -O opentelemetry-operator.yaml
# editor 等で編集

# 適用
kubectl apply -f opentelemetry-operator.yaml

kind: OpenTelemetryCollector で指定される otel collector インスタンスと呼ばれる CR を作成することで otel collector pod をクラスタにデプロイできます。
マニフェスト例は上記のドキュメントに記載されていますが、基本的には spec.config 以下に collector の receivers, processors, exporters の設定を記載します。これは otel collector 内部で使用される config ファイルとなっており、デプロイ時に configmap として作成され pod にマウントされます。
また、tracing の送信先に指定されている jaeger は次の段階で作成します。

apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: otel-collector
  namespace: tracing
spec:
  config: |
    receivers:
      otlp:
        protocols:
          grpc:
          http:
    processors:
      memory_limiter:
        check_interval: 1s
        limit_percentage: 75
        spike_limit_percentage: 15
      batch:
        send_batch_size: 10000
        timeout: 10s
    exporters:
      # NOTE: Prior to v0.86.0 use `logging` instead of `debug`.
      logging:
        verbosity: detailed
      otlp:
        endpoint: jaeger-collector:4317
        tls:
          insecure: true
      otlphttp:
        endpoint: http://jaeger-collector:4318
        tls:
          insecure: true
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [logging,otlp,otlphttp]

デプロイすると collector に対応する pod や service が作成されます。

NAME                                            READY   STATUS    RESTARTS   AGE
pod/otel-collector-collector-768f67fcfc-f2whp   1/1     Running   0          3d7h

NAME                                          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
service/otel-collector-collector              ClusterIP   10.105.119.177   <none>        4317/TCP,4318/TCP   10d
service/otel-collector-collector-headless     ClusterIP   None             <none>        4317/TCP,4318/TCP   10d
service/otel-collector-collector-monitoring   ClusterIP   10.103.168.241   <none>        8888/TCP            10d

NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/otel-collector-collector   1/1     1            1           10d

NAME                                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/otel-collector-collector-768f67fcfc   1         1         1       10d

Jaeger

otel collector は他のアプリケーションから送信されたデータを受信・加工して他の backend に送信するコンポーネントであるので、単体では tracing data の可視化はできません。
tracing を可視化するための backend はいくつかありますが、ここでは jaeger を使用します。

https://www.jaegertracing.io/

operator のインストール

jaeger も otel collector と同様、operator をインストールした後 jaeger 本体をインストールします。
インストールはドキュメントの Installing the Operator on Kubernetes にそって、github release にある最新の マニフェストをクラスタに適用します。

Instance 作成

operator をインストールした後は、kind: Jaeger の jaeger instance リソースを作成することで jaeger の運用に必要なコンポーネントが作成されます。

jaeger instance には Deployment strategy というプロパティがあり、使用可能なものは以下となっています。

  • AllInOne: 全てのコンポーネントを 1 つの pod で動作させる。収集した tracing データは pod 内で保存され永続されない。
  • Production: データを外部のストレージバックエンドに保存して永続化させる。
  • Streaming: Production の場合に加えて jaeger とストレージバックエンド間に kafka 等のストリーミング機能を配置する。大規模な環境で有効。

AllInOne はデータが永続化されないので開発目的にのみ適しています。
ここでは収集した tracing データを外部の elasticsearch に保存するため production で作成します。

apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: jaeger
  namespace: tracing
spec:
  strategy: production
  ingress:
    enabled: false
  storage:
    type: elasticsearch
    options:
      es:
        server-urls: http://elastic.centre.com:9201
        index-prefix: tracing

インストールが完了すると以下のようなサービスが作成されます。

NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                              AGE
jaeger-agent                ClusterIP   None             <none>        5775/UDP,5778/TCP,6831/UDP,6832/UDP,14271/TCP                        3d
jaeger-collector            ClusterIP   10.105.212.124   <none>        9411/TCP,14250/TCP,14267/TCP,14268/TCP,14269/TCP,4317/TCP,4318/TCP   3d
jaeger-collector-headless   ClusterIP   None             <none>        9411/TCP,14250/TCP,14267/TCP,14268/TCP,14269/TCP,4317/TCP,4318/TCP   3d
jaeger-query                ClusterIP   10.98.137.111    <none>        16686/TCP,16685/TCP,16687/TCP                                        3d

各サービスは pod 内の各コンポーネントへのエンドポイントになっています。

  • jaeger-agent: 非推奨となっており、otel-collector と機能がかぶっているので基本的に使用しない。
  • jaeger-collector: otel collector が収集・加工したコンポーネントを送信する際のエンドポイントとして使用。
  • jaeger-query: 保存したデータを web UI 等で検索する際に使用。

また、jaeger-query port 16686 にアクセスすることで jaeger web UI が閲覧できます。ここで jaeger collector が受信し backend storage に保存された trace を閲覧できます。

Grafana との連携

Grafana はデフォルトで jaeger をサポートしているため、datasource に jaeger-query の url を指定することで grafana UI から jaeger の trace を参照することができます。

確認できるデータは jaeger とほぼ変わらないため、grafana の UI が好きな場合はこっちで見てもいいかも知れません。


grafana で trace を見た様子。service, operation, tag で検索できる点や trace, span を確認できる点は jaeger UI と同じ。

Tracing の収集

Tracing を収集・可視化するための基盤を構築したので、あとはアプリケーションから Tracing データを収集します。
自作のアプリケーションの場合、主に以下の方法で tracing データを otel collector に送信することができます。

  • アプリケーション内で opentelemery SDK を使って tracing データを生成する箇所をコード内に記述する。
  • otel operator の Auto-instrumentation の機能を利用する。

一方で、自作アプリケーション以外のプロダクト(例えば OSS プロダクトなど)から tracing データ収集を考える場合、まずそのコンポーネントが tracing に対応している必要があります。
対応している場合、大抵は tracing データを otel collector やその他 backend に送信する手順が記載されているため、それに従って構成していく流れとなります。
ここでは例として、以前の記事で使用したいくつかの OSS コンポーネントの tracing データを収集・可視化し、どのようなデータが取れるかについて見ていきます。

Thanos

thanos は k8s クラスタのメトリクスを収集する記事 において prometheus と組み合わて使用しました。
thanos は tracing データの収集に対応しており、クエリを実行してメトリクスを収集する際の内部の API 実行履歴などを可視化することができます。

https://thanos.io/tip/thanos/tracing.md/

tracing を有効化するためには、thanos query, thanos store の実行時引数に tracing データ送信先の otel collector の設定等を指定します。
ここでは同クラスタ上で稼働する otel collector にデータを送信するよう store, query の deployment マニフェストに設定を追加して再デプロイします。

thanos-query.yml
spec:
  template:
    spec:
      containers:
      - args:
        - --http-address=0.0.0.0:19192
        ...
+        - |
+          --tracing.config=type: OTLP
+          config:
+            client_type: grpc
+            service_name: thanos-query
+            project_id: myproject
+            sample_factor: 16
+            insecure: true
+            endpoint: otel-collector-collector.tracing:4317
thanos-store.yml
spec:
  template:
    spec:
      containers:
      - args:
        - store
        ...
+        - |
+          --tracing.config=type: OTLP
+          config:
+            client_type: grpc
+            service_name: thanos-store
+            project_id: myproject
+            sample_factor: 16
+            insecure: true
+            endpoint: otel-collector-collector.tracing:4317

デプロイ後は thanos-query, store に関する様々な tracing データが otel collector を通じて jaeger に記録されます。
以前の記事では Grafana を使ってクラスタのメトリクスを可視化しました。この裏では thanos query が sidecar や store に対して promQL を実行してメトリクスを取得しているため、 その実行記録やクエリにかかる時間などを確認することができます。
例えば、時刻範囲を指定して特定のメトリクスを表示する際は以下のように query_range API が実行されるため、その記録が jaeger UI 上で確認できます。


query_range の実行記録。実行にかかった時間等が確認できる。

また、span の内容から実際に実行されたクエリの中身の確認もできます。


実行した promQL の中身が確認できる

Tekton

クラウドネイティブの CI/CD プラットフォームである Tekton は ローカル環境に簡易 CI/CD 環境を構築して試す tekton 編 の記事で使用しました。
tekton はつい最近の 2023/9 にリリースされた v0.52.0 で tracing の config に対応しました。

https://github.com/tektoncd/pipeline/releases/tag/v0.52.0

🔨 Add configmap for tracing config (#6897)
Tracing endpoint configuration is now moved from environment variable to the configmap config-tracing. Tracing can be now configured dynamically without needing to restart the controller. Refer the example configuration provided as part of the ConfigMap for the configuration options and format.

tekton tracing では task や pipeline を実行した際、各 task の処理にどれくらいの時間がかかっているか等のデータを可視化できます。

Tracing 有効化の設定

Github に tracing データの送信設定についての記載がありますが、ドキュメントにはまとまってなさそうです。現時点では thrift 形式で jaeger collector に送信する設定のみ対応しており、OTLP で otel collector には送信できない模様。

tekton pipeline をインストールした際に config-tracing という configmap が作成されますが、この中に tracing 送信先のエンドポイントを追加すると有効化されます。
ここでは同クラスタ上の jaeger collector の thrift のエンドポイント (port 14268) を指定します。

config-tracing
apiVersion: v1
data:
+  enabled: "true"
+  endpoint: http://jaeger-collector.tracing.svc.cluster.local:14268/api/traces
kind: ConfigMap

設定後は以下の service 名で tracing データが収集されます。

  • taskrun-reconciler
  • pipelinerun-reconciler

確認

operation はいろいろありますが、今回は試しに以前作成した pipeline を実行する時の動作を確認します。
以前の記事で作成した pipeline は以下の 2 つのタスクで構成されています。

  • gitlab から Dockerfile を含むソースコードを pull
  • Dockerfile からイメージをビルドし、registry に push

この pipeline を実行した際にかかった時間は tekton CLI でも確認できます。今回の実行では 4 分 29 秒かかりました。

$ tkn pipelinerun list  -A
NAMESPACE          NAME              STARTED      DURATION   STATUS
workspace-tekton   build-image       1 hour ago   4m29s      Succeeded

一方、jaeger UI ではこの pipeline 内での API 実行記録を span として確認できます。こちらで確認できる pipeline の時間は 4 分 31 秒となっており、微妙に上記と差がありますがだいたい同じなっています。

trace の中身を見ていくと、createTaskRun の記録が 2 つ確認できます。これが pipeline 内の各タスクを実行する taskrun オブジェクトの作成 API に対応しており、Start time で実行時刻が確認できます。
はじめのタスクは pipeline 実行がトリガーされてから 20.8 msec 後に実行されており、2 つめは 10.61 sec となっています。


これより、1 つめのタスクは 約 10 秒程度で完了していることがわかります。一方、pipeline 全体の実行時刻は 4 分 31 秒 なので 2 つめのタスク完了に 4 分程度かかっていたことがわかります。1 つめのタスクは git clone するだけなので短時間で完了したが、2 つめのタスクのイメージビルドに時間がかかっていたということが tracing データから判別できます。

直近の実行記録であれば pod の実行ログやイベントからも確認できますが、内部 API のどの箇所で時間がかかっているのか、また、運用していく内に全体の実行時刻が増えてくるような場合が発生した際に、過去の実行記録と比較してどの箇所で実行時間が増大しているのかを検証するといった点で tracing データを活用することができます。

アーキテクチャについて

最後に otel collector と jaeger を使ったアーキテクチャについて見ておきます。
k8s クラスタで tracing データを収集する際のアーキテクチャは、jaeger のドキュメント にわかりやすい図が記載されています。


https://www.jaegertracing.io/docs/1.50/architecture/#with-opentelemetry-collector より引用

上記の 2 つのアーキテクチャの違いは、左が otel collector をアプリケーションと同じ pod に sidecar として注入する構成であるのに対し、右は otel collector と jaeger collector をアプリケーションから分離する構成となっています。今回は otel collector と jaeger collector を operator を介してそれぞれ別 pod に構築しているので右の構成になっています。なお、それぞれの構成のメリット、デメリットについては上記ドキュメントに書かれています。

おわりに

otel collector と jaeger をインストールして k8s クラスタの tracing データ収集基盤を構築しました。tracing は導入してもすぐに役立つということはあまりないですが、特にマイクロサービスアーキテクチャのような多数のコンポーネントが分散する環境においてボトルネックとなる箇所の特定や改善など、長期的な運用を考える際に重要となってきます。

Discussion