丢弃什么,又该等待谁
一句话总结
最新位置和支付记录不是同一种事件。根据可以丢掉什么,慢订阅者的策略会有所不同。
为什么需要它
救援队已经移动到下一条小巷,地图上却依次重播着 5 分钟前的位置。服务器报告说一条也没有丢,但对用户来说这是毫无用处的直播。反过来,如果在支付记录中删掉旧条目,只显示最新条目,可能会遗漏资金流动本身。消除损失与守住服务目的,并不总是同一种选择。
本模块比较三种策略:等到有空间为止、丢弃等待最久的状态、把慢订阅者从发布对象中分离。它们都有优缺点。不要以为设置一个队列大小,策略就会自动确定。QueueFull 之后做什么,决定了产品的行为。
工作原理
等待策略可以用 await queue.put 来实现。它不会悄悄丢数据,但如果发布者遍历订阅者列表并逐个等待 put,整个遍历就会停在某一个慢订阅者上。给每个订阅者各设一个队列,并不意味着自动隔离。还要看生产者以什么顺序和方式等待这些队列。
最新状态策略是在队列已满时取出等待最久的一个条目,再放入新条目。容量为 2,10 和 11 在等待,来了 12,就剩下 11 和 12。如果丢弃刚到的 12,名字叫 latest,实际上保留的却是旧画面的策略。已经被 worker 取走正在处理的条目无法从队列中拿出,所以这个策略不会回退进行中的工作。
被丢弃的条目也是通过 get 从队列取走的,所以要对应调用一次 task_done。否则 queue.join 可能永远等下去。这时 join 结束,也不代表全部成功。有的是有意丢弃的,有的可能是处理失败的。成功、丢弃、失败的次数要作为业务指标另行记录。课程中会返回被丢弃的条目,以便直观比较策略。
分离策略不会悄悄修改已满的队列,而是抛出 SlowConsumer 异常。发布函数把该订阅者从字典中移除,并请求所有者终止。对于剩下的订阅者继续传递。如果在第一次失败时就 return,列表中排在后面的正常订阅者就会错过事件。如果直接遍历正在修改的字典,可能产生遍历错误,所以使用名称与队列配对的快照。
| 数据 | 要考虑的策略 | 还需要什么 |
|---|---|---|
| 当前位置、进度 | 省略过时的等待状态 | 最新快照、缺失标记 |
| 聊天、通知记录 | 分离慢连接后重新连接 | 保存的日志、最后确认的位置 |
| 支付、库存变更 | 持久存储与重试 | 幂等处理、原子状态变更 |
断开连接可以回收服务器资源,但并不能解决传递问题。如果订阅者收到了事件,却在发送 ACK 之前断开,服务器就不知道是否已处理。重试可能产生重复,不重试则可能遗漏。分离是缩小故障范围的措施,与传递保证是另外的设计。
在现场相遇的样子
仪表板可能不需要绘制每秒变化几十次的 CPU 使用率的所有中间值。但如果用同样的方式压缩审计日志,就会丢失事故路径。要为每个频道明确策略,并同时观察分离慢连接的次数和订阅者重新同步的时间。即使只是断开次数增加,正常订阅者的延迟指标看起来仍可能很好。
本实验的最后一步,在真实 TCP 上验证连接分离策略。fast 对每个事件都发 ACK,slow 搁置第一个事件的 ACK。发布从 0 到 8 时,要确认 fast 是否全部收到、只有 slow 被分离了一次。不依赖固定的 sleep,而是使用 ACK 和事件屏障来确认执行顺序。这不是吞吐量基准测试,而是对部分故障的功能验证。
下一项检查要做什么
请用同样的输入写出策略的差异。slow 正在处理 0,容量为 2 的队列里 1 和 2 在等待,这时 3 到达。等待策略会让发布者停住。最新状态策略丢弃 1,保留 2 和 3。分离策略拒绝这次插入,并通知连接所有者。还必须确定由谁来清理被分离队列中的 1 和 2。无论哪种情况,已经在处理中的 0 是否成功,都不会自行确定。
如果把这些差异压缩成一行日志的成功或失败,就会错过原因。队列饱和、业务 ACK 超时、远端 EOF、管理员强制分离,是互不相同的事件。区分指标名称和失败消息,就更容易判断应该增加重新连接、改善消费速度,还是更改数据策略。简单地把所有错误都捕获后继续发送的做法,可能造成直播看起来还活着、数据却已消失的结果。
用测验来确认每种策略让谁等待、丢失什么。请在后面的实验中实现 offer_latest、offer_disconnect 和 broadcast,并说明为什么对同样的输入要做不同的处理。