Workflows 的最佳实践

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

本文并未详尽列出所有建议,也不会教您如何使用 Workflows 的基础知识。本文档假定您已大致了解整体情况和 Workflows。 Google Cloud 如需了解详情,请参阅 Google Cloud Well-Architected FrameworkWorkflows 概览

选择最佳通信模式

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

  • 服务到服务的直接通信

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

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

请务必考虑上述每个选项的优缺点,并为您的应用场景选择最佳模式。例如,与其他选项相比,服务到服务的直接通信可能更易于实现,但它会紧密耦合您的服务。相比之下,事件驱动型架构可让您松散耦合服务;不过,监控和调试可能会更加复杂。最后,像 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 提交构建

    或者,您可以使用 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]}

封装表达式

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

${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 调用等简单任务,例如 发出 HTTP 调用。Workflows 可以调用服务、解析响应以及 为其他连接的服务构建输入。 调用服务可以避免产生额外调用、额外依赖项和调用服务的服务复杂性。考虑将不包含业务逻辑的服务替换为声明式 API 调用,并使用 Workflows 来抽象复杂性。

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

仅存储所需内容

控制内存消耗,以免遇到 资源限制或指示 此限制的错误,例如 ResourceLimitErrorMemoryLimitExceededErrorResultSizeLimitExceededError

有选择地存储 变量中的内容,仅过滤和 存储所需内容。如果服务返回的载荷过大,请使用单独的函数为您进行调用,并仅返回所需内容。

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

您可以通过分配 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 模式

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

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

使用回调进行等待

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

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

编排长时间运行的作业

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

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

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

容器化长时间运行的任务

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

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

从 Workflows 运行命令行工具

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

The Google Cloud CLI 包括 gcloudbqkubectl 命令行工具,但没有直接从 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,以便您只需为内部步骤付费,而无需为外部步骤付费。

  • 应用自定义重试政策 ,以平衡延迟时间和可靠性需求与费用。重试频率越高,延迟时间越短,可靠性越高,但费用也可能会增加。

  • 使用等待长时间运行的操作的连接器时,请设置 自定义轮询政策 ,以优化延迟时间以降低费用。例如,如果您预计某项操作需要一个多小时,您可能需要一项政策,该政策最初会在发生即时故障时在一分钟后进行轮询,然后每 15 分钟轮询一次。

  • 分配合并为一个步骤。

  • 避免过度使用 sys.log 步骤。考虑改用 调用日志记录

  • 了解哪些操作被视为一个步骤。如果操作本身不计为步骤,则在适用步骤中使用时,这些操作会被计为步骤。例如,以下内容计为一个步骤:

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

    下表列出了计入和不计入 最大步骤数限制的关键操作:

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

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

    不计为一个步骤

最佳实践摘要

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

常规提示
最佳实践

后续步骤