Carpe Diem

備忘録

OpenTelemetryで分散トレーシング

概要

マイクロサービス構成など分散システムにおいて分散トレーシングを行いたい場合は、サービス間でのContextの伝播が必要になります。

OpenTelemetryでサービス間での伝播をしたい場合の実装方法を説明します。

環境

  • Go v1.20.2
  • go.opentelemetry.io/otel/trace v1.14.0

今回のアーキテクチャイメージは以下です。

前提知識

用語

Contextの伝播に関わる用語は以下があります。

用語 説明
Carrier 伝播させるデータを載せる媒体。HTTPヘッダなど
Inject Carrierに値をセットする
Extract Carrierから値を取り出す

イメージは以下です。

ref: https://github.com/openzipkin/b3-propagation#overall-process

現状Propagatorの種類はTextMapPropagator1つだけです。今後バイナリデータを扱うPropagetorが追加される予定です。

Propagators Format

分散トレースはこれまで伝播に使う際のヘッダー名や値のフォーマットなどが以下のように複数存在します。

ref: Propagators API | OpenTelemetry

そして依存するサービスによっては対応しているフォーマットが異なります。

例えばIstioはB3 multi headerやW3C TraceContextをサポートしています。

実装

propagator

先に結論を述べると次のようなPropagatorの設定がクライアント側(gateway)、サーバ側(backend)の両方に必要です。

otel.SetTextMapPropagator(
        propagation.NewCompositeTextMapPropagator(
                propagation.TraceContext{},
                propagation.Baggage{},
        ),
)

なのでtrace用packageを用意しておき、その中のinit()関数で上記を設定すると自動的にクライアント側もサーバ側も反映されるようになります。

func init() {
        otel.SetTextMapPropagator(
                propagation.NewCompositeTextMapPropagator(
                        propagation.TraceContext{},
                        propagation.Baggage{},
                ),
        )
}

gateway

middleware

前回同様http serverにotelhttp.NewHandler()というミドルウェアを入れます。下のtelemetry.NewHTTPMiddlewareはそれをラップしたものです。

   mux := http.NewServeMux()
    mux.Handle("/", http.HandlerFunc(h.alive))
    mux.Handle("/hello", http.HandlerFunc(h.hello))
    http.ListenAndServe(":8000", telemetry.NewHTTPMiddleware()(mux))

grpc client interceptor

grpc clientにinterceptorを設定します。

conn, err := grpc.Dial(addr,
        grpc.WithInsecure(),
        grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
)
if err != nil {
        log.Fatal(err)
}
c := pb.NewGreeterClient(conn)

backend

grpc serverにinterceptorを設定します。

s := grpc.NewServer(grpc.UnaryInterceptor(telemetry.NewUnaryServerInterceptor()))
pb.RegisterGreeterServer(s, &server{})
err = s.Serve(lis)
if err != nil {
        log.Fatal(err)
}

ヘルスチェックを除いたgRPCを対象とします。

func NewUnaryServerInterceptor(opts ...otelgrpc.Option) grpc.UnaryServerInterceptor {
        opts = append(opts,
                otelgrpc.WithInterceptorFilter(
                        filters.Not(filters.HealthCheck()),
                ),
        )
        return otelgrpc.UnaryServerInterceptor(opts...)
}

Filterを入れることで以下のようにサンプリング対象とならなくなります。

ref: https://github.com/openzipkin/b3-propagation#overall-process

動作検証

docker-composeで一気に立ち上げます。

version: "3"

services:
  gateway:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - TARGET=gateway
    ports:
      - 8000:8000
    environment:
      - BACKEND_ADDR=backend-grpc:8080
      - EXPORTER_ENDPOINT=http://jaeger:14268/api/traces
  backend-grpc:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - TARGET=backend-grpc
    environment:
      - EXPORTER_ENDPOINT=http://jaeger:14268/api/traces
  jaeger:
    image: "jaegertracing/all-in-one:1.42"
    ports:
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
      - "16686:16686"
      - "4317:4317"
      - "4318:4318"
      - "14250:14250"
      - "14268:14268"
      - "14269:14269"
      - "9411:9411"
    environment:
      - COLLECTOR_ZIPKIN_HOST_PORT=:9411
      - COLLECTOR_OTLP_ENABLED=true

backendサーバもトレースに含まれていることが確認できます。

その他

サンプルコード

今回のサンプルコードはこちらです。

github.com

まとめ

OpenTelemetryでの分散トレースの実装方法を紹介しました。
ほぼSDKで用意されているので簡単に導入ができるようになっています。

参考