DevOps定着バイブル

マイクロサービスにおける分散トレーシングの実践:OpenTelemetryを活用したDevOps運用効率化

Tags: DevOps, マイクロサービス, OpenTelemetry, 分散トレーシング, オブザーバビリティ

はじめに

今日のソフトウェア開発において、マイクロサービスアーキテクチャは高いスケーラビリティと開発効率を実現する一方で、システムの複雑性を増大させる側面も持ち合わせています。特に、複数のサービスが連携し合う環境では、パフォーマンス問題や障害発生時の根本原因特定が困難になるという運用上の課題が顕在化します。このような課題を解決するために、DevOpsのプラジカルなアプローチとしてオブザーバビリティの重要性が高まっています。

本記事では、マイクロサービスアーキテクチャにおける運用課題の解決策として、分散トレーシングに焦点を当てます。具体的には、オープンソースの標準仕様であるOpenTelemetryを導入し、どのようにしてシステムの振る舞いを可視化し、DevOpsの実践において運用効率を向上させるかについて、具体的な実装例を交えながら解説します。

分散トレーシングの基本概念

分散トレーシングは、マイクロサービスや分散システムにおけるリクエストのライフサイクル全体を追跡し、その処理パスやパフォーマンス情報を可視化する技術です。個々のサービス間のインタラクションを詳細に把握することで、システム全体のボトルネック特定や障害調査を効率化します。

主要な概念は以下の通りです。

モノリシックなアプリケーションでは、単一のプロセス内で全ての処理が完結するため、スタックトレースやログの解析によって問題箇所を特定することが比較的容易でした。しかし、マイクロサービスではリクエストが複数のサービスを横断するため、個々のサービスのログを追うだけでは全体像の把握が困難です。分散トレーシングは、このサービス間の「壁」を越えてリクエストの流れを一元的に可視化することで、この問題を解決します。

OpenTelemetryのアーキテクチャとコンポーネント

OpenTelemetryは、クラウドネイティブ環境におけるテレメトリーデータ(メトリクス、ログ、トレース)の生成、収集、エクスポートのためのベンダーニュートラルな仕様、ツール、API、SDKのセットです。これにより、特定の監視ツールに依存せず、柔軟なオブザーバビリティ基盤を構築できます。

OpenTelemetryの主要なコンポーネントとその役割は以下の通りです。

OpenTelemetry Collectorは、システムからテレメトリーデータを受け取り、それを様々な監視バックエンドに柔軟にルーティングできるため、オブザーバビリティの単一窓口としての役割を果たします。これにより、アプリケーションコードから特定のベンダーへの依存を排除し、将来的なバックエンドの変更にも柔軟に対応できるようになります。

OpenTelemetryによる実装ガイド

ここでは、PythonとGo言語を用いたOpenTelemetry SDKの導入と、OpenTelemetry Collectorの設定例を紹介します。

各言語でのSDK導入例

PythonでのOpenTelemetry導入

PythonアプリケーションにOpenTelemetryを導入し、簡単なWebサービスにおけるトレースを生成する例です。ここではFlaskを例にしますが、他のフレームワークでも同様に適用可能です。

# app.py
from flask import Flask, request
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
import requests

# Resource定義 (サービス名などを設定)
resource = Resource.create({
    "service.name": "my-python-service",
    "service.version": "1.0.0",
})

# TracerProviderの初期化
provider = TracerProvider(resource=resource)
# Console Span Exporter (デバッグ用)
# processor = BatchSpanProcessor(ConsoleSpanExporter())
# OTLP Span Exporter (Collectorへの送信用)
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="localhost:4317", insecure=True))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument() # requestsライブラリの自動計測

tracer = trace.get_tracer(__name__)

@app.route("/")
def hello_world():
    # カスタムスパンの作成
    with tracer.start_as_current_span("do-some-work") as span:
        span.set_attribute("http.method", request.method)
        span.set_attribute("request.path", request.path)
        # 外部サービス呼び出し(requestsが計測される)
        response = requests.get("http://localhost:5001/data")
        span.set_attribute("external_service.status_code", response.status_code)
        return f"<p>Hello, World! Data: {response.text}</p>"

@app.route("/data")
def get_data():
    with tracer.start_as_current_span("get-data-from-db"):
        # データベースアクセスなどの処理をシミュレート
        import time
        time.sleep(0.05)
    return "<p>Some data from data service</p>"

if __name__ == "__main__":
    app.run(port=5000, debug=True)

上記のコードでは、FlaskInstrumentorRequestsInstrumentorを使用して、Flaskアプリケーションとrequestsライブラリの呼び出しを自動的に計測しています。また、tracer.start_as_current_spanを使用してカスタムスパンを作成し、特定の処理ブロックを追跡しています。OTLPSpanExporterは、生成されたスパンをOpenTelemetry CollectorのgRPCエンドポイント(デフォルトでlocalhost:4317)に送信します。

GoでのOpenTelemetry導入

Go言語でのOpenTelemetry導入例です。ここではnet/httpをベースにした簡単なWebサービスを例に、カスタムインストゥルメンテーションとOTLPエクスポーターの設定を示します。

// main.go
package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

var tracer = otel.Tracer("my-go-service")

func initTracer() *sdktrace.TracerProvider {
    ctx := context.Background()

    // OTLP Exporterのセットアップ
    // Collectorがlocalhost:4317でgRPCを受け付けていると仮定
    conn, err := grpc.DialContext(ctx, "localhost:4317",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithBlock(),
    )
    if err != nil {
        log.Fatalf("failed to connect: %v", err)
    }

    traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
    if err != nil {
        log.Fatalf("failed to create trace exporter: %v", err)
    }

    // Resourceの定義 (サービス名などを設定)
    resource := resource.NewWithAttributes(
        semconv.SchemaURL,
        semconv.ServiceName("my-go-service"),
        semconv.ServiceVersion("1.0.0"),
    )

    // TracerProviderの初期化
    bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithSampler(sdktrace.AlwaysSample()),
        sdktrace.WithResource(resource),
        sdktrace.WithSpanProcessor(bsp),
    )
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
    return tp
}

func main() {
    tp := initTracer()
    defer func() {
        if err := tp.Shutdown(context.Background()); err != nil {
            log.Printf("Error shutting down tracer provider: %v", err)
        }
    }()

    // HTTPハンドラの定義
    http.Handle("/", otelhttp.NewHandler(http.HandlerFunc(handler), "hello-world"))
    http.Handle("/data", otelhttp.NewHandler(http.HandlerFunc(dataHandler), "get-data"))

    log.Println("Server started on :5001")
    log.Fatal(http.ListenAndServe(":5001", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    _, span := tracer.Start(ctx, "do-some-go-work")
    defer span.End()

    span.SetAttributes(attribute.String("http.method", r.Method))
    span.SetAttributes(attribute.String("request.path", r.URL.Path))

    // 別のサービスを呼び出す
    req, err := http.NewRequestWithContext(ctx, "GET", "http://localhost:5000/", nil)
    if err != nil {
        http.Error(w, "Failed to create request", http.StatusInternalServerError)
        return
    }
    client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
    resp, err := client.Do(req)
    if err != nil {
        http.Error(w, "Failed to call external service", http.StatusInternalServerError)
        return
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        http.Error(w, "Failed to read response body", http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "Hello from Go! Called Python service: %s", string(body))
}

func dataHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    _, span := tracer.Start(ctx, "get-data-from-go-db")
    defer span.End()

    time.Sleep(50 * time.Millisecond) // データベースアクセスをシミュレート
    fmt.Fprintln(w, "Go data response")
}

Go言語の場合も、otelhttp.NewHandlerotelhttp.NewTransportを用いてHTTPクライアント・サーバーを自動的に計測できます。tracer.Startでカスタムスパンを作成し、コンテキストを通じてスパンを伝播させます。

OpenTelemetry Collectorの活用

OpenTelemetry Collectorは、テレメトリーデータの中央収集ポイントとして機能します。以下に、Collectorの簡単な設定例(config.yaml)を示します。この設定では、OTLP形式でデータを受信し、コンソールに表示(デバッグ用)しつつ、Jaegerにエクスポートする構成になっています。

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
      http:

exporters:
  logging: # デバッグ用。Collectorが受け取ったトレースをコンソールに出力
    verbosity: detailed
  jaeger:
    endpoint: "jaeger:14250" # Jaeger CollectorのgRPCエンドポイント
    tls:
      insecure: true # デモやローカル環境用。本番環境では適切にTLSを設定

processors:
  batch: # スパンをバッチ処理して効率的にエクスポート
    send_batch_size: 100
    timeout: 10s

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging, jaeger] # loggingとjaegerの両方にエクスポート
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging] # メトリクスはloggingのみにエクスポート
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging] # ログはloggingのみにエクスポート

このconfig.yamlを使ってCollectorを起動することで、PythonやGoのアプリケーションから送信されたOTLP形式のトレースデータがCollectorによって受信され、設定されたExporter(例: Jaeger)に転送されます。Jaeger UIでトレースを確認できるようになります。

Docker Composeを使用した簡単なOpenTelemetry CollectorとJaegerの起動例です。

# docker-compose.yaml
version: '3.8'
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.92.0 # contrib版はより多くのExporter/Receiverを含む
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317" # OTLP gRPC
      - "4318:4318" # OTLP HTTP
      - "8888:8888" # Collector metrics
    depends_on:
      - jaeger

  jaeger:
    image: jaegertracing/all-in-one:1.50
    ports:
      - "16686:16686" # Jaeger UI
      - "14250:14250" # Jaeger gRPC Collector
      - "14268:14268" # Jaeger HTTP Collector

このdocker-compose.yamlと前述のotel-collector-config.yamlを同じディレクトリに置き、docker compose upで実行することで、ローカルでOpenTelemetry CollectorとJaeger環境を構築できます。アプリケーションからlocalhost:4317にデータを送信すれば、http://localhost:16686でJaeger UIを確認できるはずです。

実践的な運用例とベストプラクティス

分散トレーシングがもたらすDevOpsへの影響

OpenTelemetryを用いた分散トレーシングの導入は、DevOpsの実践において多岐にわたる恩恵をもたらします。

まとめと今後の展望

マイクロサービスアーキテクチャの複雑化に伴い、システムの可視化はDevOpsの重要な要素となっています。OpenTelemetryを用いた分散トレーシングは、この可視化を強力に推進し、開発・運用の効率を飛躍的に向上させる実践的なアプローチです。

本記事では、分散トレーシングの基本概念から、PythonとGoを用いたOpenTelemetryの実装例、そしてOpenTelemetry Collectorを活用したデータ収集・エクスポートの具体的な手順を解説しました。これらの知見を基に、皆様の環境でもOpenTelemetryの導入を検討し、マイクロサービス運用の効率化、ひいてはDevOps文化の定着に貢献できることを期待いたします。

今後は、分散トレーシングとメトリクス、ログの統合による包括的なオブザーバビリティの実現、あるいはAIを活用した異常検知と根本原因分析の自動化など、OpenTelemetryとそのエコシステムはさらなる進化を遂げるでしょう。常に最新の技術動向を追い、より堅牢で効率的なシステム運用を目指していくことが重要です。