Suggerimenti generali per lo sviluppo

Questa guida fornisce le best practice per progettare, implementare, testare ed eseguire il deployment di un servizio Cloud Run for Anthos. Per ulteriori suggerimenti, consulta la sezione Migrazione di un servizio esistente.

Scrittura di servizi efficaci

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

Evitare le attività in background

Quando un'applicazione in esecuzione su Cloud Run for Anthos termina la gestione di una richiesta, l'accesso alla CPU dell'istanza di container verrà disabilitato o fortemente limitato. Pertanto, non dovresti avviare thread o routine in background eseguiti al di fuori dell'ambito dei gestori delle richieste.

L'esecuzione di thread in background può causare comportamenti imprevisti perché qualsiasi richiesta successiva alla stessa istanza container ripristina le attività in background sospese.

L'attività in background è tutto ciò che accade dopo che la risposta HTTP è stata consegnata. Controlla il codice per assicurarti che tutte le operazioni asincrone terminino prima di consegnare la risposta.

Se sospetti che nel tuo servizio sia possibile un'attività in background che non sia immediatamente evidente, puoi controllare i log: cerca tutto ciò che è stato registrato dopo la voce relativa alla richiesta HTTP.

Eliminazione di file temporanei

Nell'ambiente Cloud Run, l'archiviazione su disco è un file system in memoria. I file scritti sul disco consumano memoria altrimenti disponibile per il tuo servizio e possono rimanere tra le chiamate. La mancata eliminazione di questi file può causare un errore di memoria insufficiente e un successivo avvio a freddo.

Ottimizzazione del rendimento

Questa sezione descrive le best practice per ottimizzare il rendimento.

Avvio rapido dei servizi

Poiché le istanze di container vengono ridimensionate in base alle esigenze, un metodo tipico è inizializzare completamente l'ambiente di esecuzione. Questo tipo di inizializzazione è chiamato "avvio a freddo". Se una richiesta client attiva un avvio a freddo, l'avvio dell'istanza di container genera una latenza aggiuntiva.

La routine per l'avvio consiste in:

  • Avvio del servizio
    • Avvio del container
    • Esecuzione del comando entrypoint per avviare il server.
  • Controllo della porta di servizio aperta.

L'ottimizzazione per la velocità di avvio del servizio riduce al minimo la latenza che ritarda un'istanza di container in base alle richieste.

Utilizzo saggio delle dipendenze

Se usi un linguaggio dinamico con librerie dipendenti, ad esempio importando moduli in Node.js, il tempo di caricamento di questi moduli aumenta la latenza durante l'avvio a freddo. Riduci la latenza di avvio in questi modi:

  • Riduci al minimo il numero e le dimensioni delle dipendenze per creare un servizio snello.
  • Carica lentamente il codice che viene utilizzato raramente, se supportato dalla tua lingua.
  • Utilizza ottimizzazioni di caricamento del codice come l'ottimizzazione del caricatore automatico del compositore di PHP.

Utilizzo delle variabili globali

In Cloud Run for Anthos, non puoi presumere che lo stato del servizio venga mantenuto tra le richieste. Tuttavia, Cloud Run for Anthos riutilizza le singole istanze di container per gestire il traffico continuo, in modo da poter dichiarare una variabile nell'ambito globale per consentirne il riutilizzo nelle chiamate successive. Non è possibile sapere in anticipo se una singola richiesta riceve i vantaggi di questo riutilizzo.

Puoi anche memorizzare nella cache gli oggetti in memoria se sono costosi da ricreare a ogni richiesta di servizio. Lo spostamento dalla logica della richiesta all'ambito globale genera 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();
  }
}

Eseguire l'inizializzazione lazy di variabili globali

L'inizializzazione delle variabili globali avviene sempre durante l'avvio, aumentando il tempo di avvio a freddo. Utilizza l'inizializzazione lazy per gli oggetti utilizzati di rado per rimandare il costo del tempo e diminuire gli orari di avvio a freddo.

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

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();
  }
}

Ottimizzazione della contemporaneità

Le istanze Cloud Run for Anthos possono gestire più richieste contemporaneamente, "contemporaneamente", fino a una contemporaneità massima configurabile. È diverso da Cloud Functions, che utilizza concurrency = 1.

Dovresti mantenere l'impostazione predefinita di contemporaneità massima, a meno che il tuo codice non abbia requisiti di contemporaneità specifici.

Ottimizzare la contemporaneità per il servizio

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

Per ottimizzare il servizio per la massima contemporaneità stabile:

  1. Ottimizza le prestazioni del servizio.
  2. Imposta il livello previsto di supporto della contemporaneità in qualsiasi configurazione di contemporaneità a livello di codice. Non tutti gli stack tecnologici richiedono questa impostazione.
  3. Eseguire il deployment del servizio.
  4. Imposta la contemporaneità di Cloud Run for Anthos per il tuo servizio è uguale o inferiore a qualsiasi configurazione a livello di codice. In assenza di una configurazione a livello di codice, utilizza la contemporaneità prevista.
  5. Utilizza gli strumenti di test di carico che supportano una contemporaneità configurabile. Devi confermare che il servizio rimanga stabile sotto carico e 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 correttamente, torna al passaggio 2 e aumenta la contemporaneità.

Continua a eseguire l'iterazione fino a trovare la massima contemporaneità stabile.

Abbinamento del ricordo 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.

Evitare uno stato globale modificabile

Se vuoi sfruttare lo stato globale modificabile in un contesto simultaneo, esegui ulteriori passaggi nel codice per assicurarti che questa operazione venga eseguita in sicurezza. Riduci al minimo la contesa limitando le variabili globali all'inizializzazione una tantum e riutilizzandole 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 condizioni di gara.

Sicurezza dei container

Molte pratiche di sicurezza software per uso generico si applicano alle applicazioni containerizzate. Ci sono alcune pratiche specifiche per i container o allineate alla filosofia e all'architettura dei container.

Per migliorare la sicurezza dei container:

  • Utilizza immagini di base gestite in modo attivo e sicuro, come immagini di base gestite o le immagini ufficiali di Docker Hub.

  • Applica aggiornamenti di sicurezza ai tuoi servizi ricreando regolarmente le immagini container e eseguendo nuovamente il deployment dei tuoi servizi.

  • Includi nel container solo ciò che è necessario per eseguire il servizio. Codice extra, pacchetti e strumenti sono potenziali vulnerabilità di sicurezza. Vedi sopra per l'impatto sul rendimento correlato.

  • Implementa un processo di compilazione deterministico che include specifiche versioni di software e librerie. Questo impedisce che il codice non verificato venga incluso nel container.

  • Imposta l'esecuzione del container come utente diverso da root con l'istruzioneDockerfile USER. Alcune immagini container potrebbero già aver configurato un utente specifico.

Automazione della scansione di sicurezza

Abilita lo strumento di analisi delle vulnerabilità delle immagini di Container Registry per eseguire la scansione di sicurezza delle immagini di container archiviate in Container Registry.

Puoi anche utilizzare l'Autorizzazione binaria per assicurarti che venga eseguito il deployment solo di immagini container sicure.

Creazione di immagini container minime

È probabile che le immagini container di grandi dimensioni aumentino le vulnerabilità di sicurezza perché contengono più di quanto necessario per il codice.

In Cloud Run for Anthos, le dimensioni dell'immagine container non influiscono sul avvio a freddo o sul tempo di elaborazione della richiesta e non vengono conteggiate nella memoria disponibile del container.

Per creare un container minimo, valuta la possibilità di lavorare da un'immagine di base snella come:

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

Se il tuo servizio ha un processo di compilazione ricco di strumenti, valuta la possibilità di utilizzare build multi-fase per mantenere il container leggero durante l'esecuzione.

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