Utiliser les métriques côté client pour résoudre les problèmes de latence élevée

Bien que Memorystore pour Redis fournisse des métriques côté serveur en temps réel pour surveiller le débit, l'utilisation du processeur et l'utilisation de la mémoire, ces données seules ne suffisent pas à expliquer pourquoi votre application cliente présente une latence élevée dans des systèmes distribués complexes.

Les métriques côté client résolvent ce problème en offrant une transparence totale sur le cycle complet requête-réponse. Elles mesurent une commande à partir du moment où l'application l'initie jusqu'à ce qu'elle traite la réponse. En capturant ces points de données, vous pouvez déterminer avec précision si la latence provient de la logique de l'application, du chemin réseau ou du serveur Redis.

Avant de commencer

Assurez-vous que votre application cliente utilise un compte de service et que les rôles Identity and Access Management (IAM) suivants lui sont attribués :

  • roles/cloudtrace.agent (Agent Cloud Trace)
  • roles/monitoring.metricWriter (Rédacteur de métriques Monitoring)

Pour en savoir plus sur l'attribution de rôles, consultez le guide de démarrage rapide Attribuer un rôle IAM à l'aide de la Google Cloud console.

Activer l'API Cloud Monitoring

Pour exporter des métriques côté client vers Monitoring, l'API Monitoring doit être activée dans votre application. L'exportation et la visualisation de ces métriques dans Monitoring vous permettent d'identifier la cause première des goulots d'étranglement et de déterminer l'origine de la latence.

Pour activer l'API Monitoring, procédez comme suit :

  1. Dans la Google Cloud console, accédez à la page API et services.

    Accéder aux API et aux services

  2. Sélectionnez le projet dans lequel vous avez créé l'instance Memorystore pour Redis.

  3. Cliquez sur Activer les API et les services.

  4. Recherchez monitoring.

  5. Dans les résultats de recherche, cliquez sur API Cloud Monitoring.

  6. Si API activée s'affiche, l'API est déjà activée. Sinon, cliquez sur Activer.

Activer l'API Cloud Trace

Pour afficher les traces distribuées dans Trace, vous devez activer l'API Trace. Vous pouvez ensuite utiliser l'explorateur Trace pour afficher ces traces, diagnostiquer les goulots d'étranglement et isoler la source de latence dans votre application.

Pour activer l'API Trace, procédez comme suit :

  1. Dans la Google Cloud console, accédez à la page API et services.

    Accéder aux API et aux services

  2. Sélectionnez le projet dans lequel vous avez créé l'instance Memorystore pour Redis.

  3. Cliquez sur Activer les API et les services.

  4. Recherchez trace.

  5. Dans les résultats de recherche, cliquez sur API Cloud Trace.

  6. Si API activée s'affiche, l'API est déjà activée. Sinon, cliquez sur Activer.

Activer les métriques côté client

Pour activer les métriques côté client, ajoutez le OpenTelemetry SDK, l'exportateur Cloud Monitoring et l'exportateur Cloud Trace au code de votre application. L'instrumentation OpenTelemetry, qui s'exécute directement dans la bibliothèque cliente Redis de votre application, capture les métriques. Cela permet à votre application d'enregistrer des points de données de latence et de les exporter vers Monitoring et Trace pour la visualisation.

Pour activer les métriques côté client, vous pouvez utiliser Go, Java, Node.js, ou Python. Les informations permettant d'activer les métriques pour chaque langage s'affichent dans les onglets suivants.

Go

  1. Pour installer les dépendances OpenTelemetry et Google Cloud d'exportateur requises, exécutez les commandes suivantes dans votre 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. Pour activer les métriques côté client, créez un fichier main.go et ajoutez-y le code suivant :

    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. Exécutez votre application pendant au moins une minute pour donner à l'exportateur suffisamment de temps pour regrouper et envoyer les métriques publiées à Monitoring.

Java

  1. Pour installer les dépendances OpenTelemetry et Google Cloud d'exportateur, ajoutez le code suivant au fichier pom.xml de votre application :

    <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. Pour activer les métriques côté client, créez un fichier RedisTelemetryApp.java et ajoutez-y le code suivant :

    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. Exécutez votre application pendant au moins une minute pour donner à l'exportateur suffisamment de temps pour regrouper et envoyer les métriques publiées à Monitoring.

Node.js

  1. Pour installer les dépendances OpenTelemetry et Google Cloud d'exportateur requises, exécutez les commandes suivantes dans votre 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. Pour activer les métriques côté client, créez un fichier server.js et ajoutez-y le code suivant :

    
    '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. Exécutez votre application pendant au moins une minute pour donner à l'exportateur suffisamment de temps pour regrouper et envoyer les métriques publiées à Monitoring.

Python

  1. Pour installer les dépendances OpenTelemetry et Google Cloud d'exportateur requises, exécutez les commandes suivantes dans votre 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. Pour activer les métriques côté client, créez un fichier main.py et ajoutez-y le code suivant :

    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. Exécutez votre application pendant au moins une minute pour donner à l'exportateur suffisamment de temps pour regrouper et envoyer les métriques publiées à Monitoring.

Afficher les métriques dans Monitoring

Une fois que vous avez activé les métriques côté client et exécuté votre application pendant au moins une minute pour donner à l'exportateur suffisamment de temps pour regrouper et envoyer les métriques à Monitoring, utilisez Monitoring pour visualiser vos métriques, les regrouper par opération ou instance, et appliquer des agrégateurs pour surveiller les performances de votre application.

Pour afficher les métriques dans Monitoring, procédez comme suit :

  1. Dans la Google Cloud console, accédez à la page Explorateur de métriques.

    Accéder à l'explorateur de métriques

  2. Sélectionnez votre Google Cloud projet.

  3. Cliquez sur Sélectionner une métrique.

  4. Recherchez workload.googleapis.com/redis.

  5. Sélectionnez une métrique côté client. Regroupez les données par operation et instance si nécessaire, puis choisissez un agrégateur. Pour découvrir plus d'options, consultez la section Sélectionner des métriques lors de l'utilisation de l'explorateur de métriques.

Afficher les traces distribuées dans Trace

Une fois que votre application commence à exporter des données, vous pouvez utiliser Trace pour visualiser le cycle complet requête-réponse de vos commandes Redis. L'affichage de vos traces distribuées dans Trace vous permet de diagnostiquer les goulots d'étranglement afin d'isoler rapidement la source exacte de la latence dans votre application.

Pour afficher les traces distribuées dans Trace, procédez comme suit :

  1. Dans la Google Cloud console, accédez à la page Explorateur Trace.

    Accéder à l'explorateur Trace

  2. Sélectionnez une trace récente représentée par un point sur le graphique à nuage de points.

  3. Examinez la vue en cascade pour isoler la source de latence en identifiant les goulots d'étranglement suivants :

    • Durée totale de la requête : la barre de niveau supérieur (parent) indique le temps total que vous devez attendre pour que l'opération se termine.

    • Latence du réseau et du serveur (DAR) : les barres enfants (telles que celles libellées GET ou SET) indiquent le temps que la commande a passé à transiter sur le réseau et à s'exécuter sur le serveur Memorystore pour Redis.

    • Blocage de la connexion client : s'il existe un grand espace horizontal vide avant le début du segment enfant Redis, le thread de l'application est bloqué en attente d'une connexion TCP disponible à partir du pool de connexions.

    • Blocage de l'analyse de l'application : s'il existe un grand espace horizontal vide après la fin du segment enfant Redis, l'application a du mal à analyser ou à traiter la charge utile renvoyée. Cela se produit souvent avec des chaînes JSON de plusieurs mégaoctets.

    • Nouvelles tentatives : si vous voyez plusieurs plages enfants courtes pour la même commande qui se produisent de manière séquentielle dans la même trace parent, votre client peut rencontrer une perte de paquets réseau et doit déclencher sa boucle de nouvelle tentative avec interruption exponentielle.

Résoudre les problèmes

Cette section répertorie les problèmes de performances courants que vous pouvez identifier à l'aide de métriques côté client, explique leurs causes premières et fournit des conseils pour résoudre ces problèmes.

Problème Cause Résoudre les problèmes

Votre application connaît un pic de latence soudain, mais Memorystore pour Redis semble parfaitement sain.

  • workload.googleapis.com/
    redis_client_blocking_latency
    (métrique côté client) : pic
  • workload.googleapis.com/redis_client_rtt (métrique côté client) : faible / typique
  • redis.googleapis.com/commands/
    usec_per_call
    (métrique du serveur Memorystore pour Redis) : faible / typique
  • redis.googleapis.com/clients/connected (métrique du serveur Memorystore pour Redis) : se stabilise à un nombre spécifique
Le goulot d'étranglement se situe strictement dans votre application. Vos threads tentent d'exécuter des commandes Redis, mais le pool de connexions est complètement épuisé. La valeur élevée de redis_client_blocking_latency représente le temps que votre code passe à attendre un socket TCP disponible avant que la commande ne soit envoyée au réseau. Pour gérer le trafic simultané plus élevé, augmentez les limites de taille du pool de connexions dans la configuration de votre client Redis (par exemple, MaxActive pour Go, MaxTotal pour Java ou max_connections pour Node.js et Python).

La requête se termine, mais le point de terminaison prend beaucoup plus de temps que prévu. Il n'y a aucun problème lié à l'état de votre réseau ou de votre serveur.

  • workload.googleapis.com/
    redis_application_blocking_latency
    (métrique côté client) : pic
  • workload.googleapis.com/redis_client_rtt (métrique côté client ) : faible / typique
  • redis.googleapis.com/commands/
    usec_per_call
    (métrique du serveur Memorystore pour Redis) : faible / typique
  • redis.googleapis.com/stats/
    network_traffic
    (Bytes out) (métrique du serveur Memorystore pour Redis) : pics importants
Memorystore pour Redis exécute la commande et le réseau transfère rapidement la charge utile (faible DAR). Toutefois, la charge utile renvoyée est volumineuse (par exemple, une chaîne JSON de 15 Mo). Votre application connaît une valeur élevée de redis_application_blocking_latency car elle consomme des ressources excessives lors de l'allocation de mémoire et de la désérialisation de cette chaîne volumineuse en un objet. Optimisez votre modèle de données. Ne stockez pas de blobs JSON volumineux dans des clés uniques. Décomposez les données à l'aide de hachages Redis (HSET) et utilisez HGET ou HMGET pour ne récupérer que les champs spécifiques dont vous avez besoin.

La latence de votre application destinée aux utilisateurs augmente, mais vos métriques Redis indiquent une faible latence du serveur et des extractions de pool de connexions typiques.

  • workload.googleapis.com/redis_retry_count (métrique côté client) : pics
  • workload.googleapis.com/
    redis_connectivity_error_count
    (métrique côté client) : peut afficher des incréments temporaires
  • workload.googleapis.com/redis_client_rtt (métrique côté client) : faible / typique pour les requêtes réussies
  • redis.googleapis.com/commands/
    usec_per_call
    (métrique du serveur Memorystore pour Redis) : faible / typique
Étant donné que redis_client_rtt ne capture que le DAR des requêtes réussies, il ne reflète pas la durée du délai avant expiration d'un paquet ayant échoué. Lorsque votre application rencontre des pertes de paquets temporaires ou des réinitialisations TCP, la logique de nouvelle tentative de votre client instrumenté incrémente le redis_retry_count et déclenche sa boucle de nouvelle tentative avec interruption exponentielle. Cela introduit un temps de veille entre les tentatives (par exemple, 100ms, 200ms, ou 400ms). L'utilisateur rencontre une latence totale élevée, mais la cause première sous-jacente est une perte de paquets réseau, qui déclenche des délais de veille côté client. Vérifiez vos journaux de flux VPC pour détecter les paquets perdus, la limitation de la bande passante, ou les anomalies de routage entre régions. Si vous rencontrez des délais avant expiration agressifs, assurez-vous que les délais avant expiration de votre connexion client (socket_timeout ou connect_timeout) sont supérieurs au DAR attendu pour tenir compte de la gigue réseau temporaire.

Tout s'arrête et toutes les couches du pipeline de télémétrie signalent une latence élevée.

  • workload.googleapis.com/redis_client_rtt (côté client métrique) : élevée
  • redis.googleapis.com/commands/
    usec_per_call
    (métrique du serveur Memorystore pour Redis) : élevée
  • redis.googleapis.com/stats/
    cpu_utilization_main_thread
    (métrique du serveur Memorystore pour Redis) : élevée (par exemple, proche de 1 s/s, ou 100%)
  • Cascade de traces : indique qu'une commande prend beaucoup de temps
Redis est monothread. Lorsque vous exécutez une commande de complexité temporelle O(N) (telle que KEYS *, SMEMBERS sur un ensemble massif ou HGETALL sur un hachage avec des millions de champs), le moteur Redis s'interrompt pour répondre à cette requête. Pendant l'exécution de cette commande, toutes les autres requêtes d'application sont mises en file d'attente, ce qui entraîne un pic de latence à l'échelle du système. Étant donné que votre redis_client_rtt personnalisé correspond à la latence du serveur (commands/usec_per_call), le serveur qui exécute la commande est le goulot d'étranglement.

Ouvrez Trace et examinez les commandes Redis sur les plages lentes plages pour identifier la requête qui provoque le blocage. Remplacez les commandes bloquantes par des commandes non bloquantes dans votre code.

Pour parcourir de grands ensembles de données de manière incrémentielle sans verrouiller le thread du serveur, utilisez SCAN, SSCAN, ou HSCAN.

Votre application signale une latence de base élevée et constante pour toutes les commandes Redis, même lorsque le trafic est faible.

  • workload.googleapis.com/redis_client_rtt (métrique côté client) : constamment élevée (p50 et p99 sont tous deux supérieurs à 30-100 ms)
  • redis.googleapis.com/commands/
    usec_per_call
    (métrique du serveur Memorystore pour Redis) : extrêmement faible (< 1 ms)
  • workload.googleapis.com/
    redis_client_blocking_latency

    et redis_application_blocking_latency : faible / typique
Le serveur Redis exécute les commandes instantanément, mais votre application et votre instance sont déployées dans des régions différentes (par exemple, us-central1 et us-east1). Chaque paquet réseau doit transiter par l'infrastructure physique Google Cloud entre ces centres de données géographiques. Cela entraîne une pénalité de latence interrégionale obligatoire à la vitesse de la lumière pour chaque aller-retour. Pour réduire la latence, déployez votre application dans la même région et la même zone que votre instance. Pour afficher la région de votre application et instance, utilisez la Google Cloud console.

Étape suivante