OpenTelemetry を使用して Spanner コンポーネントのレイテンシを調査する

このトピックでは、Spanner コンポーネントを調べてレイテンシの原因を特定し、OpenTelemetry を使用してそのレイテンシを可視化する方法について説明します。このトピックのコンポーネントの概要については、Spanner リクエストのレイテンシ ポイントをご覧ください。

OpenTelemetry は、トレース、指標、ログなどのテレメトリー データを作成および管理できるオープンソースのオブザーバビリティ フレームワークとツールキットです。これは、OpenTracing と OpenCensus の統合の結果です。詳細については、OpenTelemetry とはをご覧ください。

Spanner クライアント ライブラリは、OpenTelemetry オブザーバビリティ フレームワークを使用して指標とトレースを提供します。レイテンシ ポイントを特定するの手順に沿って、Spanner でレイテンシが発生しているコンポーネントを見つけます。

準備

レイテンシ指標のキャプチャを始める前に、OpenTelemetry による手動計測について理解しておいてください。 テレメトリー データをエクスポートするための適切なオプションを使用して、OpenTelemetry SDK を構成する必要があります。利用可能な OpenTelemetry エクスポータ オプションは複数あります。OpenTelemetry Protocol(OTLP)エクスポータを使用することをおすすめします。その他のオプションには、Google Cloud エクスポータまたは Google Managed Service for Prometheus Exporter での OTel Collector の使用が含まれます。

Compute Engine でアプリケーションを実行している場合は、Ops エージェントを使用して OpenTelemetry Protocol の指標とトレースを収集できます。詳細については、OTLP 指標とトレースを収集するをご覧ください。

依存関係の追加

OpenTelemetry SDK と OTLP エクスポータを構成するには、次の依存関係をアプリケーションに追加します。

Java

<dependency>
  <groupId>com.google.cloud</groupId>
  <artifactId>google-cloud-spanner</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-sdk-metrics</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-sdk-trace</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

Go

go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1
go.opentelemetry.io/otel/metric v1.24.0
go.opentelemetry.io/otel/sdk v1.23.1
go.opentelemetry.io/otel/sdk/metric v1.23.1

OpenTelemetry オブジェクトを挿入する

次に、OTLP エクスポータを使用して OpenTelemetry オブジェクトを作成し、SpannerOptions を使用して OpenTelemetry オブジェクトを挿入します。

Java

// Enable OpenTelemetry metrics and traces before Injecting OpenTelemetry
SpannerOptions.enableOpenTelemetryMetrics();
SpannerOptions.enableOpenTelemetryTraces();

// Create a new meter provider
SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder()
    // Use Otlp exporter or any other exporter of your choice.
    .registerMetricReader(
        PeriodicMetricReader.builder(OtlpGrpcMetricExporter.builder().build()).build())
    .build();

// Create a new tracer provider
SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder()
    // Use Otlp exporter or any other exporter of your choice.
    .addSpanProcessor(SimpleSpanProcessor.builder(OtlpGrpcSpanExporter
        .builder().build()).build())
        .build();

// Configure OpenTelemetry object using Meter Provider and Tracer Provider
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
    .setMeterProvider(sdkMeterProvider)
    .setTracerProvider(sdkTracerProvider)
    .build();

// Inject OpenTelemetry object via Spanner options or register as GlobalOpenTelemetry.
SpannerOptions options = SpannerOptions.newBuilder()
    .setOpenTelemetry(openTelemetry)
    .build();
Spanner spanner = options.getService();

Go

// Ensure that your Go runtime version is supported by the OpenTelemetry-Go compatibility policy before enabling OpenTelemetry instrumentation.
// Refer to compatibility here https://github.com/googleapis/google-cloud-go/blob/main/debug.md#opentelemetry

import (
	"context"
	"fmt"
	"io"
	"log"
	"strconv"
	"strings"

	"cloud.google.com/go/spanner"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/metric"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
	"google.golang.org/api/iterator"
)

func enableOpenTelemetryMetricsAndTraces(w io.Writer, db string) error {
	// db = `projects/<project>/instances/<instance-id>/database/<database-id>`
	ctx := context.Background()

	// Create a new resource to uniquely identify the application
	res, err := newResource()
	if err != nil {
		log.Fatal(err)
	}

	// Enable OpenTelemetry traces by setting environment variable GOOGLE_API_GO_EXPERIMENTAL_TELEMETRY_PLATFORM_TRACING to the case-insensitive value "opentelemetry" before loading the client library.
	// Enable OpenTelemetry metrics before injecting meter provider.
	spanner.EnableOpenTelemetryMetrics()

	// Create a new tracer provider
	tracerProvider, err := getOtlpTracerProvider(ctx, res)
	defer tracerProvider.ForceFlush(ctx)
	if err != nil {
		log.Fatal(err)
	}
	// Register tracer provider as global
	otel.SetTracerProvider(tracerProvider)

	// Create a new meter provider
	meterProvider := getOtlpMeterProvider(ctx, res)
	defer meterProvider.ForceFlush(ctx)

	// Inject meter provider locally via ClientConfig when creating a spanner client or set globally via setMeterProvider.
	client, err := spanner.NewClientWithConfig(ctx, db, spanner.ClientConfig{OpenTelemetryMeterProvider: meterProvider})
	if err != nil {
		return err
	}
	defer client.Close()
	return nil
}

func getOtlpMeterProvider(ctx context.Context, res *resource.Resource) *sdkmetric.MeterProvider {
	otlpExporter, err := otlpmetricgrpc.New(ctx)
	if err != nil {
		log.Fatal(err)
	}
	meterProvider := sdkmetric.NewMeterProvider(
		sdkmetric.WithResource(res),
		sdkmetric.WithReader(sdkmetric.NewPeriodicReader(otlpExporter)),
	)
	return meterProvider
}

func getOtlpTracerProvider(ctx context.Context, res *resource.Resource) (*sdktrace.TracerProvider, error) {
	traceExporter, err := otlptracegrpc.New(ctx)
	if err != nil {
		return nil, err
	}

	tracerProvider := sdktrace.NewTracerProvider(
		sdktrace.WithResource(res),
		sdktrace.WithBatcher(traceExporter),
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
	)

	return tracerProvider, nil
}

func newResource() (*resource.Resource, error) {
	return resource.Merge(resource.Default(),
		resource.NewWithAttributes(semconv.SchemaURL,
			semconv.ServiceName("otlp-service"),
			semconv.ServiceVersion("0.1.0"),
		))
}

クライアントの往復レイテンシをキャプチャして可視化する

クライアントのラウンドトリップ レイテンシは、クライアントが GFE と Spanner API フロントエンドの両方からデータベースに送信する Spanner API リクエストの最初のバイトと、クライアントがデータベースから受信したレスポンスの最後のバイトの間の時間(ミリ秒単位)です。

クライアントの往復レイテンシをキャプチャする

Spanner クライアントの往復レイテンシ指標は、OpenTelemetry を使用してサポートされていません。ブリッジで OpenCensus を使用して指標をインストゥルメント化し、データを OpenTelemetry に移行できます。

クライアントの往復レイテンシを可視化する

指標を取得した後、Cloud Monitoring でクライアントの往復レイテンシを可視化できます。

クライアントの往復レイテンシ指標で、5 パーセンタイル レイテンシを示すグラフの例を次に示します。パーセンタイルのレイテンシを 50 パーセンタイルまたは 99 パーセンタイルのいずれかに変更するには、[Aggregator] メニューを使用します。

このプログラムは roundtrip_latency というビューを作成します。この文字列は、Cloud Monitoring にエクスポートされたときの指標の名前の一部となります。

Cloud Monitoring クライアントの往復レイテンシ

GFE レイテンシをキャプチャして可視化する

Google Front End(GFE)のレイテンシは、Google ネットワークがクライアントからリモート プロシージャ コールを受信してから、GFE がレスポンスの最初のバイトを受信するまでの時間(ミリ秒単位)です。

GFE レイテンシをキャプチャする

Spanner クライアント ライブラリを使用して次のオプションを有効にすると、GFE のレイテンシ指標をキャプチャできます。

Java

static void captureGfeMetric(DatabaseClient dbClient) {
  // GFE_latency and other Spanner metrics are automatically collected
  // when OpenTelemetry metrics are enabled.

  try (ResultSet resultSet =
      dbClient
          .singleUse() // Execute a single read or query against Cloud Spanner.
          .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
    }
  }
}

Go

// GFE_Latency and other Spanner metrics are automatically collected
// when OpenTelemetry metrics are enabled.
func captureGFELatencyMetric(ctx context.Context, client spanner.Client) error {
	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := client.Single().Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID, albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
	}
}

GFE レイテンシを可視化する

指標を取得した後、Cloud Monitoring でクエリの GEE レイテンシを可視化できます。

以下は、GFE レイテンシ指標の分布集計を示すグラフの例です。パーセンタイルのレイテンシを 5 番目、50 番目、95 番目、または 99 番目のパーセンタイルに変更するには、[アグリゲータ] メニューを使用します。

このプログラムは spanner/gfe_latency というビューを作成します。この文字列は、Cloud Monitoring にエクスポートされたときの指標の名前の一部となります。

Cloud Monitoring GFE のレイテンシ。

Spanner API リクエストのレイテンシをキャプチャして可視化する

Spanner API リクエストのレイテンシとは、Spanner API フロントエンドが受信するリクエストの最初のバイトから Spanner API フロントエンドが送信するレスポンスの最後のバイトまでの時間(秒単位)です。

Spanner API リクエストのレイテンシをキャプチャする

このレイテンシは、デフォルトで Cloud Monitoring の指標の一部として利用できます。キャプチャしてエクスポートする操作は必要ありません。

Spanner API リクエストのレイテンシを可視化する

Metrics Explorer グラフツールを使用すると、Cloud Monitoring で spanner.googleapis.com/api/request_latencies 指標用のグラフを可視化できます。

以下は、Spanner API リクエストのレイテンシ指標の 5 パーセンタイルのレイテンシを示すグラフの例です。パーセンタイルのレイテンシを 50 パーセンタイルまたは 99 パーセンタイルのいずれかに変更するには、[Aggregator] メニューを使用します。

Cloud Monitoring API リクエストのレイテンシ

クエリのレイテンシをキャプチャして可視化する

このクエリのレイテンシは、Spanner データベースで SQL クエリを実行するのにかかる時間の長さ(ミリ秒単位)です。

クエリのレイテンシをキャプチャする

次の言語に対する、クエリのレイテンシをキャプチャできます。

Java

static void captureQueryStatsMetric(OpenTelemetry openTelemetry, DatabaseClient dbClient) {
  // Register query stats metric.
  // This should be done once before start recording the data.
  Meter meter = openTelemetry.getMeter("cloud.google.com/java");
  DoubleHistogram queryStatsMetricLatencies =
      meter
          .histogramBuilder("spanner/query_stats_elapsed")
          .setDescription("The execution of the query")
          .setUnit("ms")
          .build();

  // Capture query stats metric data.
  try (ResultSet resultSet = dbClient.singleUse()
      .analyzeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"),
          QueryAnalyzeMode.PROFILE)) {

    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
    }

    String value = resultSet.getStats().getQueryStats()
        .getFieldsOrDefault("elapsed_time", Value.newBuilder().setStringValue("0 msecs").build())
        .getStringValue();
    double elapsedTime = value.contains("msecs")
        ? Double.parseDouble(value.replaceAll(" msecs", ""))
        : Double.parseDouble(value.replaceAll(" secs", "")) * 1000;
    queryStatsMetricLatencies.record(elapsedTime);
  }
}

Go

func captureQueryStatsMetric(ctx context.Context, mp metric.MeterProvider, client spanner.Client) error {
	meter := mp.Meter(spanner.OtInstrumentationScope)
	// Register query stats metric with OpenTelemetry to record the data.
	// This should be done once before start recording the data.
	queryStats, err := meter.Float64Histogram(
		"spanner/query_stats_elapsed",
		metric.WithDescription("The execution of the query"),
		metric.WithUnit("ms"),
		metric.WithExplicitBucketBoundaries(0.0, 0.01, 0.05, 0.1, 0.3, 0.6, 0.8, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0,
			16.0, 20.0, 25.0, 30.0, 40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0,
			300.0, 400.0, 500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
			100000.0),
	)
	if err != nil {
		fmt.Print(err)
	}

	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := client.Single().QueryWithStats(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			// Record query execution time with OpenTelemetry.
			elapasedTime := iter.QueryStats["elapsed_time"].(string)
			elapasedTimeMs, err := strconv.ParseFloat(strings.TrimSuffix(elapasedTime, " msecs"), 64)
			if err != nil {
				return err
			}
			queryStats.Record(ctx, elapasedTimeMs)
			return nil
		}
		if err != nil {
			return err
		}
		var singerID, albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
	}
}

クエリのレイテンシを可視化する

指標を取得した後、Cloud Monitoring でクエリのレイテンシを可視化できます。

以下は、GFE レイテンシ指標の分布集計を示すグラフの例です。パーセンタイルのレイテンシを 5 番目、50 番目、95 番目、または 99 番目のパーセンタイルに変更するには、[アグリゲータ] メニューを使用します。

このプログラムでは query_stats_elapsed と呼ばれる OpenCensus ビューが作成されます。 この文字列は、Cloud Monitoring にエクスポートされたときの指標の名前の一部となります。

Cloud Monitoring クエリのレイテンシ

次のステップ