Porady i wskazówki

W tym dokumencie opisujemy 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 jeśli są wywoływane wielokrotnie. Dzięki temu możesz ponowić wywołanie, jeśli poprzednie się nie powiedzie w trakcie wykonywania kodu. Więcej informacji znajdziesz w artykule Ponawianie wywołań funkcji wyzwalanych przez zdarzenia.

Nie uruchamiaj działań w tle

Działanie w tle to wszystko, co dzieje się po zakończeniu działania funkcji. Wywołanie funkcji kończy się, gdy funkcja zwraca wartość lub w inny sposób sygnalizuje zakończenie, np. przez wywołanie argumentu callback w funkcjach wyzwalanych przez zdarzenia w Node.js. Żaden kod uruchomiony po prawidłowym zakończeniu nie będzie miał dostępu do procesora i nie będzie wykonywany.

Ponadto, gdy kolejne wywołanie jest wykonywane w tym samym środowisku, działanie w tle jest wznawiane, co zakłóca nowe wywołanie. Może to prowadzić do nieoczekiwanych zachowań i błędów, które trudno zdiagnozować. Dostęp do sieci po zakończeniu działania funkcji zwykle powoduje zresetowanie połączeń (kod błędu ECONNRESET).

Działanie w tle można często wykryć w logach poszczególnych wywołań, wyszukując wszystko, co zostało zarejestrowane po wierszu informującym o zakończeniu wywołania. Działanie w tle może być czasami ukryte głębiej w kodzie, zwłaszcza gdy występują operacje asynchroniczne, takie jak wywołania zwrotne lub timery. Sprawdź kod, aby upewnić się, że wszystkie operacje asynchroniczne zakończą się przed zakończeniem działania funkcji.

Zawsze usuwaj pliki tymczasowe

Lokalna pamięć dyskowa 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 ostatecznie doprowadzić do błędu braku pamięci i kolejnego uruchomienia „na zimno”.

Pamięć używaną przez poszczególne funkcje możesz sprawdzić, wybierając ją na liście funkcji w konsoli Google Cloud i wybierając wykres Zużycie pamięci.

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

Możesz zmniejszyć wymagania dotyczące pamięci podczas przetwarzania większych plików za pomocą potokowania. 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.

Platforma funkcji

Aby mieć pewność, że te same zależności są instalowane spójnie w różnych środowiskach, zalecamy dodanie biblioteki platformy funkcji do menedżera pakietów i przypięcie zależności do określonej wersji platformy funkcji.

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

Jeśli platforma funkcji nie jest wyraźnie wymieniona jako zależność, zostanie automatycznie dodana 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, testowania i interakcji z Cloud Functions.

Programowanie lokalne

Wdrożenie funkcji zajmuje trochę czasu, dlatego często szybciej jest przetestować kod funkcji lokalnie.

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

Unikaj przekroczenia limitu czasu wdrożenia podczas inicjowania

Jeśli wdrożenie funkcji nie powiedzie się z powodu przekroczenia limitu czasu, prawdopodobnie oznacza to, że kod w zakresie globalnym funkcji wykonuje się zbyt długo podczas procesu wdrożenia.

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

Aby uniknąć przekroczenia limitu czasu, zastosuj jedną z tych strategii:

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

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) Zwiększ limit czasu wykrywania

Jeśli nie możesz refaktoryzować kodu, aby użyć onInit(), możesz zwiększyć limit czasu wdrożenia interfejsu wiersza poleceń za pomocą 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, dlatego nie możesz nawiązywać niezabezpieczonych połączeń z serwerem SMTP. Zalecamy wysyłanie e-maili za pomocą usługi zewnętrznej, takiej jak SendGrid. Inne opcje wysyłania e-maili znajdziesz w samouczku Wysyłanie e-maili z instancji w Google Compute Engine.

Wydajność

W tej sekcji opisujemy sprawdzone metody optymalizacji wydajności.

Unikaj niskiej współbieżności

Uruchomienia „na zimno” są kosztowne, dlatego możliwość ponownego użycia niedawno uruchomionych instancji podczas nagłego wzrostu obciążenia jest świetną optymalizacją. Ograniczenie współbieżności ogranicza możliwość wykorzystania istniejących instancji, co powoduje więcej uruchomień „na zimno”.

Zwiększenie współbieżności pomaga odroczyć obsługę wielu żądań na instancję, co ułatwia radzenie sobie z nagłymi wzrostami obciążenia.

Mądrze korzystaj z zależności

Ponieważ funkcje są bezstanowe, środowisko wykonawcze jest często inicjowane od zera (podczas tzw. uruchomienia „na zimno”). Gdy wystąpi uruchomienie „na zimno”, oceniany jest kontekst globalny funkcji.

Jeśli funkcje importują moduły, czas wczytywania tych modułów może zwiększyć opóźnienie wywołania 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żywaj zmiennych globalnych, aby ponownie używać 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.

W ten sposób możesz buforować obiekty, których ponowne tworzenie przy każdym wywołaniu funkcji może być kosztowne. Przeniesienie takich obiektów z treści funkcji do zakresu globalnego może znacznie zwiększyć wydajność. Ten przykład tworzy złożony obiekt tylko raz na instancję funkcji i udostępnia go wszystkim wywołaniom funkcji docierającym 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 API w zakresie globalnym. Przykłady znajdziesz w artykule Optymalizacja sieci.

Ogranicz uruchomienia „na zimno”, ustawiając minimalną liczbę instancji

Domyślnie Cloud Functions skaluje liczbę instancji na podstawie liczby żądań przychodzących. Możesz zmienić to domyślne działanie, 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 Twoja aplikacja jest wrażliwa na opóźnienia, zalecamy ustawienie minimalnej liczby instancji i dokończenie inicjowania podczas wczytywania.

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

Uwagi dotyczące uruchomienia „na zimno” i inicjowania

Inicjowanie globalne odbywa się podczas wczytywania. Bez niego pierwsze żądanie musiałoby dokończyć inicjowanie i wczytać moduły, co spowodowałoby większe opóźnienie.

Inicjowanie globalne ma jednak też wpływ na uruchomienia „na zimno”. Aby zminimalizować ten wpływ, zainicjuj tylko to, co jest potrzebne do obsługi 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 opóźnienia. W takim przypadku dokończenie inicjowania podczas wczytywania i buforowanie przydatnych danych zapewnia, że pierwsze żądanie nie będzie musiało tego robić i zostanie obsłużone z małym opóźnieniem.

Jeśli zainicjujesz zmienne w zakresie globalnym, w zależności od języka, długi czas inicjowania może powodować 2 zachowania: - w przypadku niektórych kombinacji języków i bibliotek asynchronicznych platforma funkcji może działać asynchronicznie i natychmiast zwracać wartość, co powoduje, że kod jest nadal uruchamiany w tle, co może powodować problemy, takie jak brak dostępu do procesora. Aby tego uniknąć, zablokuj inicjowanie modułu zgodnie z opisem poniżej. Zapewnia to również, że żą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 współbieżności podczas nagłych wzrostów obciążenia.

Przykład wstępnego wczytywania asynchronicznej biblioteki Node.js

Node.js z Firestore to przykład asynchronicznej biblioteki Node.js. Aby skorzystać z minimalnej liczby instancji, ten kod dokańcza wczytywanie i inicjowanie podczas wczytywania, blokując wczytywanie modułu.

Używany jest TLA, co oznacza, że wymagany jest ES6. W tym celu użyj rozszerzenia .mjs dla kodu Node.js lub dodaj 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 globalnych zmiennych inicjowanych z opóźnieniem. 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 zdefiniujesz kilka funkcji, a różne funkcje będą używać różnych zmiennych. Jeśli nie użyjesz inicjowania z opóźnieniem, 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 Cold Boot Time.