memory_limiterはなぜ先頭で、batchはなぜ最後なのか
一言でいうと
コレクターの設定で、パイプラインに並べたプロセッサーの順序がそのままシグナルに適用される処理順序です。短い文ですが、運用で生む違いは大きいです。memory_limiterは先頭、batchは末尾、tail_samplingはbatchの前、機微情報の削除はメタデータを付けるプロセッサーの後です。
なぜ必要なのか
SDKがバックエンドへ直接送っても、動作はします。それでもコレクターを置く理由は5つあります。
- ポリシーを再デプロイなしで変更できます。サンプリング比率、属性フィルター、保持対象は、運用中に調整することになる値です。それがアプリの環境変数にあると、20個のサービスをロールアウトしなければなりません。
- アプリをバックエンド障害から分離します。バックエンドが遅くなるとSDKの送信キューが満杯になり、アプリのメモリが上がります。コレクターが前にあれば、その圧力を代わりに受け止めます。
- バックエンドを入れ替えられます。シグナルごとに別の宛先を使ったり、2つのバックエンドを並行運用したりすることが、設定ファイル1か所で完結します。
- 機微情報をアプリの外で消します。トークンやメールアドレスが属性に混ざる事故は、必ず起きます。コレクターに防御線を置けば、事故対応が再デプロイではなく設定変更になります。
- テールサンプリングができます。トレースの結果を見て判断するには、スパンが1か所に集まっている必要があり、その場所はアプリにはなれません。
どう動くのか
設定の最上位セクションは6つです。receivers、processors、exporters、connectors、extensions、service。重要なのは、前の5つにコンポーネントを定義しただけでは有効にならないという点です。service.pipelines(拡張はservice.extensions)から参照してはじめて、実際に動きます。設定を書き終えたのに何も起きない事故の半分は、ここから生まれます。
connectorsは、あるパイプラインの出力を別のパイプラインの入力につなぐコンポーネントです。トレースからREDメトリクスを取り出すspanmetricsが代表的です。エクスポーターであると同時にレシーバーでもあるため、独立したセクションを持ちます。
順序が生む違いを1つずつ見ます。
memory_limiterが先頭にある理由は、このプロセッサーがデータを減らすのではなく、メモリ圧迫を検知したときに、そもそも拒否して上流へbackpressureを返すためです。後ろに置くと、すでにパースと変換でメモリを使い切ったあとに拒否することになり、保護の効果が失われます。tail_samplingはbatchより前に置く必要があります。サンプリングの判断はトレース単位ですが、batchが先に適用されると、同じトレースのスパンが別々のバッチに散らばり、判断の対象が完全でなくなります。batchはほぼ常に最後です。バッチは送信効率のための段階なので、その後ろに何かを付けると、バッチをほどいてまた束ねる無駄が生じます。- 削除とハッシュ化は、順序を逆に考える必要があります。
k8sattributesのようにメタデータを付けるプロセッサーは新しい属性を作り出すことがあるため、消す処理はその後に置いてはじめて漏れなく適用されます。
memory_limiterの値の見積もりには慣例があります。
| コンテナのメモリ | limit_mib |
spike_limit_mib |
GOMEMLIMIT |
|---|---|---|---|
| 1Gi | 820 | 164 | 656MiB |
| 2Gi | 1638 | 328 | 1310MiB |
| 4Gi | 3276 | 655 | 2621MiB |
limit_mibはコンテナ上限のおよそ80%、spike_limit_mibはその20%です。コンテナの上限よりlimit_mibが高いと、プロセッサーが介入する前にカーネルが先にプロセスを強制終了します。
batchでよくある間違いは、send_batch_sizeを上限と勘違いすることです。この値は「これだけたまったらすぐ送れ」というトリガーで、実際のバッチサイズの上限はsend_batch_max_sizeです。後者を指定しないと、バッチが予想よりずっと大きくなることがあります。
現場での姿
症状は3つに分かれます。
データがまったく入ってこない場合です。一時的にdebugエクスポーターを付けて、レシーバーまで到達しているかを最初に確認します。受信メトリクスが0なら、アプリがまだ何も送っていないか、宛先を間違えて見ているかのどちらかで、このとき最もよくある原因が、gRPCとHTTPのポートを取り違えて書いたケースです。
入ってはくるのにバックエンドにない場合です。送信失敗メトリクスを最初に見て、失敗がないのに消えているなら、キューのメトリクスと拒否のメトリクスを見ます。ここでtail_samplingが原因であることがかなり多いです。ポリシーが意図より攻撃的だと、データは正常に送信されているのに、バックエンドに特定のトレースだけがない形になり、メトリクスは何の異常も報告しません。パイプラインから一時的に外して、再現するかを見るのが最も早い切り分け方です。
定期的に落ちる場合です。ほとんどがメモリ圧迫です。memory_limiterがすでにあるのに落ちるなら、上限がコンテナの制限と合っていないケースを疑います。
読むべきコレクター自身のメトリクスは、3段階に分かれます。受信はotelcol_receiver_accepted_spansとotelcol_receiver_refused_spansのペア、処理はotelcol_processor_incoming_itemsとoutgoing_itemsの比較、送信はotelcol_exporter_sent_spans、send_failed_spans、enqueue_failed_spans、そしてqueue_sizeとqueue_capacityの比率です。アラートを1つだけ設定するなら、キューのメトリクスを選びます。キューが容量に張り付いた状態が続くと、その次に来るのは、ほとんど常に拒否とデータの損失です。
次のラボですること
/root/otca-collector/config.yamlを1つ、最初から最後まで書きます。OTLPレシーバーの2つのポート、コンテナ上限の80%に設定したmemory_limiter、Kubernetesメタデータの付与、機微属性の削除とハッシュ化、トリガーと上限をどちらも指定したbatch、キューとリトライを有効にしたエクスポーター、health_checkとzpagesの拡張、そしてプロセッサーの順序を守った3つのパイプラインまでです。