OpenTelemetry를 사용하여 Spanner 구성요소의 지연 시간 검사

이 주제에서는 Spanner 구성요소를 검사하여 지연 시간의 원인을 찾고 OpenTelemetry를 사용하여 해당 지연 시간을 시각화하는 방법을 설명합니다. 이 주제의 구성요소에 대한 대략적인 개요는 Spanner 요청의 지연 시간 지점을 참조하세요.

OpenTelemetry는 trace, 측정항목, 로그와 같은 원격 분석 데이터를 만들고 관리할 수 있게 해주는 오픈소스 관측 가능성 프레임워크 및 툴킷입니다. 이는 OpenTracing과 OpenCensus를 병합한 결과물입니다. 자세한 내용은 OpenTelemetry란?을 참조하세요.

Spanner 클라이언트 라이브러리는 OpenTelemetry 관측 가능성 프레임워크를 사용하여 측정항목과 trace를 제공합니다. 지연 시간 지점 식별의 절차에 따라 Spanner에서 지연 시간이 발생하는 구성요소를 찾습니다.

시작하기 전에

지연 시간 측정항목 캡처를 시작하기 전에 OpenTelemetry를 사용한 수동 계측을 숙지하세요. 원격 분석 데이터를 내보내는 데 적절한 옵션으로 OpenTelemetry SDK를 구성해야 합니다. 여러 가지 OpenTelemetry 내보내기 도구 옵션을 사용할 수 있습니다. OpenTelemetry 프로토콜(OTLP) 내보내기 도구를 사용하는 것이 좋습니다. 다른 옵션으로는 Google Cloud 내보내기 도구 또는 Google Managed Service for Prometheus 내보내기 도구와 함께 OTel Collector를 사용하는 방법이 포함됩니다.

Compute Engine에서 애플리케이션을 실행하는 경우 운영 에이전트를 사용하여 OpenTelemetry 프로토콜 측정항목 및 trace를 수집할 수 있습니다. 자세한 내용은 OTLP 측정항목 및 trace 수집을 참조하세요.

종속 항목 추가

OpenTelemetry SDK 및 OTLP 내보내기 도구를 구성하려면 애플리케이션에 다음 종속 항목을 추가합니다.

자바

<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.24.0
go.opentelemetry.io/otel/sdk/metric v1.23.1

OpenTelemetry 객체 삽입

그런 다음 OTLP 내보내기 도구를 사용하여 OpenTelemetry 객체를 만들고 SpannerOptions를 사용하여 OpenTelemetry 객체를 삽입합니다.

자바

// 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 요청의 첫 번째 바이트와 클라이언트가 데이터베이스에서 수신한 응답의 마지막 바이트 사이의 시간(밀리초)입니다.

클라이언트 왕복 지연 시간 캡처

OpenTelemetry를 사용하는 경우 Spanner 클라이언트 왕복 지연 시간 측정항목은 지원되지 않습니다. 브리지와 함께 OpenCensus를 사용하면 측정항목을 계측하고 데이터를 OpenTelemetry로 마이그레이션할 수 있습니다.

클라이언트 왕복 지연 시간 시각화

측정항목을 검색한 후 Cloud Monitoring에서 클라이언트 왕복 지연 시간을 시각화할 수 있습니다.

다음 예시는 클라이언트 왕복 지연 시간 측정항목의 5번째 백분위수 지연 시간을 보여주는 그래프입니다. 백분위수 지연 시간을 50번째 또는 99번째 백분위수로 변경하려면 애그리게이터 메뉴를 사용합니다.

이 프로그램은 roundtrip_latency라는 뷰를 만듭니다. 이 문자열은 Cloud Monitoring으로 내보낼 때 측정항목 이름의 일부가 됩니다.

Cloud Monitoring 클라이언트 왕복 지연 시간

GFE 지연 시간 캡처 및 시각화

Google 프런트엔드(GFE) 지연 시간은 Google 네트워크에서 클라이언트의 리모트 프로시져 콜을 수신한 후 GFE가 응답의 첫 번째 바이트를 수신할 때까지의 시간(밀리초)입니다.

GFE 지연 시간 캡처

Spanner 클라이언트 라이브러리를 사용하여 다음 옵션을 사용 설정하면 GFE 지연 시간 측정항목을 캡처할 수 있습니다.

자바

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에서 GFE 지연 시간을 시각화할 수 있습니다.

다음 예시는 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 요청 지연 시간 시각화

측정항목 탐색기 차트 도구를 사용하여 Cloud Monitoring에서 spanner.googleapis.com/api/request_latencies 측정항목에 대한 그래프를 시각화할 수 있습니다.

다음은 Spanner API 요청 지연 시간 측정항목에 대한 5번째 백분위수 지연 시간을 보여주는 그래프의 예시입니다. 백분위수 지연 시간을 50번째 또는 99번째 백분위수로 변경하려면 애그리게이터 메뉴를 사용합니다.

Cloud Monitoring API 요청 지연 시간

쿼리 지연 시간 캡처 및 시각화

쿼리 지연 시간은 Spanner 데이터베이스에서 SQL 쿼리를 실행하는 데 걸리는 시간(밀리초)입니다.

쿼리 지연 시간 캡처

다음 언어의 쿼리 지연 시간을 캡처할 수 있습니다.

자바

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 쿼리 지연 시간

다음 단계