【问题标题】:How to move from Firebase Functions to Cloud Run after encountering 540s timeout limit?遇到 540 秒超时限制后如何从 Firebase Functions 迁移到 Cloud Run?
【发布时间】:2021-09-17 06:42:31
【问题描述】:

我在阅读this Reddit thread,其中一位用户提到 540 秒是 Firebase Functions 的限制,建议迁移到 Cloud Run。

正如其他人所说,540 秒是最大超时时间,如果您想在不更改代码的其他内容的情况下增加它,请考虑迁移到 Cloud Run。 ​- @samtstern on Reddit

看了Node.JS QuickStart documentation之后 以及 YouTube 和 Google 上的其他内容,我没有找到一个很好的指南来解释如何将您的 Firebase 函数移动到 Cloud Run。

我阅读的内容未解决的问题之一,例如:我应该用什么替换 firebase-functions 包来定义函数?等等……

那么,我怎样才能将我的 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')

【问题讨论】:

  • 如果有的话,当他们读到“我没有找到一个好的指南解释”时,他们可能会投反对票。指南有点超出 StackOverflow 的核心范围,而是专注于快速和快速的问答风格。我的大多数指南式答案都有否定的代表,没有任何解释。
  • @samthecodingman 可能是这样

标签: javascript node.js firebase google-cloud-functions google-cloud-run


【解决方案1】:

前言: 以下步骤已针对更广泛的受众进行了推广,而不仅仅是 OP 的问题(涵盖 HTTP 事件、计划和 Pub/Sub 函数),并改编自问题中链接的文档:Deploying Node.JS Images on Cloud Run.

第 0 步:代码/架构审查

通常情况下,超过 9 分钟的 Cloud Function 超时是由于代码中的错误导致的 - 请务必在切换到 Cloud Run 之前对此进行评估,因为这只会使问题变得更糟。其中最常见的是顺序异步处理而不是并行异步处理(通常由在for/while 循环中使用await 引起)。

如果您的代码正在执行需要很长时间的有意义的工作,请考虑将其分片到可以全部并行处理输入数据的“子函数”。您可以使用单个函数来触发一个函数的多个实例,而不是为数据库中的每个用户处理数据,每个实例处理不同的用户 ID 范围,例如 a-l\uf8ffm-z\uf8ffA-L\uf8ff、@987654353 @和0-9\uf8ff

最后,Cloud Run 和 Cloud Functions 非常相似,它们旨在接受请求,处理它,然后返回响应。 Cloud Functions 的限制时间最长为 9 分钟,Cloud Runs 的限制时间最长为 60 分钟。一旦该响应完成(因为服务器结束响应、客户端丢失连接或客户端中止请求),实例被严重限制或终止。虽然您可以在使用 Cloud Run 时使用 WebSockets 和 gRPC 在服务器和客户端之间进行持久通信,但它们仍然受到此限制。有关详细信息,请参阅 Cloud Run: General development tips 文档。

与其他无服务器解决方案一样,您的客户端和服务器需要能够处理与不同实例的连接。您的代码不应使用本地状态(如会话数据的本地存储)。有关详细信息,请参阅 Setting request timeout 文档。

第 1 步:安装 Google Cloud SDK

这一步我会推荐你​​到Installing Google Cloud SDK documentation

安装后,致电gcloud auth login 并使用用于目标 Firebase 项目的帐户登录。

第 2 步:获取您的 Firebase 项目设置

打开您的project settings in the Firebase Console 并记下您的项目 ID 和您的默认 GCP 资源位置

Firebase Functions 和 Cloud Run 实例应尽可能与您的 GCP 资源位于同一位置。在 Firebase Functions 中,这是通过 changing the region in code 实现的,并使用 CLI 进行部署。对于 Cloud Run,您可以在命令行上将这些参数指定为标志(或使用 Google Cloud Console)。对于以下说明并为简单起见,我将使用us-central1 作为我的默认 GCP 资源位置nam5 (us-central)

如果在您的项目中使用 Firebase 实时数据库,请访问您的 RTDB settings in the Firebase Console 并记下您的数据库 URL。这通常是https://PROJECT_ID.firebaseio.com/ 的形式。

如果在您的项目中使用 Firebase 存储,请访问您的 Cloud Storage settings in the Firebase Console 并记下您的 Bucket URI。从这个 URI 中,我们需要注意主机(忽略 gs:// 部分),它通常是 PROJECT_ID.appspot.com 的形式。

您可以复制以下表格以帮助跟踪:

Project ID: PROJECT_ID
Database URL: https://PROJECT_ID.firebaseio.com
Storage Bucket: PROJECT_ID.appspot.com
Default GCP Resource Location:
Chosen Cloud Run Region:

第 3 步:创建目录

在您的 Firebase 项目目录或您选择的目录中,创建一个新的 cloudrun 文件夹。

与 Firebase Cloud Functions(您可以在单个代码模块中定义多个函数)不同,每个 Cloud Run 映像都使用自己的代码模块。因此,每个 Cloud Run 映像都应存储在自己的目录中。

由于我们要定义一个名为 helloworld 的 Cloud Run 实例,我们将在 cloudrun 内创建一个名为 helloworld 的目录。

mkdir cloudrun
mkdir cloudrun/helloworld
cd cloudrun/helloworld

第四步:创建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_NAMEREGION_IDPROJECT_ID 将根据步骤 2 中的详细信息进行适当替换。我们还安装了 expressbody-parser 来处理传入的请求。

还有一些模块脚本可以帮助部署。

Script Name Description
image Submits the image to Cloud Build to be added to the Container Registry for other commands.
deploy:public Deploys the image from the above command to be used by Cloud Run (while allowing any requester to invoke it) and returns its service URL (which is partly randomized).
deploy:private Deploys the image from the above command to be used by Cloud Run (while requiring that the requester that invokes it is an authorized user/service account) and returns its service URL (which is partly randomized).
describe Gets the statistics & configuration of the deployed Cloud Run.
find Extracts only the service URL from the response of npm 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 environment variable 指定它希望您的代码监听的端口。

变体:迁移 HTTP 事件函数

当您使用来自firebase-functions 包的HTTP Event function 时,它会在内部处理body-parsing on your behalfFunctions Framework 为此使用 body-parser packagedefines the parsers here

要处理用户授权,您可以使用此validateFirebaseIdToken() 中间件来检查请求中提供的 ID 令牌。

对于基于 HTTP 的 Cloud Run,需要配置 CORS 才能从浏览器调用它。这可以通过安装cors package 并进行适当配置来完成。在下面的示例中,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 时,您可以选择使用哪种方法来调用它(GETPOST(默认)、PUTHEADDELETE)。要复制 Cloud Function 的 datacontext 参数,最好使用 POST,因为这些参数将在请求正文中传递。与 Firebase Functions 一样,来自 Cloud Scheduler 的这些请求可能会被重试,因此请务必发送至 handle idempotency appropriately

注意:即使 Cloud Scheduler 调用请求的主体是 JSON 格式的,该请求也会使用 Content-Type: text/plain 提供服务,我们需要处理它。

这段代码改编自Functions Framework source(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}`);
});

注意:函数框架通过发回带有X-Google-Status: error 标头的HTTP 200 OK 响应来处理错误。这实际上意味着“成功失败”。作为一个局外人,我不确定为什么要这样做,但我可以假设它是为了让调用者知道不必费心重试该函数——它只会得到相同的结果。

$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_IDSERVICE_NAMESERVICE_URLIAM_ACCOUNT 需要酌情替换。

接下来我们需要create a service account,Cloud Scheduler 可以使用它来调用 Cloud Run。您可以随意调用它,例如scheduled-run-invoker。此服务帐户的电子邮件将在下一步中称为IAM_ACCOUNT。这个Google Cloud Tech YouTube video(从正确的位置开始,大约 15 秒)将快速显示您需要做什么。创建帐户后,您可以在接下来的 30 秒左右的视频之后 create the Cloud Scheduler job 或使用以下命令:

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 source(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}`);
});

【讨论】:

  • 很好的答案,谢谢 :) 期待您的编辑:“编辑:令人讨厌的是,我刚刚注意到您的 Cloud Run 目标是 Cloud Scheduler 而不是 HTTP 客户端,因为这个答案涵盖了。我'当我下次有机会调整 Cloud Scheduler 的过程时,我会回到这一点。最重要的变化是在部署和调整 express 以侦听 app.post('/') 时使用 --no-allow-unauthenticated。 "
  • @TheProgrammer 已更新。
  • samethecodingman 的回答非常好。我还想添加以下视频,我发现它对我的特定用例很有帮助,以防其他人发现它有用:youtube.com/watch?v=XIwbIimM49Y
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-03
  • 2021-06-09
  • 1970-01-01
  • 1970-01-01
  • 2018-04-19
相关资源
最近更新 更多