Examina la latencia en un componente de Spanner con OpenTelemetry

En este tema, se describe cómo examinar un componente de Spanner para encontrar la fuente de latencia y visualizar esa latencia con OpenTelemetry. Para obtener una descripción general de alto nivel de los componentes de este tema, consulta Puntos de latencia en una solicitud de Spanner.

OpenTelemetry es un framework y kit de herramientas de observabilidad de código abierto que te permite crear y administrar datos de telemetría, como seguimientos, métricas y registros. Es el resultado de una combinación entre OpenTracing y OpenCensus. Para obtener más información, consulta ¿Qué es OpenTelemetry?

Las bibliotecas cliente de Spanner proporcionan métricas y seguimientos con el uso del framework de observabilidad de OpenTelemetry. Sigue el procedimiento en Identifica el punto de latencia para encontrar los componentes o componentes que muestran latencia en Spanner.

Antes de comenzar

Antes de comenzar a capturar métricas de latencia, familiarízate con la instrumentación manual con OpenTelemetry. Debes configurar el SDK de OpenTelemetry con las opciones adecuadas para exportar tus datos de telemetría. Existen varias opciones de exportadores de OpenTelemetry disponibles. Recomendamos usar el exportador de OpenTelemetry Protocol (OTLP). Otras opciones incluyen usar un colector OTel con Google Cloud Exporter o el exportador de Google Managed Service para Prometheus.

Si ejecutas tu aplicación en Compute Engine, puedes usar el Agente de operaciones para recopilar métricas y seguimientos del protocolo de OpenTelemetry. Para obtener más información, consulta Recopila métricas y seguimientos de OTLP.

Agrega dependencias

Para configurar el SDK de OpenTelemetry y el exportador de OTLP, agrega las siguientes dependencias a tu aplicación.

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

Cómo insertar el objeto OpenTelemetry

Luego, crea un objeto OpenTelemetry con el exportador de OTLP y, luego, inserta el objeto OpenTelemetry con SpannerOptions.

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"),
		))
}

Captura y visualiza la latencia de ida y vuelta del cliente

La latencia de ida y vuelta del cliente es el tiempo (en milisegundos) entre el primer byte de la solicitud a la API de Spanner que el cliente envía a la base de datos (a través del GFE y el frontend de la API de Spanner) y el último byte de respuesta que el cliente recibe de la base de datos.

Cómo capturar la latencia de ida y vuelta del cliente

La métrica de latencia de ida y vuelta del cliente de Spanner no es compatible con OpenTelemetry. Puedes instrumentar la métrica mediante OpenCensus con un puente y migrar los datos a OpenTelemetry.

Visualiza la latencia de ida y vuelta del cliente

Después de recuperar las métricas, puedes visualizar la latencia de ida y vuelta del cliente en Cloud Monitoring.

A continuación, se muestra un ejemplo de un gráfico en el que se ilustra la latencia del percentil 5 para la métrica de latencia de ida y vuelta del cliente. Para cambiar la latencia del percentil al percentil 50 o 99, usa el menú Agregador.

El programa crea una vista llamada roundtrip_latency. Esta string se convierte en parte del nombre de la métrica cuando se exporta a Cloud Monitoring.

Latencia de ida y vuelta del cliente de Cloud Monitoring.

Captura y visualiza la latencia de GFE

La latencia de Google Front End (GFE) es el tiempo (en milisegundos) que transcurre entre el momento en que la red de Google recibe una llamada de procedimiento remoto del cliente y el momento en el que GFE recibe el primer byte de la respuesta.

Captura la latencia de GFE

Puedes capturar las métricas de latencia de GFE si habilitas las siguientes opciones mediante la biblioteca cliente de Spanner.

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
		}
	}
}

Visualiza la latencia de GFE

Después de recuperar las métricas, puedes visualizar la latencia de GFE en Cloud Monitoring.

Este es un ejemplo de un gráfico en el que se ilustra la agregación de distribución para la métrica de latencia de GFE. Para cambiar la latencia del percentil al percentil 5, 50, 95 o 99, usa el menú Agregador.

El programa crea una vista llamada spanner/gfe_latency. Esta string se convierte en parte del nombre de la métrica cuando se exporta a Cloud Monitoring.

Latencia de GFE de Cloud Monitoring

Captura y visualiza la latencia de solicitudes a la API de Spanner

La latencia de solicitudes a la API de Spanner es la cantidad de tiempo (en segundos) entre el primer byte de solicitud que recibe el frontend de la API de Spanner y el último byte de respuesta que envía el frontend de la API de Spanner.

Captura la latencia de solicitud a la API de Spanner

De forma predeterminada, esta latencia está disponible como parte de las métricas de Cloud Monitoring. No tienes que hacer nada para capturarlos y exportarlos.

Visualiza la latencia de la solicitud a la API de Spanner

Puedes usar la herramienta de gráficos Explorador de métricas para visualizar el gráfico de la métrica spanner.googleapis.com/api/request_latencies en Cloud Monitoring.

Este es un ejemplo de un gráfico en el que se ilustra la latencia del percentil 5 para la métrica de latencia de la solicitud a la API de Spanner. Para cambiar la latencia del percentil al percentil 50 o 99, usa el menú Agregador.

Latencia de solicitud a la API de Cloud Monitoring.

Captura y visualiza la latencia de las consultas

La latencia de las consultas es el tiempo (en milisegundos) que se tarda en ejecutar consultas de SQL en la base de datos de Spanner.

Cómo capturar la latencia de las consultas

Puedes capturar la latencia de las consultas para los siguientes lenguajes:

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
		}
	}
}

Visualiza la latencia de las consultas

Después de recuperar las métricas, puedes visualizar la latencia de las consultas en Cloud Monitoring.

Este es un ejemplo de un gráfico en el que se ilustra la agregación de distribución para la métrica de latencia de GFE. Para cambiar la latencia del percentil al percentil 5, 50, 95 o 99, usa el menú Agregador.

El programa crea una vista de OpenCensus llamada query_stats_elapsed. Esta string se vuelve parte del nombre de la métrica cuando se exporta a Cloud Monitoring.

Latencia de consultas de Cloud Monitoring.

¿Qué sigue?