Mẹo và thủ thuật

Tài liệu này mô tả các phương pháp hay nhất để thiết kế, triển khai, kiểm thử và triển khai Cloud Functions.

Tính chính xác

Phần này mô tả các phương pháp hay nhất chung để thiết kế và triển khai Cloud Functions.

Viết các hàm đẳng nhất

Các hàm của bạn phải tạo ra cùng một kết quả ngay cả khi chúng được gọi nhiều lần. Điều này cho phép bạn thử lại một lệnh gọi nếu lệnh gọi trước đó không thành công trong quá trình thực hiện mã. Để biết thêm thông tin, hãy xem phần thử lại các hàm dựa trên sự kiện.

Không bắt đầu các hoạt động trong nền

Hoạt động ở chế độ nền là mọi hoạt động diễn ra sau khi hàm của bạn đã chấm dứt. Một lệnh gọi hàm sẽ kết thúc khi hàm trả về hoặc báo hiệu hoàn tất, chẳng hạn như bằng cách gọi đối số callback trong các hàm dựa trên sự kiện Node.js. Mọi mã chạy sau khi chấm dứt một cách êm đẹp đều không thể truy cập vào CPU và sẽ không có tiến trình nào.

Ngoài ra, khi một lệnh gọi tiếp theo được thực thi trong cùng một môi trường, hoạt động ở chế độ nền của bạn sẽ tiếp tục, gây trở ngại cho lệnh gọi mới. Điều này có thể dẫn đến hành vi không mong muốn và các lỗi khó chẩn đoán. Việc truy cập vào mạng sau khi một hàm kết thúc thường dẫn đến việc các kết nối bị đặt lại (mã lỗi ECONNRESET).

Bạn thường có thể phát hiện hoạt động trong nền trong nhật ký từ các lệnh gọi riêng lẻ, bằng cách tìm mọi thứ được ghi nhật ký sau dòng cho biết lệnh gọi đã hoàn tất. Đôi khi, hoạt động ở chế độ nền có thể nằm sâu hơn trong mã, đặc biệt là khi có các thao tác không đồng bộ như lệnh gọi lại hoặc bộ hẹn giờ. Xem lại mã của bạn để đảm bảo tất cả các thao tác không đồng bộ đều hoàn tất trước khi bạn chấm dứt hàm.

Luôn xoá tệp tạm thời

Bộ nhớ ổ đĩa cục bộ trong thư mục tạm thời là một hệ thống tệp trong bộ nhớ. Các tệp mà bạn ghi sẽ tiêu tốn bộ nhớ có sẵn cho hàm của bạn và đôi khi vẫn tồn tại giữa các lệnh gọi. Nếu không xoá rõ ràng các tệp này, có thể cuối cùng sẽ dẫn đến lỗi hết bộ nhớ và lần khởi động nguội tiếp theo.

Bạn có thể xem bộ nhớ mà một hàm riêng lẻ sử dụng bằng cách chọn hàm đó trong danh sách hàm trong Google Cloud Console và chọn biểu đồ Mức sử dụng bộ nhớ.

Nếu cần truy cập vào bộ nhớ dài hạn, hãy cân nhắc sử dụng Cloud Runlệnh gắn ổ đĩa với Cloud Storage hoặc ổ đĩa NFS.

Bạn có thể giảm yêu cầu về bộ nhớ khi xử lý các tệp lớn hơn bằng cách sử dụng quy trình truyền dữ liệu. Ví dụ: bạn có thể xử lý một tệp trên Cloud Storage bằng cách tạo một luồng đọc, truyền tệp đó qua một quy trình dựa trên luồng và ghi luồng đầu ra trực tiếp vào Cloud Storage.

Functions Framework

Để đảm bảo rằng các phần phụ thuộc giống nhau được cài đặt nhất quán trên các môi trường, bạn nên thêm thư viện Functions Framework vào trình quản lý gói và ghim phần phụ thuộc vào một phiên bản cụ thể của Functions Framework.

Để thực hiện việc này, hãy thêm phiên bản ưu tiên vào tệp khoá có liên quan (ví dụ: package-lock.json cho Node.js hoặc requirements.txt cho Python).

Nếu Functions Framework không được liệt kê rõ ràng là một phần phụ thuộc, thì phần phụ thuộc này sẽ tự động được thêm trong quá trình tạo bản dựng bằng phiên bản mới nhất hiện có.

Công cụ

Phần này cung cấp hướng dẫn về cách sử dụng các công cụ để triển khai, kiểm thử và tương tác với Cloud Functions.

Phát triển tại địa phương

Việc triển khai hàm mất một chút thời gian, vì vậy, thường sẽ nhanh hơn nếu bạn kiểm thử mã của hàm theo cách cục bộ.

Nhà phát triển Firebase có thể sử dụng Trình mô phỏng Cloud Functions Firebase CLI.

Tránh hết thời gian triển khai trong quá trình khởi tạo

Nếu quá trình triển khai hàm của bạn không thành công do lỗi hết thời gian chờ, thì có thể là do mã phạm vi chung của hàm mất quá nhiều thời gian để thực thi trong quá trình triển khai.

CLI Firebase có thời gian chờ mặc định để khám phá các hàm của bạn trong quá trình triển khai. Nếu logic khởi chạy trong mã nguồn của các hàm (đang tải mô-đun, thực hiện lệnh gọi mạng, v.v.) vượt quá thời gian chờ này, thì quá trình triển khai có thể không thành công.

Để tránh hết thời gian chờ, hãy sử dụng một trong các chiến lược sau:

Sử dụng lệnh gọi onInit() để tránh chạy mã khởi tạo trong quá trình triển khai. Mã bên trong lệnh gọi onInit() sẽ chỉ chạy khi hàm được triển khai cho các hàm Cloud Run, chứ không phải trong chính quy trình triển khai.

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}")

(Cách khác) Tăng thời gian chờ khám phá

Nếu không thể tái cấu trúc mã để sử dụng onInit(), bạn có thể tăng thời gian chờ triển khai của CLI bằng cách sử dụng biến môi trường FUNCTIONS_DISCOVERY_TIMEOUT:

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

Sử dụng Sendgrid để gửi email

Cloud Functions không cho phép kết nối đi trên cổng 25, vì vậy, bạn không thể thực hiện các kết nối không an toàn với máy chủ SMTP. Bạn nên gửi email bằng dịch vụ của bên thứ ba, chẳng hạn như SendGrid. Bạn có thể tìm thấy các lựa chọn khác để gửi email trong hướng dẫn Gửi email từ một phiên bản cho Google Compute Engine.

Hiệu suất

Phần này mô tả các phương pháp hay nhất để tối ưu hoá hiệu suất.

Tránh tình trạng có ít người dùng đồng thời

Vì quá trình khởi động nguội tốn nhiều tài nguyên, nên việc có thể sử dụng lại các phiên bản đã khởi động gần đây trong thời gian tải tăng đột biến là một cách tối ưu hoá hiệu quả để xử lý tải. Việc giới hạn mức độ đồng thời sẽ hạn chế cách tận dụng các phiên bản hiện có, do đó sẽ phát sinh nhiều lượt khởi động nguội hơn.

Tăng tính đồng thời giúp trì hoãn nhiều yêu cầu cho mỗi phiên bản, giúp bạn dễ dàng xử lý các đợt tăng đột biến về tải.

Sử dụng các phần phụ thuộc một cách khôn ngoan

Vì các hàm không có trạng thái, nên môi trường thực thi thường được khởi động từ đầu (trong quá trình được gọi là khởi động nguội). Khi quá trình khởi động nguội diễn ra, ngữ cảnh chung của hàm sẽ được đánh giá.

Nếu các hàm của bạn nhập mô-đun, thì thời gian tải cho các mô-đun đó có thể làm tăng độ trễ khi gọi trong quá trình khởi động nguội. Bạn có thể giảm độ trễ này cũng như thời gian cần thiết để triển khai hàm bằng cách tải đúng các phần phụ thuộc và không tải các phần phụ thuộc mà hàm của bạn không dùng.

Sử dụng các biến chung để dùng lại các đối tượng trong những lần gọi sau này

Không có gì đảm bảo rằng trạng thái của một hàm sẽ được giữ lại cho các lệnh gọi trong tương lai. Tuy nhiên, Cloud Functions thường tái chế môi trường thực thi của một lệnh gọi trước đó. Nếu bạn khai báo một biến trong phạm vi chung, thì giá trị của biến đó có thể được dùng lại trong các lệnh gọi tiếp theo mà không cần phải tính toán lại.

Bằng cách này, bạn có thể lưu vào bộ nhớ đệm các đối tượng có thể tốn kém để tạo lại trên mỗi lệnh gọi hàm. Việc di chuyển các đối tượng như vậy từ phần nội dung hàm sang phạm vi chung có thể giúp cải thiện đáng kể hiệu suất. Ví dụ sau đây chỉ tạo một đối tượng lớn một lần cho mỗi phiên bản hàm và chia sẻ đối tượng đó trên tất cả các lệnh gọi hàm đạt đến phiên bản đã cho:

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}")
  

Hàm HTTP này nhận một đối tượng yêu cầu (flask.Request) và trả về văn bản phản hồi hoặc bất kỳ tập hợp giá trị nào có thể chuyển thành đối tượng Response bằng cách sử dụng make_response.

Việc lưu vào bộ nhớ đệm các kết nối mạng, tham chiếu thư viện và đối tượng ứng dụng API trong phạm vi toàn cục là đặc biệt quan trọng. Hãy xem phần Tối ưu hoá hoạt động nối mạng để biết các ví dụ.

Giảm số lượt khởi động nguội bằng cách đặt số lượng thực thể tối thiểu

Theo mặc định, Cloud Functions sẽ điều chỉnh quy mô số lượng phiên bản dựa trên số lượng yêu cầu đến. Bạn có thể thay đổi hành vi mặc định này bằng cách thiết lập số lượng phiên bản tối thiểu mà Cloud Functions phải duy trì trạng thái sẵn sàng để xử lý các yêu cầu. Việc đặt số lượng phiên bản tối thiểu sẽ giảm số lần khởi động nguội của ứng dụng. Bạn nên đặt số lượng phiên bản tối thiểu và hoàn tất quá trình khởi tạo tại thời điểm tải nếu ứng dụng của bạn nhạy cảm với độ trễ.

Hãy xem phần Kiểm soát hành vi thu phóng để biết thêm thông tin về các lựa chọn thời gian chạy này.

Lưu ý về quy trình khởi động nguội và khởi tạo

Quá trình khởi chạy toàn cầu diễn ra tại thời điểm tải. Nếu không có, yêu cầu đầu tiên sẽ cần hoàn tất quá trình khởi tạo và tải các mô-đun, do đó sẽ làm tăng độ trễ.

Tuy nhiên, quá trình khởi tạo trên toàn cầu cũng ảnh hưởng đến quá trình khởi động nguội. Để giảm thiểu tác động này, hãy chỉ khởi chạy những gì cần thiết cho yêu cầu đầu tiên, nhằm giữ độ trễ của yêu cầu đầu tiên ở mức thấp nhất có thể.

Điều này đặc biệt quan trọng nếu bạn đã định cấu hình số lượng phiên bản tối thiểu như mô tả ở trên cho một hàm nhạy cảm với độ trễ. Trong trường hợp đó, việc hoàn tất quá trình khởi tạo tại thời điểm tải và lưu vào bộ nhớ đệm dữ liệu hữu ích đảm bảo rằng yêu cầu đầu tiên không cần thực hiện việc này và được phân phát với độ trễ thấp.

Nếu bạn khởi tạo các biến trong phạm vi chung, tuỳ thuộc vào ngôn ngữ, thời gian khởi tạo dài có thể dẫn đến 2 hành vi: – đối với một số tổ hợp ngôn ngữ và thư viện không đồng bộ, khung hàm có thể chạy không đồng bộ và trả về ngay lập tức, khiến mã tiếp tục chạy ở chế độ nền, điều này có thể gây ra các vấn đề như không truy cập được vào CPU. Để tránh điều này, bạn nên chặn khi khởi tạo mô-đun như mô tả bên dưới. Điều này cũng đảm bảo rằng các yêu cầu sẽ không được phân phát cho đến khi quá trình khởi tạo hoàn tất. – mặt khác, nếu quá trình khởi tạo là đồng bộ, thì thời gian khởi tạo dài sẽ khiến quá trình khởi động nguội kéo dài hơn, điều này có thể gây ra vấn đề, đặc biệt là với các hàm có tính đồng thời thấp trong thời gian tải tăng đột biến.

Ví dụ về việc làm nóng trước một thư viện node.js không đồng bộ

Node.js với Firestore là một ví dụ về thư viện node.js không đồng bộ. Để tận dụng min_instances, đoạn mã sau đây sẽ hoàn tất quá trình tải và khởi tạo tại thời gian tải, chặn quá trình tải mô-đun.

TLA được sử dụng, tức là bạn phải dùng ES6, sử dụng tiện ích .mjs cho mã node.js hoặc thêm type: module vào tệp 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.
});

Ví dụ về hoạt động khởi tạo chung

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}.")
  

Hàm HTTP này sử dụng các biến chung được khởi chạy một cách trì hoãn. Hàm này nhận một đối tượng yêu cầu (flask.Request) và trả về văn bản phản hồi hoặc bất kỳ tập hợp giá trị nào có thể chuyển thành đối tượng Response bằng cách sử dụng make_response.

Điều này đặc biệt quan trọng nếu bạn xác định một số hàm trong một tệp và các hàm khác nhau sử dụng các biến khác nhau. Trừ phi bạn sử dụng tính năng khởi tạo trễ, nếu không, bạn có thể lãng phí tài nguyên cho các biến được khởi tạo nhưng không bao giờ được dùng.

Tài nguyên khác

Tìm hiểu thêm về cách tối ưu hoá hiệu suất trong video "Google Cloud Performance Atlas" Cloud Functions Thời gian khởi động nguội.