The*_*mer 3 javascript node.js firebase google-cloud-functions google-cloud-run
我正在阅读此 Reddit 帖子,其中一位用户提到 540 秒是 Firebase 函数的限制,建议迁移到 Cloud Run。
\n\n\n正如其他人所说,540 秒是最大超时,如果您想增加它而不更改代码的其他内容,请考虑迁移到 Cloud Run。\xe2\x80\x8b- Reddit上的@samtstern
\n
查看Node.JS 快速入门文档\n以及 YouTube 和 Google 上的其他内容后,我没有找到解释如何将 Firebase Function 移至 Cloud Run 的良好指南。
\n我读到的内容没有解决的问题之一,例如:我用什么替换firebase-functions包来定义函数?ETC...
那么,如何将我的 Firebase Function 转移到 Cloud Run 才不会遇到 540 秒的最大超时限制?
\n\xe2\x80\x8bconst functions = require(\'firebase-functions\');\n\xe2\x80\x8bconst runtimeOpts = {timeoutSeconds: 540,memory: \'2GB\'}\n\xe2\x80\x8bexports.hourlyData = functions.runWith(runtimeOpts).pubsub.schedule(\'every 1 hours\')\nRun Code Online (Sandbox Code Playgroud)\n
sam*_*man 12
2022 年 8 月 3 日,Firebase 发布了 Cloud Functions(第二代),它们是在 Cloud Run 容器中执行的 Cloud Functions,使得此答案变得多余。可以在此处找到相关文档的链接。
\n以下答案仅供历史用途。它与 Cloud Functions(第二代)的实现细节并不完全一致。
\n前言:以下步骤已针对更广泛的受众进行了推广,而不仅仅是 OP 的问题(涵盖 HTTP 事件、计划和 Pub/Sub 功能),并且已根据问题中链接的文档进行了改编:在以下位置部署 Node.JS 映像云跑。
\n通常,超过 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:一般开发技巧文档。
\n与其他无服务器解决方案一样,您的客户端和服务器需要能够处理与不同实例的连接。您的代码不应使用本地状态(例如会话数据的本地存储)。有关更多信息,请参阅设置请求超时文档。
\n我将建议您参阅安装 Google Cloud SDK 文档来执行此步骤。
\n安装后,gcloud auth login使用目标 Firebase 项目所用的帐户进行调用并登录。
在 Firebase 控制台中打开您的项目设置,并记下您的项目 ID和默认 GCP 资源位置。
\nFirebase 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 Storage,请访问Firebase 控制台中的 Cloud Storage 设置并记下您的Bucket URI。从这个 URI 中,我们需要记下主机(忽略该gs://部分),该主机通常采用PROJECT_ID.appspot.com.
您可以复制以下表格以帮助跟踪:
\n| 项目编号: | PROJECT_ID |
| 数据库网址: | https://PROJECT_ID.firebaseio.com |
| 储物桶: | PROJECT_ID.appspot.com |
| 默认 GCP 资源位置: | |
| 选择的云运行区域: |
在您的 Firebase 项目目录或您选择的目录中,创建一个新cloudrun文件夹。
与 Firebase Cloud Functions(您可以在单个代码模块中定义多个函数)不同,每个 Cloud Run 映像都使用自己的代码模块。因此,每个 Cloud Run 映像都应存储在其自己的目录中。
\n当我们要定义一个名为 的 Cloud Run 实例时helloworld,我们将创建一个名为helloworldinside的目录cloudrun。
mkdir cloudrun\nmkdir cloudrun/helloworld\ncd cloudrun/helloworld\nRun Code Online (Sandbox Code Playgroud)\npackage.json为了正确部署 Cloud Run 映像,我们需要提供一个package.json用于在已部署的容器中安装依赖项的镜像。
该文件的格式package.json类似于:
{\n "name": "SERVICE_NAME",\n "description": "",\n "version": "1.0.0",\n "private": true,\n "main": "index.js",\n "scripts": {\n "start": "node index.js"\n "image": "gcloud builds submit --tag gcr.io/PROJECT_ID/SERVICE_NAME --project PROJECT_ID",\n "deploy:public": "gcloud run deploy SERVICE_NAME --image gcr.io/PROJECT_ID/SERVICE_NAME --allow-unauthenticated --region REGION_ID --project PROJECT_ID",\n "deploy:private": "gcloud run deploy SERVICE_NAME --image gcr.io/PROJECT_ID/SERVICE_NAME --no-allow-unauthenticated --region REGION_ID --project PROJECT_ID",\n "describe": "gcloud run services describe SERVICE_NAME --region REGION_ID --project PROJECT_ID --platform managed",\n "find": "gcloud run services describe SERVICE_NAME --region REGION_ID --project PROJECT_ID --platform managed --format=\'value(status.url)\'"\n },\n "engines": {\n "node": ">= 12.0.0"\n },\n "author": "You",\n "license": "Apache-2.0",\n "dependencies": {\n "express": "^4.17.1",\n "body-parser": "^1.19.0",\n /* ... */\n },\n "devDependencies": {\n /* ... */\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n在上面的文件中,SERVICE_NAME、REGION_ID和PROJECT_ID将根据情况与步骤 2 中的详细信息进行交换。我们还安装express和body-parser来处理传入的请求。
还有一些模块脚本可以帮助部署。
\n| 脚本\xc2\xa0名称 | 描述 |
|---|---|
image | 将映像提交到 Cloud Build 以添加到容器注册表以供其他命令使用。 |
deploy:public | 部署上述命令中的映像以供 Cloud Run 使用(同时允许任何请求者调用它)并返回其服务 URL(部分随机)。 |
deploy:private | 部署上述命令中的映像以供 Cloud Run 使用(同时要求调用它的请求者是授权用户/服务帐户)并返回其服务 URL(部分随机)。 |
describe | 获取已部署的 Cloud Run 的统计信息和配置。 |
find | 仅从响应中提取服务 URLnpm run describe |
注意:这里的“授权用户”是指与该项目关联的 Google 帐户,而不是普通的 Firebase 用户。要允许 Firebase 用户调用您的 Cloud Run,您必须使用deploy:publicCloud Run 代码进行部署并处理令牌验证,从而适当地拒绝请求。
作为填写此文件的示例,您将得到以下内容:
\n{\n "name": "helloworld",\n "description": "Simple hello world sample in Node with Firebase",\n "version": "1.0.0",\n "private": true,\n "main": "index.js",\n "scripts": {\n "start": "node index.js"\n "image": "gcloud builds submit --tag gcr.io/com-example-cloudrun/helloworld --project com-example-cloudrun",\n "deploy:public": "gcloud run deploy helloworld --image gcr.io/com-example-cloudrun/helloworld --allow-unauthenticated --region us-central1 --project com-example-cloudrun",\n "deploy:public": "gcloud run deploy helloworld --image gcr.io/com-example-cloudrun/helloworld --no-allow-unauthenticated --region us-central1 --project com-example-cloudrun",\n "describe": "gcloud run services describe helloworld --region us-central1 --project com-example-cloudrun --platform managed",\n "find": "gcloud run services describe helloworld --region us-central1 --project com-example-cloudrun --platform managed --format=\'value(status.url)\'"\n },\n "engines": {\n "node": ">= 12.0.0"\n },\n "author": "You",\n "license": "Apache-2.0",\n "dependencies": {\n /* ... */\n },\n "devDependencies": {\n /* ... */\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n要告诉 Cloud Build 将哪个容器用于您的 Cloud Run 映像,您必须Dockerfile为您的映像创建一个容器。为了防止将错误的文件发送到服务器,您还应该指定一个.dockerignore文件。
在此文件中,我们使用步骤 2 中的 Firebase 项目设置来重新创建process.env.FIREBASE_CONFIG环境变量。该变量由 Firebase Admin SDK 使用,并包含以下 JSON 字符串信息:
{\n databaseURL: "https://PROJECT_ID.firebaseio.com",\n storageBucket: "PROJECT_ID.appspot.com",\n projectId: "PROJECT_ID"\n}\nRun Code Online (Sandbox Code Playgroud)\n这是cloudrun/helloworld/Dockerfile:
# Use the official lightweight Node.js 14 image.\n# https://hub.docker.com/_/node\nFROM node:14-slim\n\n# Create and change to the app directory.\nWORKDIR /usr/src/app\n\n# Copy application dependency manifests to the container image.\n# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).\n# Copying this first prevents re-running npm install on every code change.\nCOPY package*.json ./\n\n# Install production dependencies.\n# If you add a package-lock.json, speed your build by switching to \'npm ci\'.\n# RUN npm ci --only=production\nRUN npm install --only=production\n\n# Copy local code to the container image.\nCOPY . ./\n\n# Define default configuration for Admin SDK\n# databaseURL is usually "https://PROJECT_ID.firebaseio.com", but may be different.\n# TODO: Update me\nENV FIREBASE_CONFIG={"databaseURL":"https://PROJECT_ID.firebaseio.com","storageBucket":"PROJECT_ID.appspot.com","projectId":"PROJECT_ID"}\n\n# Run the web service on container startup.\nCMD [ "node", "index.js" ]\nRun Code Online (Sandbox Code Playgroud)\n这是cloudrun/helloworld/.dockerignore:
Dockerfile\n.dockerignore\nnode_modules\nnpm-debug.log\nRun Code Online (Sandbox Code Playgroud)\nPORT当启动新的 Cloud Run 实例时,它通常会使用环境变量指定希望您的代码侦听的端口。
当您使用包中的HTTP 事件函数firebase-functions时,它会在内部代表您处理正文解析。函数框架为此使用该body-parser包并在此处定义解析器。
要处理用户授权,您可以使用此validateFirebaseIdToken()中间件来检查请求中提供的 ID 令牌。
对于基于 HTTP 的 Cloud Run,需要配置 CORS 才能从浏览器调用它。cors这可以通过安装包并适当配置它来完成。在下面的示例中,cors将反映发送给它的来源。
Dockerfile\n.dockerignore\nnode_modules\nnpm-debug.log\nRun Code Online (Sandbox Code Playgroud)\n在该$FIREBASE_PROJECT_DIR/cloudrun/helloworld目录中,执行以下命令来部署您的映像:
const express = require(\'express\');\nconst cors = require(\'cors\')({origin: true});\nconst app = express();\n\napp.use(cors);\n\n// To replicate a Cloud Function\'s body parsing, refer to\n// https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/d894b490dda7c5fd4690cac884fd9e41a08b6668/src/server.ts#L47-L95\n// app.use(/* body parsers */);\n\napp.enable(\'trust proxy\'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)\napp.disable(\'x-powered-by\'); // Disables the \'x-powered-by\' header added by express (best practice)\n\n// Start of your handlers\n\napp.get(\'/\', (req, res) => {\n const name = process.env.NAME || \'World\';\n res.send(`Hello ${name}!`);\n});\n\n// End of your handlers\n\nconst port = process.env.PORT || 8080;\napp.listen(port, () => {\n console.log(`helloworld: listening on port ${port}`);\n});\nRun Code Online (Sandbox Code Playgroud)\n使用 Cloud Scheduler 调用 Cloud Run 时,您可以选择使用哪种方法来调用它(GET、POST(默认)、PUT、HEAD、DELETE)。要复制云函数data和context参数,最好使用它们,POST因为这些将在请求正文中传递。与 Firebase Functions 一样,来自 Cloud Scheduler 的这些请求可能会重试,因此请确保正确处理幂等性。
注意:尽管 Cloud Scheduler 调用请求的正文是 JSON 格式,但该请求仍由Content-Type: text/plain我们需要处理的 提供。
此代码改编自Functions Framework 源代码(Google LLC、Apache 2.0)
\nnpm run image // builds container & stores to container repository\nnpm run deploy:public // deploys container image to Cloud Run\nRun Code Online (Sandbox Code Playgroud)\n注意:函数框架通过发回HTTP 200 OK带有X-Google-Status: error标头的响应来处理错误。这实际上意味着“失败成功”。作为一个局外人,我不确定为什么这样做,但我可以假设它是为了让调用者知道不必费心重试该函数 - 它只会得到相同的结果。
在该$FIREBASE_PROJECT_DIR/cloudrun/helloworld目录中,执行以下命令来部署您的映像:
const express = require(\'express\');\nconst { json } = require(\'body-parser\');\n\nasync function handler(data, context) {\n /* your logic here */\n const name = process.env.NAME || \'World\';\n console.log(`Hello ${name}!`);\n}\n\nconst app = express();\n\n// Cloud Scheduler requests contain JSON using \n"Content-Type: text/plain"\napp.use(json({ type: \'*/*\' }));\n\napp.enable(\'trust proxy\'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)\napp.disable(\'x-powered-by\'); // Disables the \'x-powered-by\' header added by express (best practice)\n\napp.post(\'/*\', (req, res) => {\n const event = req.body;\n let data = event.data;\n let context = event.context;\n\n if (context === undefined) {\n // Support legacy events and CloudEvents in structured content mode, with\n // context properties represented as event top-level properties.\n // Context is everything but data.\n context = event;\n // Clear the property before removing field so the data object\n // is not deleted.\n context.data = undefined;\n delete context.data;\n }\n\n Promise.resolve()\n .then(() => handler(data, context))\n .then(\n () => {\n // finished without error\n // the return value of `handler` is ignored because\n // this isn\'t a callable function\n res.sendStatus(204); // No content\n },\n (err) => {\n // handler threw error\n console.error(err.stack);\n res.set(\'X-Google-Status\', \'error\');\n // Send back the error\'s message (as calls to this endpoint\n // are authenticated project users/service accounts)\n res.send(err.message);\n }\n )\n});\n\nconst port = process.env.PORT || 8080;\napp.listen(port, () => {\n console.log(`helloworld: listening on port ${port}`);\n});\nRun Code Online (Sandbox Code Playgroud)\n注意:在以下设置命令中(只需运行一次),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 作业,或使用以下命令:
npm run image // builds container & stores to container repository\nnpm run deploy:private // deploys container image to Cloud Run\nRun Code Online (Sandbox Code Playgroud)\n您的 Cloud Run 现在应该已经安排好了。
\n据我了解,部署过程与计划运行 ( ) 相同deploy:private,但我不确定具体细节。不过,以下是 Pub/Sub 解析器的 Cloud Run 源:
此代码改编自Functions Framework 源代码(Google LLC、Apache 2.0)
\nconst express = require(\'express\');\nconst { json } = require(\'body-parser\');\n\nconst PUBSUB_EVENT_TYPE = \'google.pubsub.topic.publish\';\nconst PUBSUB_MESSAGE_TYPE =\n \'type.googleapis.com/google.pubsub.v1.PubsubMessage\';\nconst PUBSUB_SERVICE = \'pubsub.googleapis.com\';\n\n/**\n * Extract the Pub/Sub topic name from the HTTP request path.\n * @param path the URL path of the http request\n * @returns the Pub/Sub topic name if the path matches the expected format,\n * null otherwise\n */\nconst extractPubSubTopic = (path: string): string | null => {\n const parsedTopic = path.match(/projects\\/[^/?]+\\/topics\\/[^/?]+/);\n if (parsedTopic) {\n return parsedTopic[0];\n }\n console.warn(\'Failed to extract the topic name from the URL path.\');\n console.warn(\n "Configure your subscription\'s push endpoint to use the following path: ",\n \'projects/PROJECT_NAME/topics/TOPIC_NAME\'\n );\n return null;\n};\n\nasync function handler(message, context) {\n /* your logic here */\n const name = message.json.name || message.json || \'World\';\n console.log(`Hello ${name}!`);\n}\n\nconst app = express();\n\n// Cloud Scheduler requests contain JSON using \n"Content-Type: text/plain"\napp.use(json({ type: \'*/*\' }));\n\napp.enable(\'trust proxy\'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)\napp.disable(\'x-powered-by\'); // Disables the \'x-powered-by\' header added by express (best practice)\n\napp.post(\'/*\', (req, res) => {\n const body = req.body;\n \n if (!body) {\n res.status(400).send(\'no Pub/Sub message received\');\n return;\n }\n \n if (typeof body !== "object" || body.message === undefined) {\n res.status(400).send(\'invalid Pub/Sub message format\');\n return;\n }\n\n const context = {\n eventId: body.message.messageId,\n timestamp: body.message.publishTime || new Date().toISOString(),\n eventType: PUBSUB_EVENT_TYPE,\n resource: {\n service: PUBSUB_SERVICE,\n type: PUBSUB_MESSAGE_TYPE,\n name: extractPubSubTopic(req.path),\n },\n };\n\n // for storing parsed form of body.message.data\n let _jsonData = undefined;\n\n const data = {\n \'@type\': PUBSUB_MESSAGE_TYPE,\n data: body.message.data,\n attributes: body.message.attributes || {},\n get json() {\n if (_jsonData === undefined) {\n const decodedString = Buffer.from(base64encoded, \'base64\')\n .toString(\'utf8\');\n try {\n _jsonData = JSON.parse(decodedString);\n } catch (parseError) {\n // fallback to raw string\n _jsonData = decodedString;\n }\n }\n return _jsonData;\n }\n };\n\n Promise.resolve()\n .then(() => handler(data, context))\n .then(\n () => {\n // finished without error\n // the return value of `handler` is ignored because\n // this isn\'t a callable function\n res.sendStatus(204); // No content\n },\n (err) => {\n // handler threw error\n console.error(err.stack);\n res.set(\'X-Google-Status\', \'error\');\n // Send back the error\'s message (as calls to this endpoint\n // are authenticated project users/service accounts)\n res.send(err.message);\n }\n )\n});\n\nconst port = process
| 归档时间: |
|
| 查看次数: |
2013 次 |
| 最近记录: |