Usar métricas do lado do cliente para resolver problemas de alta latência

Embora o Memorystore para Redis forneça métricas em tempo real do lado do servidor para monitorar a capacidade de processamento, a utilização da CPU e o uso da memória, esses dados sozinhos podem não explicar por que o aplicativo cliente apresenta alta latência em sistemas distribuídos complexos.

As métricas do lado do cliente resolvem esse problema, oferecendo transparência no ciclo completo de solicitação-resposta. Elas medem um comando desde o momento em que o aplicativo o inicia até que ele processe a resposta. Ao capturar esses pontos de dados, é possível determinar com precisão se a latência se origina da lógica do aplicativo, do caminho de rede ou do servidor Redis.

Antes de começar

Verifique se o aplicativo cliente usa uma conta de serviço e se os seguintes papéis do Identity and Access Management (IAM) estão atribuídos a ela:

  • roles/cloudtrace.agent (agente do Cloud Trace)
  • roles/monitoring.metricWriter (gravador de métricas do Monitoring)

Para mais informações sobre como conceder papéis, consulte o início rápido Conceder um papel do IAM usando o Google Cloud console.

Ativar a API Cloud Monitoring

Para exportar métricas do lado do cliente para o Monitoring, é necessário que a API Monitoring esteja ativada no aplicativo. A exportação e a visualização dessas métricas no Monitoring permitem identificar a causa raiz dos gargalos para determinar a origem da latência.

Para ativar a API Monitoring, faça o seguinte:

  1. No Google Cloud console do, acesse a página APIs e serviços.

    Acessar APIs e serviços

  2. Selecione o projeto em que você criou a instância do Memorystore para Redis.

  3. Clique em Ativar APIs e serviços.

  4. Pesquise monitoring.

  5. Nos resultados da pesquisa, clique em API Cloud Monitoring.

  6. Se a mensagem API ativada aparecer, a API já estará ativada. Caso contrário, clique em Ativar.

Ativar a API Cloud Trace

Para visualizar traces distribuídos no Trace, é necessário ativar a API Trace. Em seguida, use o Trace Explorer para visualizar esses traces, diagnosticar gargalos e isolar a origem da latência no aplicativo.

Para ativar a API Trace, faça o seguinte:

  1. No Google Cloud console do, acesse a página APIs e serviços.

    Acessar APIs e serviços

  2. Selecione o projeto em que você criou a instância do Memorystore para Redis.

  3. Clique em Ativar APIs e serviços.

  4. Pesquise trace.

  5. Nos resultados da pesquisa, clique em API Cloud Trace.

  6. Se a mensagem API ativada aparecer, a API já estará ativada. Caso contrário, clique em Ativar.

Ativar métricas do lado do cliente

Para ativar as métricas do lado do cliente, adicione o OpenTelemetry SDK, o exportador do Cloud Monitoring e o exportador do Cloud Trace ao código do aplicativo. A instrumentação do OpenTelemetry, que é executada diretamente na biblioteca de cliente do Redis do aplicativo, captura as métricas. Isso permite que o aplicativo grave pontos de dados de latência e os exporte para o Monitoring e o Trace para visualização.

Para ativar as métricas do lado do cliente, você pode usar Go, Java, Node.js, ou Python. As informações para ativar as métricas de cada linguagem aparecem nas guias a seguir.

Go

  1. Para instalar as dependências necessárias do OpenTelemetry e do Google Cloud exportador, execute os seguintes comandos no terminal:

      go get github.com/gomodule/redigo/redis@latest
      go get go.opentelemetry.io/otel
      go get go.opentelemetry.io/otel/sdk/trace
      go get go.opentelemetry.io/otel/sdk/metric
      go get github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace
      go get github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric
  2. Para ativar as métricas do lado do cliente, crie um arquivo main.go e adicione o seguinte código a ele:

    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"os"
    	"time"
    
    	"github.com/gomodule/redigo/redis"
    	"go.opentelemetry.io/otel"
    	"go.opentelemetry.io/otel/attribute"
    	"go.opentelemetry.io/otel/codes"
    	"go.opentelemetry.io/otel/metric"
    	"go.opentelemetry.io/otel/trace"
    
    	gcpmetric "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric"
    	gcptrace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
    	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
    	sdktrace "go.opentelemetry.io/otel/sdk/trace"
    )
    
    // MetricClient encapsulates the tracer and metric histograms to avoid package-level globals.
    type MetricClient struct {
    	tracer           trace.Tracer
    	rttHist          metric.Float64Histogram
    	clientBlockHist  metric.Float64Histogram
    	appBlockHist     metric.Float64Histogram
    	retryCounter     metric.Int64Counter
    	connErrorCounter metric.Int64Counter
    }
    
    // sleep hook enables lightning-fast unit tests by stubbing out real time.Sleep
    var sleep = time.Sleep
    
    // sinceMs calculates elapsed time in fractional milliseconds to avoid truncating sub-millisecond durations.
    func sinceMs(start time.Time) float64 {
    	return float64(time.Since(start).Microseconds()) / 1000.0
    }
    
    func initTelemetry(ctx context.Context) (*MetricClient, func(), error) {
    	traceExporter, err := gcptrace.New()
    	if err != nil {
    		return nil, nil, fmt.Errorf("gcptrace.New: %w", err)
    	}
    	tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(traceExporter))
    	otel.SetTracerProvider(tp)
    	tracer := tp.Tracer("redigo.client")
    
    	metricExporter, err := gcpmetric.New()
    	if err != nil {
    		return nil, nil, fmt.Errorf("gcpmetric.New: %w", err)
    	}
    	mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter, sdkmetric.WithInterval(10*time.Second))))
    	otel.SetMeterProvider(mp)
    	meter := mp.Meter("redigo.metrics")
    
    	rttHist, err := meter.Float64Histogram("redis_client_rtt", metric.WithUnit("ms"))
    	if err != nil {
    		return nil, nil, fmt.Errorf("redis_client_rtt histogram: %w", err)
    	}
    	clientBlockHist, err := meter.Float64Histogram("redis_client_blocking_latency", metric.WithUnit("ms"))
    	if err != nil {
    		return nil, nil, fmt.Errorf("redis_client_blocking_latency histogram: %w", err)
    	}
    	appBlockHist, err := meter.Float64Histogram("redis_application_blocking_latency", metric.WithUnit("ms"))
    	if err != nil {
    		return nil, nil, fmt.Errorf("redis_application_blocking_latency histogram: %w", err)
    	}
    	retryCounter, err := meter.Int64Counter("redis_retry_count")
    	if err != nil {
    		return nil, nil, fmt.Errorf("redis_retry_count counter: %w", err)
    	}
    	connErrorCounter, err := meter.Int64Counter("redis_connectivity_error_count")
    	if err != nil {
    		return nil, nil, fmt.Errorf("redis_connectivity_error_count counter: %w", err)
    	}
    
    	client := &MetricClient{
    		tracer:           tracer,
    		rttHist:          rttHist,
    		clientBlockHist:  clientBlockHist,
    		appBlockHist:     appBlockHist,
    		retryCounter:     retryCounter,
    		connErrorCounter: connErrorCounter,
    	}
    
    	initAttrs := metric.WithAttributes(attribute.String("operation", "startup"))
    	client.retryCounter.Add(ctx, 0, initAttrs)
    	client.connErrorCounter.Add(ctx, 0, initAttrs)
    
    	shutdown := func() {
    		tp.Shutdown(ctx)
    		mp.Shutdown(ctx)
    	}
    
    	return client, shutdown, nil
    }
    
    func (c *MetricClient) smartRedisCall(ctx context.Context, pool *redis.Pool, operationName string, commandName string, args ...interface{}) (interface{}, error) {
    	// Create a dedicated child span for the Redis command
    	ctx, span := c.tracer.Start(ctx, operationName)
    	span.SetAttributes(attribute.String("redis.command", commandName))
    	defer span.End()
    
    	maxRetries := 3
    	attempt := 0
    	metricOpts := metric.WithAttributes(attribute.String("operation", operationName))
    	var lastErr error
    
    	for attempt < maxRetries {
    		poolStart := time.Now()
    		// Use GetContext to respect context deadlines and cancellation
    		conn, err := pool.GetContext(ctx)
    		c.clientBlockHist.Record(ctx, sinceMs(poolStart), metricOpts)
    
    		if err != nil {
    			c.connErrorCounter.Add(ctx, 1, metricOpts)
    			c.retryCounter.Add(ctx, 1, metricOpts)
    			span.RecordError(err)
    			span.SetStatus(codes.Error, err.Error())
    			lastErr = err
    			attempt++
    			if attempt >= maxRetries {
    				break
    			}
    			sleep(time.Duration(100<<attempt) * time.Millisecond)
    			continue
    		}
    
    		// Check if the connection is dead
    		if err := conn.Err(); err != nil {
    			conn.Close()
    			c.connErrorCounter.Add(ctx, 1, metricOpts)
    			c.retryCounter.Add(ctx, 1, metricOpts)
    			span.RecordError(err)
    			span.SetStatus(codes.Error, err.Error())
    			lastErr = err
    			attempt++
    			if attempt >= maxRetries {
    				break
    			}
    			sleep(time.Duration(100<<attempt) * time.Millisecond)
    			continue
    		}
    
    		reqStart := time.Now()
    		// Redigo has no native DoContext; pass timeouts using redis.DoWithTimeout when context has a deadline
    		var reply interface{}
    		if deadline, ok := ctx.Deadline(); ok {
    			reply, err = redis.DoWithTimeout(conn, time.Until(deadline), commandName, args...)
    		} else {
    			reply, err = conn.Do(commandName, args...)
    		}
    		c.rttHist.Record(ctx, sinceMs(reqStart), metricOpts)
    		conn.Close()
    
    		if err != nil {
    			c.retryCounter.Add(ctx, 1, metricOpts)
    			span.RecordError(err)
    			span.SetStatus(codes.Error, err.Error())
    			lastErr = err
    			attempt++
    			if attempt >= maxRetries {
    				break
    			}
    			sleep(time.Duration(100<<attempt) * time.Millisecond)
    			continue
    		}
    
    		appStart := time.Now()
    		// Replace fmt.Sprintf to remove unnecessary string formatting overhead
    		sleep(2 * time.Millisecond)
    		c.appBlockHist.Record(ctx, sinceMs(appStart), metricOpts)
    
    		// Reset span status to Ok if the retry or execution eventually succeeds
    		span.SetStatus(codes.Ok, "")
    
    		return reply, nil
    	}
    	return nil, fmt.Errorf("max retries reached for %s: %w", operationName, lastErr)
    }
    
    func main() {
    	ctx := context.Background()
    	client, shutdown, err := initTelemetry(ctx)
    	if err != nil {
    		log.Printf("Failed to initialize telemetry: %v", err)
    		os.Exit(1)
    	}
    	defer shutdown()
    
    	redisHost := os.Getenv("REDISHOST")
    	redisPort := os.Getenv("REDISPORT")
    	if redisPort == "" {
    		redisPort = "6379"
    	}
    
    	pool := &redis.Pool{
    		MaxIdle:     10,
    		MaxActive:   20,
    		IdleTimeout: 240 * time.Second,
    		Wait:        true,
    		Dial: func() (redis.Conn, error) {
    			return redis.Dial("tcp", fmt.Sprintf("%s:%s", redisHost, redisPort))
    		},
    	}
    	defer pool.Close()
    
    	ctx, span := client.tracer.Start(ctx, "fetch_data_span")
    	defer span.End()
    
    	// Simple write and read operations
    	_, err = client.smartRedisCall(ctx, pool, "set_user", "SET", "user:123", "active")
    	if err != nil {
    		log.Printf("Error setting data: %v", err)
    	}
    	val, err := client.smartRedisCall(ctx, pool, "get_user", "GET", "user:123")
    	if err != nil {
    		log.Printf("Error fetching data: %v", err)
    	} else {
    		log.Printf("Retrieved value: %s", val)
    	}
    }
    
  3. Execute o aplicativo por pelo menos um minuto para dar ao exportador tempo suficiente para agrupar e enviar as métricas publicadas ao Monitoring.

Java

  1. Para instalar as dependências necessárias do OpenTelemetry e do Google Cloud exportador, adicione o seguinte código ao arquivo pom.xml do aplicativo:

    <dependencies>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>5.1.0</version>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-api</artifactId>
            <version>1.36.0</version>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-sdk</artifactId>
            <version>1.36.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.cloud.opentelemetry</groupId>
            <artifactId>exporter-trace</artifactId>
            <version>0.28.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.cloud.opentelemetry</groupId>
            <artifactId>exporter-metrics</artifactId>
            <version>0.28.0</version>
        </dependency>
    
        <!-- Testing Dependencies -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>4.11.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.36</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
  2. Para ativar as métricas do lado do cliente, crie um arquivo RedisTelemetryApp.java e adicione o seguinte código a ele:

    import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter;
    import com.google.cloud.opentelemetry.trace.TraceExporter;
    import io.opentelemetry.api.OpenTelemetry;
    import io.opentelemetry.api.common.AttributeKey;
    import io.opentelemetry.api.common.Attributes;
    import io.opentelemetry.api.metrics.DoubleHistogram;
    import io.opentelemetry.api.metrics.LongCounter;
    import io.opentelemetry.api.metrics.Meter;
    import io.opentelemetry.api.trace.Span;
    import io.opentelemetry.api.trace.Tracer;
    import io.opentelemetry.sdk.OpenTelemetrySdk;
    import io.opentelemetry.sdk.metrics.export.MetricExporter;
    import io.opentelemetry.sdk.metrics.SdkMeterProvider;
    import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
    import io.opentelemetry.sdk.trace.SdkTracerProvider;
    import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
    import io.opentelemetry.sdk.trace.export.SpanExporter;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    import redis.clients.jedis.exceptions.JedisConnectionException;
    
    import java.time.Duration;
    import java.util.function.Function;
    
    /**
     * Sample application demonstrating client-side metrics and tracing for
     * Google Cloud Memorystore for Redis.
     */
    public final class RedisTelemetryApp {
        /** Attribute key for Redis operation names. */
        private static final AttributeKey<String> ATTR_OPERATION =
                AttributeKey.stringKey("operation");
    
        /** Maximum number of Redis reconnection attempts. */
        private static final int MAX_RETRIES = 3;
    
        /** Maximum total connections for the Jedis pool. */
        private static final int POOL_MAX_TOTAL = 20;
    
        /** Interval in seconds for exporting metrics to Google Cloud. */
        private static final long METRIC_INTERVAL_SECONDS = 10L;
    
        /** Base multiplier for exponential backoff sleep (in milliseconds). */
        private static final long RETRY_BACKOFF_BASE_MS = 100L;
    
        /** Conversion factor from Nanoseconds to Milliseconds. */
        private static final double NANO_TO_MS = 1_000_000.0;
    
        /** Default Redis port. */
        private static final int DEFAULT_REDIS_PORT = 6379;
    
        /** OpenTelemetry Tracer instance for recording trace spans. */
        private static Tracer tracer;
    
        /** OpenTelemetry Histogram for Redis round-trip time. */
        private static DoubleHistogram rttHist;
    
        /** OpenTelemetry Histogram for pool blocking latency. */
        private static DoubleHistogram clientBlockHist;
    
        /** OpenTelemetry Histogram for application logic blocking latency. */
        private static DoubleHistogram appBlockHist;
    
        /** OpenTelemetry Counter for Redis reconnection retry events. */
        private static LongCounter retryCounter;
    
        /** OpenTelemetry Counter for Redis connectivity errors. */
        private static LongCounter connErrorCounter;
    
        /** Shared Jedis connection pool. */
        private static JedisPool jedisPool;
    
        /**
         * Private constructor to prevent instantiation of this utility class.
         */
        private RedisTelemetryApp() {
        }
    
        /**
         * Main entry point for running the sample application.
         *
         * @param args Command line arguments (not used).
         */
        public static void main(final String[] args) {
            setupTelemetry();
    
            final String host = System.getenv()
                    .getOrDefault("REDISHOST", "localhost");
            final int port = Integer.parseInt(System.getenv()
                    .getOrDefault("REDISPORT",
                            String.valueOf(DEFAULT_REDIS_PORT)));
    
            final JedisPoolConfig poolConfig = new JedisPoolConfig();
            poolConfig.setMaxTotal(POOL_MAX_TOTAL);
            poolConfig.setBlockWhenExhausted(true);
            jedisPool = new JedisPool(poolConfig, host, port);
    
            try {
                run();
            } finally {
                if (jedisPool != null) {
                    jedisPool.close();
                }
            }
        }
    
        /**
         * Executes the core business logic of reading and writing to Redis.
         *
         * @return The string retrieved from the Redis 'get' operation.
         */
        static String run() {
            final Span span = tracer.spanBuilder("process_user_span")
                    .startSpan();
            try {
                smartRedisCall("set_user", jedis ->
                        jedis.set("user:123", "active"));
    
                final String result = smartRedisCall("get_user", jedis ->
                        jedis.get("user:123"));
                System.out.println("Retrieved: " + result);
                return result;
            } catch (Exception e) {
                span.recordException(e);
                throw e;
            } finally {
                span.end();
            }
        }
    
        /**
         * Injects mocked or no-op telemetry and pool instances for unit testing.
         *
         * @param pool                The mocked or test JedisPool instance.
         * @param testOpenTelemetry The OpenTelemetry instance to use for testing.
         */
        static void initForTest(
                final JedisPool pool,
                final OpenTelemetry testOpenTelemetry) {
            jedisPool = pool;
            tracer = testOpenTelemetry.getTracer("jedis.client");
            final Meter meter = testOpenTelemetry.getMeter("jedis.metrics");
    
            rttHist = meter.histogramBuilder("redis_client_rtt")
                    .setUnit("ms").build();
            clientBlockHist = meter
                    .histogramBuilder("redis_client_blocking_latency")
                    .setUnit("ms").build();
            appBlockHist = meter
                    .histogramBuilder("redis_application_blocking_latency")
                    .setUnit("ms").build();
            retryCounter = meter.counterBuilder("redis_retry_count").build();
            connErrorCounter = meter
                    .counterBuilder("redis_connectivity_error_count")
                    .build();
    
            retryCounter.add(0, Attributes.of(ATTR_OPERATION, "startup"));
            connErrorCounter.add(0, Attributes.of(ATTR_OPERATION, "startup"));
        }
    
        /**
         * Configures the production OpenTelemetry SDK to export Traces and Metrics
         * to Google Cloud Operations.
         */
        private static void setupTelemetry() {
            final SpanExporter traceExporter =
                    TraceExporter.createWithDefaultConfiguration();
            final SdkTracerProvider tracerProvider =
                    SdkTracerProvider.builder()
                            .addSpanProcessor(
                                    BatchSpanProcessor.builder(traceExporter)
                                            .build())
                            .build();
    
            final MetricExporter metricExporter =
                    GoogleCloudMetricExporter.createWithDefaultConfiguration();
            final SdkMeterProvider meterProvider =
                    SdkMeterProvider.builder()
                            .registerMetricReader(
                                    PeriodicMetricReader.builder(metricExporter)
                                            .setInterval(Duration.ofSeconds(
                                                    METRIC_INTERVAL_SECONDS))
                                            .build())
                            .build();
    
            final OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
                    .setTracerProvider(tracerProvider)
                    .setMeterProvider(meterProvider)
                    .buildAndRegisterGlobal();
    
            tracer = openTelemetry.getTracer("jedis.client");
            final Meter meter = openTelemetry.getMeter("jedis.metrics");
    
            rttHist = meter.histogramBuilder("redis_client_rtt")
                    .setUnit("ms").build();
            clientBlockHist = meter
                    .histogramBuilder("redis_client_blocking_latency")
                    .setUnit("ms").build();
            appBlockHist = meter
                    .histogramBuilder("redis_application_blocking_latency")
                    .setUnit("ms").build();
            retryCounter = meter.counterBuilder("redis_retry_count").build();
            connErrorCounter = meter
                    .counterBuilder("redis_connectivity_error_count")
                    .build();
    
            retryCounter.add(0, Attributes.of(ATTR_OPERATION, "startup"));
            connErrorCounter.add(0, Attributes.of(ATTR_OPERATION, "startup"));
        }
    
        /**
         * Wraps a Redis operation with latency metrics, reconnection retry logic,
         * and trace spans.
         *
         * @param <T>           The return type of the Redis operation.
         * @param operationName The name of the operation for metric attributes.
         * @param operation     The Redis command lambda to execute safely.
         * @return The return value from the Redis command.
         */
        private static <T> T smartRedisCall(
                final String operationName,
                final Function<Jedis, T> operation) {
            int attempt = 0;
            final Attributes attrs = Attributes.of(ATTR_OPERATION,
                    operationName);
    
            final Span span = tracer.spanBuilder(operationName).startSpan();
    
            try {
                while (attempt < MAX_RETRIES) {
                    final long poolStart = System.nanoTime();
                    try (Jedis jedis = jedisPool.getResource()) {
                        clientBlockHist.record((System.nanoTime() - poolStart)
                                / NANO_TO_MS, attrs);
    
                        final long reqStart = System.nanoTime();
                        final T response = operation.apply(jedis);
                        rttHist.record((System.nanoTime() - reqStart)
                                / NANO_TO_MS, attrs);
    
                        final long appStart = System.nanoTime();
                        @SuppressWarnings("unused")
                        final String dummy = String.valueOf(response);
                        appBlockHist.record((System.nanoTime() - appStart)
                                / NANO_TO_MS, attrs);
    
                        return response;
                    } catch (JedisConnectionException e) {
                        attempt++;
                        connErrorCounter.add(1, attrs);
                        retryCounter.add(1, attrs);
                        span.recordException(e);
                        if (attempt >= MAX_RETRIES) {
                            throw e;
                        }
                        try {
                            Thread.sleep((long) (Math.pow(2, attempt)
                                    * RETRY_BACKOFF_BASE_MS));
                        } catch (InterruptedException ie) {
                            Thread.currentThread().interrupt();
                        }
                    }
                }
                return null;
            } finally {
                span.end();
            }
        }
    }
  3. Execute o aplicativo por pelo menos um minuto para dar ao exportador tempo suficiente para agrupar e enviar as métricas publicadas ao Monitoring.

Node.js

  1. Para instalar as dependências necessárias do OpenTelemetry e do Google Cloud exportador, execute os seguintes comandos no terminal:

      npm install redis@^4.6.0 @opentelemetry/api@^1.9.0
      @opentelemetry/sdk-trace-node@^2.1.0
      @opentelemetry/sdk-trace-base@^2.1.0
      @opentelemetry/sdk-metrics@^2.1.0
      @opentelemetry/instrumentation@^0.205.0
      @opentelemetry/instrumentation-redis@^0.67.0
      @google-cloud/opentelemetry-cloud-trace-exporter@^3.0.0
      @google-cloud/opentelemetry-cloud-monitoring-exporter@^0.21.0
      @opentelemetry/resources@^2.1.0
  2. Para ativar as métricas do lado do cliente, crie um arquivo server.js e adicione o seguinte código a ele:

    
    'use strict';
    
    const {trace, metrics} = require('@opentelemetry/api');
    const {NodeTracerProvider} = require('@opentelemetry/sdk-trace-node');
    const {BatchSpanProcessor} = require('@opentelemetry/sdk-trace-base');
    const {
      TraceExporter,
    } = require('@google-cloud/opentelemetry-cloud-trace-exporter');
    const {
      MeterProvider,
      PeriodicExportingMetricReader,
    } = require('@opentelemetry/sdk-metrics');
    const {
      MetricExporter,
    } = require('@google-cloud/opentelemetry-cloud-monitoring-exporter');
    const {RedisInstrumentation} = require('@opentelemetry/instrumentation-redis');
    const {registerInstrumentations} = require('@opentelemetry/instrumentation');
    const {performance} = require('perf_hooks');
    
    // FIX: Pass spanProcessors in the constructor options for NodeTracerProvider in SDK 2.x
    const provider = new NodeTracerProvider({
      spanProcessors: [new BatchSpanProcessor(new TraceExporter())],
    });
    provider.register();
    
    registerInstrumentations({
      instrumentations: [new RedisInstrumentation()],
    });
    
    const redis = require('redis');
    
    const metricExporter = new MetricExporter();
    const metricReader = new PeriodicExportingMetricReader({
      exporter: metricExporter,
      exportIntervalMillis: 10000,
    });
    const meterProvider = new MeterProvider({readers: [metricReader]});
    metrics.setGlobalMeterProvider(meterProvider);
    
    const tracer = trace.getTracer('redis.client.node');
    const meter = metrics.getMeter('redis.metrics.node');
    
    const rttHist = meter.createHistogram('redis_client_rtt', {unit: 'ms'});
    const appBlockHist = meter.createHistogram(
      'redis_application_blocking_latency',
      {unit: 'ms'}
    );
    const retryCounter = meter.createCounter('redis_retry_count');
    const connErrorCounter = meter.createCounter('redis_connectivity_error_count');
    
    retryCounter.add(0, {operation: 'startup'});
    connErrorCounter.add(0, {operation: 'startup'});
    
    const REDISHOST = process.env.REDISHOST || 'localhost';
    const REDISPORT = process.env.REDISPORT || 6379;
    
    const client = redis.createClient({
      socket: {
        host: REDISHOST,
        port: REDISPORT,
        reconnectStrategy: retries => {
          connErrorCounter.add(1, {error: 'socket_reconnect'});
          if (retries > 5) return new Error('Max retries reached');
          return Math.min(retries * 100, 3000);
        },
      },
    });
    client.on('error', err => console.log('Redis Client Error', err));
    
    async function smartRedisCall(operationName, func, ...args) {
      let attempt = 0;
      while (attempt < 3) {
        try {
          const reqStart = performance.now();
          const response = await func(...args);
          rttHist.record(performance.now() - reqStart, {operation: operationName});
    
          const appParseStart = performance.now();
          // eslint-disable-next-line no-unused-vars
          const _ = String(response);
          appBlockHist.record(performance.now() - appParseStart, {
            operation: operationName,
          });
    
          return response;
        } catch (e) {
          attempt++;
          retryCounter.add(1, {operation: operationName});
          if (attempt >= 3) throw e;
          await new Promise(resolve =>
            setTimeout(resolve, Math.pow(2, attempt) * 100)
          );
        }
      }
    }
    
    async function main() {
      await client.connect();
    
      await tracer.startActiveSpan('process_user_span', async span => {
        try {
          // Simple write and read operations
          await smartRedisCall(
            'set_user',
            client.set.bind(client),
            'user:123',
            'active'
          );
    
          const result = await smartRedisCall(
            'get_user',
            client.get.bind(client),
            'user:123'
          );
          console.log('Retrieved:', result);
        } catch (e) {
          span.recordException(e);
        } finally {
          span.end();
        }
      });
    
      await client.quit();
      await provider.forceFlush();
      await meterProvider.forceFlush();
    }
    
    // Only run the script automatically if it is executed directly (e.g. `node server.js`)
    if (require.main === module) {
      main().catch(console.error);
    }
    
    // Export for testability
    module.exports = {
      main,
      smartRedisCall,
    };
    
  3. Execute o aplicativo por pelo menos um minuto para dar ao exportador tempo suficiente para agrupar e enviar as métricas publicadas ao Monitoring.

Python

  1. Para instalar as dependências necessárias do OpenTelemetry e do Google Cloud exportador, execute os seguintes comandos no terminal:

      pip install redis==7.0.1 opentelemetry-api==1.39.1
      opentelemetry-sdk==1.39.1
      opentelemetry-instrumentation-redis==0.60b1
      opentelemetry-exporter-gcp-trace==1.11.0
      opentelemetry-exporter-gcp-monitoring==1.11.0a0
  2. Para ativar as métricas do lado do cliente, crie um arquivo main.py e adicione o seguinte código ao aplicativo:

    import os
    import time
    
    from opentelemetry import metrics, trace
    from opentelemetry.exporter.cloud_monitoring import (
        CloudMonitoringMetricsExporter,
    )
    from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
    from opentelemetry.instrumentation.redis import RedisInstrumentor
    from opentelemetry.sdk.metrics import MeterProvider
    from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.sdk.trace.export import BatchSpanProcessor
    import redis
    from redis.exceptions import ConnectionError, TimeoutError
    
    
    
    
    def init_telemetry():
        """Initializes OpenTelemetry with GCP Exporters and returns the SDK objects."""
        # 1. Initialize Tracing
        tracer_provider = TracerProvider()
        tracer_provider.add_span_processor(
            BatchSpanProcessor(CloudTraceSpanExporter())
        )
        trace.set_tracer_provider(tracer_provider)
        tracer = trace.get_tracer("redis.client")
    
        # 2. Initialize Metrics
        metrics_exporter = CloudMonitoringMetricsExporter()
        metric_reader = PeriodicExportingMetricReader(
            metrics_exporter, export_interval_millis=10000
        )
        meter_provider = MeterProvider(metric_readers=[metric_reader])
        metrics.set_meter_provider(meter_provider)
        meter = metrics.get_meter("redis.metrics")
    
        # Bundle all metric handlers safely into a dictionary
        redis_metrics = {
            "rtt_hist": meter.create_histogram("redis_client_rtt", unit="ms"),
            "client_block_hist": meter.create_histogram(
                "redis_client_blocking_latency", unit="ms"
            ),
            "app_block_hist": meter.create_histogram(
                "redis_application_blocking_latency", unit="ms"
            ),
            "retry_counter": meter.create_counter("redis_retry_count"),
            "conn_error_counter": meter.create_counter(
                "redis_connectivity_error_count"
            ),
        }
    
        redis_metrics["retry_counter"].add(0, {"operation": "startup"})
        redis_metrics["conn_error_counter"].add(0, {"operation": "startup"})
    
        # 3. Setup Redis Auto-Instrumentation
        RedisInstrumentor().instrument()
    
        return tracer, redis_metrics, tracer_provider, meter_provider
    
    
    def init_redis_pool():
        """Initializes and returns the Redis ConnectionPool and Client."""
        redis_host = os.environ.get("REDISHOST", "localhost")
        redis_port = int(os.environ.get("REDISPORT", 6379))
    
        redis_pool = redis.ConnectionPool(
            host=redis_host,
            port=redis_port,
            max_connections=10,
            decode_responses=True,
        )
        redis_client = redis.Redis(connection_pool=redis_pool)
        return redis_pool, redis_client
    
    
    def smart_redis_call(
        operation_name, func, redis_pool, metrics, *args, **kwargs
    ):
        """Executes a Redis operation with metrics and retry handling (No Globals!)."""
        max_retries = 3
        attempt = 0
    
        pool_start = time.time()
        try:
            conn = redis_pool.get_connection()
            redis_pool.release(conn)
        except Exception:
            pass
    
        if metrics and metrics.get("client_block_hist"):
            metrics["client_block_hist"].record(
                (time.time() - pool_start) * 1000, {"operation": operation_name}
            )
    
        while attempt < max_retries:
            try:
                req_start = time.time()
                response = func(*args, **kwargs)
    
                if metrics and metrics.get("rtt_hist"):
                    metrics["rtt_hist"].record(
                        (time.time() - req_start) * 1000,
                        {"operation": operation_name},
                    )
    
                app_start = time.time()
                _ = str(response)
    
                if metrics and metrics.get("app_block_hist"):
                    metrics["app_block_hist"].record(
                        (time.time() - app_start) * 1000,
                        {"operation": operation_name},
                    )
    
                return response
    
            except (ConnectionError, TimeoutError) as e:
                attempt += 1
                if metrics and metrics.get("conn_error_counter"):
                    metrics["conn_error_counter"].add(
                        1, {"operation": operation_name}
                    )
                if metrics and metrics.get("retry_counter"):
                    metrics["retry_counter"].add(1, {"operation": operation_name})
                if attempt >= max_retries:
                    raise e
                time.sleep((2**attempt) * 0.1)
    
    if __name__ == "__main__":
        tracer, redis_metrics, tracer_provider, meter_provider = init_telemetry()
        redis_pool, redis_client = init_redis_pool()
    
        if tracer:
            with tracer.start_as_current_span("process_user_span"):
                try:
                    # Simple write and read operations
                    smart_redis_call(
                        "set_user",
                        redis_client.set,
                        redis_pool,
                        redis_metrics,
                        "user:123",
                        "active",
                    )
    
                    result = smart_redis_call(
                        "get_user",
                        redis_client.get,
                        redis_pool,
                        redis_metrics,
                        "user:123",
                    )
                    print(f"Retrieved: {result}")
                except Exception as e:
                    print(f"Error: {e}")
    
            tracer_provider.force_flush()
            meter_provider.force_flush()
  3. Execute o aplicativo por pelo menos um minuto para dar ao exportador tempo suficiente para agrupar e enviar as métricas publicadas ao Monitoring.

Visualizar métricas no Monitoring

Depois de ativar as métricas do lado do cliente e executar o aplicativo por pelo menos um minuto para dar ao exportador tempo suficiente para agrupar e enviar métricas ao Monitoring, use o Monitoring para visualizar as métricas, agrupá-las por operação ou instância e aplicar agregadores para monitorar o desempenho do aplicativo.

Para visualizar métricas no Monitoring, faça o seguinte:

  1. No Google Cloud console do, acesse a página Metrics Explorer.

    Acessar o Metrics Explorer

  2. Selecionar o Google Cloud projeto.

  3. Clique em Selecionar uma métrica.

  4. Pesquise workload.googleapis.com/redis.

  5. Selecione uma métrica do lado do cliente. Agrupe os dados por operation e instance, conforme necessário, e escolha um agregador. Para conhecer outras opções, consulte Selecionar métricas ao usar o Metrics Explorer.

Visualizar traces distribuídos no Trace

Depois que o aplicativo começar a exportar dados, você poderá usar o Trace para visualizar o ciclo completo de solicitação-resposta dos comandos do Redis. A visualização dos traces distribuídos no Trace permite diagnosticar gargalos para isolar rapidamente a origem exata da latência no aplicativo.

Para visualizar traces distribuídos no Trace, faça o seguinte:

  1. No Google Cloud console, acesse a página Explorador de traces.

    Acessar Explorador de traces

  2. Selecione um trace recente representado por um ponto no gráfico de dispersão.

  3. Examine a visualização em cascata para isolar a origem da latência, identificando os seguintes gargalos:

    • Duração total da solicitação: a barra de nível superior (principal) mostra o tempo total de espera para que a operação seja concluída.

    • Latência de rede e servidor (RTT): as barras filhas (como as marcadas como GET ou SET) mostram o tempo que o comando passou viajando pela rede e sendo executado no servidor do Memorystore para Redis.

    • Bloqueio de conexão do cliente: se houver uma lacuna horizontal grande e vazia antes do início do período filho do Redis, a linha de execução do aplicativo estará aguardando uma conexão TCP disponível do pool de conexões.

    • Bloqueio de análise do aplicativo: se houver uma lacuna horizontal grande e vazia após o término do período filho do Redis, o aplicativo terá dificuldades para analisar ou processar o payload retornado. Isso geralmente acontece com strings JSON de vários megabytes.

    • Repetições: se você encontrar vários períodos filhos curtos para o mesmo comando ocorrendo sequencialmente no mesmo trace pai, o cliente poderá sofrer perda de pacotes de rede e precisará acionar o loop de repetição de backoff exponencial.

Resolver problemas

Esta seção lista problemas comuns de desempenho que podem ser identificados usando métricas do lado do cliente, explica as causas raiz e fornece orientações sobre como solucionar os problemas.

Problema Causa Resolver problemas

O aplicativo apresenta um pico repentino de latência, mas o Memorystore para Redis parece totalmente íntegro.

  • workload.googleapis.com/
    redis_client_blocking_latency
    (métrica do lado do cliente): pico
  • workload.googleapis.com/redis_client_rtt (métrica do lado do cliente): baixa / típica
  • redis.googleapis.com/commands/
    usec_per_call
    (métrica do servidor do Memorystore para Redis): baixa / típica
  • redis.googleapis.com/clients/connected (métrica do servidor do Memorystore para Redis): linhas planas em um número específico
O gargalo está estritamente dentro do aplicativo. As linhas de execução tentam executar comandos do Redis, mas o pool de conexões está totalmente esgotado. A alta redis_client_blocking_latency representa o tempo que o código passa esperando por um soquete TCP disponível antes que o comando seja enviado à rede. Para processar o tráfego simultâneo mais alto, aumente os limites de tamanho do pool de conexões na configuração do cliente do Redis (por exemplo, MaxActive para Go, MaxTotal para Java ou max_connections para Node.js e Python).

A solicitação é concluída, mas o endpoint leva muito mais tempo do que o esperado. Não há problemas associados à integridade da rede ou do servidor.

  • workload.googleapis.com/
    redis_application_blocking_latency
    (métrica do lado do cliente): pico
  • workload.googleapis.com/redis_client_rtt (métrica do lado do cliente): baixa / típica
  • redis.googleapis.com/commands/
    usec_per_call
    (métrica do servidor do Memorystore para Redis): baixa / típica
  • redis.googleapis.com/stats/
    network_traffic
    (bytes enviados) (métrica do servidor do Memorystore para Redis): picos acentuados
O Memorystore para Redis executa o comando e a rede transfere o payload rapidamente (RTT baixo). No entanto, o payload retornado é grande (por exemplo, uma string JSON de 15 MB). O aplicativo apresenta uma redis_application_blocking_latency alta porque o aplicativo consome recursos excessivos ao alocar memória e desserializar essa string grande em um objeto. Otimize o modelo de dados. Não armazene blobs JSON enormes em chaves únicas. Divida os dados usando hashes do Redis (HSET) e use HGET ou HMGET para recuperar apenas os campos específicos de que você precisa.

A latência do aplicativo voltado ao usuário aumenta, mas as métricas do Redis informam uma latência baixa do servidor e check-outs típicos do pool de conexões.

  • workload.googleapis.com/redis_retry_count (métrica do lado do cliente): picos
  • workload.googleapis.com/
    redis_connectivity_error_count
    (métrica do lado do cliente): pode mostrar incrementos temporários
  • workload.googleapis.com/redis_client_rtt (métrica do lado do cliente): baixa / típica para solicitações bem-sucedidas
  • redis.googleapis.com/commands/
    usec_per_call
    (métrica do servidor do Memorystore para Redis): baixa / típica
Como redis_client_rtt captura apenas o RTT de solicitações bem-sucedidas, ele não reflete a duração do tempo limite de um pacote com falha. Quando o aplicativo apresenta quedas de pacotes temporárias ou redefinições de TCP, a lógica de repetição do cliente instrumentado incrementa o redis_retry_count e aciona o loop de espera exponencial. Isso introduz um tempo de espera entre as tentativas (por exemplo, 100ms, 200ms, ou 400ms). O usuário tem uma latência total alta, mas a causa raiz subjacente é uma perda de pacotes de rede, que aciona atrasos de espera do lado do cliente. Verifique os registros de fluxos de VPC para pacotes descartados, limitação de largura de banda, ou anomalias de roteamento entre regiões. Se você tiver tempos limite agressivos, verifique se os tempos limite de conexão do cliente (socket_timeout ou connect_timeout) são maiores que o RTT esperado para considerar a instabilidade de rede temporária.

Tudo para e todas as camadas do pipeline de telemetria informam alta latência.

  • workload.googleapis.com/redis_client_rtt (métrica do lado do cliente): alta
  • redis.googleapis.com/commands/
    usec_per_call
    (métrica do servidor do Memorystore para Redis): alta
  • redis.googleapis.com/stats/
    cpu_utilization_main_thread
    (métrica do servidor do Memorystore para Redis): alta (por exemplo, perto de 1 s/s, ou 100%)
  • Cascata de trace: mostra um comando que leva muito tempo
O Redis é de linha de execução única. Quando você executa um comando de complexidade de tempo O(N) como KEYS *, SMEMBERS em um conjunto enorme ou HGETALL em um hash com milhões de campos, o mecanismo do Redis pausa para atender a essa solicitação. Enquanto esse comando é executado, todas as outras solicitações de aplicativos são enfileiradas, causando um pico de latência em todo o sistema. Como o redis_client_rtt personalizado corresponde à latência do servidor (commands/usec_per_call), o servidor que executa o comando é o gargalo.

Abra o Trace e examine os comandos do Redis nos períodos lentos para identificar qual consulta causa o bloqueio. Substitua comandos de bloqueio por comandos sem bloqueio no código.

Para iterar conjuntos de dados grandes de forma incremental sem bloquear a linha de execução do servidor, use SCAN, SSCAN, ou HSCAN.

O aplicativo informa uma latência de linha de base consistente e elevada para todos os comandos do Redis, mesmo quando o tráfego é baixo.

  • workload.googleapis.com/redis_client_rtt (métrica do lado do cliente): consistentemente elevada (p50 e p99 são ~30 a 100 ms ou mais)
  • redis.googleapis.com/commands/
    usec_per_call
    (métrica do servidor do Memorystore para Redis): extremamente baixa (< 1 ms)
  • workload.googleapis.com/
    redis_client_blocking_latency

    e redis_application_blocking_latency: baixa / típica
O servidor Redis executa comandos instantaneamente, mas o aplicativo e a instância são implantados em regiões diferentes (por exemplo, us-central1 e us-east1). Cada pacote de rede precisa viajar pela infraestrutura em nuvem física do Google Cloud entre esses data centers geográficos. Isso resulta em uma penalidade de latência entre regiões obrigatória de velocidade da luz para cada ida e volta. Para reduzir a latência, implante o aplicativo para que ele resida na mesma região e zona da instância. Para conferir a região do aplicativo e da instância, use o Google Cloud console.

A seguir