[博客翻译]队列反转控制流,但需要流控制


原文地址:https://www.enterpriseintegrationpatterns.com/ramblings/queues_flow_control.html


当我们谈论异步系统的核心组件时,队列是一个关键元素。它们能够改变控制流程,帮助我们构建高吞吐量的系统,在负载过重时也能保持优雅。但这并非免费的午餐:为了正常运作,队列需要流量控制的支持。

想象一下,就像时间维度下的控制流图,我们用到达率和离开率来描述队列。到达率代表一段时间内的消息数量,比如每秒多少条。这个速率会随时间变化,反映了系统的动态行为。高峰到达率可能导致系统过载,而到达率上升的速度(即变化率)则决定了系统应对突发负载时需要多快地扩展(亚马逊最近就提升了这一指标的处理能力)。

绘制出随时间变化的到达率曲线,我们可以直观地看到队列的力量。它们能平滑消息流量,也就是网络术语中的“流量整形”:
1.png

左侧是不均匀的消息到达,右侧则是稳定且有序的处理速率。当到达率低于处理速率时,队列会缩小,空闲后,处理速率会跟随到达率,直到后者小于前者。如果到达率超过处理速率,队列又会开始工作。现实中,许多队列允许接收者批量处理消息,但这点稍后再讨论。

流量整形的优势在于,它使得设计和调整系统以适应右侧的稳定流量比左侧的波动流量要容易得多。使用队列和工作池处理请求的系统在高负载下表现更佳,尽管响应时间会上升,但系统不会因自身负荷过大而崩溃,因为处理速率保持稳定。

相比之下,同步系统可能会忙于接受新请求,以至于无法处理现有请求,导致随着负载增加,系统吞吐量反而下降。这显然不是我们想要的结果。

2.png

因此,队列被广泛视为保护系统免受促销活动、营销冲击或拒绝服务攻击的缓冲器。即使在高到达率下将消息塞入队列,也不太可能使系统过载。同时,工作者们则按最佳速率持续工作。

然而,队列并非万能。每个队列都有容量限制,虽然通常比处理能力(如工作节点)大,因为存储消息成本较低。实际上,“无限”只是意味着没有明确的上限,而非无穷无尽。例如,AWS Lambda的实例限制就体现了这一点。

为了确保在持续高负载下正常运行,队列系统需要某种形式的流量控制。流量控制的目标是管理发送者和接收者之间的数据传输速率,防止快速发送者淹没慢速接收者。

队列本身通过解耦发送者和接收者的控制流提供了一定程度的流量控制,但为了有效管理,还需要额外的控制手段。主要有三种机制:

3.png

  1. 超时(Time-to-Live,TTL):设定一个有限的生存期,过期的消息会被从队列中移除,为新消息腾出空间。这对于随着时间衰减价值的数据(如过时的CPU利用率警报)或过期订单(客户可能对几周前的订单被处理感到惊讶)特别适用。
  2. 尾部丢弃(Tail Drop):相反,新到达的消息会被丢弃。这适用于旧消息非常宝贵,或者发送者有反馈机制,知道消息被丢弃后可以重新尝试的情况。
  3. 后压(Backpressure):通知上游系统,队列无法处理更多消息,从而让它们降低发送速率,比如显示错误信息给用户。

虽然这些策略可能并不理想,但明确的流量控制总比任由过多流量涌入要好。例如,AWS的Serverless DA团队在构建Serverlesspresso应用时发现,只有两个咖啡师的后台很容易因免费咖啡需求激增而过载。他们使用队列,但发现用户最多愿意等待两分钟。如果继续排队,会导致资源浪费和用户体验下降。于是,他们采用了后压策略,当队列达到特定大小时不再接受新订单。

丢弃新到达的消息看似商业决策不佳,但如果电商网站过载,它会试图处理队列中的订单,而新用户无法下单,因为看到错误信息。这其实也是一种后压,比如购物车功能就是为后续下单保留的。当用户尝试下单时收到500错误,这就是典型的尾部丢弃。

我坚信从实际场景中学习架构,所以你会在很多地方看到各种形式的流量控制,比如热门俱乐部外的长队(比如拉斯维加斯棕榈赌场的鬼吧)。顾客可能会自行实施超时政策,直到放弃等待。门童可能会直接拒绝新来的客人,或者设置临时等候区作为后压措施。

大型云服务提供商对于处理高负载自然有独到之处。不出所料,我们可以通过他们的工程师撰写的文章了解AWS如何运用这些机制,尽管他们可能使用不同的术语:

  • “我们发现,限制新请求在队列中停留的时间至关重要,如果时间过长,就丢弃。”——相当于超时。
  • “应用负载均衡器拒绝过多的流量。”——类似尾部丢弃。

像RabbitMQ这样的流行消息队列系统内置了流量控制,以保护队列免受内存使用过度增长的影响:

  • RabbitMQ节点会对发布连接应用后压机制,减缓连接速度,以避免队列无法跟上。

预设的交付速率限制是一种主动的流量整形。设计接收消息的系统时,如果知道其极限,可以避免后退。许多云服务提供了对推送交付的速率限制选项,实质上是对消息到达速率施加恒定的压力。

EventBridge Pipes对外部API(HTTP调用)有固定的速率限制,通过API目的地的调用速率设置。它直接异步调用Lambda函数,没有明确的限制(假设内置驱动程序有类似GCP的算法)。其他事件源,如SNS,使用事件源映射,配置最大批次大小(以消息计数)和最大时间(以秒计)。

GCP Pub/Sub没有明确的设置,而是依赖于慢启动算法,只要下游系统能处理负载(表示99%的确认率和低于1秒的推送请求延迟),就会增加交付速度。如果接收方开始吃力,发送方会放慢速度。

Azure Event Grid的推送交付速率限制为每秒5000条消息,但我找不到它在过载下游服务时如何降速的信息。一篇Azure博客文章提到了动态流量逐步增加,但那是针对拉取交付的。

4.png

总结来说,理解流量控制有助于我们构建包含队列的稳健系统。关键教训是:

  • 队列反转控制流,但需要流量控制。

描述异步消息系统动态行为的语言为我们构建可靠系统提供了有用的补充,它不仅关注消息的数据流,还考虑了系统的实时响应。