当我们谈论异步系统的核心组件时,队列是一个关键元素。它们能够改变控制流程,帮助我们构建高吞吐量的系统,在负载过重时也能保持优雅。但这并非免费的午餐:为了正常运作,队列需要流量控制的支持。
想象一下,就像时间维度下的控制流图,我们用到达率和离开率来描述队列。到达率代表一段时间内的消息数量,比如每秒多少条。这个速率会随时间变化,反映了系统的动态行为。高峰到达率可能导致系统过载,而到达率上升的速度(即变化率)则决定了系统应对突发负载时需要多快地扩展(亚马逊最近就提升了这一指标的处理能力)。
绘制出随时间变化的到达率曲线,我们可以直观地看到队列的力量。它们能平滑消息流量,也就是网络术语中的“流量整形”:
左侧是不均匀的消息到达,右侧则是稳定且有序的处理速率。当到达率低于处理速率时,队列会缩小,空闲后,处理速率会跟随到达率,直到后者小于前者。如果到达率超过处理速率,队列又会开始工作。现实中,许多队列允许接收者批量处理消息,但这点稍后再讨论。
流量整形的优势在于,它使得设计和调整系统以适应右侧的稳定流量比左侧的波动流量要容易得多。使用队列和工作池处理请求的系统在高负载下表现更佳,尽管响应时间会上升,但系统不会因自身负荷过大而崩溃,因为处理速率保持稳定。
相比之下,同步系统可能会忙于接受新请求,以至于无法处理现有请求,导致随着负载增加,系统吞吐量反而下降。这显然不是我们想要的结果。
因此,队列被广泛视为保护系统免受促销活动、营销冲击或拒绝服务攻击的缓冲器。即使在高到达率下将消息塞入队列,也不太可能使系统过载。同时,工作者们则按最佳速率持续工作。
然而,队列并非万能。每个队列都有容量限制,虽然通常比处理能力(如工作节点)大,因为存储消息成本较低。实际上,“无限”只是意味着没有明确的上限,而非无穷无尽。例如,AWS Lambda的实例限制就体现了这一点。
为了确保在持续高负载下正常运行,队列系统需要某种形式的流量控制。流量控制的目标是管理发送者和接收者之间的数据传输速率,防止快速发送者淹没慢速接收者。
队列本身通过解耦发送者和接收者的控制流提供了一定程度的流量控制,但为了有效管理,还需要额外的控制手段。主要有三种机制:
- 超时(Time-to-Live,TTL):设定一个有限的生存期,过期的消息会被从队列中移除,为新消息腾出空间。这对于随着时间衰减价值的数据(如过时的CPU利用率警报)或过期订单(客户可能对几周前的订单被处理感到惊讶)特别适用。
- 尾部丢弃(Tail Drop):相反,新到达的消息会被丢弃。这适用于旧消息非常宝贵,或者发送者有反馈机制,知道消息被丢弃后可以重新尝试的情况。
- 后压(Backpressure):通知上游系统,队列无法处理更多消息,从而让它们降低发送速率,比如显示错误信息给用户。
虽然这些策略可能并不理想,但明确的流量控制总比任由过多流量涌入要好。例如,AWS的Serverless DA团队在构建Serverlesspresso应用时发现,只有两个咖啡师的后台很容易因免费咖啡需求激增而过载。他们使用队列,但发现用户最多愿意等待两分钟。如果继续排队,会导致资源浪费和用户体验下降。于是,他们采用了后压策略,当队列达到特定大小时不