Workflows 的最佳实践

使用 Workflows 编排服务时,您可以参考此处列出的最佳实践。

此列表并未详尽列出所有建议,也未介绍如何使用 Workflows 的基础知识。本文档假定您已大致了解整个 Google Cloud 格局和 Workflows。如需了解详情,请参阅 Google Cloud Well-Architected Framework 和工作流概览。

选择最佳沟通模式

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

  • 直接服务间通信

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

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

请务必考虑上述每种方案的优缺点,并根据您的使用情形选择最佳模式。例如,直接的服务到服务通信可能比其他选项更易于实现,但它会紧密耦合您的服务。相比之下,事件驱动型架构可让您松散耦合服务;不过,监控和调试可能会更加复杂。最后,像 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 产品的连接器,可以申请。

了解如何使用连接器;如需查看可用连接器的详细参考信息,请参阅连接器参考。

并行运行工作流步骤

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

应用重试和 Saga 模式

设计可应对暂时性服务故障和永久性服务故障的弹性工作流。Workflows 的错误可能由失败的 HTTP 请求、函数、连接器引发,也可能由您自己的工作流代码生成。添加了错误处理和重试机制,以避免一个步骤中的失败导致整个工作流失败。

  • 您可以使用 语法raise引发自定义错误。
  • 您可以使用 try/except 块捕获错误。
  • 您可以使用 try/retry 块重试步骤,并定义最大重试次数。

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

使用回调进行等待

回调允许工作流执行操作等待其他服务向回调端点发出请求;该请求将继续执行工作流。

借助回调,您可以告知工作流指定事件已发生,然后等待该事件而不进行轮询。 例如,您可以创建一个工作流,以便在产品再次有货或发货时向您发出通知;或等待人工互动,例如审核订单或验证翻译。您还可以使用回调和 Eventarc 触发器等待事件。

编排长时间运行的作业

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

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

Cloud Run 作业用于运行执行工作(即作业)并在完成后退出的代码。借助 Workflows,您可以在工作流中执行 Cloud Run 作业,以执行更复杂的数据处理或编排现有作业的系统。不妨试用本教程,了解如何使用 Workflows 执行 Cloud Run 作业。

容器化长时间运行的任务

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

借助工作流,您可以自动执行以下操作:创建虚拟机、在虚拟机上运行容器以及删除虚拟机。这样一来,您就可以使用服务器并运行容器,但它会抽象化管理这两者的复杂性。如果您在使用 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 代码块会再添加三个步骤(分别用于重试、尝试和调用),使总步骤数达到六个。

    不计为步数

最佳做法摘要

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

常规提示
最佳做法

后续步骤