Suggerimenti generali per lo sviluppo

Questa guida fornisce le migliori pratiche per la progettazione, l'implementazione, il test e la distribuzione di un servizio Cloud Run. Per ulteriori suggerimenti, vedere Migrazione di un servizio esistente.

Scrivere servizi efficaci

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

Attività di sfondo

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 individuare qualsiasi voce registrata dopo quella relativa alla 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 sulle istanze in modo da poter eseguire le attività in background al di fuori delle richieste e avere comunque accesso alla CPU.

Evitate le attività in background se utilizzate la fatturazione su richiesta.

Se devi impostare il tuo servizio su fatturazione basata su richiesta, quando il servizio Cloud Run termina di elaborare una richiesta, l'accesso dell'istanza alla CPU verrà disabilitato o fortemente limitato. Se si utilizza questo tipo di fatturazione, è sconsigliabile avviare thread o routine in background che vengano eseguiti al di fuori dell'ambito dei gestori delle richieste.

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

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

Elimina i file temporanei

Nell'ambiente Cloud Run, l'archiviazione su disco è un filesystem in memoria. I file scritti su disco consumano memoria altrimenti disponibile per il servizio e possono persistere tra un'esecuzione e l'altra. La mancata eliminazione di questi file può causare un errore di memoria insufficiente e, di conseguenza, un rallentamento dell'avvio del container.

Segnala gli errori

Gestisci tutte le eccezioni e impedisci che il servizio si blocchi in caso di errori. Un arresto anomalo provoca un avvio lento del container, mentre il traffico viene messo in coda per un'istanza sostitutiva.

Consulta la Guida alla segnalazione degli errori 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 necessità, il loro tempo di avvio influisce sulla latenza del servizio. Cloud Run separa l'avvio dell'istanza dall'elaborazione della richiesta, quindi in alcuni casi una richiesta deve attendere l'avvio di una nuova istanza prima di poter essere elaborata. Questo accade comunemente quando un servizio viene scalato da zero.

La routine di avvio è costituita da:

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

L'ottimizzazione della velocità di avvio del 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 boosting della CPU all'avvio per aumentare temporaneamente l'allocazione della CPU durante l'avvio dell'istanza al fine di ridurre la latenza di avvio.

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

È possibile configurare istanze minime e 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 simultanee 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.

Si noti che una richiesta in attesa dell'avvio di un'istanza verrà mantenuta in sospeso in una coda come segue:

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

Utilizza le dipendenze in modo strategico

Se si utilizza un linguaggio dinamico con librerie dipendenti, come ad esempio l'importazione di moduli in Node.js, il tempo di caricamento di tali moduli contribuisce alla latenza di avvio.

Riduci la latenza di avvio nei seguenti modi:

  • Per creare un servizio snello, riduci al minimo il numero e le dimensioni delle dipendenze.
  • Se il linguaggio di programmazione lo supporta, carica in modo differito il codice che viene utilizzato raramente.
  • Utilizza ottimizzazioni del caricamento del codice come l'ottimizzazione del caricamento automatico di Composer di PHP.

Utilizzare variabili globali

In Cloud Run, non si può presumere che lo stato del servizio venga mantenuto tra una richiesta e l'altra. Tuttavia, Cloud Run riutilizza le singole istanze per gestire il traffico in corso, quindi è possibile dichiarare una variabile con ambito globale per consentirne il riutilizzo del valore nelle invocazioni successive. Non è possibile sapere in anticipo se una singola richiesta beneficerà di questo riutilizzo.

È inoltre possibile memorizzare nella cache gli oggetti se la loro ricreazione ad ogni richiesta di servizio risulta onerosa. Spostando questa logica dalla fase di richiesta alla fase globale si ottengono prestazioni migliori.

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 differita per gli oggetti usati raramente, in modo da posticipare il costo temporale e ridurre i tempi di avvio del container.

Uno svantaggio dell'inizializzazione differita è l'aumento della latenza per le prime richieste alle nuove istanze. Ciò può causare un overscaling e richieste eliminate quando deploy 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();
  }
}

Utilizzare un ambiente di esecuzione diverso

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

Ottimizzare la concorrenza

Le istanze Cloud Run possono gestire più richieste simultaneamente, "in parallelo", fino a un massimo di concorrenza configurabile .

Cloud Run regola automaticamente il numero di processi simultanei fino al valore massimo configurato.

Il valore predefinito di 80 per la concorrenza massima è adatto a molte immagini container. Tuttavia, devi:

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

Ottimizza la concorrenza per il tuo servizio

Il numero di richieste simultanee 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 e ottenere la massima concorrenza stabile:

  1. Ottimizza le prestazioni del tuo servizio.
  2. Imposta il livello di supporto alla concorrenza previsto in qualsiasi configurazione di concorrenza a livello di codice. Non tutti gli stack tecnologici richiedono tale impostazione.
  3. Distribuisci il tuo servizio.
  4. Imposta la concorrenza di Cloud Run per il tuo servizio in modo che sia uguale o inferiore a qualsiasi configurazione a livello di codice. Se non è presente una configurazione a livello di codice, utilizza il livello di concorrenza previsto.
  5. Utilizzare strumenti di test di carico load test che supportino una concorrenza configurabile. È necessario verificare che il servizio rimanga stabile con il carico e la concorrenza previsti.
  6. Se il servizio non funziona correttamente, passa al passaggio 1 per migliorarlo oppure al passaggio 2 per ridurre la concorrenza. Se il servizio funziona correttamente, torna al passaggio 2 e aumenta la concorrenza.

Continua a iterare finché non trovi la massima concorrenza stabile.

Adattare la memoria alla concorrenza

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

Evitare lo stato globale mutabile

Se si desidera utilizzare uno stato globale modificabile in un contesto concorrente, è necessario adottare ulteriori misure nel codice per garantire che ciò avvenga in modo sicuro. Riduci al minimo la contesa limitando le variabili globali all'inizializzazione e al riutilizzo una tantum, come descritto sopra in Performance.

Se si utilizzano variabili globali mutabili in un servizio che gestisce più richieste contemporaneamente, è necessario utilizzare blocchi o mutex per prevenire condizioni di competizione.

Compromessi tra velocità di trasmissione, latenza e costi.

Regolare l'impostazione del numero massimo di richieste simultanee può aiutare a trovare un equilibrio tra velocità di trasmissione, latenza e costo del servizio.

In generale, un'impostazione del numero massimo di richieste simultanee più basso comporta una latenza inferiore e un throughput inferiore per istanza. Con un numero massimo di richieste simultanee inferiore, un minor numero di richieste compete per le risorse all'interno di ciascuna istanza e ogni richiesta ottiene prestazioni migliori. Tuttavia, poiché ogni istanza può gestire un numero inferiore di richieste contemporaneamente, la velocità di elaborazione per istanza è inferiore e il servizio necessita di un numero maggiore di istanze per gestire lo stesso traffico.

Nella direzione opposta, un'impostazione del numero massimo di richieste simultanee più elevata generalmente comporta una latenza maggiore e un throughput maggiore 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 una maggiore latenza. Ma ogni istanza può elaborare più richieste contemporaneamente, in modo che il servizio necessiti complessivamente di un minor numero di istanze per elaborare lo stesso traffico.

Considerazioni sui costi

Il prezzo di Cloud Run è per tempo di istanza. Se imposti fatturazione basata sull'istanza, il tempo dell'istanza corrisponde alla durata totale di ciascuna istanza. Se imposti la fatturazione basata sulle richieste, il tempo di istanza è il tempo che ogni istanza trascorre a elaborare almeno una richiesta.

L'impatto del numero massimo di richieste simultanee sulla fatturazione dipende dal pattern di traffico. La riduzione delle richieste simultanee massime può comportare una fattura inferiore se l'impostazione più bassa porta a

  • Latenza ridotta
  • Casi in cui il lavoro viene completato più velocemente
  • Istanze che si arrestano più rapidamente anche se sono necessarie più istanze totali

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

Il modo migliore per ottimizzare la fatturazione è attraverso test di carico utilizzando diverse impostazioni di richieste simultanee massime per identificare l'impostazione che si traduce nel tempo di istanza fatturabile più basso, come si vede nella metrica di monitoraggio container/billable_instance_timemonitor.

Sicurezza dei container

Molte pratiche di sicurezza software di uso generale si applicano anche ai servizi containerizzati. Esistono alcune pratiche specifiche per i container o che sono in linea con la filosofia e l'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 di sicurezza ai tuoi servizi ricreando regolarmente le immagini dei container e ridistribuendo i servizi.

  • Includi nel container solo ciò che è necessario per eseguire il tuo servizio. Codice aggiuntivo, pacchetti e strumenti non necessari rappresentano potenziali vulnerabilità di sicurezza. Vedi sopra per il relativo impatto sulle prestazioni.

  • Implementare un processo di compilazione deterministico che includa versioni specifiche di software e librerie. In questo modo, il codice non verificato non viene incluso nel contenitore.

  • Imposta il tuo contenitore in modo che venga eseguito come un utente diverso daroot con ilDockerfileUSER dichiarazione. Alcune immagini container potrebbero già avere un utente specifico configurato.

  • Impedisci l'utilizzo delle funzionalità di anteprima tramite policy organizzative personalizzate.

Automatizzare la scansione di sicurezza

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

Creare immagini container minimali

Le immagini container di grandi dimensioni tendono ad aumentare le vulnerabilità di sicurezza perché contengono più elementi di quelli necessari al codice.

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. Anche le dimensioni dell'immagine container non vengono conteggiate ai fini della memoria disponibile del container.

Per creare un container minimale, si consiglia di partire da un'immagine base essenziale come:

Ubuntu è più grande in termini di dimensioni, ma è un'immagine di base comunemente utilizzata con un ambiente server più completo pronto all'uso.

Se il tuo servizio ha un processo di compilazione che richiede molti strumenti, considera l'utilizzo di compilation multi-stage per mantenere il tuo container leggero in fase di esecuzione.

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