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を作成しない |
ツール推奨
分散トレーシングを実装する過程で、以下のツールがデータフォーマットとエンコードの問題を処理するのに役立ちます:
- JSONフォーマットツール — OTel Collector設定とSpan JSONデータのフォーマット、デバッグに便利
- Base64エンコードツール — Trace IDとSpan IDのエンコード、システム間の受け渡しに使用
- ハッシュ計算ツール — サンプリング決定用のハッシュ値生成、同じTraceのサンプリング一貫性を確保
まとめ:分散トレーシングはマイクロサービスのオブザーバビリティの「X線」です——これなしでは、症状しか見えず、原因が見えません。OpenTelemetryはAPIとSDKを統一し、自動計装はHTTP/gRPC/DBをカバーし、手動計装はビジネスのクリティカルパスを補完し、Tail Samplingはエラートレースの損失を防ぎ、Collectorはバッチ処理と転送を担当します。2026年、Jaegerは開発デバッグに、Tempoは本番ストレージに使用するのが最適な組み合わせです。覚えておいてください:分散トレーシングのないマイクロサービスシステムは、監視のないブラックボックスのようなもの——問題が起きても推測することしかできません。
ブラウザローカルツールを無料で試す →