Cloud Run 作业使用入门

1. 简介

96d07289bb51daa7.png

概览

虽然 Cloud Run 服务非常适合针对 HTTP 请求运行无限期侦听的容器,但 Cloud Run 作业可能更适合运行以完成操作并且不处理请求的容器。例如,如果以 Cloud Run 作业的形式实现,则处理数据库中的记录、处理 Cloud Storage 存储桶中的文件列表或长时间运行的操作(例如计算 Pi)的效果会比较好。

作业无法处理请求或侦听端口。这意味着 Cloud Run 作业与 Cloud Run 服务不同,应该不会捆绑 Web 服务器。作业容器应在完成后退出。

在 Cloud Run 作业中,您可以通过指定多个任务来并行运行容器的多个副本。每个任务代表容器的一个正在运行的副本。如果每个任务都可以独立处理您的一部分数据,则使用多个任务非常有用。例如,当 10 项任务逐个并行处理 1000 个记录或文件时,可以更快速地处理来自 Cloud SQL 的 10000 条记录或来自 Cloud Storage 的 10000 个文件。

作业工作流

Cloud Run 作业使用起来很简单,只需两个步骤:

  1. 创建作业。此步骤封装了运行作业所需的所有配置,例如容器映像、区域、环境变量。
  2. 运行该作业。这将创建一个新的作业执行。(可选)使用 Cloud Scheduler 将作业设置为按计划运行。

预览版限制

在预览版期间,Cloud Run 作业具有以下限制:

  • 每个区域的每个项目最多可同时运行 50 个执行作业(来自相同或不同作业)。
  • 您可以在 Cloud Console 的 Cloud Run 作业页面查看现有作业、启动执行作业和监控执行状态。请使用 gcloud 创建新作业,因为 Cloud Console 目前不支持创建新作业。
  • 请勿将 Cloud Run 作业用于生产工作负载。因为无法保证可靠性或性能。Cloud Run 作业在向 Google Analytics(分析)发布之前,可能会以不向后兼容的方式更改,恕不另行通知。

在此 Codelab 中,您首先要探索 Node.js 应用,了解如何截取网页屏幕截图并将其存储到 Cloud Storage 中。然后,为该应用构建容器映像,以作业的形式在 Cloud Run 上运行应用,更新作业以处理更多网页,然后使用 Cloud Scheduler 按计划运行该作业。

学习内容

  • 如何使用应用来截取网页屏幕截图。
  • 如何为应用构建容器映像。
  • 如何为应用创建 Cloud Run 作业。
  • 如何以 Cloud Run 作业的形式运行应用。
  • 如何更新作业。
  • 如何使用 Cloud Scheduler 安排作业。

2. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud Console,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 帐号,则必须创建一个

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串,您可以随时对其进行更新。
  • 项目 ID 在所有 Google Cloud 项目中必须是唯一的,并且不可变(一经设置便无法更改)。Cloud Console 会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为 PROJECT_ID),因此如果您不喜欢某个 ID,请再生成一个随机 ID,还可以尝试自己创建一个,并确认是否可用。然后,项目创建后,ID 会处于“冻结”状态。
  • 第三个值是一些 API 使用的项目编号。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud Console 中启用结算功能,才能使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。要关闭资源以避免产生超出本教程范围的费用,请按照此 Codelab 末尾提供的任何“清理”说明操作。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

启动 Cloud Shell

虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

Google Cloud Console 中,点击右上角工具栏中的 Cloud Shell 图标:

55efc1aaa7a4d3ad.png

预配和连接到环境应该只需要片刻时间。完成后,您应该会看到如下内容:

7ffe5cbb04455448.png

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证功能。只需一个浏览器,即可完成本实验中的所有工作。

设置 gcloud

在 Cloud Shell 中,设置项目 ID 以及要将 Cloud Run 作业部署到的区域。将它们保存为 PROJECT_IDREGION 变量。您可以从某个 Cloud Run 位置中选择一个区域。

PROJECT_ID=[YOUR-PROJECT-ID]
REGION=[YOUR-REGION]
gcloud config set core/project $PROJECT_ID
gcloud config set run/region $REGION

启用 API

启用所有必要的服务:

gcloud services enable \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com \
  run.googleapis.com

3.获取代码

您首先要探索 Node.js 应用,了解如何截取网页屏幕截图并将其存储到 Cloud Storage 中。之后,您可以为该应用构建容器映像,并以作业的形式在 Cloud Run 上运行该应用。

在 Cloud Shell 中,运行以下命令以从代码库克隆应用代码:

git clone https://github.com/GoogleCloudPlatform/jobs-demos.git

转到包含该应用的目录:

cd jobs-demos/screenshot

您应该会看到此文件布局:

screenshot
 |
 ├── Dockerfile
 ├── README.md
 ├── screenshot.js
 ├── package.json

以下是对每个文件的简要说明:

  • screenshot.js 包含应用的 Node.js 代码。
  • package.json 定义库依赖项。
  • Dockerfile 定义容器映像。

4.探索代码

如需浏览代码,请点击 Cloud Shell 窗口顶部的 Open Editor 按钮,以使用内置文本编辑器。

f78880c00c0af1ef.png

以下是对每个文件的简要说明。

screenshot.js

screenshot.js 首先将 Puppeteer 和 Cloud Storage 添加为依赖项。Puppeteer 是一个用于截取网页屏幕截图的 Node.js 库:

const puppeteer = require('puppeteer');
const {Storage} = require('@google-cloud/storage');

系统提供了一个用于初始化 Puppeteer 的 initBrowser 函数,以及一个用于截取给定网址屏幕截图的 takeScreenshot 函数:

async function initBrowser() {
  console.log('Initializing browser');
  return await puppeteer.launch();
}

async function takeScreenshot(browser, url) {
  const page = await browser.newPage();

  console.log(`Navigating to ${url}`);
  await page.goto(url);

  console.log(`Taking a screenshot of ${url}`);
  return await page.screenshot({
    fullPage: true
  });
}

接下来,还提供了一个用于获取或创建 Cloud Storage 存储桶的函数,以及一个用于将网页屏幕截图上传到存储桶的函数:

async function createStorageBucketIfMissing(storage, bucketName) {
  console.log(`Checking for Cloud Storage bucket '${bucketName}' and creating if not found`);
  const bucket = storage.bucket(bucketName);
  const [exists] = await bucket.exists();
  if (exists) {
    // Bucket exists, nothing to do here
    return bucket;
  }

  // Create bucket
  const [createdBucket] = await storage.createBucket(bucketName);
  console.log(`Created Cloud Storage bucket '${createdBucket.name}'`);
  return createdBucket;
}

async function uploadImage(bucket, taskIndex, imageBuffer) {
  // Create filename using the current time and task index
  const date = new Date();
  date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
  const filename = `${date.toISOString()}-task${taskIndex}.png`;

  console.log(`Uploading screenshot as '${filename}'`)
  await bucket.file(filename).save(imageBuffer);
}

最后,main 函数是入口点:

async function main(urls) {
  console.log(`Passed in urls: ${urls}`);

  const taskIndex = process.env.CLOUD_RUN_TASK_INDEX || 0;
  const url = urls[taskIndex];
  if (!url) {
    throw new Error(`No url found for task ${taskIndex}. Ensure at least ${parseInt(taskIndex, 10) + 1} url(s) have been specified as command args.`);
  }
  const bucketName = process.env.BUCKET_NAME;
  if (!bucketName) {
    throw new Error('No bucket name specified. Set the BUCKET_NAME env var to specify which Cloud Storage bucket the screenshot will be uploaded to.');
  }

  const browser = await initBrowser();
  const imageBuffer = await takeScreenshot(browser, url).catch(async err => {
    // Make sure to close the browser if we hit an error.
    await browser.close();
    throw err;
  });
  await browser.close();

  console.log('Initializing Cloud Storage client')
  const storage = new Storage();
  const bucket = await createStorageBucketIfMissing(storage, bucketName);
  await uploadImage(bucket, taskIndex, imageBuffer);

  console.log('Upload complete!');
}

main(process.argv.slice(2)).catch(err => {
  console.error(JSON.stringify({severity: 'ERROR', message: err.message}));
  process.exit(1);
});

请注意关于 main 方法的以下事项:

  • 网址将作为参数传递。
  • 存储桶名称是作为用户定义的 BUCKET_NAME 环境变量传入的。存储桶名称在所有 Google Cloud 项目中必须是全局唯一的。
  • CLOUD_RUN_TASK_INDEX 环境变量由 Cloud Run 作业传递。Cloud Run 作业可以将应用的多个副本作为唯一任务运行。CLOUD_RUN_TASK_INDEX 表示正在运行的任务的索引。如果代码在 Cloud Run 作业之外运行,该变量值默认为零。当应用作为多个任务运行时,每个任务/容器都会提取其负责的网址,截取屏幕截图,然后将图片保存到存储桶。

package.json

package.json 文件用于定义应用并指定 Cloud Storage 和 Puppeteer 的依赖项:

{
  "name": "screenshot",
  "version": "1.0.0",
  "description": "Create a job to capture screenshots",
  "main": "screenshot.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Google LLC",
  "license": "Apache-2.0",
  "dependencies": {
    "@google-cloud/storage": "^5.18.2",
    "puppeteer": "^13.5.1"
  }
}

Dockerfile

Dockerfile 用于为应用定义包含所有必需库和依赖项的容器映像:

FROM node:17-alpine

# Installs latest Chromium (92) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      npm

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -g pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy all files
COPY . .

# Run everything after as a non-privileged user.
USER pptruser

ENTRYPOINT ["node", "screenshot.js"]

5. 构建和发布容器映像

Artifact Registry 是 Google Cloud 上的容器映像存储和管理服务。如需了解详情,请参阅使用容器映像。Artifact Registry 可以将 Docker 和 OCI 容器映像存储在 Docker 存储库中。

创建名为 containers 的新 Artifact Registry 存储库:

gcloud artifacts repositories create containers --repository-format=docker --location=$REGION

构建并发布容器映像:

gcloud builds submit -t $REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot:v1

几分钟后,您应该会看到在 Artifact Registry 中构建和托管的容器映像。

62e50ebe805f9a9c.png

6.创建作业

在创建作业之前,您需要创建一个用于运行作业的服务帐号。

gcloud iam service-accounts create screenshot-sa --display-name="Screenshot app service account"

向服务帐号授予 storage.admin 角色,使其可用于创建存储桶和对象。

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --role roles/storage.admin \
  --member serviceAccount:screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com

您现在可以创建一个 Cloud Run 作业,其中包含运行作业所需的配置。

gcloud beta run jobs create screenshot \
  --image=$REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot:v1 \
  --args="https://example.com" \
  --args="https://cloud.google.com" \
  --tasks=2 \
  --task-timeout=5m \
  --set-env-vars=BUCKET_NAME=screenshot-$PROJECT_ID \
  --service-account=screenshot-sa@$PROJECT_ID.iam.gserviceaccount.com

这样会创建一个 Cloud Run 作业,但不会运行该作业。

注意网页是如何作为参数传入的。用于保存屏幕截图的存储桶名称会以环境变量的形式传入。

您可以使用 --tasks 标记指定要运行的多个任务,以便于并行运行容器的多个副本。每个任务代表容器的一个正在运行的副本。如果每个任务都可以独立处理您的一部分数据,则使用多个任务非常有用。为了方便起见,每个任务都知道其存储在 CLOUD_RUN_TASK_INDEX 环境变量中的索引。您的代码负责确定哪个任务处理哪个部分的数据。请注意此示例中的 --tasks=2。这样可以确保针对我们要处理的 2 个网址运行 2 个容器。

每个任务最长可以运行 1 小时。您可以使用 --task-timeout 标记缩短此超时时间,如此示例中所示。所有任务都必须成功,作业才能成功完成。默认情况下不会重试失败的任务。您可以将任务配置为在失败后重试。如果任何任务超过重试次数,整个作业都会失败。

默认情况下,您的作业将并行运行尽可能多的任务。此值等于作业的任务数量,最多为 100 个。如果作业要访问扩容能力有限的后端,您可能需要设置较小的最大并行数量。例如,支持有限数量的活跃连接的数据库。您可以使用 --parallelism 标记减少最大并行数量。

7. 运行作业

在运行作业之前,请列出作业以查看其是否已创建:

gcloud beta run jobs list

✔
JOB: screenshot
REGION: $REGION
LAST RUN AT:
CREATED: 2022-02-22 12:20:50 UTC

使用以下命令运行作业:

gcloud beta run jobs execute screenshot

这样将执行该作业。您可以列出当前和过去的执行作业:

gcloud beta run jobs executions list --job screenshot

...
JOB: screenshot
EXECUTION: screenshot-znkmm
REGION: $REGION
RUNNING: 1
COMPLETE: 1 / 2
CREATED: 2022-02-22 12:40:42 UTC

描述执行作业。您应该会看到绿色对勾标记和 tasks completed successfully 消息:

gcloud beta run jobs executions describe screenshot-znkmm
✔ Execution screenshot-znkmm in region $REGION
2 tasks completed successfully

Image:           $REGION-docker.pkg.dev/$PROJECT_ID/containers/screenshot at 311b20d9...
Tasks:           2
Args:            https://example.com https://cloud.google.com
Memory:          1Gi
CPU:             1000m
Task Timeout:    3600s
Parallelism:     2
Service account: 11111111-compute@developer.gserviceaccount.com
Env vars:
  BUCKET_NAME    screenshot-$PROJECT_ID

您还可以在 Cloud Console 的 Cloud Run 作业页面上查看状态:

e59ed4e532b974b1.png

如果您检查 Cloud Storage 存储桶,应该会看到已创建的两个屏幕截图文件:

f2f86e60b94ba47c.png

有时,您可能需要在执行作业完成之前将其停止,原因可能是您已经意识到需要使用不同的参数运行作业,或者代码中存在错误,以及您不想使用不必要的计算时间。

要停止执行作业,您需要删除执行作业:

gcloud beta run jobs executions delete screenshot-znkmm

8. 更新作业

Cloud Run 作业不会在下一次执行时自动获取容器的新版本。如果您更改作业的代码,则需要重新构建容器并更新作业。使用带标记的映像将有助于您确定当前使用的是哪个版本的映像。

同样,如果您要更新某些配置变量,也需要更新作业。后续作业执行将使用新的容器和配置设置。

更新作业并在 --args 标记中更改应用截取屏幕截图的页面。同时,更新 --tasks 标记以反映页面数量。

gcloud beta run jobs update screenshot \
  --args="https://www.pinterest.com" \
  --args="https://www.apartmenttherapy.com" \
  --args="https://www.google.com" \
  --tasks=3

再次运行该作业。这次运行时,传入 --wait 标记以等待执行完成:

gcloud beta run jobs execute screenshot --wait

几秒钟后,您应该会看到添加到存储桶中的另外 3 个屏幕截图:

ce91c96dcfd271bb.png

9. 安排作业

到目前为止,此 Codelab 展示了如何手动运行作业。在实际场景中,您可能需要运行作业来响应事件或按计划运行作业。您可以通过 Cloud Run REST API 执行此操作。我们来看看如何使用 Cloud Scheduler 按计划运行屏幕截图作业。

首先,确保已启用 Cloud Scheduler API:

gcloud services enable cloudscheduler.googleapis.com

创建一个 Cloud Scheduler 作业,以在每天 9:00 运行 Cloud Run 作业:

PROJECT_NUMBER="$(gcloud projects describe $(gcloud config get-value project) --format='value(projectNumber)')"

gcloud scheduler jobs create http screenshot-scheduled --schedule "0 9 * * *" \
   --http-method=POST \
   --uri=https://$REGION-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/$PROJECT_ID/jobs/screenshot:run \
   --oauth-service-account-email=$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
   --location $REGION

验证 Cloud Scheduler 作业是否已创建并准备好调用 Cloud Run 作业:

gcloud scheduler jobs list

ID: screenshot-scheduled
LOCATION: $REGION
SCHEDULE (TZ): 0 9 * * * (Etc/UTC)
TARGET_TYPE: HTTP
STATE: ENABLED

如需进行测试,请手动触发 Cloud Scheduler:

gcloud scheduler jobs run screenshot-scheduled

几秒钟后,您应该会看到 Cloud Scheduler 中的调用添加的另外 3 个屏幕截图:

971ea598020cf9ba.png

10. 恭喜

恭喜,您已完成此 Codelab!

所学内容

  • 如何使用应用来截取网页屏幕截图。
  • 如何为应用构建容器映像。
  • 如何为应用创建 Cloud Run 作业。
  • 如何以 Cloud Run 作业的形式运行应用。
  • 如何更新作业。
  • 如何使用 Cloud Scheduler 安排作业。