Utilizzare le metriche lato client per risolvere i problemi di latenza elevata

Sebbene Memorystore for Redis fornisca metriche lato server in tempo reale per monitorare la velocità effettiva, l'utilizzo della CPU e la memoria utilizzata, questi dati da soli potrebbero non spiegare perché l'applicazione client riscontra una latenza elevata all'interno di sistemi distribuiti complessi.

Le metriche lato client risolvono questo problema fornendo trasparenza sull'intero ciclo richiesta-risposta. Misurano un comando dal momento in cui l'applicazione lo avvia fino a quando l'applicazione elabora la risposta. Acquisendo questi punti dati, puoi determinare con precisione se la latenza ha origine dalla logica dell'applicazione, dal percorso di rete o dal server Redis.

Prima di iniziare

Assicurati che l'applicazione client utilizzi un account di servizio e che siano assegnati i seguenti ruoli Identity and Access Management (IAM):

  • roles/cloudtrace.agent (agente Cloud Trace)
  • roles/monitoring.metricWriter (Monitoring Metric Writer)

Per ulteriori informazioni sulla concessione dei ruoli, consulta la guida rapida Concedi un ruolo IAM utilizzando la Google Cloud console.

Abilita l'API Cloud Monitoring

Per esportare le metriche lato client in Monitoring, l'applicazione richiede l'abilitazione dell'API Monitoring. L'esportazione e la visualizzazione di queste metriche in Monitoring ti consentono di individuare la causa principale dei colli di bottiglia per determinare l'origine della latenza.

Per abilitare l'API Monitoring:

  1. Nella Google Cloud console, vai alla pagina API e servizi.

    Vai ad API e servizi

  2. Seleziona il progetto in cui hai creato l'istanza Memorystore for Redis.

  3. Fai clic su Abilita API e servizi.

  4. Cerca monitoring.

  5. Nei risultati di ricerca, fai clic su API Cloud Monitoring.

  6. Se viene visualizzato il messaggio API abilitata, l'API è già abilitata. In caso contrario, fai clic su Abilita.

Abilita l'Cloud Trace API

Per visualizzare le tracce distribuite in Trace, devi abilitare l'API Trace. Puoi quindi utilizzare Trace Explorer per visualizzare queste tracce, diagnosticare i colli di bottiglia e isolare l'origine della latenza nella tua applicazione.

Per abilitare l'API Trace:

  1. Nella Google Cloud console, vai alla pagina API e servizi.

    Vai ad API e servizi

  2. Seleziona il progetto in cui hai creato l'istanza Memorystore for Redis.

  3. Fai clic su Abilita API e servizi.

  4. Cerca trace.

  5. Nei risultati di ricerca, fai clic su API Cloud Trace.

  6. Se viene visualizzato il messaggio API abilitata, l'API è già abilitata. In caso contrario, fai clic su Abilita.

Abilita le metriche lato client

Per abilitare le metriche lato client, aggiungi l'OpenTelemetry OpenTelemetry, l'esportatore Cloud Monitoring e l'esportatore Cloud Trace al codice dell'applicazione. L'instrumentazione OpenTelemetry, che viene eseguita direttamente all'interno della libreria client Redis dell'applicazione, acquisisce le metriche. In questo modo, l'applicazione può registrare i punti dati di latenza ed esportarli in Monitoring e Trace per la visualizzazione.

Per abilitare le metriche lato client, puoi utilizzare Go, Java, Node.js, o Python. Le informazioni sull'abilitazione delle metriche per ogni lingua sono riportate nelle schede seguenti.

Vai

  1. Per installare le dipendenze richieste di OpenTelemetry ed Google Cloud esportatore, esegui i seguenti comandi nel terminale:

      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. Per abilitare le metriche lato client, crea un file main.go e aggiungi il seguente codice:

    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. Esegui l'applicazione per almeno un minuto per dare all'esportatore il tempo sufficiente per raggruppare e inviare le metriche pubblicate a Monitoring.

Java

  1. Per installare le dipendenze richieste di OpenTelemetry e Google Cloud esportatore dependencies, aggiungi il seguente codice al file pom.xml dell'applicazione:

    <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. Per abilitare le metriche lato client, crea un file RedisTelemetryApp.java e aggiungi il seguente codice:

    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. Esegui l'applicazione per almeno un minuto per dare all'esportatore il tempo sufficiente per raggruppare e inviare le metriche pubblicate a Monitoring.

Node.js

  1. Per installare le dipendenze richieste di OpenTelemetry ed Google Cloud esportatore, esegui i seguenti comandi nel terminale:

      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. Per abilitare le metriche lato client, crea un file server.js e aggiungi il seguente codice:

    
    '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. Esegui l'applicazione per almeno un minuto per dare all'esportatore il tempo sufficiente per raggruppare e inviare le metriche pubblicate a Monitoring.

Python

  1. Per installare le dipendenze richieste di OpenTelemetry ed Google Cloud esportatore, esegui i seguenti comandi nel terminale:

      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. Per abilitare le metriche lato client, crea un file main.py e aggiungi il seguente codice all'applicazione:

    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. Esegui l'applicazione per almeno un minuto per dare all'esportatore il tempo sufficiente per raggruppare e inviare le metriche pubblicate a Monitoring.

Visualizza le metriche in Monitoring

Dopo aver abilitato le metriche lato client ed eseguito l'applicazione per almeno un minuto per dare all'esportatore il tempo sufficiente per raggruppare e inviare le metriche a Monitoring, utilizza Monitoring per visualizzare le metriche, raggrupparle per operazione o istanza e applicare gli aggregatori per monitorare le prestazioni dell'applicazione.

Per visualizzare le metriche in Monitoring:

  1. Nella Google Cloud console, vai alla pagina Esplora metriche.

    Vai a Esplora metriche

  2. Selezionare il tuo Google Cloud progetto.

  3. Fai clic su Seleziona una metrica.

  4. Cerca workload.googleapis.com/redis.

  5. Seleziona una metrica lato client. Raggruppa i dati per operation e instance, se necessario, e scegli un aggregatore. Per esplorare altre opzioni, consulta Seleziona le metriche durante l'utilizzo di Esplora metriche.

Visualizza le tracce distribuite in Trace

Dopo che l'applicazione inizia a esportare i dati, puoi utilizzare Trace per visualizzare l'intero ciclo richiesta-risposta dei comandi Redis. La visualizzazione delle tracce distribuite in Trace ti consente di diagnosticare i colli di bottiglia in modo da poter isolare rapidamente l'origine esatta della latenza nella tua applicazione.

Per visualizzare le tracce distribuite in Trace:

  1. Nella Google Cloud console, vai alla pagina Esplora tracce.

    Vai a Trace Explorer

  2. Seleziona una traccia recente rappresentata da un punto nel grafico a dispersione.

  3. Esamina la visualizzazione a cascata per isolare l'origine della latenza identificando i seguenti colli di bottiglia:

    • Durata totale della richiesta: la barra di primo livello (principale) mostra il tempo totale di attesa per il completamento dell'operazione.

    • Latenza di rete e del server (RTT): le barre secondarie (ad esempio quelle con etichetta GET o SET) mostrano il tempo impiegato dal comando per attraversare la rete ed essere eseguito sul server Memorystore for Redis.

    • Blocco della connessione client: se è presente un ampio spazio orizzontale vuoto prima dell'inizio dell'intervallo secondario Redis, il thread dell'applicazione è bloccato in attesa di una connessione TCP disponibile dal pool di connessioni.

    • Blocco dell'analisi dell'applicazione: se è presente un ampio spazio orizzontale vuoto dopo la fine dell'intervallo secondario Redis, l'applicazione ha difficoltà ad analizzare o elaborare il payload restituito. Questo accade spesso con le stringhe JSON di più megabyte.

    • Nuovi tentativi: se vedi più intervalli secondari brevi per lo stesso comando che si verificano in sequenza all'interno della stessa traccia principale, il client potrebbe riscontrare una perdita di pacchetti di rete e deve attivare il ciclo di nuovi tentativi con backoff esponenziale.

Risoluzione dei problemi

Questa sezione elenca i problemi di prestazioni comuni che puoi identificare utilizzando le metriche lato client, spiega le cause principali e fornisce indicazioni per la risoluzione dei problemi.

Problema Causa Risoluzione dei problemi

L'applicazione riscontra un picco di latenza improvviso, ma Memorystore for Redis sembra completamente integro.

  • workload.googleapis.com/
    redis_client_blocking_latency
    (metrica lato client): picco
  • workload.googleapis.com/redis_client_rtt (metrica lato client): bassa / tipica
  • redis.googleapis.com/commands/
    usec_per_call
    (metrica del server Memorystore for Redis): bassa / tipica
  • redis.googleapis.com/clients/connected (metrica del server Memorystore for Redis): si stabilizza a un numero specifico
Il collo di bottiglia si trova esclusivamente all'interno dell'applicazione. I thread tentano di eseguire i comandi Redis, ma il pool di connessioni è completamente esaurito. L'elevata redis_client_blocking_latency rappresenta il tempo impiegato dal codice in attesa di un socket TCP disponibile prima che il comando venga inviato alla rete. Per gestire il traffico simultaneo più elevato, aumenta i limiti delle dimensioni del pool di connessioni nella configurazione del client Redis (ad esempio, MaxActive per Go, MaxTotal per Java o max_connections per Node.js e Python).

La richiesta viene completata, ma l'endpoint richiede molto più tempo del previsto. Non ci sono problemi associati all'integrità della rete o del server.

  • workload.googleapis.com/
    redis_application_blocking_latency
    (metrica lato client): picco
  • workload.googleapis.com/redis_client_rtt (metrica lato client): bassa / tipica
  • redis.googleapis.com/commands/
    usec_per_call
    (metrica del server Memorystore for Redis): bassa / tipica
  • redis.googleapis.com/stats/
    network_traffic
    (byte in uscita) (metrica del server Memorystore for Redis): picchi elevati
Memorystore for Redis esegue il comando e la rete trasferisce rapidamente il payload (RTT basso). Tuttavia, il payload restituito è di grandi dimensioni (ad esempio, una stringa JSON di 15 MB). L'applicazione riscontra un'elevata redis_application_blocking_latency perché l'applicazione consuma risorse eccessive durante l'allocazione della memoria e la deserializzazione di quella stringa di grandi dimensioni in un oggetto. Ottimizza il modello dei dati. Non archiviare blob JSON di grandi dimensioni in singole chiavi. Suddividi i dati utilizzando gli hash Redis (HSET) e utilizza HGET o HMGET per recuperare solo i campi specifici di cui hai bisogno.

La latenza dell'applicazione rivolta agli utenti aumenta, ma le metriche Redis indicano una bassa latenza del server e i checkout del pool di connessioni tipici.

  • workload.googleapis.com/redis_retry_count (metrica lato client): picchi
  • workload.googleapis.com/
    redis_connectivity_error_count
    (metrica lato client): potrebbe mostrare incrementi temporanei
  • workload.googleapis.com/redis_client_rtt (metrica lato client): bassa / tipica per le richieste riuscite
  • redis.googleapis.com/commands/
    usec_per_call
    (metrica del server Memorystore for Redis): bassa / tipica
Poiché redis_client_rtt acquisisce solo l'RTT delle richieste riuscite, non riflette la durata del timeout di un pacchetto non riuscito. Quando l'applicazione riscontra interruzioni temporanee dei pacchetti o ripristini TCP, la logica di nuovi tentativi del client instrumentato incrementa redis_retry_count e attiva il ciclo di backoff esponenziale. Questo introduce un tempo di sospensione tra i tentativi (ad esempio, 100ms, 200ms, o 400ms). L'utente riscontra una latenza totale elevata, ma la causa principale sottostante è una perdita di pacchetti di rete, che attiva ritardi di sospensione lato client. Controlla i log dei flussi VPC per verificare la presenza di pacchetti eliminati, limitazione della larghezza di banda, o anomalie di routing tra regioni. Se riscontri timeout aggressivi, assicurati che i timeout di connessione client (socket_timeout o connect_timeout) siano maggiori dell'RTT previsto per tenere conto del jitter di rete temporaneo.

Tutto si ferma e tutti i livelli della pipeline di telemetria segnalano una latenza elevata.

  • workload.googleapis.com/redis_client_rtt (metrica lato client): elevata
  • redis.googleapis.com/commands/
    usec_per_call
    (metrica del server Memorystore for Redis): elevata
  • redis.googleapis.com/stats/
    cpu_utilization_main_thread
    (metrica del server Memorystore for Redis): elevata (ad esempio, prossima a 1 s/s, o 100%)
  • Cascata di Trace: mostra un comando che richiede molto tempo di tempo
Redis è a thread singolo. Quando esegui un comando di complessità temporale O(N) , ad esempio KEYS *, SMEMBERS su un set di grandi dimensioni o HGETALL su un hash con milioni di campi, il motore Redis si mette in pausa per soddisfare la richiesta. Mentre il comando è in esecuzione, ogni altra richiesta dell'applicazione viene messa in coda, causando un picco di latenza a livello di sistema. Poiché la redis_client_rtt personalizzata corrisponde alla latenza del server (commands/usec_per_call), il server che esegue il comando è il collo di bottiglia.

Apri Trace ed esamina i comandi Redis negli intervalli lenti per identificare la query che causa il blocco. Sostituisci i comandi di blocco con quelli non bloccanti nel codice.

Per scorrere i set di dati di grandi dimensioni in modo incrementale senza bloccare il thread del server, utilizza SCAN, SSCAN, o HSCAN.

L'applicazione segnala una latenza di base elevata e coerente per tutti i comandi Redis, anche quando il traffico è basso.

  • workload.googleapis.com/redis_client_rtt (metrica lato client): costantemente elevata (p50 e p99 sono entrambe ~30-100 ms+)
  • redis.googleapis.com/commands/
    usec_per_call
    (metrica del server Memorystore for Redis): estremamente bassa (< 1 ms)
  • workload.googleapis.com/
    redis_client_blocking_latency

    e redis_application_blocking_latency: bassa / tipica
Il server Redis esegue i comandi istantaneamente, ma l'applicazione e l'istanza vengono implementate in regioni diverse (ad esempio, us-central1 e us-east1). Ogni pacchetto di rete deve attraversare l'infrastruttura cloud fisica di Google Cloud tra questi data center geografici. Ciò comporta una penalità di latenza tra regioni obbligatoria alla velocità della luce per ogni andata e ritorno. Per ridurre la latenza, implementa l'applicazione in modo che risieda nella stessa regione e zona dell'istanza. Per visualizzare la regione dell'applicazione e l'istanza, utilizza la Google Cloud console.

Passaggi successivi