javascript - 遇到 540 秒超时限制后如何从 Firebase Functions 迁移到 Cloud Run?
问题描述
我正在阅读这个 Reddit 线程,其中一位用户提到 540 秒是 Firebase 函数的限制,建议迁移到 Cloud Run。
正如其他人所说,540 秒是最大超时时间,如果您想在不更改代码的其他内容的情况下增加它,请考虑迁移到 Cloud Run。- Reddit上的@samtstern
在查看了 YouTube 和 Google 上的Node.JS 快速入门文档 和其他内容后,我没有找到一个很好的指南来解释如何将您的 Firebase 函数移动到 Cloud Run。
我阅读的内容未解决的问题之一,例如:我应该用什么替换firebase-functions
包来定义函数?ETC...
那么,我怎样才能将我的 Firebase 功能转移到 Cloud Run 以不遇到 540 秒最大超时限制?
const functions = require('firebase-functions');
const runtimeOpts = {timeoutSeconds: 540,memory: '2GB'}
exports.hourlyData = functions.runWith(runtimeOpts).pubsub.schedule('every 1 hours')
解决方案
前言:以下步骤已针对更广泛的受众进行了推广,而不仅仅是 OP 的问题(涵盖 HTTP 事件、计划和 Pub/Sub 函数),并改编自问题中链接的文档:在 Cloud Run 上部署 Node.JS 图像。
第 0 步:代码/架构审查
通常情况下,超过 Cloud Function 的 9 分钟超时是由于代码中的错误导致的 - 请务必在切换到 Cloud Run 之前对此进行评估,因为这只会使问题变得更糟。其中最常见的是顺序异步处理而不是并行异步处理(通常由await
在for
/while
循环中使用引起)。
如果您的代码正在执行需要很长时间的有意义的工作,请考虑将其分片到可以并行处理输入数据的“子函数”。您可以使用单个函数来触发一个函数的多个实例,每个实例负责处理不同的用户 ID 范围,例如a-l\uf8ff
、m-z\uf8ff
、和A-L\uf8ff
,而不是为数据库中的每个用户处理数据。M-Z\uf8ff
0-9\uf8ff
最后,Cloud Run 和 Cloud Functions 非常相似,它们旨在接受请求,处理它,然后返回响应。Cloud Functions 的限制时间最长为 9 分钟,Cloud Runs 的限制时间最长为 60 分钟。一旦该响应完成(因为服务器结束响应、客户端丢失连接或客户端中止请求),实例将被严重限制或终止。虽然您可以在使用 Cloud Run 时使用 WebSockets 和 gRPC 在服务器和客户端之间进行持久通信,但它们仍然受到此限制。有关更多信息,请参阅Cloud Run:一般开发技巧文档。
与其他无服务器解决方案一样,您的客户端和服务器需要能够处理与不同实例的连接。您的代码不应使用本地状态(如会话数据的本地存储)。有关详细信息,请参阅设置请求超时文档。
第 1 步:安装 Google Cloud SDK
我将向您介绍此步骤的安装 Google Cloud SDK 文档。
安装后,调用gcloud auth login
并使用用于目标 Firebase 项目的帐户登录。
第 2 步:获取您的 Firebase 项目设置
在 Firebase 控制台中打开您的项目设置,并记下您的项目 ID和默认 GCP 资源位置。
Firebase Functions 和 Cloud Run 实例应尽可能与您的 GCP 资源位于同一位置。在 Firebase Functions 中,这是通过更改代码中的区域并使用 CLI 进行部署来实现的。对于 Cloud Run,您可以在命令行上将这些参数指定为标志(或使用 Google Cloud Console)。对于以下说明和简单起见,我将使用us-central1
我的默认 GCP 资源位置为nam5 (us-central)
.
如果在您的项目中使用 Firebase 实时数据库,请访问Firebase 控制台中的 RTDB 设置并记下您的数据库 URL。这通常是形式https://PROJECT_ID.firebaseio.com/
。
如果在您的项目中使用 Firebase 存储,请访问Firebase 控制台中的 Cloud Storage 设置并记下您的Bucket URI。从这个 URI 中,我们需要注意主机(忽略gs://
部分),它通常是PROJECT_ID.appspot.com
.
您可以复制以下表格以帮助跟踪:
项目编号: | PROJECT_ID |
数据库网址: | https://PROJECT_ID.firebaseio.com |
存储桶: | PROJECT_ID.appspot.com |
默认 GCP 资源位置: | |
选择的云运行区域: |
第 3 步:创建目录
在您的 Firebase 项目目录或您选择的目录中,创建一个新cloudrun
文件夹。
与可以在单个代码模块中定义多个函数的 Firebase Cloud Functions 不同,每个 Cloud Run 映像都使用自己的代码模块。因此,每个 Cloud Run 映像都应存储在自己的目录中。
由于我们要定义一个名为 的 Cloud Run 实例helloworld
,我们将创建一个名为helloworld
inside的目录cloudrun
。
mkdir cloudrun
mkdir cloudrun/helloworld
cd cloudrun/helloworld
第 4 步:创建package.json
为了正确部署 Cloud Run 映像,我们需要提供一个package.json
用于在已部署容器中安装依赖项的组件。
该package.json
文件的格式类似于:
{
"name": "SERVICE_NAME",
"description": "",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "node index.js"
"image": "gcloud builds submit --tag gcr.io/PROJECT_ID/SERVICE_NAME --project PROJECT_ID",
"deploy:public": "gcloud run deploy SERVICE_NAME --image gcr.io/PROJECT_ID/SERVICE_NAME --allow-unauthenticated --region REGION_ID --project PROJECT_ID",
"deploy:private": "gcloud run deploy SERVICE_NAME --image gcr.io/PROJECT_ID/SERVICE_NAME --no-allow-unauthenticated --region REGION_ID --project PROJECT_ID",
"describe": "gcloud run services describe SERVICE_NAME --region REGION_ID --project PROJECT_ID --platform managed",
"find": "gcloud run services describe SERVICE_NAME --region REGION_ID --project PROJECT_ID --platform managed --format='value(status.url)'"
},
"engines": {
"node": ">= 12.0.0"
},
"author": "You",
"license": "Apache-2.0",
"dependencies": {
"express": "^4.17.1",
"body-parser": "^1.19.0",
/* ... */
},
"devDependencies": {
/* ... */
}
}
在上述文件中SERVICE_NAME
,REGION_ID
和PROJECT_ID
将根据步骤 2 中的详细信息进行适当替换。我们还安装express
并body-parser
处理传入的请求。
还有一些模块脚本可以帮助部署。
脚本名称 | 描述 |
---|---|
image |
将映像提交到 Cloud Build 以添加到 Container Registry 以执行其他命令。 |
deploy:public |
部署来自上述命令的映像以供 Cloud Run 使用(同时允许任何请求者调用它)并返回其服务 URL(部分随机化)。 |
deploy:private |
部署上述命令中的映像以供 Cloud Run 使用(同时要求调用它的请求者是授权用户/服务帐户)并返回其服务 URL(部分随机)。 |
describe |
获取已部署 Cloud Run 的统计信息和配置。 |
find |
从响应中仅提取服务 URLnpm run describe |
注意:这里的“授权用户”是指与项目关联的 Google 帐户,而不是普通的 Firebase 用户。要允许 Firebase 用户调用您的 Cloud Run,您必须使用deploy:public
Cloud Run 代码中的令牌验证和处理令牌验证来部署它,从而适当地拒绝请求。
作为填写此文件的示例,您将获得以下信息:
{
"name": "helloworld",
"description": "Simple hello world sample in Node with Firebase",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "node index.js"
"image": "gcloud builds submit --tag gcr.io/com-example-cloudrun/helloworld --project com-example-cloudrun",
"deploy:public": "gcloud run deploy helloworld --image gcr.io/com-example-cloudrun/helloworld --allow-unauthenticated --region us-central1 --project com-example-cloudrun",
"deploy:public": "gcloud run deploy helloworld --image gcr.io/com-example-cloudrun/helloworld --no-allow-unauthenticated --region us-central1 --project com-example-cloudrun",
"describe": "gcloud run services describe helloworld --region us-central1 --project com-example-cloudrun --platform managed",
"find": "gcloud run services describe helloworld --region us-central1 --project com-example-cloudrun --platform managed --format='value(status.url)'"
},
"engines": {
"node": ">= 12.0.0"
},
"author": "You",
"license": "Apache-2.0",
"dependencies": {
/* ... */
},
"devDependencies": {
/* ... */
}
}
第 5 步:创建容器文件
要告诉 Cloud Build 为您的 Cloud Run 映像使用哪个容器,您必须Dockerfile
为您的映像创建一个容器。为防止将错误的文件发送到服务器,您还应该指定一个.dockerignore
文件。
在此文件中,我们使用第 2 步中的 Firebase 项目设置来重新创建process.env.FIREBASE_CONFIG
环境变量。此变量由 Firebase Admin SDK 使用,并包含以下 JSON 字符串形式的信息:
{
databaseURL: "https://PROJECT_ID.firebaseio.com",
storageBucket: "PROJECT_ID.appspot.com",
projectId: "PROJECT_ID"
}
这里是cloudrun/helloworld/Dockerfile
:
# Use the official lightweight Node.js 14 image.
# https://hub.docker.com/_/node
FROM node:14-slim
# Create and change to the app directory.
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./
# Install production dependencies.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN npm install --only=production
# Copy local code to the container image.
COPY . ./
# Define default configuration for Admin SDK
# databaseURL is usually "https://PROJECT_ID.firebaseio.com", but may be different.
# TODO: Update me
ENV FIREBASE_CONFIG={"databaseURL":"https://PROJECT_ID.firebaseio.com","storageBucket":"PROJECT_ID.appspot.com","projectId":"PROJECT_ID"}
# Run the web service on container startup.
CMD [ "node", "index.js" ]
这里是cloudrun/helloworld/.dockerignore
:
Dockerfile
.dockerignore
node_modules
npm-debug.log
第 6 步:创建和部署您的入口点
启动新的 Cloud Run 实例时,它通常会使用PORT
环境变量指定它希望您的代码监听的端口。
变体:迁移 HTTP 事件函数
当您使用包中的HTTP 事件函数时,它会在内部代表您firebase-functions
处理正文解析。Functions Framework为此使用包并在此处定义解析器。body-parser
要处理用户授权,您可以使用此validateFirebaseIdToken()
中间件来检查随请求提供的 ID 令牌。
对于基于 HTTP 的 Cloud Run,需要配置 CORS 才能从浏览器调用它。这可以通过安装cors
软件包并适当配置来完成。在下面的示例中,cors
将反映发送给它的来源。
const express = require('express');
const cors = require('cors')({origin: true});
const app = express();
app.use(cors);
// To replicate a Cloud Function's body parsing, refer to
// https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/d894b490dda7c5fd4690cac884fd9e41a08b6668/src/server.ts#L47-L95
app.use(/* body parsers */);
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
// Start of your handlers
app.get('/', (req, res) => {
const name = process.env.NAME || 'World';
res.send(`Hello ${name}!`);
});
// End of your handlers
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});
在该$FIREBASE_PROJECT_DIR/cloudrun/helloworld
目录中,执行以下命令来部署您的映像:
npm run image // builds container & stores to container repository
npm run deploy:public // deploys container image to Cloud Run
变体:使用 Cloud Scheduler 调用
使用 Cloud Scheduler 调用 Cloud Run 时,您可以选择使用哪种方法来调用它(GET
, POST
(默认值), PUT
, HEAD
, DELETE
)。要复制云函数data
和context
参数,最好使用POST
它们,因为这些将在请求正文中传递。与 Firebase 函数一样,来自 Cloud Scheduler 的这些请求可能会被重试,因此请确保正确处理幂等性。
注意:即使 Cloud Scheduler 调用请求的主体是 JSON 格式的,该请求也会使用Content-Type: text/plain
,我们需要处理。
此代码改编自Functions Framework 源代码(Google LLC、Apache 2.0)
const express = require('express');
const { json } = require('body-parser');
async function handler(data, context) {
/* your logic here */
const name = process.env.NAME || 'World';
console.log(`Hello ${name}!`);
}
const app = express();
// Cloud Scheduler requests contain JSON using
"Content-Type: text/plain"
app.use(json({ type: '*/*' }));
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
app.post('/*', (req, res) => {
const event = req.body;
let data = event.data;
let context = event.context;
if (context === undefined) {
// Support legacy events and CloudEvents in structured content mode, with
// context properties represented as event top-level properties.
// Context is everything but data.
context = event;
// Clear the property before removing field so the data object
// is not deleted.
context.data = undefined;
delete context.data;
}
Promise.resolve()
.then(() => handler(data, context))
.then(
() => {
// finished without error
// the return value of `handler` is ignored because
// this isn't a callable function
res.sendStatus(204); // No content
},
(err) => {
// handler threw error
console.error(err.stack);
res.set('X-Google-Status', 'error');
// Send back the error's message (as calls to this endpoint
// are authenticated project users/service accounts)
res.send(err.message);
}
)
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});
注意: Functions Framework 通过发回HTTP 200 OK
带有标头的响应来处理错误X-Google-Status: error
。这实际上意味着“成功失败”。作为一个局外人,我不确定为什么要这样做,但我可以假设它是为了让调用者知道不必费心重试该函数——它只会得到相同的结果。
在该$FIREBASE_PROJECT_DIR/cloudrun/helloworld
目录中,执行以下命令来部署您的映像:
npm run image // builds container & stores to container repository
npm run deploy:private // deploys container image to Cloud Run
注意:在以下设置命令(只需运行一次)中,PROJECT_ID
需要根据需要替换SERVICE_NAME
、SERVICE_URL
和。IAM_ACCOUNT
接下来,我们需要创建一个Cloud Scheduler 可以用来调用 Cloud Run 的服务帐户。您可以随意调用它,例如scheduled-run-invoker
. 此服务帐户的电子邮件将IAM_ACCOUNT
在下一步中引用。这段Google Cloud Tech YouTube 视频(从正确的位置开始,大约 15 秒)将快速显示您需要做什么。创建帐户后,您可以在接下来的 30 秒左右的视频后创建 Cloud Scheduler 作业,或使用以下命令:
gcloud scheduler jobs create http scheduled-run-SERVICE_NAME /
--schedule="every 1 hours" /
--uri SERVICE_URL /
--attempt-deadline 60m /
--http-method post /
--message-body='{"optional-custom-data":"here","if-you":"want"}' /
--oidc-service-account-email IAM_ACCOUNT
--project PROJECT_ID
您的 Cloud Run 现在应该已安排好。
变体:使用 Pub/Sub 调用
据我了解,部署过程与计划运行 ( deploy:private
) 相同,但我不确定具体细节。不过,这里是 Pub/Sub 解析器的 Cloud Run 源代码:
此代码改编自Functions Framework 源代码(Google LLC、Apache 2.0)
const express = require('express');
const { json } = require('body-parser');
const PUBSUB_EVENT_TYPE = 'google.pubsub.topic.publish';
const PUBSUB_MESSAGE_TYPE =
'type.googleapis.com/google.pubsub.v1.PubsubMessage';
const PUBSUB_SERVICE = 'pubsub.googleapis.com';
/**
* Extract the Pub/Sub topic name from the HTTP request path.
* @param path the URL path of the http request
* @returns the Pub/Sub topic name if the path matches the expected format,
* null otherwise
*/
const extractPubSubTopic = (path: string): string | null => {
const parsedTopic = path.match(/projects\/[^/?]+\/topics\/[^/?]+/);
if (parsedTopic) {
return parsedTopic[0];
}
console.warn('Failed to extract the topic name from the URL path.');
console.warn(
"Configure your subscription's push endpoint to use the following path: ",
'projects/PROJECT_NAME/topics/TOPIC_NAME'
);
return null;
};
async function handler(message, context) {
/* your logic here */
const name = message.json.name || message.json || 'World';
console.log(`Hello ${name}!`);
}
const app = express();
// Cloud Scheduler requests contain JSON using
"Content-Type: text/plain"
app.use(json({ type: '*/*' }));
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
app.post('/*', (req, res) => {
const body = req.body;
if (!body) {
res.status(400).send('no Pub/Sub message received');
return;
}
if (typeof body !== "object" || body.message === undefined) {
res.status(400).send('invalid Pub/Sub message format');
return;
}
const context = {
eventId: body.message.messageId,
timestamp: body.message.publishTime || new Date().toISOString(),
eventType: PUBSUB_EVENT_TYPE,
resource: {
service: PUBSUB_SERVICE,
type: PUBSUB_MESSAGE_TYPE,
name: extractPubSubTopic(req.path),
},
};
// for storing parsed form of body.message.data
let _jsonData = undefined;
const data = {
'@type': PUBSUB_MESSAGE_TYPE,
data: body.message.data,
attributes: body.message.attributes || {},
get json() {
if (_jsonData === undefined) {
const decodedString = Buffer.from(base64encoded, 'base64')
.toString('utf8');
try {
_jsonData = JSON.parse(decodedString);
} catch (parseError) {
// fallback to raw string
_jsonData = decodedString;
}
}
return _jsonData;
}
};
Promise.resolve()
.then(() => handler(data, context))
.then(
() => {
// finished without error
// the return value of `handler` is ignored because
// this isn't a callable function
res.sendStatus(204); // No content
},
(err) => {
// handler threw error
console.error(err.stack);
res.set('X-Google-Status', 'error');
// Send back the error's message (as calls to this endpoint
// are authenticated project users/service accounts)
res.send(err.message);
}
)
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});
推荐阅读
- matlab - matlab多维图例可能吗?
- ebay-api - eBay 库存 API:获取 InventoryItems 始终返回 0 结果
- hybris - Count(*) 不显示空值
- javascript - 如果点数组相距很远,则将它们分成多个点数组
- javascript - 覆盖所有 target="_blank" 以在同一页面上打开链接的最佳方法
- javascript - 将数据从 mongodb 添加到 google GeoChart API
- java - FileUpload 问题:处理多部分/表单数据请求失败。在套接字上读取意外的 EOF
- python - Numpy将二维数组附加在一起
- python - 定义实例属性以存储两个“常量”类属性之一
- android - Expo PDF - 无法查看 7 页 PDF 的第一页和第三页