コンテンツへスキップ

Knative、OpenTelemetry、Jaegerを用いた分散トレーシング

公開日:2021-08-30、改訂日:2024-01-17

Knative、OpenTelemetry、Jaegerを用いた分散トレーシング

著者:Ben Moss、ソフトウェアエンジニア @ VMware

システムの理解と診断を試みる際に、最初に習得する基本的なツールの1つはスタックトレースです。スタックトレースは、プログラムが実行しているロジックの流れを構造化されたビューで提供し、特定の状態に陥った経緯を理解するのに役立ちます。分散トレーシングは、この考え方をより高い抽象化レベルに適用し、プログラム間のメッセージの流れを可視化しようとする業界の取り組みです。

Knative Eventingは、近年多くの人が好む分散アーキテクチャを構築するためのビルディングブロックのセットです。ブローカー、トリガー、チャネル、フローを通じてプログラム間の接続を記述および組み立てするための言語を提供しますが、この強力な機能には、イベントのトリガー方法の特定が困難になるスパゲッティ状のコードを作成するリスクが伴います。この記事では、Eventingで分散トレーシングを設定する方法を説明し、プログラムの理解を深め、Eventingの内部動作についても少し説明します。

トレーシング環境の概要

トレーシングの方法を学ぶ際に最初に直面する問題の1つは、エコシステムの把握です。Zipkin、Jaeger、OpenTelemetry、OpenCensus、OpenTracingなど、無数の選択肢があります。どれを使用すべきでしょうか?朗報は、これらの最後の3つの「Open」ライブラリが、メトリクスとトレーシングの標準化を試みていることです。これにより、ストレージと可視化ツールをすぐに決める必要がなくなり、それらの間での切り替えが(ほとんど)問題なく行えるようになります。OpenCensusとOpenTracingはどちらも、トレーシングとメトリクスの断片化された状況を統一しようとして開始されましたが、結果として悲劇的/滑稽な新しい異なる競合する標準が誕生しました。OpenTelemetryは、OpenCensusとOpenTracingを統合しようとする最新の取り組みです。

xkcd comic "How Standards Proliferate"

現在のKnativeのトレーシングサポートはOpenCensusのみと互換性がありますが、OpenTelemetryコミュニティは、システムにおけるこのようなギャップを埋めるためのツールを提供しています。この記事では、OpenCensusとOpenTelemetryの組み合わせを通じてJaegerを使用することに重点を置きますが、使用するツールに関係なく、より広範な教訓は適用されるはずです。

はじめに

Knative ServingとEventingがインストールされたクラスタがあることを前提とします。クラスタがない場合は、Knativeクイックスタートを試してみることをお勧めしますが、理論的にはどのような設定でも機能するはずです。

Knativeをインストールしたら、OpenTelemetryオペレーターをクラスタに追加します。これはcert-managerに依存しています。これら2つのインストール時に注意すべき点として、cert-managerのWebhookポッドが起動するまで待つ必要があります。そうでないと、証明書の作成時に多くの「接続拒否」エラーが発生します。`kubectl -n cert-manager wait --for=condition=Ready pods --all`を実行すると、cert-managerの準備が完了するまでブロックされます。`kubectl wait`はデフォルトで30秒のタイムアウトを使用するため、イメージのダウンロード速度によっては、クラスタでより長い時間がかかる場合があります。

kubectl apply -f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml &&
kubectl -n cert-manager wait --for=condition=Ready pods --all &&
kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/download/v0.40.0/opentelemetry-operator.yaml

次に、Jaegerオペレーター(はい、もう1つのオペレーターです。これが最後だと誓います)を設定します。

kubectl create namespace observability &&
kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/crds/jaegertracing.io_jaegers_crd.yaml &&
kubectl create -n observability \
    -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/service_account.yaml \
    -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/role.yaml \
    -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/role_binding.yaml \
    -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/v1.28.0/deploy/operator.yaml

起動したら、次のコマンドを実行してJaegerインスタンスを作成できます。

kubectl apply -n observability -f - <<EOF
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: simplest
EOF

オペレーターポッドとJaegerポッド自体の起動を待つ必要があるため、スピンアップには時間がかかる場合があります。起動したら、JaegerオペレーターはJaegerのKubernetes Ingressを作成しますが、Kindで実行しているため、Ingressはインストールされていません。問題ありません。ポートフォワーディングで十分です。`kubectl -n observability port-forward service/simplest-query 16686`を実行すると、Jaegerダッシュボードがhttp://localhost:16686でアクセスできるようになります。

次に、OpenTelemetryコレクターを作成します。これは、プログラムからのトレースを受け取り、Jaegerに転送する役割を果たします。コレクターは、異なるプロトコルを使用するシステムを相互に接続できる抽象化です。Zipkinトレースのみをエクスポートする場合でも、コレクターを使用してJaegerが消費できる形式に変換できます。このコレクター定義は、OpenTelemetryオペレーターに、Zipkinインスタンスのようにトレースをリッスンするが、デバッグのためにログとJaegerインスタンスの両方にエクスポートするコレクターを作成するように指示します。

kubectl apply -f - <<EOF
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: otel
  namespace: observability
spec:
  config: |
    receivers:
      zipkin:
    exporters:
      logging:
      jaeger:
        endpoint: "simplest-collector.observability:14250"
        tls:
          insecure: true

    service:
      pipelines:
        traces:
          receivers: [zipkin]
          processors: []
          exporters: [logging, jaeger]
EOF

すべて正常であれば、`observability`名前空間に3つのポッド(Jaegerオペレーター、Jaegerインスタンス、OpenTelemetryコレクター)が実行されているはずです。

最後に、EventingとServingがすべてのトレースをコレクターに送信するように設定できます。

for ns in knative-eventing knative-serving; do
  kubectl patch --namespace "$ns" configmap/config-tracing \
   --type merge \
   --patch '{"data":{"backend":"zipkin","zipkin-endpoint":"http://otel-collector.observability:9411/api/v2/spans", "debug": "true"}}'
done

ここの`debug`フラグは、Knativeにすべてのトレースをコレクターに送信するように指示します。一方、実際のデプロイメントでは、代表的なトレースのサブセットのみを取得するためにサンプリングレートを設定する必要があるでしょう。

Hello, world?

トレースインフラストラクチャの展開と設定が完了したので、いくつかのサービスを展開して活用を開始できます。ハートビートイメージContainerSourceとしてデプロイし、すべてが正しく接続されていることをテストします。

kubectl apply -f - <<EOF
apiVersion: sources.knative.dev/v1
kind: ContainerSource
metadata:
  name: heartbeats
spec:
  template:
    spec:
      containers:
        - image: gcr.io/knative-nightly/knative.dev/eventing/cmd/heartbeats:latest
          name: heartbeats
          args:
            - --period=1
          env:
            - name: POD_NAME
              value: "heartbeats"
            - name: POD_NAMESPACE
              value: "default"
            - name: K_CONFIG_TRACING
              value: '{"backend":"zipkin","debug":"true","sample-rate":"1","zipkin-endpoint":"http://otel-collector.observability:9411/api/v2/spans"}'
  sink:
    uri: http://dev.null
EOF

現時点では、このコンテナはハートビートを存在しないドメインhttp://dev.nullに送信するだけなので、このポッドのログを確認すると、多くのDNS解決エラーが表示されます。しかし、`otel-collector`ポッドのログを調べると、サービスからのトレースを正常に受信していることがわかります。これは設定が機能していることを確認できますが、トレースの観点からはそれほど興味深いものではありません!ハートビートを受信するKnativeサービスを追加して、現実的なものにしていきましょう。

kubectl apply -f - <<EOF
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: event-display
spec:
  template:
    spec:
      containers:
        - image: gcr.io/knative-nightly/knative.dev/eventing/cmd/event_display:latest
          env:
            - name: K_CONFIG_TRACING
              value: '{"backend":"zipkin","debug":"true","zipkin-endpoint":"http://otel-collector.observability:9411/api/v2/spans"}'
EOF

ハートビートサービスを更新し、代わりにここにハートビートを送信するようにします。

kubectl apply -f - <<EOF
apiVersion: sources.knative.dev/v1
kind: ContainerSource
metadata:
  name: heartbeats
spec:
  template:
    spec:
      containers:
        - image: gcr.io/knative-nightly/knative.dev/eventing/cmd/heartbeats:latest
          name: heartbeats
          args:
            - --period=1
          env:
            - name: POD_NAME
              value: "heartbeats"
            - name: POD_NAMESPACE
              value: "default"
            - name: K_CONFIG_TRACING
              value: '{"backend":"zipkin","debug":"true","zipkin-endpoint":"http://otel-collector.observability:9411/api/v2/spans"}'
  sink:
    ref:
      apiVersion: serving.knative.dev/v1
      kind: Service
      name: event-display
EOF

これらのサービスが展開されたら、Jaegerダッシュボードに戻って確認すると、より興味深いトレースが表示されます。

Screenshot of the Jaeger UI of a traces

Jaegerの「システムアーキテクチャ」タブでは、トポロジのグラフも表示されます。そこには、アクティベーターという、ご存知かもしれないし、そうでないかもしれないコンポーネントも含まれています。

Screenshot of a Jaeger architecture diagram

これは、Knative ServingがKnativeサービスのネットワークパスに追加するコンポーネントで、サービスがリクエストを処理できない場合にリクエストをバッファリングし、オートスケーラーにリクエストメトリクスを報告します。また、私のクラスタでは約2msのわずかなオーバーヘッドを追加することもわかります。Knativeを設定して、さまざまなシナリオでアクティベーターをパスから除外することはできますが、それは別のブログ記事のトピックです :)

高度な設定

ハートビートサービスから直接ではなく、ブローカートリガーを介してメッセージを送信することで、トポロジをもう少し複雑にしてみましょう。イベント表示サービスにすべてのメッセージを転送するブローカーとトリガーを作成し、ハートビートサービスがブローカーを指すように再構成します。

kubectl apply -f - <<EOF
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
  name: heartbeat-to-eventdisplay
spec:
  broker: default
  subscriber:
    ref:
      apiVersion: serving.knative.dev/v1
      kind: Service
      name: event-display
---
apiVersion: eventing.knative.dev/v1
kind: Broker
metadata:
  name: default
---
apiVersion: sources.knative.dev/v1
kind: ContainerSource
metadata:
  name: heartbeats
spec:
  template:
    spec:
      containers:
        - image: gcr.io/knative-nightly/knative.dev/eventing/cmd/heartbeats:latest
          name: heartbeats
          args:
            - --period=1
          env:
            - name: POD_NAME
              value: "heartbeats"
            - name: POD_NAMESPACE
              value: "default"
            - name: K_CONFIG_TRACING
              value: '{"backend":"zipkin","debug":"true","zipkin-endpoint":"http://otel-collector.observability:9411/api/v2/spans"}'
  sink:
    ref:
      apiVersion: eventing.knative.dev/v1
      kind: Broker
      name: default
EOF

ここでJaegerに戻ると、はるかに複雑なトレースが表示されます。Eventingのインメモリブローカーからの多くのホップが、ハートビートとイベント表示間のメッセージのパスに追加されます。異なるブローカー実装を使用している場合、トレースは異なりますが、いずれの場合も、システムの柔軟性と能力を高めるために複雑さを追加しています。

Screenshot of a Jaeger trace with a broker and trigger

ここで、展開にもう一つの要素を追加しましょう。すべてのハートビートをイベント表示サービスに直接送信するのではなく、コインを投げて「表」が出た場合にのみ送信することにします。幸運なことに、私は数秘術理論に精通しており、このコイン投げマイクロサービスを既にコーディング済みなので、新しいKnativeサービスとしてデプロイできます。

kubectl apply -f - <<EOF
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: coinflip
spec:
  template:
    spec:
      containers:
        - image: benmoss/coinflip:latest
          env:
            - name: OTLP_TRACE_ENDPOINT
              value: otel-collector.observability:4317
---
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
  name: heartbeat-to-coinflip
spec:
  broker: default
  subscriber:
    ref:
      apiVersion: serving.knative.dev/v1
      kind: Service
      name: coinflip
  filter:
    attributes:
      type: dev.knative.eventing.samples.heartbeat
---
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
  name: heartbeat-to-eventdisplay
spec:
  broker: default
  subscriber:
    ref:
      apiVersion: serving.knative.dev/v1
      kind: Service
      name: event-display
  filter:
    attributes:
      flip: heads
EOF
このサービスでは、リリース候補のOpenTelemetryクライアントライブラリを使用してインストルメントし、gRPCプロトコルでエクスポートすることにしました。これらのトレースをJaegerに送るには、コレクターのOTLP(OpenTelemetry Protocol)レシーバーを有効にして、パイプラインに追加する必要があります。

kubectl apply -f - <<EOF
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: otel
  namespace: observability
spec:
  config: |
    receivers:
      zipkin:
      otlp:
        protocols:
          grpc:
    exporters:
      logging:
      jaeger:
        endpoint: "simplest-collector.observability.svc.cluster.local:14250"
        insecure: true

    service:
      pipelines:
        traces:
          receivers: [zipkin, otlp]
          processors: []
          exporters: [logging, jaeger]
EOF

新しいトリガー設定を見ると、ハートビートタイプのイベントをすべてコインフリップに送信するトリガーと、「flip: heads」拡張機能を持つすべてのイベントをイベント表示に送信するトリガーの2つのトリガーがあることがわかります。コインフリップサービスは受信したハートビートイベントを複製し、コインを投げて結果をCloudEvents拡張機能として追加し、無限ループを誤って発生させないようにイベントタイプも変更します。次に、このイベントをブローカーに送り返して再キューイングし、表の場合はイベント表示にディスパッチされ、裏の場合は破棄されます。

Jaegerインターフェースに戻ると、ハートビートトレースの長さが異なり、運悪く裏が出た場合は終了しますが、ジャックポットに当たり、イベント表示に転送されることもあります。イベント表示のログを調べると、イベントは依然として受信されていますが、以前よりも遅いレートであり、すべて「flip: heads」拡張機能が付いています。また、カスタムインストルメンテーションでコインフリップサービスから送信しているこれらのカスタムスパンも確認できます。

Screenshot of a span with custom metadata attached

Screenshot of a flow diagram from Jaeger

Jaegerのアーキテクチャ図から、何が起こっているのかを理解できます。イベントはハートビートサービスからブローカーを介して流れ、各トリガーに出力されます。トリガーのフィルタにより、最初はイベントはコインフリップサービスにのみ継続されます。コインフリップサービスは新しいイベントを返信し、ブローカーとフィルタを介して再び流れます。今回はコインフリップトリガーによって拒否されますが、イベント表示トリガーによって受け入れられます。

まとめ

これらすべてを通して、Knativeと優れた可観測性ツールの価値の両方について少し学ぶことができたことを願っています。異なるプロトコルを使用するシステムを統合し、それらをすべて共有されたJaegerインスタンスに送るために、OpenTelemetry Collectorをどのように活用できるかを確認しました。作成したトポロジは、ある意味では些細なことですが、現実のイベント駆動型システムをどのように構成できるかを示すのに十分な興味深く複雑なものだったと思います。可観測性とメトリクスのエコシステムは大きく、圧倒的に感じることもありますが、一度設定してしまえば、システムの理解とトラブルシューティングにおいて、非常に役立ちます。

リンク/参照

サイトトラフィックを理解するために、分析とCookieを使用しています。サイトの使用に関する情報は、その目的でGoogleと共有されます。詳細情報。