Allgemeine Tipps zur Entwicklung

Diese Anleitung enthält Best Practices zum Entwerfen, Implementieren, Testen und Bereitstellen eines Cloud Run-Dienstes. Weitere Tipps finden Sie unter Vorhandenen Dienst migrieren.

Erfolgreich Dienste schreiben

In diesem Abschnitt werden allgemeine Best Practices für das Erstellen und Implementieren eines Cloud Run-Dienstes beschrieben.

Hintergrundaktivitäten vermeiden

Wenn eine Anwendung, die in Cloud Run ausgeführt wird, die Verarbeitung einer Anfrage beendet, wird der Zugriff der Containerinstanz auf die CPU deaktiviert oder stark eingeschränkt. Daher sollten Sie keine Hintergrundthreads oder Routinen starten, die außerhalb des Bereichs der Anfrage-Handler ausgeführt werden.

Das Ausführen von Hintergrundthreads kann zu unerwartetem Verhalten führen, da jede nachfolgende Anfrage an dieselbe Containerinstanz angehaltene Hintergrundaktivitäten fortsetzt.

Als Hintergrundaktivität werden alle Aktivitäten bezeichnet, die nach Eingang der HTTP-Antwort erfolgen. Überprüfen Sie den Code, um sicherzugehen, dass alle asynchronen Vorgänge abgeschlossen sind, bevor Sie eine Antwort übermitteln.

Wenn Sie vermuten, dass in Ihrem Dienst versteckte Hintergrundaktivitäten stattfinden, können Sie die Logs überprüfen: Suchen Sie nach Einträgen, die nach dem Eintrag für die HTTP-Anfrage protokolliert wurden.

Temporäre Dateien löschen

In der Cloud Run-Umgebung werden die Daten in einem In-Memory-Dateisystem gespeichert. In das System geschriebene Dateien belegen Speicher, der ansonsten für den Dienst verfügbar ist. Die Dateien können zwischen Aufrufen bestehen bleiben. Werden sie nicht explizit gelöscht, kann es zu einem Fehler aufgrund Speichermangels und zu einem anschließenden Kaltstart kommen.

Fehler melden

Behandeln Sie alle Ausnahmen und lassen Sie den Dienst bei Fehlern nicht abstürzen. Ein Absturz führt zu einem Kaltstart und etwaiger Traffic wird bei einer Ersatzcontainerinstanz in die Warteschlange gestellt.

Informationen zur ordnungsgemäßen Erstellung von Fehlerberichten finden Sie im Error Reporting-Leitfaden.

Leistungsoptimierung

In diesem Abschnitt erfahren Sie mehr über die Best Practices zur Optimierung der Leistung.

Dienste schnell starten

Da Containerinstanzen nach Bedarf skaliert werden, besteht eine typische Methode darin, die Ausführungsumgebung vollständig zu initialisieren. Diese Art der Initialisierung wird als „Kaltstart“ bezeichnet. Wenn eine Clientanfrage einen Kaltstart auslöst, führt der Start der Containerinstanz zu zusätzlicher Latenz.

Die Startroutine besteht aus:

  • Starten des Dienstes
    • Container starten
    • Den Befehl entrypoint ausführen, um den Server zu starten
  • Prüfen auf einen offenen Dienstport

Durch die Optimierung auf Startgeschwindigkeit wird die Latenz minimiert, sodass die Containerinstanz schneller mit der Bearbeitung von Dienstanfragen beginnen kann.

Mindestanzahl der Instanzen zur Verringerung von Kaltstarts verwenden

Sie können Mindestinstanzen und Gleichzeitigkeit konfigurieren, um Kaltstarts zu minimieren. Die Verwendung einer Mindestanzahl an Instanzen von 1 bedeutet beispielsweise, dass Ihr Dienst bereit ist, bis zu der Anzahl gleichzeitiger Anfragen zu empfangen, die für Ihren Dienst konfiguriert wurden, ohne dass ein Kaltstart auftritt. Die Mindestanzahl an Instanzen, die Sie zum Eliminieren eines Kaltstarts benötigen, entspricht der Größe Ihrer erwarteten Traffic-Spitze geteilt durch die Gleichzeitigkeit.

Abhängigkeiten sinnvoll nutzen

Wenn Sie eine dynamische Sprache mit abhängigen Bibliotheken verwenden, z. B. Module in Node.js importieren, erhöht die Ladezeit für diese Module die Latenzzeit während eines Kaltstarts. Reduzieren Sie die Startverzögerung auf folgende Weise:

  • Minimieren Sie die Anzahl und Größe der Abhängigkeiten, um einen schlanken Service zu erstellen.
  • Laden Sie selten verwendeten Code erst bei Bedarf, sofern Ihre Sprache dies unterstützt.
  • Verwenden Sie Codeladeoptimierungen wie die Composer-Autoloader-Optimierung von PHP.

Globale Variablen verwenden

In Cloud Run können Sie nicht davon ausgehen, dass der Dienststatus zwischen den Anfragen beibehalten wird. Tatsache ist aber, dass Cloud Run die einzelnen Containerinstanzen zur Verarbeitung des laufenden Traffics wiederverwendet. Deshalb können Sie eine globale Variable deklarieren, deren Wert in nachfolgenden Aufrufen wiederverwendet wird. Jedoch kann nicht vorhergesagt werden, ob später eine der Anfragen von dieser Wiederverwendung profitiert.

Sie können Objekte auch im Speicher zwischenspeichern, wenn deren Neuerstellung bei jeder Serviceanfrage zu ressourcenintensiv wäre. Wenn Sie diese Funktion aus der Anfragelogik in den globalen Bereich verschieben, wird die Leistung verbessert.

Node.js

// Global (instance-wide) scope
// This computation runs at instance cold-start
const instanceVar = heavyComputation();

/**
 * HTTP function that declares a variable.
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
exports.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

# Global (instance-wide) scope
# This computation runs at instance cold-start
instance_var = heavy_computation()

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>.
    """
    function_var = light_computation()
    return 'Per instance: {}, per function: {}'.format(
        instance_var, 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()
}

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

Globale Variablen nur bei Bedarf initialisieren

Die Initialisierung globaler Variablen erfolgt immer beim Start, wodurch sich die Kaltstartzeit erhöht. Selten verwendete Objekte sollten daher nur bei Bedarf initialisiert werden, um den Zeitaufwand zu verschieben und den Kaltstart zu beschleunigen.

Node.js

// 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.
 */
exports.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

# 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

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 'Lazy: {}, non-lazy: {}.'.format(lazy_global, 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"
)

// client is lazily initialized by LazyGlobal.
var client *storage.Client
var clientOnce sync.Once

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

Gleichzeitigkeit optimieren

Cloud Run-Instanzen können bis zur konfigurierbaren maximalen Gleichzeitigkeit mehrere Anfragen gleichzeitig verarbeiten. Dies unterscheidet sich von Cloud Functions, das concurrency = 1 verwendet.

Sie sollten die Standardeinstellung für die Gleichzeitigkeit beibehalten, es sei denn, Ihr Code hat spezifische Gleichzeitigkeitsanforderungen.

Gleichzeitigkeit für einen Dienst optimieren

Wie viele Anfragen eine Containerinstanz gleichzeitig verarbeiten kann, ist abhängig vom Softwarepaket sowie von der Nutzung freigegebener Ressourcen, wie Variablen und Datenbankverbindungen.

So optimieren Sie einen Dienst für maximal stabile Gleichzeitigkeit:

  1. Optimieren Sie die Leistung des Dienstes.
  2. Legen Sie bei der Konfiguration auf Codeebene fest, in welchem Umfang Gleichzeitigkeit unterstützt werden soll. Nicht alle Softwarepakete erfordern eine solche Einstellung.
  3. Stellen Sie den Dienst bereit.
  4. Legen Sie für Cloud Run dieselbe oder eine geringere Gleichzeitigkeit fest als auf Codeebene. Wenn auf Codeebene nichts konfiguriert ist, verwenden Sie die erwartete Gleichzeitigkeit.
  5. Verwenden Sie Lasttest-Tools, die eine konfigurierbare Gleichzeitigkeit unterstützen. Wichtig ist, dass der Dienst unter der erwarteten Last und Gleichzeitigkeit stabil bleibt.
  6. Bei schlechter Leistung kehren Sie zu Schritt 1 zurück, um den Dienst weiter zu überarbeiten, oder zu Schritt 2, um die Gleichzeitigkeit zu reduzieren. Wenn der Dienst eine gute Leistung zeigt, fahren Sie mit Schritt 2 fort und erhöhen die Gleichzeitigkeit.

Wiederholen Sie diese Schritte, bis Sie eine maximal stabile Gleichzeitigkeit erreichen.

Speicher auf Gleichzeitigkeit abstimmen

Jede Anfrage, die der Dienst bearbeitet, benötigt etwas zusätzlichen Speicher. Wenn Sie also die Gleichzeitigkeit nach oben oder unten skalieren, sollten Sie gleichzeitig das Speicherlimit anpassen.

Veränderliche globale Zustände vermeiden

Wenn Sie veränderliche globale Zustände zusammen mit Gleichzeitigkeit nutzen möchten, müssen Sie den Code so anpassen, dass dies sicher funktioniert. Konflikte lassen sich minimieren, indem Sie globale Variablen nur einmal initialisieren und ihre Wiederverwendung beschränken, wie oben unter Leistung beschrieben.

Wenn Sie in einem Dienst, der mehrere Anfragen gleichzeitig verarbeitet, veränderliche globale Variablen einsetzen, kommen Sie nicht umhin, Sperren oder Mutexe zu verwenden, um Race-Bedingungen zu verhindern.

Containersicherheit

Viele Sicherheitskonzepte, die für Standardsoftware gelten, werden auch bei containerisierten Anwendungen verwendet. Es gibt jedoch einige Praktiken, die entweder nur für Container gelten, oder die sich an der Philosophie und Architektur von Containern orientieren.

So verbessern Sie die Containersicherheit:

  • Verwenden Sie sichere Basis-Images, die laufend aktualisiert werden, z. B. verwaltete Basis-Images oder offizielle Images von Docker Hub.

  • Wenden Sie Sicherheitsupdates auf Ihre Dienste an, indem Sie die Container-Images regelmäßig neu erstellen und die Dienste neu bereitstellen.

  • Übernehmen Sie in den Container nur das, was zur Dienstausführung wirklich erforderlich ist. Zusätzlicher Code, Pakete und Tools sind generell potenzielle Sicherheitslücken. Informationen zur Auswirkung auf die Leistung finden Sie weiter oben.

  • Implementieren Sie einen deterministischen Build-Prozess, der bestimmte Software- und Bibliotheksversionen enthält. Dies verhindert, dass nicht verifizierter Code in den Container aufgenommen wird.

  • Legen Sie den Container mit der Dockerfile USER-Anweisung so fest, dass er als anderer Nutzer als root ausgeführt wird. Für einige Container-Images ist möglicherweise bereits ein bestimmter Nutzer konfiguriert.

Sicherheitsscans automatisieren

Aktivieren Sie die Image-Scanfunktion der Container Registry, um die in der Container Registry gespeicherten Container-Images auf Sicherheitslücken zu untersuchen.

Minimale Container-Images erstellen

Große Container-Images können zu größeren Sicherheitslücken führen, da sie mehr enthalten, als für den Code erforderlich ist.

Bei Cloud Run wirkt sich die Größe des Container-Images nicht auf Kaltstart- oder Anforderungsverarbeitungszeit aus und wird nicht auf den verfügbaren Arbeitsspeicher Ihres Containers angerechnet.

Orientieren Sie sich bei der Erstellung eines minimalen Containers an einem schlanken Basis-Image wie zum Beispiel:

Ubuntu ist größer, wird aber gerne als Basis-Image verwendet, weil es standardmäßig eine umfangreichere Serverumgebung mitbringt.

Wenn der Build-Prozess für Ihren Dienst viele Tools umfasst, sollten Sie mehrstufige Builds verwenden, um den Ressourcenverbrauch des Containers während der Laufzeit möglichst gering zu halten.

Hier einige Artikel zur Erstellung ressourcensparender Container-Images: