不要用重试压垮零食仓库
一句话总结
重试不是抹掉失败的按钮。必须确定对哪种错误、在什么时间之前、再尝试几次,并且让这份预算在重启之后依然保留。
为什么需要它
节日零食订单服务器短暂变慢了。如果十个投递方同时立刻重新发送,服务器上就会在原有任务之上再叠加重试。响应因此变得更慢,于是又触发重试,原本片刻即可恢复的故障被拉长。即使前面的模块已经阻止了同一业务 ID 的重复效果,处理请求的 CPU 和连接也不是免费的。防止业务重复与抑制负载是两个不同的问题。
反过来,如果把所有失败都当作永久失败而丢弃,就会因为一次短暂的连接错误而丢掉已受理的订单。因此要把临时性失败、永久性业务错误和含义不明的失败区分开。实验中的 Retryable 是传输适配器归类为可以再次尝试的错误,Permanent 是需要人来修改内容的错误。未知异常既不包装成成功,也不包装成隔离,而是传递给调用方。这次不会构建对所有服务都以同一种方式解释某个 HTTP 状态的分类器。
工作原理
每个业务会保存 attempts、max_attempts、available_at 和 deadline。attempts 不是网络成功的次数,而是先占成功并开始尝试的次数。即使进程在发送请求之前就退出,也不会回退已经消耗的尝试次数。因为如果把无法确定何时发送的失败当作免费尝试,每次都在同一位置崩溃的情况就可能无限重复。这一策略是以故障检测和运维人员重新驱动(redrive)为前提的保守预算。
实验中的延迟公式如下。attempt 从 1 开始,jitter 是调用方选择的 0 到 1 之间的比例。结果向下取整为整数毫秒,并且先应用上限。
window = min(cap_ms, base_ms * 2**(attempt - 1))
delay_ms = floor(window * jitter)
如果 base=100、cap=1000、jitter=0.5,第一次失败后等待 50ms,第二次之后等待 100ms。在 base=100、cap=250 的情况下,第三次尝试不会直接使用 400,而是取 250 的一半,即 125ms。测试会直接传入 0、1 以及小数比例来确认边界。如果实际调用方每次都只使用 0.5,所有工作者就会按相同的模式等待,因此得不到随机分散的效果。可注入的比例是让测试具有确定性的装置,并没有验证随机数发生器本身。
available_at 是一项预约,表示到了这个时间才能再次领取。不会在函数内 sleep 并占着队列的写锁。把失败结果保存为 pending 并结束调用,之后由工作者领取已到期的预约。没有可处理任务的调用以 None 结束。这里不构建常驻轮询守护进程,只实现单次执行单元,因此在生产环境中还需要单独的唤醒、终止和观测策略。
deadline 不会在每次新的重试时延长。即使 now+delay 与 deadline 相等,那时也已经没有可开始尝试的预算,因此以 deadline 为理由移入隔离。如果达到最大尝试次数,则是 exhausted。只有指数退避而没有次数或截止时间,不过是一个缓慢地无限循环的程序。反过来,即使把次数设得很小,只要某个发送函数永远不返回,整个调用也无法结束。传输适配器的超时需要另外设置。
在现场相遇的样子
发生故障时,要检查是否在多个层级重复设置了重试。如果浏览器调用三次、API 服务器调用三次、外部 SDK 调用三次,那么原本一次的业务可能会产生多得多的请求。不能只看某一层的 max_attempts 就断定整个系统的负载上限。本实验的最大尝试次数是 jobs 表中一行的预算,并没有实现整个服务的每秒请求限制或令牌桶。
把时刻保存到 DB 中这一点也很重要。从进程启动起经过的时间,在重启后基准点可能改变,所以不能直接用作持久化的预约时刻。实验中把所有工作者都按同一基准解释的整数 ms 作为参数传入。run_once 在发送前后调用两次 clock,如果一次调用内时刻倒退就报错。多台主机的实际时钟差异、重启和时钟校正则是另行设计的对象。这项检查并不能校准全球各地工作者的时钟。
观测时,除重试次数之外,还要分别统计等待中的任务、租约中的任务、被隔离的任务,以及等待最久的业务已存在的时长。pending 减少并不意味着全部成功,它们也可能被移到了 dead。如果因为租约到期而由其他工作者接手,外部请求就可能重复,所以前面学过的接收端 inbox 对相同 ID 的处理依然是必要的。预算和防重复只保留其中一个是不够的。
下一项检查要做什么
在接下来的测验中,会计算延迟上限、截止时间边界和尝试次数的含义。在之后的综合实验中,要实现 retry_delay 和持久队列,并拒绝 bool、无穷大以及超出范围的值。还要确认即使用相同 ID 重新受理,已有的截止时间和已消耗的尝试次数也不会被重置。
参考:AWS 的退避与抖动说明。数量、尝试范围以及预约与隔离的契约,是这个零食投递实验的设计。