使用 OpenTelemetry 检查 Spanner 组件中的延迟时间

本主题介绍如何检查 Spanner 组件以查找延迟来源并使用 OpenTelemetry 直观呈现延迟。如需大致了解本主题中的组件,请参阅 Spanner 请求中的延迟时间点

OpenTelemetry 是一个开源可观测性框架和工具包,可用于创建和管理遥测数据,例如跟踪记录、指标和日志。这是 OpenTracing 与 OpenCensus 之间合并的结果。如需了解详情,请参阅什么是 OpenTelemetry?

Spanner 客户端库利用 OpenTelemetry 可观测性框架提供指标和跟踪记录。按照确定延迟点中的过程,查找在 Spanner 中显示延迟的组件或组件。

准备工作

在开始捕获延迟时间指标之前,请先熟悉使用 OpenTelemetry 手动插桩。您必须使用用于导出遥测数据的相应选项配置 OpenTelemetry SDK。我们提供了多种 OpenTelemetry 导出器选项。我们建议使用 OpenTelemetry 协议 (OTLP) 导出器。其他方案包括将 OTel 收集器Google Cloud ExporterGoogle Managed Service for Prometheus Exporter 结合使用。

如果您在 Compute Engine 上运行应用,则可以使用 Ops Agent 来收集 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.23.1
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.23.1
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 请求的第一个字节与客户端从数据库收到的响应的最后一个字节之间的时长(以毫秒为单位)。

捕获客户端往返延迟时间

使用 OpenTelemetry 时,不支持 Spanner 客户端往返延迟时间指标。您可以通过迁移桥使用 OpenCensus 对指标进行插桩,并将数据迁移到 OpenTelemetry。

直观呈现客户端往返延迟时间

检索指标后,您可以在 Cloud Monitoring 中直观呈现客户端往返延迟时间。

以下示例图表说明了客户端往返延迟时间指标的第 5 百分位延迟时间。如需将百分位延迟时间更改为第 50 或第 99 百分位,请使用聚合器菜单。

该程序会创建一个名为 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 中直观呈现 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 请求延迟时间

您可以使用 Metrics Explorer 图表工具直观呈现 Cloud Monitoring 中 spanner.googleapis.com/api/request_latencies 指标的图表。

以下示例图表说明了 Spanner API 请求延迟时间指标的第 5 百分位延迟时间。如需将百分位延迟时间更改为第 50 或第 99 百分位,请使用聚合器菜单。

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 查询延迟时间。

后续步骤