Suggerimenti generali per lo sviluppo

Questa guida fornisce le best practice per progettare, implementare, testare e implementare un servizio di pubblicazione Knative. Per altri suggerimenti, vedi Migrazione di un servizio esistente.

Scrivere servizi efficaci

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

Come evitare attività in background

Quando un'applicazione in esecuzione su Knative serving termina l'elaborazione di una richiesta, l'accesso della CPU all'istanza del contenitore viene disattivato o limitato notevolmente. 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ò causare comportamenti imprevisti perché una successiva richiesta alla stessa istanza di container ripristina attività in background.

Per attività in background si intende tutto ciò che accade dopo che la risposta HTTP è stata disponibili. Esamina il codice per assicurarti che tutte le operazioni asincrone vengano completate prima di inviare la risposta.

Se sospetti la presenza di attività in background nel tuo servizio che non sono è subito evidente che puoi controllare i log: cerca tutto ciò che registrato dopo la voce della richiesta HTTP.

Eliminazione dei 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 elimini questi file, prima o poi genererà 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 dei container vengono scalate in base alle esigenze, un metodo tipico è inizializzare completamente l'ambiente di esecuzione. Questo tipo di initialization è chiamato "cold start". Se una richiesta del client attiva un attacco freddo l'avvio dell'istanza di container determina una latenza aggiuntiva.

La routine di avvio è costituita da:

  • Avvio del servizio
  • Controllo della porta del servizio aperta.

L'ottimizzazione per la velocità di avvio del servizio riduce al minimo la latenza che ritarda l'invio di richieste da parte di un'istanza del contenitore.

Utilizzare le dipendenze in modo oculato

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 la dimensione delle dipendenze per creare un servizio snello.
  • Carica lentamente il codice utilizzato di rado, se il tuo linguaggio lo supporta.
  • Usa ottimizzazioni del caricamento del codice come PHP ottimizzazione del caricatore automatico del compositore.

Utilizzo di variabili globali

In Knative serving, non si può presumere che lo stato del servizio sia mantenuto tra richieste. Tuttavia, Knative serving riutilizza le singole istanze di container per gestire il traffico continuo, quindi puoi dichiarare una variabile in ambito globale il suo valore affinché venga riutilizzato nelle chiamate successive. Eventuali richieste individuali beneficio di questo riutilizzo non può essere noto in anticipo.

Puoi anche memorizzare nella cache gli oggetti se sono costosi da ricreare richiesta di servizio. Lo spostamento dalla logica di richiesta all'ambito globale 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}"

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

Esecuzione dell'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 usati di rado per posticipare il costo in termini di 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

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

Ottimizzazione della concorrenza

Le istanze di servizio Knative possono gestire più richieste contemporaneamente, "in contemporanea", fino a una contemporaneità massima configurabile. È diverso dalle funzioni di Cloud Run, che utilizza concurrency = 1.

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

Ottimizzazione della concorrenza per il servizio

Il numero di richieste in parallelo che ogni istanza di container può gestire può essere limitato dall'architettura tecnologica 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 il rendimento del servizio.
  2. Imposta il livello previsto di supporto della contemporaneità in qualsiasi contemporaneità a livello di codice configurazione. Non tutti gli stack tecnologici richiedono un'impostazione di questo tipo.
  3. Esegui il deployment del servizio.
  4. Imposta la contemporaneità di Knative serving per il tuo servizio uguale o inferiore a qualsiasi configurazione a livello di codice. Se non esiste una configurazione a livello di codice, utilizza della contemporaneità prevista.
  5. Utilizzare i test di carico che supportano una contemporaneità 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, torna al passaggio 2 e aumenta la contemporaneità.

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

Adattamento della 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 modificabile in un contesto simultaneo, prendi in considerazione passaggi nel codice per assicurarti che questa operazione venga eseguita 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.

Sicurezza dei container

Molte pratiche di sicurezza del software per uso generico si applicano alle applicazioni containerizzate. Esistono alcune pratiche specifiche dei container o che sono in linea con la filosofia e l'architettura dei container.

Per migliorare la sicurezza del contenitore:

  • Usa immagini di base protette e gestite attivamente, come quelle di Google immagini di base o dalle immagini ufficiali di Docker Hub.

  • Applica gli aggiornamenti della sicurezza ai tuoi servizi ricreando regolarmente il container ed eseguire nuovamente il deployment dei 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.

  • Implementare 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 container potrebbe essere già configurato un utente specifico.

Automazione della scansione di sicurezza

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

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

Creazione di immagini container minime

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

In Knative Serving, le dimensioni dell'immagine del contenitore non influiscono sul tempo di avvio a freddo o sull'elaborazione delle richieste e non vengono conteggiate ai fini della memoria disponibile del contenitore.

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

Ubuntu ha 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 container lean: