Skyscanner が実践する OpenTelemetry のスケーリング: 24 の本番クラスターにまたがる Collector の管理

Developer Experience SIG は、さまざまな業界や規模の企業における実際の OpenTelemetry デプロイメントを紹介するブログ記事シリーズを公開しています。 この記事では、スコットランドのエジンバラに拠点を置くグローバルな旅行検索プラットフォーム Skyscanner を取り上げます。

全世界で 1,400 人の従業員を擁し、24 の本番 Kubernetes クラスターで 1,000 以上のマイクロサービスを運用している Skyscanner の OpenTelemetry 導入事例は、大規模な組織にとって貴重な教訓を提供します。

組織構造

6 名のプラットフォームエンジニアで構成される Hubble チームが、Skyscanner の Collector の大部分を管理しています。 より大きなプラットフォームエンジニアリング組織の一部として、主に Java ベースのマイクロサービスアーキテクチャを実行するコンピューティングプラットフォームを担当しています。

サービスチーム自体は、デプロイメントやテレメトリー収集のインフラストラクチャから抽象化されています。 Java サービスの場合、チームは事前設定された OpenTelemetry Java エージェントを含むベース Docker イメージを継承します。 Python および Node.js サービスの場合、プラットフォームチームが環境やリソース属性に基づいた適切なデフォルト値を設定するラッパーライブラリを提供しています。 これらのアプローチにより、ボイラープレートのセットアップが最小化され、サービスチームは OpenTelemetry の深い知識を必要とせずに、すぐにオブザーバビリティを利用できます。

OpenTelemetry の導入

Skyscanner の OpenTelemetry 導入は 2021 年に始まりました。 当時、同社は社内で構築したオープンソーススタックから商用ベンダーへの移行を進めていましたが、ベンダーロックインを避けたいと考えていました。

「ベンダーに依存しない方法でベンダーに移行したかったのです」と、Skyscanner の Hubble プラットフォームチームのソフトウェアエンジニアである Neil Fordyce は説明しました。

このベンダーに依存しないアプローチにより、テレメトリーインフラストラクチャの中核として OpenTelemetry Collector を採用することになりました。

アーキテクチャ: 集中ルーティング、分散収集

Skyscanner の Collector アーキテクチャは、Istio ベースのインテリジェントルーティングを備えた中央 DNS エンドポイントを特徴としています。 サービスがグローバルのどこで実行されていても、どのクラスターにあっても、テレメトリーはこの単一のアドレスに送信されます。 Istio がリクエストを最も近い利用可能な Collector にルーティングします。

デプロイメントは 2 つの異なる Collector パターンで構成されています。

Gateway Collector(Replica Set): 大部分のサービスからの大量の OTLP トラフィック(トレースとメトリクス)を処理し、処理の大半がここで行われます。

Agent Collector(DaemonSet): まだネイティブに OTLP をサポートしていないオープンソースやプラットフォームサービスから Prometheus エンドポイントをスクレイピングします。

Skyscanner アーキテクチャ図

設定: シンプルに始めて段階的に進化

2021 年に Skyscanner が初めて Collector をデプロイしたとき、設定はごくシンプルなものでした。 メモリリミッター、バッチプロセッサー、そしてトレース用の OTLP エクスポーターだけでした。

時間とともに設定は有機的に進化しました。 メトリクスパイプラインの追加、Istio スパンの取り込みの統合、スパンからメトリクスへの変換の実装、そしてノイズ削減とコスト管理のためのフィルタープロセッサーの追加が行われました。

Istio サービスメッシュのスパンをプラットフォームメトリクスに変換

Skyscanner の Collector の最も革新的な活用法の 1 つは、Istio サービスメッシュのスパンからメトリクスを生成することです。

Istio のネイティブメトリクスは、Prometheus デプロイメントを圧倒するカーディナリティの爆発的増加の問題を抱えていました。 さらに、Skyscanner はコードを所有していない多くの既製サービスを運用しており、それでも一貫したメトリクスが必要でした。

彼らの解決策は次のとおりです。 Istio がスパンを出力するように設定し(元々は Zipkin フォーマットでしたが、現在 Istio は OTLP をサポートしています)、Zipkin レシーバーを使って Collector に取り込み、セマンティック規約に合うように変換し、span metrics connector を使用して、アプリケーション計装なしで一貫したメトリクスを生成します。

「アプリケーションオーナーがコードに計装を追加する必要なく、プラットフォームレベルでそれを実現できるのです」と Neil は述べました。

span metrics connector の設定は、スパンから主要なディメンションを抽出します。

connectors:
  spanmetrics:
    aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
    dimensions:
      - name: http.status_code
      - name: grpc.status_code
      - name: rpc.service
      - name: rpc.method
      - name: prot
      - name: flag
      - name: k8s.deployment.name
      - name: k8s.replicaset.name
      - name: destination_subset
    dimensions_cache_size: 15000000
    histogram:
      exponential:
        max_size: 160
      unit: ms
    metrics_flush_interval: 30s

Collector はこれらのメトリクスを http.client.durationhttp.server.duration などのセマンティック規約名に変換し、クラスター、サービス名、HTTP ステータスコードごとに集約します。 これにより、コード変更なしですべてのサービスにプラットフォームレベルの HTTP メトリクスが提供され、セマンティック規約に準拠した一貫した命名規則が実現し、ネイティブの Istio メトリクスよりも低いカーディナリティが得られます。

404 エラーの課題

Collector の設定に関する注目すべき課題の 1 つは、キャッシュサービスがキャッシュにエントリが存在しないことを示すために HTTP 404 を返すケースでした。 Collector はこれらの 404 をエラーとして扱い、実際には正常で大量に発生する動作に対して 100% のトレースサンプリングをトリガーしていました。

解決策は、これらの特定の 404 レスポンスのエラーステータスを解除するフィルタープロセッサーを追加することでした。

processors:
  span/unset_cache_client_404:
    include:
      attributes:
        - key: http.response.status_code
          value: ^404$
        - key: server.address
          value: ^(service-x\.skyscanner\.net|service-y\.skyscanner\.net|service-z\.skyscanner\.net|service-z-\w{2}-\w+-\d\.int\.\w{2}-\w+-\d\.skyscanner\.com)$
      match_type: regexp
      regexp:
        cacheenabled: true
        cachemaxnumentries: 1000
    status:
      code: Unset

このプロセッサーは、特定のキャッシュサービスからの 404 ステータスコードを持つスパンにマッチし、エラーステータスを解除することで、エラーベースのサンプリングがトリガーされるのを防ぎます。

「最初からフィルタープロセッサーがあれば、より高品質で使いやすいトレースが得られたでしょう」と Neil は振り返りました。

ただし、Neil は最近導入された OpenTelemetry SDK の宣言的設定により、このようなフィルタリングは中央の Collector 設定を変更するのではなく、サービスチーム自身が分散的に設定できるようになったと指摘しています。

設定の詳細

Skyscanner は、これらのパターンを実践的に理解してもらうために、本番の Collector 設定を共有しています。

Gateway Collector

Gateway Collector は処理の大部分を担当します。

  • サービスからの OTLP メトリクスとトレース、および Istio からの Zipkin スパンを受信
  • span metrics connector を使用して Istio スパンからメトリクスを生成
  • 広範な transform プロセッサーを使用して Istio 属性をセマンティック規約にマッピング
  • キャッシュサービスに対する 404 フィルタリングロジックを実装
  • メトリクスとトレースを OTLP 経由でオブザーバビリティベンダーにエクスポート

この図は、OTLP メトリクスとトレース、および Istio スパンがこれらの Gateway Collector にどのように到達するかを示しています。

Skyscanner アーキテクチャ(Gateway Collector)図

Agent Collector

Agent Collector は、各ノードからインフラストラクチャおよびプラットフォームレベルのメトリクスを収集することに特化しています。

  • さまざまなソース(node exporter、kube-state-metrics、kubelet)から Prometheus エンドポイントをスクレイピング
  • 最小限の処理を実行(メモリ制限、バッチ処理、属性のクリーンアップ)
  • メトリクスを OTLP 経由でオブザーバビリティベンダーにエクスポート

計装戦略

Skyscanner の Java 中心の環境は、OpenTelemetry の自動計装機能から大きな恩恵を受けています。 ベース Docker イメージに事前設定された Java エージェントが、HTTP および gRPC スパンの生成をすぐに利用できる形で提供します。

自動計装に対する明確な方針

チームは自動計装に対して意図的に明確な方針を持ったアプローチを取っています。 デフォルトですべてを有効にするのではなく、逆のアプローチを取ります。 共有ベース Docker イメージですべての計装を無効にし、厳選されたセットのみを明示的に有効にします。

「逆のアプローチなんです。すべてを無効にしてから、必要なものだけを有効にします」と Neil は説明しました。

ベースイメージの環境変数を使用して、Skyscanner はランタイム、HTTP、gRPC 関連の計装のうち、厳選されたセットをデフォルトで有効にしています。 これには、JAX-RS、gRPC、Jetty、一般的な HTTP クライアント、エグゼキューター計装、およびロギングコンテキスト伝搬が含まれます。 サービスチームはこれらのデフォルトを自動的に継承しますが、必要に応じて独自のサービス定義でオーバーライドしたり、追加の計装を有効にしたりすることもできます。

このモデルにより、数百のサービス間で一貫性が確保されると同時に、エッジでの柔軟性も維持されます。

Java エージェントの設定

以下のスニペットは、共有 Java ベースイメージの例です。 OpenTelemetry Java エージェントをイメージにバンドルし、組織全体のデフォルトを設定し、共通のランチャースクリプトをインストールします。

# OpenTelemetry Java エージェントのソースとして使用するイメージ
FROM ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.25.0 AS otel

# すべての Java マイクロサービスが拡張する共通ベースイメージを定義
FROM image/registry/public-java-image:x.y.z

# OTel イメージから OpenTelemetry Java エージェントをコピー
COPY --from=otel /javaagent.jar $OPEN_TELEMETRY_DIRECTORY/opentelemetry-javaagent.jar
ENV OTEL_AGENT=$OPEN_TELEMETRY_DIRECTORY/opentelemetry-javaagent.jar

# 組織全体で適切なデフォルトを設定
ENV OTEL_METRICS_EXPORTER="otlp"
ENV OTEL_TRACES_EXPORTER="otlp"
ENV OTEL_LOGS_EXPORTER="none"
ENV OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE="DELTA"
ENV OTEL_EXPERIMENTAL_METRICS_VIEW_CONFIG="otel-view.yaml"
ENV OTEL_EXPORTER_OTLP_ENDPOINT="http://otel.skyscanner.net"
ENV OTEL_INSTRUMENTATION_COMMON_DEFAULT_ENABLED="false"
ENV OTEL_INSTRUMENTATION_RUNTIME_TELEMETRY_ENABLED="true"
ENV OTEL_INSTRUMENTATION_ASYNC_HTTP_CLIENT_ENABLED="true"
ENV OTEL_INSTRUMENTATION_APACHE_HTTPCLIENT_ENABLED="true"

COPY run.sh /usr/bin/run.sh

そのランチャースクリプト run.sh は、デプロイメントが提供する環境変数から -javaagent フラグと otel.resource.attributes を構築します。

# これは OTel のリソース属性をセットアップするために使用します
# サービスの起動時に環境変数から検出できるものを設定します
# これらの変数はデプロイメントシステムによって自動的に設定されます
# 繰り返しを避けるため、一部の環境変数は省略しています
setup_otel_agent() {
   if [[ -n "$AWS_REGION" ]]; then CLOUD_REGION="cloud.region=${AWS_REGION},"; else CLOUD_REGION=""; fi
   if [[ -n "$AWS_ACCOUNT" ]]; then CLOUD_ACCOUNT_ID="cloud.account.id=${AWS_ACCOUNT},"; else CLOUD_ACCOUNT_ID=""; fi
   if [[ -n "$CLUSTER_NAME" ]]; then K8S_CLUSTER_NAME="k8s.cluster.name=${CLUSTER_NAME},"; else K8S_CLUSTER_NAME=""; fi
   if [[ -n "$SERVICE" ]]; then SERVICE_NAME="service.name=${SERVICE}"; else SERVICE_NAME=""; fi
   echo -n "-javaagent:$OTEL_AGENT" \
       "-Dotel.resource.attributes=${CLOUD_REGION}${CLOUD_ACCOUNT_ID}${K8S_CLUSTER_NAME}${SERVICE_NAME}"
}

JAVA_OPTS="-D64 -server -showversion $(setup_otel_agent) ${ADDITIONAL_JAVA_OPTS:-}"

exec java $JAVA_OPTS "$@"

最後に、個別のサービス Dockerfile は同じベースを拡張し、そのサービスが必要とする追加の計装のみを追加します。

FROM image/registry/skyscanner-java-base:x.y.z

COPY my-service.jar

# my-service がデフォルト以外の計装を有効にしたい場合に簡単に拡張できます
ENV OTEL_INSTRUMENTATION_OPENAI_ENABLED=true
ENV OTEL_INSTRUMENTATION_OKHTTP_ENABLED=true

CMD exec /usr/bin/run.sh -jar my-service.jar server

スパンは有効、メトリクスは無効(デフォルト)

Skyscanner の戦略で特に興味深いのは、メトリクスとトレースの扱い方です。 HTTP および gRPC の計装は有効になっていますが、チームは SDK が生成する HTTP および RPC メトリクスの大部分を意図的に破棄しています。 これは、前述のとおり、Istio サービスメッシュのスパンからすでに一貫した低カーディナリティのプラットフォームメトリクスを生成しているためです。

計装を完全に無効にすると(スパンも削除されてしまうため)、かわりに OpenTelemetry SDK のビューを使用してメトリクスの集約を破棄しつつ、トレーシングは維持しています。

  • HTTP および RPC メトリクスはグローバルに破棄
  • スパンは通常どおり出力を継続
  • サービスチームは、Istio が提供するものを超えた追加の粒度が必要な場合、特定のメトリクス(たとえばサーバーサイドのレイテンシ)を選択的に再有効化できる

チームが SDK メトリクスを再有効化する場合、既存の Istio 由来のメトリクスとの衝突や二重カウントを避けるために、メトリクス名を変更することがよくあります。

先に示した Java ベースイメージでは、OTEL_EXPERIMENTAL_METRICS_VIEW_CONFIG が Skyscanner のデフォルトの otel-view.yaml を指しており、view file configuration を使用しています。

# Skyscanner のデフォルトのメトリクスビュー設定
# OTEL_EXPERIMENTAL_METRICS_VIEW_CONFIG が指すファイルに保存
# Istio からのメトリクスがすでにあるため、http と rpc のメトリクスを破棄
# 計装を無効にするのではなく、トレーシングは引き続き動作させたい
- selector:
    instrument_name: http.*
  view:
    aggregation: drop
- selector:
    instrument_name: rpc.*
  view:
    aggregation: drop

サービスが特定のメトリクスを保持する必要がある場合、同じファイルを拡張できます。 一般的なユースケースは、http.route ごとにリクエストを分類することです。

# この破棄動作は、リストにさらにビューを追加して
# 明示的に保持するメトリクスを選択することで変更できます。
# たとえば、http.server.request.duration メトリクスを保持しつつ、
# http.client.* メトリクスは引き続き破棄する場合
- selector:
    instrument_name: http.server.request.duration
  view:
    # Istio メトリクスに http.server.request.duration という名前がすでにあるため、
    # 衝突や二重カウントを避けるためにリネーム
    name: app.http.server.request.duration
    attribute_keys:
      - http.request.method
      - http.route
      - http.response.status_code

このアプローチにより、Skyscanner は価値の高い分散トレースを維持し、メトリクスの重複を回避し、カーディナリティを制御し、取り込みコストを削減できます。これらすべてが、サービスオーナーが OpenTelemetry の内部を深く理解する必要なく実現されています。

全体として、この戦略は強力なプラットフォームの考え方を反映しています。 大規模に機能する適切なデフォルトを提供し、ノイズを最小限に抑え、「正しいこと」を簡単に行えるようにしながらも、高度なニーズを持つチームがさらに先に進む余地を残しています。

デプロイメントとリリース管理

Skyscanner は、必要なものがすべて含まれていたため、OpenTelemetry Collector Contrib ディストリビューションを採用しました。 チームはインタビュー中に Contrib が本番環境での使用は推奨されていないことを知り、必要なコンポーネントのみを含むカスタム Collector イメージの構築を検討する予定です。

Skyscanner は約 6 か月ごとに Collector を更新しますが、特定の機能や重要な修正を追跡している場合は、より頻繁にアップグレードします。 RSS フィードや CNCF Slack チャンネルをフォローしてリリース情報を把握しています。

ロールアウト戦略では、クラスターティア間での段階的プロモーションを採用しています。 Dev クラスター、次に 3 つの Alpha 本番クラスター、続いて 8 つの Beta 本番クラスター、最後に残りの 13 本番クラスターの順です。 Argo CD を使用してデプロイし、ティア間の変更はプルリクエストを通じてプロモートされます。

「開発テストクラスターで問題を起こしてから、先に進む前に修正したことは間違いなくあります」と Neil は言いました。

この段階的なアプローチにより、本番環境に到達する前に設定の問題を検出できています。 OpenTelemetry Collector デプロイメントの自動テストやロールバック機能はまだありませんが、これらの改善は将来の計画に含まれています。

うまくいっていること

本番環境で OpenTelemetry を導入して以来、チームの体験は非常にポジティブなものでした。

「正直なところ、かなり問題なく進んでいます」と Neil は振り返りました。

柔軟性が Collector の最大の強みとして際立っています。

「実現しようとしたことはすべて実際に提供できました。それがいかに柔軟であるかを物語っていると思います」と Neil は説明しました。

その他のハイライトとしては、OTLP プロトコルがシンプルな設定によるベンダー独立性を提供していること、明確でよく整理されたリリースノート、そしてチームメンバーが Collector コンポーネントのメモリリークを発見して修正をコントリビュートした際のコミュニティの対応力が挙げられます。

教訓とペインポイント

Skyscanner は一部のパイプラインで古い不安定な HTTP セマンティック規約をまだ使用しています。 アップグレードには、Istio 属性をセマンティック規約名にマッピングする複数の transform プロセッサールールの更新が必要であり、ドキュメントを手動で照合して設定文字列を入力する作業が伴います。

チームはセマンティック規約管理のための Weaver を認識していますが、まだワークフローに統合していません。

6 か月ごとにアップグレードするということは、一度に複数の破壊的変更に遭遇することを意味します。 リリースノートはよく書かれており変更内容が明確に文書化されていますが、6 か月分の更新を一度にレビューするのは、リリースペースに追従する場合と比べて摩擦が増えます。

他の方へのアドバイス

本番環境での経験に基づき、Skyscanner チームは次のアドバイスを提供しています。

  • シンプルに始める: メモリリミッター、バッチプロセッサー、基本的なエクスポーターだけから始めましょう。 必要に応じて複雑さを追加してください。
  • 初日からメモリリミッターを設定: スケーリングに伴うメモリの問題を防ぐために、すぐに設定しましょう。
  • フィルタープロセッサーを早期に検討: アプリケーションのステータスコードのセマンティクスを理解し、大量の「偽陽性」をフィルタリングしてコストを管理しましょう。
  • レジリエンスを過度に設計しない: テレメトリーデータの場合、シンプルなインメモリバッチ処理で十分なことが多いです。
  • 段階的なロールアウトで問題を検出: 環境ティア間での段階的プロモーションは、貴重なバリデーションを提供します。

今後の展望

この事例は、控えめな規模のプラットフォームエンジニアリングチームが、比較的低い運用オーバーヘッドで大規模な OpenTelemetry Collector インフラストラクチャを成功裏に管理できることを示しています。

このシリーズの続編もお楽しみに。さまざまな規模の組織が、それぞれ独自の課題と創造的なソリューションを持ちながら、本番環境で OpenTelemetry Collector をどのように活用しているかを引き続き紹介していきます。

独自の OpenTelemetry Collector ストーリーをお持ちですか。 CNCF の #otel-devex Slack チャンネルにご参加ください。 本番環境での OpenTelemetry の活用方法や、開発者体験の改善に向けたご意見をぜひお聞かせください。