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 hàm bất biến
Hàm của bạn phải tạo ra cùng một kết quả ngay cả khi đượ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 bài viết thử lại các hàm dựa trên sự kiện.
Không bắt đầu hoạt động trong nền
Hoạt động trong nền là mọi hoạt động diễn ra sau khi hàm của bạn kết thúc.
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 kết thúc một cách suôn sẻ đều không thể truy cập vào CPU và sẽ không tiến hành được.
Ngoài ra, khi một lời gọi tiếp theo được thực thi trong cùng một môi trường, hoạt động trong nền sẽ tiếp tục, gây nhiễu đến lời gọi mới. Điều này có thể dẫn đến hành vi và lỗi không mong muốn, gây khó khăn cho việc 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 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 hoạt động được ghi lại sau dòng cho biết lệnh gọi đã kết thúc. Đôi khi, hoạt động trong nền có thể bị ẩn 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 xét mã của bạn để đảm bảo tất cả các thao tác không đồng bộ đều kết thúc trước khi bạn kết thúc 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 thụ 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. Việc không xoá rõ ràng các tệp này có thể dẫn đến lỗi hết bộ nhớ và quá trình 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 bảng điều khiển Cloud và chọn biểu đồ Mức sử dụng bộ nhớ.
Nếu bạn cần truy cập vào bộ nhớ dài hạn, hãy cân nhắc sử dụng Cloud Run các điểm gắn kết ổ đĩ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 xử lý theo giai đoạn. 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 trực tiếp luồng đầu ra 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 đưa 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 đưa phiên bản bạn muố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ì hệ thống sẽ tự động thêm phần phụ thuộc này trong quá trình xây 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 cục bộ
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 cục bộ.
Nhà phát triển Firebase có thể sử dụng Trình mô phỏng Cloud Functions của Firebase CLI.Tránh hết thời gian chờ triển khai trong quá trình khởi chạy
Nếu quá trình triển khai hàm không thành công do lỗi hết thời gian chờ, thì có thể là do mã phạm vi toàn cục của hàm mất quá nhiều thời gian để thực thi trong quá trình triển khai.
Firebase CLI có thời gian chờ mặc định để phát hiện 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 hàm (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:
(Nên dùng) sử dụng onInit() để trì hoãn quá trình khởi chạy
Sử dụng hook onInit() để tránh chạy mã khởi chạy trong quá trình triển khai. Mã bên trong hook 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 quá 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}")
(Thay thế) Tăng thời gian chờ phát hiện
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 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 gử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 đến máy chủ SMTP. Cách được đề xuất để gửi email là sử dụng dịch vụ của bên thứ ba 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 thực thể 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 mức độ đồng thời thấp
Vì quá trình khởi động nguội tốn kém, nên việc có thể sử dụng lại các thực thể đã bắt đầu gần đây trong thời gian tải tăng đột biến là một cách tối ưu hoá tuyệt vời để xử lý tải. Việc giới hạn mức độ đồng thời sẽ giới hạn cách tận dụng các thực thể hiện có, do đó, sẽ phát sinh nhiều quá trình khởi động nguội hơn.
Việc tăng mức độ đồng thời giúp trì hoãn nhiều yêu cầu trên mỗi thực thể, giúp dễ dàng xử lý các đợt tải tăng đột biến hơn.Sử dụng các phần phụ thuộc một cách khôn ngoan
Vì các hàm là hàm không trạng thái, nên môi trường thực thi thường được khởi chạy 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, bối cảnh toàn cục 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ễ lệnh 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 các phần phụ thuộc một cách chính xác và không tải các phần phụ thuộc mà hàm của bạn không sử dụng.
Sử dụng biến toàn cục để sử dụng lại các đối tượng trong các lệnh gọi trong tương lai
Không có gì đảm bảo rằng trạng thái của một hàm sẽ được giữ nguyên cho các lệnh gọi trong tương lai. Tuy nhiên, Cloud Functions thường tái sử dụng 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 toàn cục, thì giá trị của biến đó có thể được sử 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ừ nội dung hàm sang phạm vi toàn cục 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 nặng một lần cho mỗi thực thể hàm và chia sẻ đối tượng đó trên tất cả các lệnh gọi hàm đạt đến thực thể đã 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ể được chuyển thành đối tượng
Response bằng
make_response.
Bạn đặc biệt cần 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 máy khách API trong phạm vi toàn cục. Hãy xem bài viết Tối ưu hoá mạng để biết các ví dụ.
Giảm số lần 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 thực thể 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 đặt số lượng thực thể tối thiểu mà Cloud Functions phải luôn sẵn sàng để xử lý các yêu cầu. Việc đặt số lượng thực thể 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 thực thể tối thiểu và hoàn tất quá trình khởi chạy vào thời gian tải nếu ứng dụng của bạn nhạy cảm với độ trễ.
Hãy xem Kiểm soát hành vi điều chỉnh quy mô để biết thêm thông tin về các lựa chọn thời gian chạy này.Lưu ý về quá trình khởi động nguội và khởi chạy
Quá trình khởi chạy toàn cục diễn ra vào thời gian tải. Nếu không có quá trình này, yêu cầu đầu tiên sẽ cần hoàn tất quá trình khởi chạy và tải mô-đun, do đó, sẽ phát sinh độ trễ cao hơn.
Tuy nhiên, quá trình khởi chạy toàn cục cũng có tác độ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 để 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 thực thể 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 chạy vào thời gian tải và lưu vào bộ nhớ đệm dữ liệu hữu ích sẽ đả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 xử lý với độ trễ thấp.
Nếu bạn khởi chạy các biến trong phạm vi toàn cục, thì tuỳ thuộc vào ngôn ngữ, thời gian khởi chạy 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 trong nền, có thể gây ra các vấn đề như không thể truy cập vào CPU. Để tránh điều này, bạn nên chặn quá trình khởi chạy 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 không được xử lý cho đến khi quá trình khởi chạy hoàn tất. - mặt khác, nếu quá trình khởi chạy là đồng bộ, thì thời gian khởi chạy 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ó mức độ đồng thời thấp trong thời gian tải tăng đột biến.
Ví dụ về cách làm nóng trước 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, mã sau đây sẽ hoàn tất quá trình tải và khởi chạy vào thời gian tải, chặn quá trình tải mô-đun.
TLA được sử dụng, nghĩa 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ề quá trình khởi chạy toàn cục
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 toàn cục đượ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ể được chuyển thành đối tượng Response bằ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 quá trình khởi chạy trì hoãn, nếu không, bạn có thể lãng phí tài nguyên cho các biến được khởi chạy nhưng không bao giờ được sử 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.