Suggerimenti generali per lo sviluppo

Questa guida fornisce le best practice per la progettazione, l'implementazione, il test e il deployment di un servizio Knative serving. Per altri suggerimenti, consulta 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 Knative serving.

Evitare attività in background

Quando un'applicazione in esecuzione su Knative serving termina la gestione di una richiesta, l'accesso dell'istanza container alla CPU viene disattivato o limitato. Pertanto, non devi avviare thread o routine in background che vengono eseguiti al di fuori dell'ambito dei gestori delle richieste.

L'esecuzione di thread in background può comportare un comportamento imprevisto perché qualsiasi richiesta successiva alla stessa istanza container riprende qualsiasi attività in background sospesa.

L'attività in background è tutto ciò che accade dopo la consegna della risposta HTTP. Esamina il codice per assicurarti che tutte le operazioni asincrone vengano completate prima di consegnare la risposta.

Se sospetti che nel tuo servizio possa essere presente un'attività in background non immediatamente visibile, puoi controllare i log: cerca qualsiasi elemento registrato dopo la voce relativa alla richiesta HTTP.

Eliminare i file temporanei

Nell'ambiente Cloud Run, l'archiviazione su disco è un file system in memoria. I file scritti su disco consumano la memoria altrimenti disponibile per il 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 a freddo.

Ottimizzare le prestazioni

Questa sezione descrive le best practice per ottimizzare le prestazioni.

Avviare rapidamente i servizi

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

La routine di avvio è composta da:

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

L'ottimizzazione della velocità di avvio del servizio riduce al minimo la latenza che ritarda la gestione delle richieste da parte di un'istanza container.

Utilizzare le dipendenze in modo strategico

Se utilizzi un linguaggio dinamico con librerie dipendenti, ad esempio l'importazione di moduli in Node.js, il tempo di caricamento di questi moduli aggiunge latenza durante un avvio a freddo. 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 linguaggio lo supporta.
  • Utilizza le ottimizzazioni del caricamento del codice, ad esempio l'ottimizzazione dell'autoloader del compositore di PHP .

Utilizzare le variabili globali

In Knative serving, non puoi presupporre che lo stato del servizio venga mantenuto tra le richieste. Tuttavia, Knative serving riutilizza le singole istanze container per gestire il traffico continuo, 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 riceverà il vantaggio di questo riutilizzo.

Puoi anche memorizzare nella cache gli oggetti in memoria se la loro ricreazione a ogni richiesta di servizio è costosa. Lo spostamento di questa logica 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();
  }
}

Eseguire l'inizializzazione lazy delle variabili globali

L'inizializzazione delle variabili globali avviene sempre durante l'avvio, il che aumenta il tempo di avvio a freddo. Utilizza l'inizializzazione lazy per gli oggetti utilizzati di rado per posticipare il costo del tempo e ridurre i tempi 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  # 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();
  }
}

Ottimizzare la concorrenza

Le istanze Knative serving possono gestire più richieste contemporaneamente, "in parallelo", fino a una concorrenza massima configurabile. Questo è diverso dalle funzioni Cloud Run, che utilizzano concurrency = 1.

Devi mantenere l'impostazione predefinita della concorrenza massima, a meno che il codice non abbia requisiti di concorrenza specifici.

Ottimizzare la concorrenza per il servizio

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

Per ottimizzare il servizio per la massima concorrenza stabile:

  1. Ottimizza le prestazioni del servizio.
  2. Imposta il livello previsto di supporto della concorrenza in qualsiasi configurazione della concorrenza a livello di codice. Non tutti gli stack tecnologici richiedono questa impostazione.
  3. Esegui il deployment del servizio.
  4. Imposta la concorrenza di Knative serving per il servizio 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 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 iterazioni finché non trovi la concorrenza massima stabile.

Abbinare 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 modificare anche il limite di memoria.

Evitare lo stato globale modificabile

Se vuoi sfruttare lo stato globale modificabile in un contesto di concorrenza, esegui passaggi aggiuntivi nel codice per assicurarti che venga eseguito in modo sicuro. Riduci al minimo la contesa limitando le variabili globali all'inizializzazione una tantum e al riutilizzo 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 competizione.

Sicurezza dei container

Molte pratiche di sicurezza software di uso generale si applicano alle applicazioni containerizzate. 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 di sicurezza ai servizi ricreando regolarmente le immagini container e eseguendo nuovamente 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. Per l'impatto sulle prestazioni correlato, vedi sopra .

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

  • Imposta l'esecuzione del container come utente diverso da root con l' istruzione USER di Dockerfile. Alcune immagini container potrebbero avere già un utente specifico configurato.

Automatizzare l'analisi della sicurezza

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

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

Creare immagini container minime

Le immagini container di grandi dimensioni aumentano probabilmente le vulnerabilità di sicurezza perché contengono più di quanto necessario per il codice.

In Knative serving, le dimensioni dell'immagine container non influiscono sul tempo di avvio a freddo o di elaborazione delle richieste e non vengono conteggiate nella 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 servizio ha un processo di compilazione con molti strumenti, valuta la possibilità di utilizzare build a più fasi per mantenere il container leggero durante il runtime.

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