Workflows 的最佳实践

借助 Workflows,您可以跨Google Cloud协调和自动执行服务。应用推荐的架构实践有助于提高可靠性、优化执行费用,并构建弹性、可维护的编排。

这不是详尽的建议列表,也不会教您如何使用 Workflows 的基础知识。本文档假定您已经对整体 Google Cloud 环境和 Workflows 有了大致了解。如需了解详情,请参阅 Google Cloud Well-Architected Framework 和 Workflows 概览。

选择最佳通信模式

在设计用于部署多项服务的微服务架构时,您可以从以下通信模式中进行选择:

  • 直接服务间通信

  • 间接事件驱动型通信(也称为编舞)

  • 自动配置、协调和管理(也称为编排)

请务必考虑上述每个选项的优缺点,并为您的用例选择最佳模式。例如,直接服务到服务通信可能比其他选项更容易实现,但它会紧密耦合您的服务。相比之下,事件驱动型架构可让您松散耦合服务;但是,监控和调试可能会更复杂。最后,像 Workflows 这样的中央编排器虽然灵活性较低,但可让您协调服务之间的通信,而无需紧密耦合直接服务到服务通信,也无需编排事件的复杂性。

您还可以结合使用多种通信模式。例如,在事件驱动型编排中,密切相关的服务在由事件触发的编排中进行管理。同样,您可以设计一个系统,其中一个编排会向另一个编排系统发送 Pub/Sub 消息。

常规提示

决定将 Workflows 用作服务编排器后,请注意以下实用提示。

避免对网址进行硬编码

您可以避免使用硬编码网址,从而支持可在多个环境中移植且更易于维护的工作流。您可以通过以下方式实现此目的:

  • 将网址定义为运行时参数。

    当通过客户端库或 API 调用工作流时,这会很有帮助。(但是,如果您的工作流是由 Eventarc 中的事件触发的,并且唯一可以传递的参数是事件载荷,则此方法不起作用。)

    示例

    main:
      params: [args]
      steps:
        - init:
            assign:
              - url1: ${args.urls.url1}
              - url2: ${args.urls.url2}

    运行工作流时,您可以指定网址。例如:

    gcloud workflows run multi-env --data='{"urls":{"url1": "URL_ONE", "url2": "URL_TWO"}}'
  • 使用环境变量,并创建一个根据部署到的环境进行动态配置的工作流。或者,创建一个可作为模板重复使用并根据单独维护的环境变量进行配置的工作流。

  • 使用替换技术,您可以创建单个工作流定义文件,但可以使用替换工作流中占位符的工具来部署变体。例如,您可以使用 Cloud Build 部署工作流,并在 Cloud Build 配置文件中添加一个步骤,以替换工作流中的占位网址。

    示例

    steps:
    ‐ id: 'replace-urls'
      name: 'gcr.io/cloud-builders/gcloud'
      entrypoint: bash
      args:
        - -c
        - |
          sed -i -e "s~REPLACE_url1~$_URL1~" workflow.yaml
          sed -i -e "s~REPLACE_url2~$_URL2~" workflow.yaml
    ‐ id: 'deploy-workflow'
      name: 'gcr.io/cloud-builders/gcloud'
      args: ['workflows', 'deploy', 'multi-env-$_ENV', '--source', 'workflow.yaml']

    然后,您可以在构建时替换变量值。例如:

    gcloud builds submit --config cloudbuild.yaml \
        --substitutions=_ENV=staging,_URL1="URL_ONE",_URL2="URL_TWO"

    如需了解详情,请参阅通过 CLI 和 API 提交 build。

    或者,您也可以使用 Terraform 预配基础架构,并定义一个配置文件,该文件使用输入变量为每个环境创建工作流。

    示例

    variable "project_id" {
      type = string
    }
    
    variable "url1" {
      type = string
    }
    
    variable "url2" {
      type = string
    }
    
    locals {
      env = ["staging", "prod"]
    }
    
    # Define and deploy staging and production workflows
    resource "google_workflows_workflow" "multi-env-workflows" {
      for_each = toset(local.env)
    
      name            = "multi-env-${each.key}"
      project         = var.project_id
      region          = "us-central1"
      source_contents = templatefile("${path.module}/workflow.yaml", { url1 : "${var.url1}-${each.key}", url2 : "${var.url2}-${each.key}" })
    }

    在配置的根模块中声明变量后,可以通过多种方式为变量赋值。例如

    terraform apply -var="project_id=PROJECT_ID" -var="url1=URL_ONE" -var="url2=URL_TWO"
  • 使用 Secret Manager 连接器在 Secret Manager 中安全地存储网址并检索网址。

使用嵌套步骤

每个工作流都必须至少有一个步骤。默认情况下,Workflows 会将步骤视为有序列表,并逐个执行,直到所有步骤都运行完毕。从逻辑上讲,某些步骤应该组合在一起,您可以使用 steps 块来嵌套一系列步骤。这很方便,因为您可以指向正确的原子步骤来处理一组步骤。

示例

main:
    params: [input]
    steps:
    - callWikipedia:
        steps:
        - checkSearchTermInInput:
            switch:
                - condition: ${"searchTerm" in input}
                  assign:
                    - searchTerm: ${input.searchTerm}
                  next: readWikipedia
        - getCurrentDate:
            call: http.get
            args:
                url: https://timeapi.io/api/Time/current/zone?timeZone=Europe/Amsterdam
            result: currentDate
        - setFromCallResult:
            assign:
                - searchTerm: ${currentDate.body.dayOfWeek}
        - readWikipedia:
            call: http.get
            args:
                url: https://en.wikipedia.org/w/api.php
                query:
                    action: opensearch
                    search: ${searchTerm}
            result: wikiResult
    - returnOutput:
            return: ${wikiResult.body[1]}

封装表达式

所有表达式都必须以 $ 开头,并用大括号括起来:

${EXPRESSION}

为避免 YAML 解析问题,您可以用英文引号将表达式括起来。例如,包含冒号的表达式可能会导致意外行为,因为冒号会被解读为定义映射。要解决此问题,您可以使用英文单引号将 YAML 表达式括起来:

'${"Name: " + myVar}'

您还可以使用跨多行的表达式。例如,在使用 Workflows BigQuery 连接器时,您可能需要用引号将 SQL 查询括起来。

示例

- runQuery:
    call: googleapis.bigquery.v2.jobs.query
    args:
        projectId: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
        body:
            useLegacySql: false
            useQueryCache: false
            timeoutMs: 30000
            # Find top 100 titles with most views on Wikipedia
            query: ${
                "SELECT TITLE, SUM(views)
                FROM `bigquery-samples.wikipedia_pageviews." + table + "`
                WHERE LENGTH(TITLE) > 10
                GROUP BY TITLE
                ORDER BY SUM(VIEWS) DESC
                LIMIT 100"
                }
    result: queryResult

如需查看完整的工作流定义,请参阅并行运行多个 BigQuery 作业。

使用声明式调用

使用 Workflows 从工作流本身调用服务并处理结果,以及执行发出 HTTP 调用等简单任务。Workflows 可以调用服务、解析响应以及为其他连接的服务构建输入。调用服务可以避免产生额外调用、额外依赖项和调用服务的服务复杂性。考虑将不含业务逻辑的服务替换为声明式 API 调用,并使用 Workflows 抽象化复杂性。

不过,您应该创建服务来完成任何对 Workflows 来说过于复杂的工作;例如,实现 Workflows 表达式及其标准库不支持的可重用业务逻辑、复杂计算或转换。与使用 YAML 或 JSON 和 Workflows 语法相比,在代码中实现复杂情况通常更容易。

仅存储所需内容

控制内存消耗,以免遇到资源限制或指示此情况的错误,例如 ResourceLimitError、MemoryLimitExceededError 或 ResultSizeLimitExceededError。

请谨慎选择要存储在变量中的内容,仅过滤和存储所需内容。如果服务返回的载荷过大,请使用单独的函数为您发出调用,并仅返回所需内容。

您可以通过清除变量来释放内存。例如,您可能需要释放后续步骤所需的内存。或者,您可能有一些调用会产生您不关心的结果,您可以完全忽略这些结果。

您可以通过分配 null 来清除变量。在 YAML 中,您还可以为变量赋予空值或 ~。这会识别可以安全回收的内存。

示例

  - step:
      assign:
        - bigVar:

使用子工作流和外部工作流

您可以使用子工作流来定义要多次调用的一段逻辑或一组步骤,从而简化工作流定义。子工作流类似于编程语言中的函数或例程。它们可以接受参数并返回值,让您能够创建更复杂的工作流,从而实现更广泛的应用。

请注意,子工作流是工作流定义的本地工作流,不能在其他工作流中重复使用。不过,您可以从其他工作流调用工作流。Workflows 连接器可以帮助您完成此操作。如需了解详情,请参阅 Workflow Executions API 和 Workflows API 的连接器概览。

使用 Workflows 连接器

Workflows 提供了许多连接器,可让您更轻松地在工作流中访问其他 Google Cloud 产品。连接器会为您处理请求的格式设置,提供方法和参数,让您无需了解Google Cloud API 的详细信息,从而简化了调用服务的过程。连接器还具有用于处理重试和长时间运行的操作的内置行为,因此您可以避免迭代和等待调用完成;连接器会为您处理这些操作。

如果您需要调用 Google Cloud API,请先检查是否存在适用于该 API 的 Workflows 连接器。如果您没有看到 Google Cloud 产品的连接器,可以申请一个。

了解如何使用连接器,如需详细了解可用连接器,请参阅连接器参考文档。

并行运行工作流步骤

虽然 Workflows 可以按顺序运行步骤,但您也可以并行运行独立步骤。在某些情况下,这可以显著加快工作流执行速度。如需了解详情,请参阅并行执行工作流步骤。

应用重试和 Saga 模式

设计具有弹性且可以处理暂时性和永久性服务故障的工作流。例如,Workflows 可能会因 HTTP 请求、函数、连接器失败或由您自己的工作流代码生成而引发错误。添加错误处理和重试机制,以便一个步骤中的失败不会导致整个工作流失败。

某些业务事务跨多个服务,因此您需要一种机制来实现跨服务的事务。Saga 设计模式是一种在分布式事务场景中管理微服务之间数据一致性的方法。Saga 是一系列事务,它会为每个事务发布一个事件,并触发下一个事务。如果事务失败,Saga 会执行补偿性事务,以抵消序列中前面的失败。在 GitHub 上试用 Workflows 中的重试和 Saga 模式教程。

使用回调进行等待

回调允许工作流执行等待其他服务向回调端点发出请求;该请求会恢复工作流的执行。

借助回调,您可以向工作流发出信号,表明指定事件已发生,并等待该事件,而无需轮询。例如,您可以创建一个工作流,在商品有货或商品发货时通知您;或者等待人工互动,例如审核订单或验证翻译。您还可以使用回调和 Eventarc 触发器等待事件。

编排长时间运行的作业

如果您需要执行长时间运行的批处理工作负载,可以使用批处理或 Cloud Run 作业,并使用 Workflows 来管理服务。这样,您就可以结合各种优势,高效地预配和编排整个流程。

Batch 是一项全代管式服务,可让您在 Compute Engine 虚拟机 (VM) 实例上调度、排队和执行批处理工作负载。您可以使用适用于 Batch 的 Workflows 连接器来安排和运行批量作业。如需了解详情,请尝试本教程。

Cloud Run 作业用于运行执行操作(作业)并在操作完成时退出的代码。Workflows 允许您在工作流中执行 Cloud Run 作业,以执行更复杂的数据处理或编排现有作业的系统。试用教程,该教程演示了如何使用 Workflows 执行 Cloud Run 作业。

在各个服务之间协调超时和载荷大小

当 Workflows 调用下游 Google Cloud 服务时,请求超时、HTTP 响应限制和内存限制方面的差异可能会导致 TimeoutError、ValueError 或 MemoryLimitExceededError 失败。将 Workflows 资源限制与目标服务的限制进行比较,然后选择与工作负载时长和载荷大小相匹配的集成模式。例如:

集成场景 Workflows 限制 目标服务限制 推荐的模式
对 Cloud Run 服务或 Cloud Run 函数的同步 HTTP 调用(http.get、http.post、http.request)
  • HTTP 请求超时时长上限为 30 分钟(1,800 秒),其中 http.* 调用的默认超时时长为 300 秒,连接器 HTTP 请求的默认超时时长为 1,800 秒
  • 在引发 ValueError 之前,HTTP 响应大小上限为 2 MB
  • 在引发 MemoryLimitExceededError 之前,累积变量内存为 512 KB
  • Cloud Run 服务和 Cloud Run functions(第 2 代)的请求超时上限为 60 分钟(3,600 秒),第 1 代函数的请求超时上限为 9 分钟(540 秒)
  • HTTP/1.1 响应大小上限为 32 MB

请参阅为服务配置请求超时和 Cloud Run 的资源限制。

对于运行时间超过 30 分钟的工作负载,请勿对 Cloud Run 服务使用同步 http.* 调用。

请改用 Cloud Run Admin API 连接器执行 Cloud Run 作业,或使用异步 Workflows 回调端点。

Cloud Run Admin API 连接器支持长时间运行的操作 (LRO) 轮询,默认情况下会在 30 分钟(1,800 秒)后超时。对于运行时间超过 30 分钟的作业,请同时配置 Cloud Run 作业的任务超时(默认为 10 分钟,最长可配置为 168 小时或 7 天)和连接器调用上的 connector_params.timeout(最长为 1 年或 31536000 秒)。

BigQuery 查询和大型 API 载荷(googleapis.bigquery.v2.jobs.query 和 REST 响应)
  • 在引发 MemoryLimitExceededError 之前,累积变量内存为 512 KB
  • UTF-8 字符串长度上限为 256 KB
  • 在引发 ValueError 之前,HTTP 响应大小上限为 2 MB

BigQuery 查询结果和 REST API 载荷可能超过几兆字节或千兆字节。

请参阅 BigQuery 查询作业配额和限制。

避免将大型结果集拉取到工作流变量中。

使用 maxResults 分页,使用 EXPORT DATA 将大型查询输出写入目标表或 Cloud Storage 存储桶,或将载荷转换和过滤分流到 Cloud Run 服务或 Cloud Run functions 函数,然后再将精简摘要返回到 Workflows。

将长时间运行的任务容器化

您可以使用 Workflows 和 Compute Engine 自动执行长时间运行的容器。例如,您可以将长时间运行的任务容器化,以便它可以在任何位置运行,然后在 Compute Engine 虚拟机上运行容器,以实现工作流执行的最长持续时间(一年)。

使用 Workflows,您可以自动创建虚拟机、在虚拟机上运行容器以及删除虚拟机。这样,您就可以使用服务器并运行容器,但它会消除管理这两者的复杂性,并且在您使用 Cloud Run functions 或 Cloud Run 等服务时遇到时间限制时,它会很有帮助。试用 GitHub 上的将长时间运行的容器与 Workflows 和 Compute Engine 搭配使用教程。

通过 Workflows 运行命令行工具

Cloud Build 是一项服务,可将您的构建作为一系列构建步骤在 Google Cloud 上执行,其中每个构建步骤都在 Docker 容器中运行。执行构建步骤与在脚本中执行命令非常相似。

Google Cloud CLI 包含 gcloud、bq 和 kubectl 命令行工具,但无法直接从 Workflows 运行 gcloud CLI 命令。不过,Cloud Build 提供了包含 gcloud CLI 的容器映像。您可以通过 Cloud Build 步骤在这些容器中运行 gcloud CLI 命令,并且可以使用 Cloud Build 连接器在 Workflows 中创建该步骤。

示例

在工作流中运行 gcloud:

# This example shows how to execute gcloud commands from Workflows
# using Cloud Build and returns the output

main:
  steps:
  - execute_command:
      call: gcloud
      args:
          args: "workflows list"
      result: result
  - return_result:
      return: ${result}

gcloud:
  params: [args]
  steps:
  - create_build:
      call: googleapis.cloudbuild.v1.projects.builds.create
      args:
        projectId: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
        parent: ${"projects/" + sys.get_env("GOOGLE_CLOUD_PROJECT_ID") + "/locations/global"}
        body:
          serviceAccount: ${sys.get_env("GOOGLE_CLOUD_SERVICE_ACCOUNT_NAME")}
          options:
            logging: CLOUD_LOGGING_ONLY
          steps:
          - name: gcr.io/google.com/cloudsdktool/cloud-sdk
            entrypoint: /bin/bash
            args: ${["-c", "gcloud " + args + " > $$BUILDER_OUTPUT/output"]}
      result: result_builds_create
  - return_build_result:
      return: ${text.split(text.decode(base64.decode(result_builds_create.metadata.build.results.buildStepOutputs[0])), "\n")}

Run kubectl in a workflow:

# This example shows how to execute kubectl commands from Workflows
# using Cloud Build and returns the output

main:
  steps:
  - execute_command:
      call: kubectl
      args:
          args: "--help"
      result: result
  - return_result:
      return: ${result}

kubectl:
  params: [args]
  steps:
  - create_build:
      call: googleapis.cloudbuild.v1.projects.builds.create
      args:
        projectId: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
        parent: ${"projects/" + sys.get_env("GOOGLE_CLOUD_PROJECT_ID") + "/locations/global"}
        body:
          serviceAccount: ${sys.get_env("GOOGLE_CLOUD_SERVICE_ACCOUNT_NAME")}
          options:
            logging: CLOUD_LOGGING_ONLY
          steps:
          - name: gcr.io/cloud-builders/kubectl
            entrypoint: /bin/bash
            args: ${["-c", "kubectl " + args + " > $$BUILDER_OUTPUT/output"]}
      result: result_builds_create
  - return_build_result:
      return: ${text.split(text.decode(base64.decode(result_builds_create.metadata.build.results.buildStepOutputs[0])), "\n")}

使用 Terraform 创建工作流

Terraform 是一种基础架构即代码工具,可让您使用代码以可预测的方式创建、更改和改进您的云基础设施。

您可以使用 Terraform google_workflows_workflow 资源定义和部署工作流。如需了解详情,请参阅使用 Terraform 创建工作流。

为了帮助您管理和维护大型工作流,您可以在单独的 YAML 文件中创建工作流,并使用 templatefile 函数将该文件导入 Terraform,该函数会读取给定路径中的文件并将其内容呈现为模板。

示例

  # Define a workflow
  resource "google_workflows_workflow" "workflows_example" {
    name            = "sample-workflow"
    region          = var.region
    description     = "A sample workflow"
    service_account = google_service_account.workflows_service_account.id
    # Import main workflow YAML file
    source_contents = templatefile("${path.module}/workflow.yaml",{})
  }

同样,如果您有一个主工作流调用多个子工作流,则可以在单独的文件中定义主工作流和子工作流,并使用 templatefile 函数导入它们。

示例

  # Define a workflow
  resource "google_workflows_workflow" "workflows_example" {
    name            = "sample-workflow"
    region          = var.region
    description     = "A sample workflow"
    service_account = google_service_account.workflows_service_account.id
    # Import main workflow and subworkflow YAML files
    source_contents = join("", [
      templatefile(
        "${path.module}/workflow.yaml",{}
      ),

      templatefile(
        "${path.module}/subworkflow.yaml",{}
      )])
  }

请注意,如果您在调试工作流时引用行号,则通过 Terraform 配置文件导入的所有 YAML 文件都会合并并部署为单个工作流。

从 Git 代码库部署工作流

Cloud Build 使用构建触发器来实现 CI/CD 自动化。您可以将触发器配置为监听传入事件(例如,将新提交推送到代码库或发起拉取请求时),然后在有新事件传入时自动执行构建。

您可以使用 Cloud Build 构建触发器自动启动构建并从 Git 代码库部署工作流。您可以对触发器进行配置,使之针对源代码库的任何更改来部署工作流,或仅在更改符合特定条件时部署工作流。

此方法可帮助您管理部署生命周期。例如,您可以将对工作流的更改部署到预演环境,针对该环境运行测试,然后逐步将这些更改发布到生产环境。如需了解详情,请参阅使用 Cloud Build 从 Git 代码库部署工作流。

优化数据用量

运行工作流的费用极低。不过,对于高用量使用,请遵循以下准则来优化用量并降低费用:

  • 请勿使用自定义网域,而是确保对 Google Cloud服务的任何调用都使用 *.appspot.com、*.cloud.goog、*.cloudfunctions.net 或 *.run.app,以便系统针对内部步骤(而非外部步骤)向您收费。

  • 应用自定义重试政策,在延迟时间、可靠性需求和费用之间取得平衡。更频繁的重试可以降低延迟时间并提高可靠性,但也会增加费用。

  • 使用等待长时间运行的操作的连接器时,请设置自定义轮询政策,以优化延迟时间并降低费用。例如,如果您预计某项操作需要一个多小时,则可能需要一个在 1 分钟后进行初始轮询(以防立即失败),然后每 15 分钟轮询一次的政策。

  • 将分配合并为一个步骤。

  • 避免过度使用 sys.log 步骤。请考虑改用通话记录。

  • 了解哪些操作会被视为一个步骤。如果操作本身不计为步骤,但在适用的步骤中使用,则会进行统计。例如,以下操作算作一个步骤:

    - type_check:
        return: if(get_type((int("6"))) == integer, 1, 2)
    

    下表对计入和不计入步骤数上限的关键操作进行了分类:

    类别 操作
    计为一步
    • 数据操作:赋值、返回值
    • 控制流:跳转 (next)、切换、启动 for 循环以及 for 循环的每次迭代
    • 调用:调用 sys.get_env 或其他标准库函数、其他工作流或连接器
    • 并发:生成线程和并行执行
    • 错误处理:每个 raise、try、retry 和 except 块都算作一个单独的步骤,即使其他操作属于同一个较大的步骤也是如此。

      例如,包含 try 块和调用操作的步骤会计为三个步骤:一个用于主要步骤,一个用于 try,一个用于调用。添加 retry 块会增加三个步骤(重试、尝试和调用各一个),总共六个步骤。

    不计为一步

最佳做法摘要

下表总结了本文档中建议的一般提示和最佳实践。

常规提示
最佳做法

后续步骤