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を統合しようとする最新の取り組みです。
現在の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ダッシュボードに戻って確認すると、より興味深いトレースが表示されます。
Jaegerの「システムアーキテクチャ」タブでは、トポロジのグラフも表示されます。そこには、アクティベーターという、ご存知かもしれないし、そうでないかもしれないコンポーネントも含まれています。
これは、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のインメモリブローカーからの多くのホップが、ハートビートとイベント表示間のメッセージのパスに追加されます。異なるブローカー実装を使用している場合、トレースは異なりますが、いずれの場合も、システムの柔軟性と能力を高めるために複雑さを追加しています。
ここで、展開にもう一つの要素を追加しましょう。すべてのハートビートをイベント表示サービスに直接送信するのではなく、コインを投げて「表」が出た場合にのみ送信することにします。幸運なことに、私は数秘術理論に精通しており、このコイン投げマイクロサービスを既にコーディング済みなので、新しい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
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」拡張機能が付いています。また、カスタムインストルメンテーションでコインフリップサービスから送信しているこれらのカスタムスパンも確認できます。
Jaegerのアーキテクチャ図から、何が起こっているのかを理解できます。イベントはハートビートサービスからブローカーを介して流れ、各トリガーに出力されます。トリガーのフィルタにより、最初はイベントはコインフリップサービスにのみ継続されます。コインフリップサービスは新しいイベントを返信し、ブローカーとフィルタを介して再び流れます。今回はコインフリップトリガーによって拒否されますが、イベント表示トリガーによって受け入れられます。
まとめ¶
これらすべてを通して、Knativeと優れた可観測性ツールの価値の両方について少し学ぶことができたことを願っています。異なるプロトコルを使用するシステムを統合し、それらをすべて共有されたJaegerインスタンスに送るために、OpenTelemetry Collectorをどのように活用できるかを確認しました。作成したトポロジは、ある意味では些細なことですが、現実のイベント駆動型システムをどのように構成できるかを示すのに十分な興味深く複雑なものだったと思います。可観測性とメトリクスのエコシステムは大きく、圧倒的に感じることもありますが、一度設定してしまえば、システムの理解とトラブルシューティングにおいて、非常に役立ちます。