Porady i wskazówki

W tym dokumencie opisano sprawdzone metody projektowania, wdrażania, testowania i wdrażania Cloud Functions.

Poprawność

W tej sekcji opisujemy ogólne sprawdzone metody projektowania i wdrażania Cloud Functions.

Pisanie funkcji idempotentnych

Funkcje powinny dawać ten sam wynik nawet wtedy, gdy są wywoływane wielokrotnie. Dzięki temu możesz ponowić wywołanie, jeśli poprzednie nie powiedzie się w trakcie wykonywania kodu. Więcej informacji znajdziesz w artykule o ponawianiu funkcji opartych na zdarzeniach.

Nie uruchamiaj działań w tle

Aktywność w tle to wszystko, co dzieje się po zakończeniu funkcji. Wywołanie funkcji kończy się, gdy funkcja zwraca wynik lub w inny sposób sygnalizuje zakończenie, np. przez wywołanie argumentu callback w funkcjach opartych na zdarzeniach w Node.js. Żaden kod uruchomiony po łagodnym zakończeniu nie będzie miał dostępu do procesora i nie będzie mógł kontynuować działania.

Dodatkowo, gdy w tym samym środowisku zostanie wykonane kolejne wywołanie, aktywność w tle zostanie wznowiona, co zakłóci nowe wywołanie. Może to prowadzić do nieoczekiwanego działania i błędów, które są trudne do zdiagnozowania. Uzyskanie dostępu do sieci po zakończeniu działania funkcji zwykle prowadzi do zresetowania połączeń (kod błędu ECONNRESET).

Aktywność w tle można często wykryć w logach z poszczególnych wywołań, znajdując wszystko, co jest rejestrowane po wierszu informującym o zakończeniu wywołania. Aktywność w tle może być czasami ukryta głębiej w kodzie, zwłaszcza gdy występują operacje asynchroniczne, takie jak wywołania zwrotne lub liczniki czasu. Sprawdź kod, aby upewnić się, że wszystkie operacje asynchroniczne zakończą się przed zakończeniem funkcji.

Zawsze usuwaj pliki tymczasowe

Pamięć dysku lokalnego w katalogu tymczasowym to system plików w pamięci. Zapisywane pliki zużywają pamięć dostępną dla funkcji i czasami są zachowywane między wywołaniami. Jeśli nie usuniesz tych plików, może to doprowadzić do błędu braku pamięci i późniejszego uruchomienia na zimno.

Ilość pamięci używanej przez poszczególne funkcje możesz sprawdzić, wybierając funkcję na liście funkcji w konsoli Google Cloud i wybierając wykres Wykorzystanie pamięci.

Jeśli potrzebujesz dostępu do pamięci długoterminowej, rozważ użycie punktów montowania woluminów Cloud Run z Cloud Storage lub woluminów NFS.

Możesz zmniejszyć wymagania dotyczące pamięci podczas przetwarzania większych plików, korzystając z potoków. Możesz na przykład przetworzyć plik w Cloud Storage, tworząc strumień odczytu, przekazując go przez proces oparty na strumieniu i zapisując strumień wyjściowy bezpośrednio w Cloud Storage.

Functions Framework

Aby mieć pewność, że te same zależności są instalowane spójnie w różnych środowiskach, zalecamy uwzględnienie biblioteki Functions Framework w menedżerze pakietów i przypięcie zależności do konkretnej wersji Functions Framework.

Aby to zrobić, uwzględnij preferowaną wersję w odpowiednim pliku blokady (np. package-lock.json w przypadku Node.js lub requirements.txt w przypadku Pythona).

Jeśli Functions Framework nie jest wyraźnie wymieniony jako zależność, zostanie automatycznie dodany podczas procesu kompilacji przy użyciu najnowszej dostępnej wersji.

Narzędzia

W tej sekcji znajdziesz wskazówki dotyczące korzystania z narzędzi do wdrażania i testowania Cloud Functions oraz interakcji z tą usługą.

Programowanie lokalne

Wdrażanie funkcji zajmuje trochę czasu, więc często szybciej jest przetestować kod funkcji lokalnie.

Deweloperzy Firebase mogą używać emulatora Cloud Functions w interfejsie wiersza poleceń Firebase.

Unikanie przekroczenia limitu czasu wdrażania podczas inicjowania

Jeśli wdrożenie funkcji nie powiedzie się z powodu błędu przekroczenia limitu czasu, prawdopodobnie oznacza to, że kod zakresu globalnego funkcji zbyt długo się wykonuje podczas procesu wdrażania.

Interfejs wiersza poleceń Firebase ma domyślny limit czasu na wykrywanie funkcji podczas wdrażania. Jeśli logika inicjowania w kodzie źródłowym funkcji (ładowanie modułów, wykonywanie wywołań sieciowych itp.) przekroczy ten limit czasu, wdrożenie może się nie powieść.

Aby uniknąć przekroczenia limitu czasu, skorzystaj z jednej z tych strategii:

Użyj punktu zaczepienia onInit(), aby uniknąć uruchamiania kodu inicjowania podczas wdrażania. Kod wewnątrz punktu zaczepienia onInit() będzie uruchamiany tylko wtedy, gdy funkcja zostanie wdrożona w funkcjach Cloud Run, a nie podczas samego procesu wdrażania.

Node.js

const { onInit } = require('firebase-functions/v2/core');
const { onRequest } = require('firebase-functions/v2/https');

// Example of a slow initialization task
function slowInitialization() {
  // Simulate a long-running operation (e.g., loading a large model, network request).
  return new Promise(resolve => {
      setTimeout(() => {
          console.log("Slow initialization complete");
          resolve("Initialized Value");
      }, 20000); // Simulate a 20-second delay
  });
}
let initializedValue;

onInit(async () => {
  initializedValue = await slowInitialization();
});

exports.myFunction = onRequest((req, res) => {
  // Access the initialized value. It will be ready after the first invocation.
  res.send(`Value: ${initializedValue}`);
});

Python

from firebase_functions.core import init
from firebase_functions import https_fn
import time

# Example of a slow initialization task
def _slow_initialization():
  time.sleep(20)  # Simulate a 20-second delay
  print("Slow initialization complete")
  return "Initialized Value"

_initialized_value = None

@init
def initialize():
  global _initialized_value
  _initialized_value = _slow_initialization()

@https_fn.on_request()
def my_function(req: https_fn.Request) -> https_fn.Response:
  # Access the initialized value. It will be ready after the first invocation.
  return https_fn.Response(f"Value: {_initialized_value}")

(Alternatywnie) Wydłuż limit czasu wykrywania

Jeśli nie możesz refaktoryzować kodu, aby używać onInit(), możesz wydłużyć czas oczekiwania na wdrożenie w interfejsie wiersza poleceń, używając zmiennej środowiskowej FUNCTIONS_DISCOVERY_TIMEOUT:

$ export FUNCTIONS_DISCOVERY_TIMEOUT=30
$ firebase deploy --only functions

Wysyłanie e-maili za pomocą SendGrid

Cloud Functions nie zezwala na połączenia wychodzące na porcie 25, więc nie możesz nawiązywać niezabezpieczonych połączeń z serwerem SMTP. Zalecanym sposobem wysyłania e-maili jest korzystanie z usługi innej firmy, takiej jak SendGrid. Inne opcje wysyłania e-maili znajdziesz w samouczku Wysyłanie e-maili z instancji dotyczącym Google Compute Engine.

Wydajność

W tej sekcji znajdziesz sprawdzone metody optymalizacji skuteczności.

Unikaj niskiej równoczesności

Ponieważ uruchomienia na zimno są kosztowne, możliwość ponownego użycia niedawno uruchomionych instancji podczas skoku obciążenia jest świetną optymalizacją. Ograniczenie równoczesności ogranicza możliwości wykorzystania istniejących instancji, co powoduje więcej uruchomień „na zimno”.

Zwiększenie równoczesności pomaga odroczyć wiele żądań na instancję, co ułatwia obsługę skoków obciążenia.

Mądre korzystanie z zależności

Funkcje są bezstanowe, dlatego środowisko wykonawcze jest często inicjowane od zera (podczas tzw. uruchomienia „na zimno”). Gdy następuje uruchomienie „na zimno”, oceniany jest globalny kontekst funkcji.

Jeśli funkcje importują moduły, czas wczytywania tych modułów może wydłużyć czas oczekiwania na wywołanie podczas uruchomienia „na zimno”. Możesz zmniejszyć to opóźnienie, a także czas potrzebny na wdrożenie funkcji, prawidłowo wczytując zależności i nie wczytując zależności, których funkcja nie używa.

Używanie zmiennych globalnych do ponownego wykorzystywania obiektów w przyszłych wywołaniach

Nie ma gwarancji, że stan funkcji zostanie zachowany na potrzeby przyszłych wywołań. Jednak Cloud Functions często ponownie wykorzystuje środowisko wykonawcze poprzedniego wywołania. Jeśli zadeklarujesz zmienną w zakresie globalnym, jej wartość może być ponownie używana w kolejnych wywołaniach bez konieczności ponownego obliczania.

Dzięki temu możesz przechowywać w pamięci podręcznej obiekty, których odtworzenie przy każdym wywołaniu funkcji może być kosztowne. Przeniesienie takich obiektów z treści funkcji do zakresu globalnego może znacznie poprawić wydajność. W tym przykładzie ciężki obiekt jest tworzony tylko raz na instancję funkcji i jest udostępniany wszystkim wywołaniom funkcji, które docierają do danej instancji:

Node.js

console.log('Global scope');
const perInstance = heavyComputation();
const functions = require('firebase-functions');

exports.function = functions.https.onRequest((req, res) => {
  console.log('Function invocation');
  const perFunction = lightweightComputation();

  res.send(`Per instance: ${perInstance}, per function: ${perFunction}`);
});

Python

import time

from firebase_functions import https_fn

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

@https_fn.on_request()
def scope_demo(request):

  # Per-function scope
  # This computation runs every time this function is called
  function_var = light_computation()
  return https_fn.Response(f"Instance: {instance_var}; function: {function_var}")
  

Ta funkcja HTTP przyjmuje obiekt żądania (flask.Request) i zwraca tekst odpowiedzi lub dowolny zestaw wartości, które można przekształcić w obiekt Response za pomocą make_response.

Szczególnie ważne jest buforowanie połączeń sieciowych, odwołań do bibliotek i obiektów klienta interfejsu API w zakresie globalnym. Przykłady znajdziesz w artykule Optymalizowanie sieci.

Ograniczanie uruchomień „na zimno” przez ustawienie minimalnej liczby instancji

Domyślnie Cloud Functions skaluje liczbę instancji na podstawie liczby żądań przychodzących. Możesz zmienić to działanie domyślne, ustawiając minimalną liczbę instancji, które Cloud Functions musi utrzymywać w gotowości do obsługi żądań. Ustawienie minimalnej liczby instancji ogranicza uruchomienia „na zimno” aplikacji. Jeśli aplikacja jest wrażliwa na czas oczekiwania, zalecamy ustawienie minimalnej liczby instancji i ukończenie inicjowania w czasie wczytywania.

Więcej informacji o tych opcjach środowiska wykonawczego znajdziesz w artykule o kontrolowaniu zachowania skalowania.

Uwagi dotyczące uruchamiania „na zimno” i inicjowania

Inicjowanie globalne odbywa się w czasie wczytywania. Bez tego pierwsze żądanie musiałoby zakończyć inicjowanie i załadować moduły, co spowodowałoby większe opóźnienie.

Jednak inicjowanie globalne ma też wpływ na uruchomienia „na zimno”. Aby zminimalizować ten wpływ, inicjuj tylko to, co jest potrzebne do pierwszego żądania, aby opóźnienie pierwszego żądania było jak najmniejsze.

Jest to szczególnie ważne, jeśli skonfigurowano minimalną liczbę instancji zgodnie z opisem powyżej w przypadku funkcji wrażliwej na czas oczekiwania. W takim przypadku ukończenie inicjowania w czasie wczytywania i buforowanie przydatnych danych gwarantuje, że pierwsze żądanie nie będzie musiało tego robić i zostanie obsłużone z niskim opóźnieniem.

Jeśli inicjujesz zmienne w zakresie globalnym, w zależności od języka długi czas inicjowania może skutkować 2 zachowaniami: - w przypadku niektórych kombinacji języków i bibliotek asynchronicznych platforma funkcji może działać asynchronicznie i natychmiast zwracać wynik, co powoduje, że kod nadal działa w tle, co może powodować problemy, takie jak brak dostępu do procesora; Aby tego uniknąć, zablokuj inicjowanie modułu, jak opisano poniżej. Dzięki temu żądania nie będą obsługiwane, dopóki inicjowanie nie zostanie zakończone. – z drugiej strony, jeśli inicjowanie jest synchroniczne, długi czas inicjowania spowoduje dłuższe uruchomienia „na zimno”, co może być problemem, zwłaszcza w przypadku funkcji o niskiej równoczesności podczas skoków obciążenia.

Przykład wstępnego rozgrzewania asynchronicznej biblioteki node.js

Node.js z Firestore to przykład asynchronicznej biblioteki Node.js. Aby skorzystać z min_instances, poniższy kod kończy wczytywanie i inicjowanie w czasie wczytywania, blokując wczytywanie modułu.

Używany jest TLA, co oznacza, że wymagany jest ES6, przy użyciu rozszerzenia .mjs dla kodu node.js lub dodaniu type: module do pliku package.json.

{
  "main": "main.js",
  "type": "module",
  "dependencies": {
    "@google-cloud/firestore": "^7.10.0",
    "@google-cloud/functions-framework": "^3.4.5"
  }
}

Node.js

import Firestore from '@google-cloud/firestore';
import * as functions from '@google-cloud/functions-framework';

const firestore = new Firestore({preferRest: true});

// Pre-warm firestore connection pool, and preload our global config
// document in cache. In order to ensure no other request comes in,
// block the module loading with a synchronous global request:
const config = await firestore.collection('collection').doc('config').get();

functions.http('fetch', (req, res) => {

// Do something with config and firestore client, which are now preloaded
// and will execute at lower latency.
});

Przykłady inicjowania globalnego

Node.js

const functions = require('firebase-functions');
let myCostlyVariable;

exports.function = functions.https.onRequest((req, res) => {
  doUsualWork();
  if(unlikelyCondition()){
      myCostlyVariable = myCostlyVariable || buildCostlyVariable();
  }
  res.status(200).send('OK');
});

Python

from firebase_functions import https_fn

# 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

@https_fn.on_request()
def lazy_globals(request):

  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 https_fn.Response(f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}.")
  

Ta funkcja HTTP używa zmiennych globalnych inicjowanych leniwie. Przyjmuje obiekt żądania (flask.Request) i zwraca tekst odpowiedzi lub dowolny zestaw wartości, które można przekształcić w obiekt Response za pomocą make_response.

Jest to szczególnie ważne, jeśli w jednym pliku definiujesz kilka funkcji, a różne funkcje używają różnych zmiennych. Jeśli nie używasz leniwej inicjalizacji, możesz marnować zasoby na zmienne, które są inicjowane, ale nigdy nie są używane.

Dodatkowe materiały

Więcej informacji o optymalizacji wydajności znajdziesz w filmie „Google Cloud Performance Atlas” Cloud Functions Zimny rozruch.