管理伸缩风险

借助 Cloud Tasks,您可以分派大量异步任务,同时控制流量到达队列和下游服务的速率。通过应用渐进式扩容模式和速率控制,您可以在流量增长速度超过基础设施适应速度时,顺利扩缩工作负载,而不会触发队列延迟高峰或目标过载。

概览

本文档提供了有关在高流量队列中保持 Cloud Tasks 高性能的最佳实践指南。高 TPS 队列是指每秒创建或分派的任务数 (TPS) 达到 500 或更多的队列。高 TPS 队列组是一组连续的队列,例如 [queue0001、queue0002、…、queue0099],这些队列每秒总共创建或分派至少 2,000 个任务。您可以使用 Cloud Monitoring 指标(CreateTask 操作的 api/request_count 和任务尝试的 queue/task_attempt_count)查看队列或队列组的历史 TPS。高流量队列和队列组容易出现两类不同的故障:

当任务创建和分派到单个队列或队列组的速度超过队列基础架构的适应速度时,就会发生队列过载。Google 的基础设施使用自适应层,这些层会根据流量模式动态重新分配负载,而这种自适应需要时间。同样,当任务分派速率导致下游目标基础架构中的流量激增时,就会发生目标过载。在这两种情况下,请遵循 500/50/5 模式:当伸缩超过 500 TPS 时,每 5 分钟增加的流量不超过 50%。

与高 TPS 工作负载的 500/50/5 模式不同,初始队列递增会影响从冷状态或空闲状态启动的所有队列,包括配置为中等调度速率的队列。本文档回顾了可能带来伸缩风险的不同场景,并举例说明了如何管理队列扩容和应用 500/50/5 模式。

初始队列逐步增加

500/50/5 模式适用于已在稳定状态下运行的高 TPS 队列。初始队列预热是一种单独的系统机制,适用于所有队列,包括配置为中等调度速率(例如 20-100 TPS)的队列,因为它们会从冷状态或空闲状态过渡到稳态运行。

在以下任何情况下,队列都会进入冷状态或空闲状态:

  • 队列是新创建的,或在暂停后恢复。

  • 队列已处于非活跃状态几分钟,没有活跃的调度。

  • 队列在从下游 HTTP 429 或 5xx 错误响应中退避后正在恢复。如需详细了解错误退避如何影响分派速率,请参阅退避错误和强制执行的速率。

当任务到达冷队列或空闲队列时,Cloud Tasks 不会立即以配置的 max_dispatches_per_second 速率分派任务。为了保护下游目标免受突发流量高峰的影响,Cloud Tasks 会以较低的基准速率(例如大约 1 TPS)开始分派。然后,它会在定期评估间隔内逐步提高强制执行的调度速率。只要队列需求保持饱和且目标处理程序返回 HTTP 2xx 响应,强制执行的速率就会继续提高。根据您的目标速率,达到较高的 max_dispatches_per_second 值可能需要几分钟时间。

如果您的工作负载发送的流量呈尖峰状,且各尖峰之间有几分钟或更长时间的短暂空闲期,则队列会在两次突发之间重置为冷状态,并在每次突发时重复此预热过程。为了最大限度地减少对突发敏感的工作负载的增加延迟时间,请遵循以下最佳实践:

  • 保持稳定的基准流量,以便在突发流量之间保持对延迟敏感的队列处于活跃状态。

  • 重复使用现有队列,而不是频繁删除和重新创建队列。

  • 预配足够的下游目标容量,以便处理程序返回 HTTP 2xx 响应,并避免返回会重置强制执行的调度速率的 HTTP 429 或 5xx 错误响应。

诊断队列节流

如果队列分派任务的速度低于您配置的 max_dispatches_per_second 速率,请检查瓶颈是否来自冷队列或空闲队列加速、目标拥塞退避或并发限制。

冷队列或空闲队列预热

如果满足以下所有条件,则可能是初始队列增加所致:

  • 队列是最近创建的、在暂停后恢复的,或者在流量到达之前空闲了几分钟。
  • 任务尝试成功,没有目标错误,在 Monitoring 指标 cloudtasks.googleapis.com/queue/task_attempt_count 中显示 response_code="OK"。
  • 传输中的分派仍低于 max_concurrent_dispatches,而 cloudtasks.googleapis.com/queue/depth 或 cloudtasks.googleapis.com/queue/task_attempt_delays 会升高。同时,cloudtasks.googleapis.com/queue/task_attempt_count 中的调度速率会在定期评估间隔内逐步提高。

如需详细了解 Monitoring 指标,请参阅向 Monitoring 发送指标。

目标过载和拥塞退避

如果下游目标返回 HTTP 429 Too Many Requests 或 HTTP 5xx 错误响应(例如 HTTP 503 Service Unavailable),Cloud Tasks 会动态降低分派速率。这种拥塞退避可防止目标过载。

如需检查目标错误是否导致 Cloud Tasks 限制调度,请查看按 response_code 标签分组的 Monitoring 指标 cloudtasks.googleapis.com/queue/task_attempt_count。如果启用了队列日志记录,您还可以在 Logs Explorer 中检查尝试响应日志:

resource.type="cloud_tasks_queue"
jsonPayload.attemptResponseLog.status!="OK"

在 response_code 和 jsonPayload.attemptResponseLog.status 中,Cloud Tasks 都会记录规范 gRPC 状态代码,而不是数字 HTTP 状态代码。例如,HTTP 429 映射到 RESOURCE_EXHAUSTED,HTTP 503 映射到 UNAVAILABLE,HTTP 500 映射到 INTERNAL,HTTP 504 或超时映射到 DEADLINE_EXCEEDED。

如果您观察到目标出现 RESOURCE_EXHAUSTED、UNAVAILABLE 或其他服务器错误状态,请先纵向扩容目标服务实例或提高目标并发数,然后再提高队列分派限制。

如需详细了解如何启用队列日志,请参阅将 Cloud Logging 与队列搭配使用。

并发瓶颈

根据利特尔法则,下游目标延迟时间会限制最大调度吞吐量:

Max Throughput (tasks/sec) = min(
  max_dispatches_per_second,
  max_concurrent_dispatches / Downstream Latency in seconds
)

例如,如果下游执行延迟时间为 2 秒,并且 max_concurrent_dispatches 设置为 100,则吞吐量限制为每秒 50 个任务,即使 max_dispatches_per_second 配置为 500 也是如此。

如需诊断并发是否为瓶颈,请将 max_concurrent_dispatches 除以 Logging 的 jsonPayload.attemptResponseLog.attemptDuration 字段中记录的目标响应延迟时间。将此计算出的阈值与 Monitoring 指标 cloudtasks.googleapis.com/queue/task_attempt_count 中的实际调度速率进行比较。如果实际分派量在此阈值附近趋于平稳,而 cloudtasks.googleapis.com/queue/depth 或 cloudtasks.googleapis.com/queue/task_attempt_delays 却在增长,请执行以下操作之一:

  • 如果下游目标有空闲容量(例如,任务的大部分时间都花在等待外部 I/O 上),请增加 max_concurrent_dispatches。
  • 如果下游目标的资源利用率已经很高,则增加 max_concurrent_dispatches 可能会加剧目标过载。而是应横向扩容目标实例或缩短处理程序执行延迟时间。

队列过载

每当流量突然增加时,队列或队列组都可能会过载。因此,这些队列可能会遇到以下情况:

  • 任务创建延迟增加
  • 任务创建错误率增加
  • 调度率降低

为了防范这种情况,请在队列或队列组的创建或分派速率可能突然飙升的任何情况下建立控制措施。对于冷队列或队列组,最开始每秒最多执行 500 次操作,然后每 5 分钟增加 50% 的流量。理论上,使用此递增时间表,您可以在 90 分钟后达到每秒 74 万次操作。在许多情况下,都可能会发生这种情况。

例如:

  • 启动大量使用 Cloud Tasks 的新功能
  • 在队列之间移动流量
  • 在更多或更少的队列中重新平衡流量
  • 运行注入大量任务的批量作业

在这些状况和其他情况下,请遵循 500/50/5 模式。

使用 App Engine 流量分配

如果 App Engine 应用创建了任务,您可以利用 App Engine 流量分配(App Engine 标准环境或 App Engine 柔性环境)来平滑流量增长。通过在版本(标准环境或柔性环境)之间分配流量,您可以随着时间的推移逐步增加速率受管理的请求,以保护队列健康状况。例如,假设要增加新扩展的队列组的流量:假设 [queue0000, queue0199] 是一系列高 TPS 队列,在高峰期总共接收 100,000 个 TPS 创建。

将 [queue0200,queue0399] 设置为一系列新队列。在您转移所有流量后,序列中的队列数量会翻倍,并且新队列范围会接收序列总流量的 50%。

部署增加队列数量的版本时,请使用流量分配功能逐步将流量增加到新版本,从而增加到新队列:

  • 开始将 1% 的流量迁移到新版本。例如,100,000 TPS 的 1% 的 50% 会为一组新队列产生 500 TPS。
  • 每 5 分钟将发送到新版本的流量增加 50%,如下表所示:
自部署起的分钟数 总流量转移到新版本的百分比 总流量占新队列的百分比 总流量占旧版本的百分比
0 1.0 0.5 99.5
5 1.5 0.75 99.25
10 2.3 1.15 98.85
15 3.4 1.7 98.3
20 5.1 2.55 97.45
25 7.6 3.8 96.2
30 11.4 5.7 94.3
35 17.1 8.55 91.45
40 25.6 12.8 87.2
45 38.4 19.2 80.8
50 57.7 28.85 71.15
55 86.5 43.25 56.75
60 100 50 50

版本驱动的流量急剧增加

在发布会显著增加队列或队列组流量的版本时,逐步推出同样是平滑增加流量的重要机制。逐步发布实例,使初始启动时对新队列的操作总数不超过每秒 500 次,且每 5 分钟增加不超过 50%。

新的高 TPS 队列或队列组

新创建的队列特别容易受到影响。在初始发布阶段,队列组(例如 [queue0000、queue0001、…、queue0199])与单个队列一样敏感。对于这些队列,逐步推出是一项重要策略。分阶段推出可创建高 TPS 队列或队列组的新服务或更新后的服务。将初始负载保持在 500 TPS 以下,并分阶段增加 50% 或更少,间隔至少 5 分钟。

新扩展的队列组

增加队列组的总容量时,例如将 [queue0000-queue0199 扩展到 queue0000-queue0399],请遵循 500/50/5 模式。对于发布过程,新队列组的行为与单个队列的行为没有区别。将 500/50/5 模式应用于整个新群组,而不仅仅是群组中的单个队列。

对于这些队列组扩展,逐步推出是一项重要策略。如果您的流量来源是 App Engine,则可以使用流量分配。迁移服务以向增加的队列添加任务时,请逐步发布实例,使初始启动时对新队列执行的操作总数不超过 500 次,并且每 5 分钟增加的流量不超过 50%。

紧急队列组扩展

有时,您可能需要扩展现有队列组,例如,因为您预计向队列组添加任务的速度会快于该组分派任务的速度。如果您按照字典顺序将新队列的名称均匀分布在现有队列名称中,则可以立即向这些队列发送流量。确保新交错队列不超过 50%,并且每个队列的流量低于 500 TPS。此方法是使用流量分配和逐步推出的替代方法。

如需实现这种交错命名,请为以偶数结尾的队列附加后缀。例如,如果您有 200 个现有队列 [queue0000-queue0199],并且想要创建 100 个新队列,请选择 [queue0000a、queue0002a、queue0004a、…、queue0198a] 作为新队列名称,而不是 [queue0200-queue0299]。

如果您需要进一步增加,您仍然可以每 5 分钟交错增加最多 50% 的队列。

大规模或批量任务入队

当您需要添加大量任务(例如数百万或数十亿个任务)时,双重注入模式会很有用。请使用注入器队列,而不是通过单个作业创建任务。添加到注入器队列中的每个任务都会扇出,并向相关队列或队列组添加 100 个任务。您可以随着时间的推移加快注入器队列的速度,例如,从 5 TPS 开始,在目标队列或队列组中生成 500 TPS,然后每 5 分钟将速率提高 50%。

已命名的任务

创建新任务时,Cloud Tasks 默认会为该任务分配一个唯一名称。您可以使用 name 参数为任务分配自己的名称。但是,这会带来显著的性能开销,从而导致延迟增加,并可能升高与命名任务相关的错误率。依序任务名称(例如时间戳)可能会显著增加此开销。

如果您自行分配名称,请为任务名称使用分布良好的前缀,例如内容哈希。如需详细了解如何为任务命名,请参阅 Task.name 字段参考文档。

目标过载

如果队列分派突然激增,Cloud Tasks 可能会使您使用的下游服务和资源(例如 App Engine、Datastore 模式的 Firestore [Datastore] 和您的网络)过载。如果积压了大量任务,则取消暂停这些队列可能会导致这些服务过载。为防止目标过载,请遵循 500/50/5 模式:如果队列分派的 TPS 超过 500,则每 5 分钟由队列触发的流量增加不超过 50%。如需主动监控流量增加情况,请使用 Monitoring 指标。您可以使用 Monitoring 提醒来检测潜在的危险情况。

取消暂停或恢复高 TPS 队列

当您恢复已暂停的队列时,Cloud Tasks 会应用初始队列递增,并逐步将调度速率从较低的基准提高到队列配置的 max_dispatches_per_second 速率。但是,如果高 TPS 队列或您同时恢复的多个队列中积压了大量任务,则分派速率的攀升速度仍可能快于下游目标服务的扩缩速度。为防止在恢复积压大量任务的队列时目标过载,请错开恢复多个队列的时间。或者,暂时降低 max_dispatches_per_second 速率,然后按照 500/50/5 模式逐渐提高速率。如需详细了解 Cloud Tasks 如何在队列恢复时限制和增加分派,请参阅退避错误和强制执行的速率。

批量计划任务

大量任务同时调度分派也可能导致目标过载。如果您需要一次启动大量任务,请考虑使用队列速率控制来逐步提高分派速率,或提前预配目标容量。

扇出增加

更新 Cloud Tasks 调用的服务时,增加远程调用次数可能会带来生产风险。例如,假设高 TPS 队列中的任务调用处理程序 /task-foo。例如,如果新版本向处理程序添加了多个开销较大的 Datastore 调用,则新版本可能会显著增加调用 /task-foo 的费用。此类发布带来的最终结果是 Datastore 流量大幅增加,这与用户流量的变化直接相关。使用逐步推出或流量分配来管理扩容。

重试

在进行 Cloud Tasks API 调用时,您的代码可以在失败时进行重试。但是,当很大一部分请求因服务器端错误而失败时,高重试率可能会进一步使队列过载并减慢恢复速度。如果客户端检测到很大一部分请求因服务器端错误而失败,请限制传出流量。例如,您可以使用站点可靠性工程一书的处理过载一章中介绍的自适应限制算法。Google 的 gRPC 客户端库实现了此算法的变体。