Suggerimenti generali per lo sviluppo

Questa guida fornisce le best practice per progettare, implementare, testare ed eseguire il deployment di un servizio Cloud Run. Per ulteriori suggerimenti, consulta la pagina Eseguire la migrazione di un servizio esistente.

Scrivere servizi efficaci

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

Attività in background

L'attività in background è tutto ciò che accade dopo che la risposta HTTP è stata comunicata. Per determinare se nel servizio è presente un'attività in background non immediatamente evidente, controlla i log per verificare se sono presenti elementi registrati dopo la voce relativa alla richiesta HTTP.

Configura la CPU in modo che sia sempre allocata per utilizzare le attività in background

Se vuoi supportare le attività in background nel tuo servizio Cloud Run, imposta la CPU del servizio Cloud Run in modo che sia sempre allocata 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 la CPU viene allocata solo durante l'elaborazione delle richieste

Se devi impostare il servizio in modo da allocare la CPU solo durante l'elaborazione delle richieste, quando il servizio Cloud Run termina l'elaborazione di una richiesta, l'accesso dell'istanza alla CPU verrà disattivato o limitato notevolmente. Se utilizzi questo tipo di allocazione della CPU, non devi avviare thread o routine in background che vengono eseguiti al di fuori dell'ambito dei gestori delle richieste.

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

L'esecuzione di thread in background con questo tipo di allocazione della CPU può comportare un comportamento imprevisto perché qualsiasi richiesta successiva alla stessa istanza del contenitore riprende qualsiasi attività in background sospesa.

Eliminare i file temporanei

Nell'ambiente Cloud Run, lo spazio di archiviazione su disco è un file system in memoria. I file scritti su disco consumano memoria altrimenti disponibile per il servizio e possono persistere tra le invocazioni. Se non li elimini, alla fine potresti riscontrare un errore di esaurimento della memoria e tempi di avvio del contenitore lenti.

Segnalare errori

Gestisci tutte le eccezioni e non consentire al servizio di arrestarsi in caso di errori. Un arresto anomalo porta a un avvio lento del contenitore mentre il traffico è in coda per un'istanza sostitutiva.

Per informazioni su come segnalare correttamente gli errori, consulta la guida alla segnalazione degli errori.

Ottimizzazione del rendimento

Questa sezione descrive le best practice per ottimizzare il rendimento.

Avvia rapidamente i container

Poiché le istanze vengono scalate in base alle esigenze, il loro tempo di avvio influisce sulla latenza del servizio. Sebbene Cloud Run sconnetta l'avvio delle istanze dall'elaborazione delle richieste, può accadere che una richiesta debba attendere l'avvio di una nuova istanza per essere elaborata. Questo accade in particolare quando si esegue lo scale up da zero.

La routine di avvio è composta da:

  • Download dell'immagine container (utilizzando la tecnologia di streaming delle immagini container di Cloud Run)
  • Avvia il contenitore eseguendo il comando entrypoint.
  • In attesa che il contenitore inizi a rimanere in ascolto sulla porta configurata.

L'ottimizzazione per la velocità di avvio del contenitore 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 in modo da ridurre la latenza di avvio.

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

Puoi configurare le istanze minime 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 indica che il servizio è pronto per ricevere fino al numero di richieste simultanee configurate per il servizio senza dover avviare una nuova istanza.

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

  • Se vengono avviate nuove istanze, ad esempio durante uno scaling out, le richieste rimarranno in attesa per almeno il tempo di avvio medio delle istanze di container di questo servizio. Ciò include quando la richiesta avvia uno scale-out, ad esempio quando si esegue lo scale up da zero.
  • Se il tempo di avvio è inferiore a 10 secondi, le richieste rimarranno in attesa per un massimo di 10 secondi.
  • Se non sono in corso l'avvio di istanze e la richiesta non avvia un'operazione di scalabilità, le richieste rimarranno in attesa per un massimo di 10 secondi.

Utilizzare le dipendenze in modo oculato

Se utilizzi un linguaggio dinamico con librerie dipendenti, ad esempio i moduli di importazione 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 in modo lento il codice che non viene utilizzato di frequente, se la tua lingua lo supporta.
  • Utilizza ottimizzazioni di caricamento del codice come l'ottimizzazione dell'autoloader di Composer di PHP.

Utilizzare le variabili globali

In Cloud Run, non puoi assumere che lo stato del servizio venga mantenuto tra le richieste. Tuttavia, Cloud Run riutilizza le singole istanze per gestire il traffico in corso, quindi puoi dichiarare una variabile a livello globale per consentire il riutilizzo del relativo valore nelle chiamate successive. Non è possibile sapere in anticipo se una singola richiesta beneficia di questo riutilizzo.

Puoi anche memorizzare nella cache gli oggetti in memoria se sono costosi da ricreare a ogni richiesta di servizio. Se lo sposti dalla logica di richiesta all'ambito globale, il rendimento migliorerà.

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}"

Vai


// 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 contenitore. Utilizza l'inizializzazione lazy per gli oggetti utilizzati raramente per posticipare il costo in termini di tempo e ridurre i tempi di avvio del contenitore.

Uno svantaggio dell'inizializzazione lazy è un aumento della latenza per le prime richieste alle nuove istanze. Ciò può causare un sovradimensionamento e la perdita di richieste quando implementi 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

    # 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}."

Vai


// 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 notare 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 contemporaneità massima configurabile. È diverso dalle funzioni Cloud Run, che utilizzano concurrency = 1.

Cloud Run regola automaticamente la concorrenza fino al valore massimo configurato.

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

  • Riducilo se il contenitore non è in grado di elaborare molte richieste in parallelo.
  • Aumentalo se il contenitore è in grado di gestire un volume elevato di richieste.

Ottimizza la concorrenza per il tuo servizio

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

Per ottimizzare il servizio per una contemporaneità massima stabile:

  1. Ottimizza il rendimento del servizio.
  2. Imposta il livello di supporto della concorrenza previsto in qualsiasi configurazione della concorrenza a livello di codice. Non tutti gli stack tecnologici richiedono un'impostazione di questo tipo.
  3. Esegui il deployment del servizio.
  4. Imposta la concorrenza Cloud Run per il tuo servizio uguale o inferiore a qualsiasi configurazione a livello di codice. Se non è presente alcuna configurazione a livello di codice, utilizza la concorrenza prevista.
  5. Utilizza strumenti di test di carico che supportano una concorrenza configurabile. Devi verificare che il servizio rimanga stabile con il carico e la concorrenza previsti.
  6. Se il servizio non funziona correttamente, vai al passaggio 1 per migliorarlo o al passaggio 2 per ridurre la concorrenza. Se il servizio funziona correttamente, torna al passaggio 2 e aumenta la concorrenza.

Continua a eseguire l'iterazione finché non trovi la concorrenza stabile massima.

Adatta la memoria alla concorrenza

Ogni richiesta gestita dal 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 vuoi sfruttare lo stato globale mutabile in un contesto concorrente, esegui passaggi aggiuntivi nel codice per assicurarti che venga eseguito in sicurezza. Riduci al minimo le contese limitando le variabili globali all'inizializzazione una tantum e al riutilizzo, come descritto sopra nella sezione Rendimento.

Se utilizzi variabili globali mutabili in un servizio che gestisce più richieste contemporaneamente, assicurati di utilizzare lock o mutex per evitare condizioni di gara.

Compromessi tra velocità effettiva, latenza e costi

La regolazione dell'impostazione delle richieste simultanee massime può aiutarti a bilanciare il compromesso tra throughput, latenza e costo per il tuo servizio.

In generale, un numero inferiore di richieste in parallelo massime comporta una latenza inferiore e un throughput inferiore per istanza. Con un numero inferiore di richieste in parallelo massime, meno richieste competono per le risorse all'interno di ogni istanza e ogni richiesta ottiene un rendimento migliore. Tuttavia, poiché ogni istanza può gestire meno richieste contemporaneamente, il throughput per istanza è inferiore e il servizio richiede più istanze per gestire lo stesso traffico.

Al contrario, un numero maggiore di richieste simultanee massime solitamente 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

La fatturazione di Cloud Run avviene in base al tempo di istanza. Se la CPU è sempre allocata, la data e l'ora dell'istanza corrispondono alla durata totale di ciascuna istanza. Se la CPU non è sempre allocata, il tempo dell'istanza è il tempo impiegato da ogni istanza per elaborare almeno una richiesta.

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

  • Latenza ridotta
  • Istanze che completano il lavoro più velocemente
  • Interruzione dell'attività delle istanze più rapida anche se sono necessarie più istanze totali

È 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 ciascuna istanza, a causa della latenza migliorata.

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

Sicurezza dei container

Molte pratiche di sicurezza del software per uso generico 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 del contenitore:

  • Utilizza immagini di base sicure e sottoposte a manutenzione attiva, come le immagini di base di Google o le immagini ufficiali di Docker Hub.

  • Applica gli aggiornamenti della sicurezza ai tuoi servizi ricostruendo regolarmente le immagini dei contenitori e reimplementando i servizi.

  • Includi nel contenitore solo ciò che è necessario per eseguire il servizio. Il codice, i pacchetti e gli strumenti aggiuntivi sono potenziali vulnerabilità di sicurezza. Consulta la sezione precedente per informazioni sull'impatto sul rendimento.

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

  • Imposta il container in modo che venga eseguito come utente diverso da root con l'istruzione USER del Dockerfile. Per alcune immagini contenitore potrebbe essere già configurato un utente specifico.

  • Impedisci l'utilizzo delle funzionalità di anteprima utilizzando i criteri dell'organizzazione personalizzati.

Automatizzare la scansione di sicurezza

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

Crea immagini container minime

Le immagini container di grandi dimensioni probabilmente aumentano le vulnerabilità di sicurezza perché contengono più di quanto necessario per il 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 sul tempo di elaborazione delle richieste. Inoltre, le dimensioni dell'immagine del contenitore non vengono conteggiate nella memoria disponibile del contenitore.

Per creare un container minimo, ti consigliamo di utilizzare un'immagine di base snella, ad esempio:

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

Se il tuo servizio ha un processo di compilazione che richiede molti strumenti, ti consigliamo di utilizzare le costruzioni a più fasi per mantenere il contenitore leggero in fase di esecuzione.

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