Suggerimenti generali per lo sviluppo

La progettazione dei servizi Cloud Run con le best practice consigliate ti consente di ottimizzare l'efficienza del runtime, ridurre al minimo la latenza di avvio dei container e scalare in modo prevedibile i workload. Queste strategie di progettazione, concorrenza e sicurezza ti aiutano a creare servizi resilienti ed economici. Se stai spostando un servizio web esistente in Cloud Run, consulta Migrazione di un servizio esistente.

Scrivi servizi efficaci

Questa sezione descrive le best practice generali per la progettazione e l'implementazione di un servizio Cloud Run.

Attività in background

Le attività in background sono tutto ciò che accade dopo l'invio della risposta HTTP. Per determinare se nel tuo servizio è presente un'attività in background non immediatamente evidente, controlla i log per verificare se è stato registrato qualcosa dopo la voce della richiesta HTTP.

Configura la fatturazione basata sulle istanze per utilizzare le attività in background

Se vuoi supportare le attività in background nel tuo servizio Cloud Run, imposta il servizio Cloud Run sulla fatturazione basata su istanze in modo da poter eseguire attività in background al di fuori delle richieste e avere comunque accesso alla CPU.

Evita le attività in background se utilizzi la fatturazione basata sulle richieste

Se devi impostare il servizio sulla fatturazione basata sulle richieste, quando il servizio Cloud Run termina la gestione di una richiesta, l'accesso della CPU all'istanza verrà disabilitato o limitato in modo significativo. Se utilizzi questo tipo di fatturazione, non devi avviare thread o routine in background che vengono eseguiti al di fuori dell'ambito dei gestori delle richieste.

Controlla il codice per assicurarti che tutte le operazioni asincrone vengano completate prima di inviare la risposta.

L'esecuzione di thread in background con la fatturazione basata sulle richieste abilitata può comportare un comportamento imprevisto, poiché qualsiasi richiesta successiva alla stessa istanza del container riprende l'attività in background sospesa.

Elimina i file temporanei

Nell'ambiente Cloud Run, lo spazio di archiviazione su disco è un filesystem in memoria. I file scritti sul disco consumano memoria che altrimenti sarebbe disponibile per il tuo servizio e possono persistere tra le chiamate. Se non elimini questi file, alla fine potresti riscontrare un errore di memoria insufficiente e un successivo avvio lento del container.

Segnala gli errori

Gestisci tutte le eccezioni e non consentire l'arresto anomalo del servizio in caso di errori. Un arresto anomalo comporta un avvio lento del container mentre il traffico viene messo in coda per un'istanza sostitutiva.

Consulta la guida di Error Reporting per informazioni su come segnalare correttamente gli errori.

Ottimizza le prestazioni

Questa sezione descrive le best practice per ottimizzare le prestazioni.

Avvia rapidamente i container

Poiché le istanze vengono scalate in base alle esigenze, il loro tempo di avvio influisce sulla latenza del servizio. Cloud Run disaccoppia l'avvio dell'istanza e l'elaborazione delle richieste, pertanto in alcuni casi una richiesta deve attendere l'avvio di una nuova istanza prima di essere elaborata. Questo accade comunemente quando un servizio viene scalato da zero.

La routine di avvio è costituita da:

  • Download dell'immagine container (utilizzando la tecnologia di streaming delle immagini container di Cloud Run)
  • Avvio del container tramite l'esecuzione del comando entrypoint.
  • In attesa che il container inizi ad ascoltare sulla porta configurata.

L'ottimizzazione della velocità di avvio dei container riduce al minimo la latenza di elaborazione delle richieste.

Utilizza il boosting della CPU all'avvio per ridurre la latenza di avvio

Puoi attivare il boost della CPU all'avvio per aumentare temporaneamente l'allocazione della CPU durante l'avvio dell'istanza, in modo da ridurre la latenza di avvio.

Utilizza il numero minimo di istanze per ridurre i tempi di avvio dei container

Puoi configurare il numero minimo di istanze e la concorrenza per ridurre al minimo i tempi di avvio dei container. Ad esempio, l'utilizzo di un numero minimo di istanze pari a 1 significa che il servizio è pronto a ricevere fino al numero di richieste in parallelo configurate per il servizio senza dover avviare una nuova istanza. Quando utilizzi le istanze minime, evita di utilizzare le uscite di sistema che arrestano un'istanza e potenzialmente aumentano gli avvii a freddo.

Tieni presente che una richiesta in attesa dell'avvio di un'istanza verrà mantenuta in attesa in una coda come segue:

Le richieste rimarranno in attesa per un massimo di 3, 5 volte il tempo di avvio medio delle istanze di container di questo servizio o per 10 secondi, a seconda di quale sia il valore maggiore.

Utilizza le dipendenze in modo strategico

Se utilizzi un linguaggio dinamico con librerie dipendenti, ad esempio importando moduli in Node.js, il tempo di caricamento di questi moduli si aggiunge alla latenza di avvio.

Riduci la latenza di avvio nei seguenti modi:

  • Riduci al minimo il numero e le dimensioni delle dipendenze per creare un servizio snello.
  • Carica lentamente il codice utilizzato di rado, se il tuo linguaggio lo supporta.
  • Utilizza ottimizzazioni di caricamento del codice come l'ottimizzazione del caricatore automatico di Composer di PHP.

Utilizza le variabili globali

In Cloud Run, non puoi dare per scontato che lo stato del servizio venga conservato tra le richieste. Tuttavia, Cloud Run riutilizza le singole istanze per gestire il traffico in corso, quindi puoi dichiarare una variabile nell'ambito globale per consentire il riutilizzo del suo valore nelle chiamate successive. Non è possibile sapere in anticipo se una singola richiesta beneficerà di questo riutilizzo.

Puoi anche memorizzare gli oggetti nella cache in memoria se sono costosi da ricreare a ogni richiesta di servizio. Lo spostamento di questo elemento dalla logica della richiesta all'ambito globale comporta un miglioramento delle prestazioni.

Node.js

const functions = require('@google-cloud/functions-framework');

// TODO(developer): Define your own computations
const {lightComputation, heavyComputation} = require('./computations');

// Global (instance-wide) scope
// This computation runs once (at instance cold-start)
const instanceVar = heavyComputation();

/**
 * HTTP function that declares a variable.
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
functions.http('scopeDemo', (req, res) => {
  // Per-function scope
  // This computation runs every time this function is called
  const functionVar = lightComputation();

  res.send(`Per instance: ${instanceVar}, per function: ${functionVar}`);
});

Python

import time

import functions_framework


# Placeholder
def heavy_computation():
    return time.time()


# Placeholder
def light_computation():
    return time.time()


# Global (instance-wide) scope
# This computation runs at instance cold-start
instance_var = heavy_computation()


@functions_framework.http
def scope_demo(request):
    """
    HTTP Cloud Function that declares a variable.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """

    # Per-function scope
    # This computation runs every time this function is called
    function_var = light_computation()
    return f"Instance: {instance_var}; function: {function_var}"

Go


// h is in the global (instance-wide) scope.
var h string

// init runs during package initialization. So, this will only run during an
// an instance's cold start.
func init() {
	h = heavyComputation()
	functions.HTTP("ScopeDemo", ScopeDemo)
}

// ScopeDemo is an example of using globally and locally
// scoped variables in a function.
func ScopeDemo(w http.ResponseWriter, r *http.Request) {
	l := lightComputation()
	fmt.Fprintf(w, "Global: %q, Local: %q", h, l)
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class Scopes implements HttpFunction {
  // Global (instance-wide) scope
  // This computation runs at instance cold-start.
  // Warning: Class variables used in functions code must be thread-safe.
  private static final int INSTANCE_VAR = heavyComputation();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Per-function scope
    // This computation runs every time this function is called
    int functionVar = lightComputation();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Instance: %s; function: %s", INSTANCE_VAR, functionVar);
  }

  private static int lightComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).sum();
  }

  private static int heavyComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

Esegui l'inizializzazione lazy delle variabili globali

L'inizializzazione delle variabili globali avviene sempre durante l'avvio, il che aumenta il tempo di avvio del container. Utilizza l'inizializzazione lazy per gli oggetti utilizzati di rado per posticipare il costo in termini di tempo e ridurre i tempi di avvio del container.

Uno svantaggio dell'inizializzazione lazy è l'aumento della latenza per le prime richieste alle nuove istanze. Ciò può causare un overscaling e richieste ignorate quando esegui il deployment di una nuova revisione di un servizio che gestisce attivamente molte richieste.

Node.js

const functions = require('@google-cloud/functions-framework');

// Always initialized (at cold-start)
const nonLazyGlobal = fileWideComputation();

// Declared at cold-start, but only initialized if/when the function executes
let lazyGlobal;

/**
 * HTTP function that uses lazy-initialized globals
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
functions.http('lazyGlobals', (req, res) => {
  // This value is initialized only if (and when) the function is called
  lazyGlobal = lazyGlobal || functionSpecificComputation();

  res.send(`Lazy global: ${lazyGlobal}, non-lazy global: ${nonLazyGlobal}`);
});

Python

import functions_framework

# Always initialized (at cold-start)
non_lazy_global = file_wide_computation()

# Declared at cold-start, but only initialized if/when the function executes
lazy_global = None


@functions_framework.http
def lazy_globals(request):
    """
    HTTP Cloud Function that uses lazily-initialized globals.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    global lazy_global, non_lazy_global  # noqa: F824

    # This value is initialized only if (and when) the function is called
    if not lazy_global:
        lazy_global = function_specific_computation()

    return f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}."

Go


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"log"
	"net/http"
	"sync"

	"cloud.google.com/go/storage"
	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

// client is lazily initialized by LazyGlobal.
var client *storage.Client
var clientOnce sync.Once

func init() {
	functions.HTTP("LazyGlobal", LazyGlobal)
}

// LazyGlobal is an example of lazily initializing a Google Cloud Storage client.
func LazyGlobal(w http.ResponseWriter, r *http.Request) {
	// You may wish to add different checks to see if the client is needed for
	// this request.
	clientOnce.Do(func() {
		// Pre-declare an err variable to avoid shadowing client.
		var err error
		client, err = storage.NewClient(context.Background())
		if err != nil {
			http.Error(w, "Internal error", http.StatusInternalServerError)
			log.Printf("storage.NewClient: %v", err)
			return
		}
	})
	// Use client.
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class LazyFields implements HttpFunction {
  // Always initialized (at cold-start)
  // Warning: Class variables used in Servlet classes must be thread-safe,
  // or else might introduce race conditions in your code.
  private static final int NON_LAZY_GLOBAL = fileWideComputation();

  // Declared at cold-start, but only initialized if/when the function executes
  // Uses the "initialization-on-demand holder" idiom
  // More information: https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
  private static class LazyGlobalHolder {
    // Making the default constructor private prohibits instantiation of this class
    private LazyGlobalHolder() {}

    // This value is initialized only if (and when) the getLazyGlobal() function below is called
    private static final Integer INSTANCE = functionSpecificComputation();

    private static Integer getInstance() {
      return LazyGlobalHolder.INSTANCE;
    }
  }

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    Integer lazyGlobal = LazyGlobalHolder.getInstance();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Lazy global: %s; non-lazy global: %s%n", lazyGlobal, NON_LAZY_GLOBAL);
  }

  private static int functionSpecificComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).sum();
  }

  private static int fileWideComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

Utilizza un ambiente di esecuzione diverso

Potresti riscontrare tempi di avvio più rapidi utilizzando un ambiente di esecuzione diverso.

Ottimizza la concorrenza

Le istanze Cloud Run possono gestire più richieste contemporaneamente, "in parallelo", fino a una concorrenza massima configurabile.

Cloud Run regola automaticamente la contemporaneità fino al valore massimo configurato.

La concorrenza massima predefinita di 80 è adatta per molte immagini container. Tuttavia, devi:

  • Riduci il valore se il container non è in grado di elaborare molte richieste in parallelo.
  • Aumentalo se il container è in grado di gestire un volume elevato di richieste.

Ottimizza la contemporaneità per il tuo servizio

Il numero di richieste in parallelo che ogni istanza può gestire può essere limitato dallo stack tecnologico e dall'utilizzo di risorse condivise come variabili e connessioni al database.

Per ottimizzare il servizio in modo da ottenere la massima contemporaneità stabile:

  1. Ottimizza le prestazioni del servizio.
  2. Imposta il livello previsto di supporto della contemporaneità in qualsiasi configurazione della contemporaneità a livello di codice. Non tutti gli stack tecnologici richiedono questa impostazione.
  3. Esegui il deployment del servizio.
  4. Imposta la contemporaneità di Cloud Run per il tuo servizio su un valore uguale o inferiore a qualsiasi configurazione a livello di codice. Se non esiste una configurazione a livello di codice, utilizza la concorrenza prevista.
  5. Utilizza strumenti di test di carico che supportano una contemporaneità configurabile. Devi confermare che il tuo servizio rimanga stabile in base al carico e alla contemporaneità previsti.
  6. Se il servizio non funziona correttamente, vai al passaggio 1 per migliorarlo o al passaggio 2 per ridurre la contemporaneità. Se il servizio funziona bene, torna al passaggio 2 e aumenta la contemporaneità.

Continua a eseguire iterazioni finché non trovi la concorrenza stabile massima.

Abbina la memoria alla contemporaneità

Ogni richiesta gestita dal servizio richiede una certa quantità di memoria aggiuntiva. Pertanto, quando aumenti o diminuisci la contemporaneità, assicurati di regolare anche il limite di memoria.

Evita lo stato globale modificabile

Se vuoi sfruttare lo stato globale modificabile in un contesto simultaneo, adotta misure aggiuntive nel codice per assicurarti che ciò avvenga in modo sicuro. Riduci al minimo i conflitti limitando le variabili globali all'inizializzazione e al riutilizzo una tantum, come descritto sopra in Prestazioni.

Se utilizzi variabili globali modificabili in un servizio che gestisce più richieste contemporaneamente, assicurati di utilizzare blocchi o mutex per evitare race condition.

Compromessi tra throughput, latenza e costi

La regolazione dell'impostazione del numero massimo di richieste in parallelo può aiutare a bilanciare il compromesso tra velocità effettiva, latenza e costo del servizio.

In generale, un'impostazione di richieste in parallelo massime più bassa comporta una latenza inferiore e un throughput inferiore per istanza. Con un numero inferiore di richieste in parallelo massime, un numero inferiore di richieste compete per le risorse all'interno di ogni istanza e ogni richiesta ottiene prestazioni migliori. Tuttavia, poiché ogni istanza può gestire meno richieste contemporaneamente, la velocità effettiva per istanza è inferiore e il servizio richiede più istanze per gestire lo stesso traffico.

Al contrario, un'impostazione più elevata per il numero massimo di richieste in parallelo generalmente comporta una latenza e un throughput più elevati per istanza. Le richieste potrebbero dover attendere l'accesso a risorse come CPU, GPU e larghezza di banda della memoria all'interno dell'istanza, il che comporta un aumento della latenza. Tuttavia, ogni istanza può elaborare più richieste contemporaneamente, in modo che il servizio abbia bisogno di meno istanze in totale per elaborare lo stesso traffico.

Considerazioni sui costi

I prezzi di Cloud Run si basano sul tempo di istanza. Se imposti la fatturazione basata sulle istanze, il tempo dell'istanza corrisponde alla durata totale di ogni istanza. Se imposti la fatturazione basata sulle richieste, il tempo dell'istanza è il tempo impiegato da ogni istanza per elaborare almeno una richiesta.

L'impatto del numero massimo di richieste in parallelo sulla fatturazione dipende dal pattern di traffico. La riduzione del numero massimo di richieste in parallelo può comportare una fattura inferiore se l'impostazione inferiore porta a

  • Latenza ridotta
  • Istanze che completano il lavoro più velocemente
  • Le istanze vengono arrestate più velocemente anche se è richiesto un numero totale di istanze maggiore

Tuttavia, è possibile anche il contrario: la riduzione delle richieste in parallelo massime può aumentare la fatturazione se l'aumento del numero di istanze non è compensato dalla riduzione del tempo di esecuzione di ogni istanza, a causa della latenza migliorata.

Il modo migliore per ottimizzare la fatturazione è tramite i test di carico utilizzando diverse impostazioni per le richieste in parallelo massime per identificare l'impostazione che genera il tempo di istanza fatturabile più basso, come mostrato nella metrica di monitoraggio container/billable_instance_time.

Sicurezza dei container

Molte pratiche di sicurezza del software generiche si applicano ai servizi containerizzati. Esistono alcune pratiche specifiche per i container o che si allineano alla filosofia e all'architettura dei container.

Per migliorare la sicurezza dei container:

  • Utilizza immagini di base sicure e gestite attivamente, come le immagini di base di Google o le immagini ufficiali di Docker Hub.

  • Applica gli aggiornamenti della sicurezza ai tuoi servizi ricreando regolarmente le immagini container e rifacendo il deployment dei servizi.

  • Includi nel container solo ciò che è necessario per eseguire il servizio. Codice, pacchetti e strumenti aggiuntivi sono potenziali vulnerabilità di sicurezza. Consulta la sezione precedente per l'impatto sulle prestazioni correlato.

  • Implementa un processo di build deterministico che includa versioni specifiche di software e librerie. In questo modo, impedisci l'inclusione di codice non verificato nel container.

  • Configura il container in modo che venga eseguito come utente diverso da root con l'istruzione USER di Dockerfile. Alcune immagini container potrebbero avere già un utente specifico configurato.

  • Impedisci l'utilizzo delle funzionalità in anteprima utilizzando le policy dell'organizzazione personalizzate.

Automatizza l'analisi della sicurezza

Abilita l'analisi delle vulnerabilità per l'analisi della sicurezza delle immagini container archiviate in Artifact Registry.

Crea immagini container minime

Le immagini container di grandi dimensioni probabilmente aumentano le vulnerabilità della sicurezza perché contengono più di ciò di cui il codice ha bisogno.

Grazie alla tecnologia di streaming delle immagini container di Cloud Run, le dimensioni dell'immagine container non influiscono sui tempi di avvio del container o sui tempi di elaborazione delle richieste. Inoltre, le dimensioni dell'immagine container non vengono conteggiate ai fini della memoria disponibile del container.

Per creare un container minimo, valuta la possibilità di utilizzare un'immagine di base snella, ad esempio:

Ubuntu è più grande, ma è un'immagine di base di uso comune con un ambiente server out-of-the-box più completo.

Se il tuo servizio ha un processo di compilazione che richiede molti strumenti, valuta la possibilità di utilizzare build multifase per mantenere il container leggero in fase di runtime.

Queste risorse forniscono ulteriori informazioni sulla creazione di immagini container snelle: