2026年Go分散トレーシング完全ガイド:OpenTelemetryでマイクロサービスの完全なオブザーバビリティを実現

云原生

2026年Go分散トレーシング完全ガイド:OpenTelemetryでマイクロサービスの完全なオブザーバビリティを実現

もしマイクロサービスのトラブルシューティングがまだ「ログ追加→再起動→ログ確認」に頼っているなら、2026年の運用効率は2018年のままです。1つのリクエストが5つのサービス、3つのデータベース、2つのメッセージキューを経由する場合、分散トレーシングなしでは、レイテンシのボトルネックを特定することは不可能です。分散トレーシングは「オプション」ではなく、マイクロサービスのオブザーバビリティの三大柱の一つです(Metrics、Logs、Traces)。

2026年、OpenTelemetryはデファクトスタンダードとなり、JaegerとGrafana TempoはOTLPプロトコルを全面的にサポートしています。本記事では、OpenTelemetryのアーキテクチャから始め、自動計装、手動計装、コンテキスト伝播、バックエンド統合を網羅した完全なGo実装コードを提供します。

なぜ分散トレーシングはマイクロサービスに不可欠なのか?

オブザーバビリティの柱 解決する問題 代表的なツール なしの場合の影響
Metrics 「何が起きたか?」 Prometheus 問題の規模を量化できない
Logs 「どこで問題が起きたか?」 Loki/ELK 具体的なエラーを特定できない
Traces 「なぜ遅い?ボトルネックはどこ?」 Jaeger/Tempo レイテンシのボトルネックを特定できない
三つの組み合わせ 「完全な問題の全体像」 Grafana 問題の断片しか見えない

重要な洞察:5つのサービスにまたがる遅いリクエストの場合、Logsは「各サービスが遅い」ことしか教えてくれませんが、Tracesは「3番目のサービスのデータベースクエリが80%の時間を占めている」ことを教えてくれます。


一、OpenTelemetryアーキテクチャ

OpenTelemetryのコアアーキテクチャ:API → SDK → Exporter → Collector → Backend

[App] → [OTel API] → [OTel SDK] → [OTLP Exporter] → [OTel Collector] → [Jaeger/Tempo]
コンポーネント 役割 必須かどうか
OTel API 計装インターフェース はい
OTel SDK サンプリング、バッチ処理、エクスポート はい
OTLP Exporter Collectorへ送信 はい
OTel Collector 受信、処理、転送 推奨(本番環境)
Backend ストレージ、クエリ、表示 はい

1.1 OpenTelemetry Providerの初期化

package tracing

import (
    "context"
    "fmt"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func InitProvider(serviceName, collectorURL string) (func(context.Context) error, error) {
    exporter, err := otlptracegrpc.New(context.Background(),
        otlptracegrpc.WithEndpoint(collectorURL),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil {
        return nil, fmt.Errorf("creating exporter: %w", err)
    }

    res, err := resource.New(context.Background(),
        resource.WithAttributes(
            semconv.ServiceNameKey.String(serviceName),
            semconv.ServiceVersionKey.String("1.0.0"),
            semconv.DeploymentEnvironmentKey.String("production"),
        ),
    )
    if err != nil {
        return nil, fmt.Errorf("creating resource: %w", err)
    }

    provider := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter,
            sdktrace.WithBatchTimeout(5*time.Second),
            sdktrace.WithMaxExportBatchSize(512),
        ),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)),
    )

    otel.SetTracerProvider(provider)
    return provider.Shutdown, nil
}

二、自動計装 vs 手動計装

2.1 HTTP自動計装

import (
    "net/http"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {
    shutdown, err := tracing.InitProvider("user-service", "otel-collector:4317")
    if err != nil {
        log.Fatal(err)
    }
    defer shutdown(context.Background())

    mux := http.NewServeMux()
    mux.HandleFunc("/users", handleGetUsers)
    mux.HandleFunc("/orders", handleGetOrders)

    handler := otelhttp.NewHandler(mux, "user-service",
        otelhttp.WithMessageEvents(otelhttp.Read, otelhttp.Write),
    )

    http.ListenAndServe(":8080", handler)
}

2.2 gRPC自動計装

import (
    "google.golang.org/grpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)

func createGRPCServer() *grpc.Server {
    return grpc.NewServer(
        grpc.StatsHandler(otelgrpc.NewServerHandler()),
    )
}

func createGRPCClient(target string) (*grpc.ClientConn, error) {
    return grpc.Dial(target,
        grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
    )
}

2.3 データベース自動計装

import (
    "database/sql"
    "go.opentelemetry.io/contrib/instrumentation/database/sql/otsql"
)

func initDB() *sql.DB {
    db, err := otsql.Open("postgres", "postgres://localhost/mydb",
        otsql.WithAttributes(semconv.DBSystemPostgreSQL),
    )
    if err != nil {
        log.Fatal(err)
    }
    return db
}

2.4 手動計装

func ProcessOrder(ctx context.Context, order *Order) error {
    tracer := otel.Tracer("order-service")
    ctx, span := tracer.Start(ctx, "ProcessOrder",
        trace.WithAttributes(
            attribute.String("order.id", order.ID),
            attribute.Float64("order.amount", order.Amount),
        ),
    )
    defer span.End()

    ctx, validateSpan := tracer.Start(ctx, "ValidateOrder")
    if err := validate(order); err != nil {
        validateSpan.RecordError(err)
        validateSpan.SetStatus(codes.Error, err.Error())
        validateSpan.End()
        return err
    }
    validateSpan.End()

    ctx, paySpan := tracer.Start(ctx, "ProcessPayment")
    if err := processPayment(ctx, order); err != nil {
        paySpan.RecordError(err)
        paySpan.SetStatus(codes.Error, err.Error())
        paySpan.End()
        return err
    }
    paySpan.End()

    return nil
}

自動 vs 手動比較

側面 自動計装 手動計装
侵入性 ゼロ侵入 コードの修正が必要
粒度 フレームワーク級(HTTP/gRPC/DB) ビジネス級(任意の関数)
属性の豊富さ 標準属性 カスタム属性
パフォーマンスオーバーヘッド 低(フレームワーク最適化済み) 計装数に依存
推奨戦略 フレームワーク層は自動 ビジネスのクリティカルパスは手動

三、Traceコンテキスト伝播

サービス間でのTraceコンテキスト伝播は分散トレーシングの中核です。OpenTelemetryはW3C Trace Context標準を使用します。

3.1 HTTP伝播

import (
    "go.opentelemetry.io/otel/propagation"
)

func callDownstream(ctx context.Context, url string) (*http.Response, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
    return http.DefaultClient.Do(req)
}

3.2 メッセージキュー伝播

func publishMessage(ctx context.Context, topic string, msg []byte) error {
    carrier := propagation.MapCarrier{}
    otel.GetTextMapPropagator().Inject(ctx, carrier)

    kafkaMsg := &kafka.Message{
        TopicPartition: kafka.TopicPartition{Topic: &topic},
        Value:          msg,
        Headers:        make([]kafka.Header, 0, len(carrier)),
    }
    for k, v := range carrier {
        kafkaMsg.Headers = append(kafkaMsg.Headers, kafka.Header{
            Key:   k, Value: []byte(v),
        })
    }
    return producer.Produce(kafkaMsg, nil)
}

四、JaegerとTempoの統合

4.1 Jaeger All-in-One(開発環境)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jaeger
spec:
  selector:
    matchLabels:
      app: jaeger
  template:
    spec:
      containers:
      - name: jaeger
        image: jaegertracing/all-in-one:1.60
        ports:
        - containerPort: 16686
          name: ui
        - containerPort: 4317
          name: otlp-grpc
        env:
        - name: COLLECTOR_OTLP_ENABLED
          value: "true"

4.2 Grafana Tempo(本番環境)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tempo
spec:
  selector:
    matchLabels:
      app: tempo
  template:
    spec:
      containers:
      - name: tempo
        image: grafana/tempo:2.6
        args: ["-config.file=/etc/tempo/tempo.yaml"]
        volumeMounts:
        - name: config
          mountPath: /etc/tempo
      volumes:
      - name: config
        configMap:
          name: tempo-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: tempo-config
data:
  tempo.yaml: |
    server:
      http_listen_port: 3200
    distributor:
      receivers:
        otlp:
          protocols:
            grpc:
              endpoint: 0.0.0.0:4317
    storage:
      trace:
        backend: s3
        s3:
          bucket: tempo-traces
          endpoint: minio:9000

4.3 OTel Collector設定

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 512
  filter:
    error_mode: ignore
    traces:
      span:
        - 'attributes["http.route"] == "/healthz"'
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: error-policy
        type: status_code
        status_code:
          status_codes:
            - ERROR
      - name: slow-policy
        type: latency
        latency:
          threshold_ms: 1000
      - name: always-keep
        type: probabilistic
        probabilistic:
          sampling_percentage: 10

exporters:
  otlp:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [filter, tail_sampling, batch]
      exporters: [otlp]

バックエンド比較

側面 Jaeger Grafana Tempo
ストレージ Elasticsearch/Cassandra オブジェクトストレージ(S3/GCS)
コスト 高(ESクラスタ) 低(オブジェクトストレージ)
クエリレイテンシ 低(インデックス) 中(Trace IDクエリは極めて高速)
Grafanaとの統合 プラグインが必要 ネイティブ統合
適用シーン 開発/小規模 本番/大規模

5つのよくある落とし穴

# 落とし穴 影響 解決策
1 サンプリング率を100%に設定 ストレージコスト爆発、パフォーマンス低下 本番環境では0.1%-10%、エラートレースは100%保持
2 Traceコンテキストを伝播しない サービス間トレースが切断 TextMapPropagator.Inject/Extractを使用
3 SpanのEnd()を忘れる Spanが不完全、メモリリーク defer span.End()を使用
4 ホットパスで過多なSpanを作成 オーバーヘッドが大きすぎる クリティカルパスは手動計装、その他は自動計装
5 Collectorを単一障害点でデプロイ Collector障害でデータ損失 複数のCollectorインスタンス+負荷分散でデプロイ

10のよくあるエラートラブルシューティング

# エラー現象 考えられる原因 トラブルシューティング方法
1 JaegerにTraceが表示されない ExporterがCollectorに接続されていない CollectorのURLとポートを確認
2 サービス間トレースが切断 コンテキストが伝播されていない Propagator.Injectが呼び出されているか確認
3 Span属性が欠落 Resource属性が設定されていない resource.NewのWithAttributesを確認
4 サンプリング後のクリティカルトレースが損失 サンプリング率が低すぎる Tail Samplingでエラートレースを優先保持
5 Collector OOM バッチ処理キューが大きすぎる batch sizeとtimeoutを小さくする
6 Tempoクエリがタイムアウト Trace IDクエリにインデックスがない 属性クエリではなくTrace IDクエリを使用
7 gRPCトレースが不完全 otelgrpcインターセプターが追加されていない クライアントとサーバーの両方にStatsHandlerを追加
8 データベースSpanが欠落 otsqlを使用しているがドライバーが置換されていない sql.Openではなくotsql.Openを使用しているか確認
9 Kafkaメッセージトレースが切断 メッセージHeaderにTraceが注入されていない produce時にInject、consume時に抽出
10 Span数が多すぎる 自動計装+手動計装が重複 自動計装がカバーする関数内で手動Spanを作成しない

ツール推奨

分散トレーシングを実装する過程で、以下のツールがデータフォーマットとエンコードの問題を処理するのに役立ちます:


まとめ:分散トレーシングはマイクロサービスのオブザーバビリティの「X線」です——これなしでは、症状しか見えず、原因が見えません。OpenTelemetryはAPIとSDKを統一し、自動計装はHTTP/gRPC/DBをカバーし、手動計装はビジネスのクリティカルパスを補完し、Tail Samplingはエラートレースの損失を防ぎ、Collectorはバッチ処理と転送を担当します。2026年、Jaegerは開発デバッグに、Tempoは本番ストレージに使用するのが最適な組み合わせです。覚えておいてください:分散トレーシングのないマイクロサービスシステムは、監視のないブラックボックスのようなもの——問題が起きても推測することしかできません。

ブラウザローカルツールを無料で試す →

#Go分布式追踪#OpenTelemetry#链路追踪#可观测性#2026