Cloud Run 作业使用入门

Cloud Run 作业使用入门

О практической работе

subjectПоследнее обновление: мая 5, 2022
account_circleАвторы: Mete Atamel

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. 恭喜

恭喜,您已完成此 Codelab!

所学内容

  • 如何使用应用来截取网页屏幕截图。
  • 如何为应用构建容器映像。
  • 如何为应用创建 Cloud Run 作业。