指出Agent测试中重试机制掩盖了真实可靠性问题,提出在summary中记录重试次数等关键指标以提升可观测性。
我们的 agent 在无人值守的情况下运行,它在大部分生命周期里留下的唯一痕迹是一行摘要,写着 run passed。很长一段时间里这行字就够了,因为 pass 就是我们想要的。后来我们开始更仔细地读这些行,发现两个有着相同摘要的 run,实际上花费完全不一样。
其中一个发起请求、收到响应、写好输出、然后结束。另一个发起请求、什么都没收到、等了一会儿、重新发了请求,然后才结束。两个 run 结束时的状态相同,都打印了同一个词。第二个 run 告诉了我们第一个 run 没有告诉的东西——它所依赖的路径不够可靠,无法在第一次尝试就成功——然后我们把这些信息丢掉了,因为我们的报告机制只设计来回答两个可能值的问题。
这是一种特定种类的盲区,值得精确命名。重试不是 bug,是我们自己加进去的。它们存在是因为远程服务偶尔会变慢或短暂不可用,对瞬时失败的正确应对是重试而不是把人叫醒。重试完成了它的本职工作。问题在于重试还做了第二件事——没人给它分配,但它自己揽下来了:它测量了依赖的健康状况,然后一旦第二次尝试成功,就立刻丢弃了这个测量结果。
它的代价是日积月累显现出来的,而不是某一天突然出现。假设你调用的某个服务开始出现每五十次调用失败一次的状况。你的重试机制吸收了它,你的摘要显示 pass。一个月后变成每十次调用失败一次,你的重试大部分时候也能吸收它,你的摘要仍然显示 pass。那条本可以告诉你某些东西在降级的曲线,每天都在你自己的进程内部绘制出来,而每天你的进程都在任何人看到之前就把它们擦掉了。你第一次了解到这个趋势,是失败率终于超过了你的重试预算、run 变成红色的时候——而这恰恰是这条信息最没用的时刻,因为现在它已经是事故而不是预警了。
修复方法并不巧妙,而这恰恰是它让我们花了不少时间才动手的原因。我们现在把尝试次数作为一个与 outcome 同级的字段记录下来。在第一次尝试就成功的 run 和在第三次尝试才成功的 run,是相同的 outcome,却是不同的记录。重试行为没有任何改变;我们只是不再把中间的尝试当作草稿工作了。摘要行仍然写着 pass,因为它应该写 pass——一个无人值守的 agent 如果每次小毛病恢复都要大喊大叫,最终会让迟早来看它的人学会不再看它。但数字就写在旁边,而这个数字才是你用来绘图的东西。
实际上改变的是我们能问的问题的形状。之前我们只能问今天是否成功。现在我们可以问这周是否比上周花了更多次尝试才把事情做成。第二个问题在什么都没坏的日子也有答案,这意味着我们从健康的 run 中也能得到信号,而不仅仅是从失败中得到。对于一个应该无人值守运行的系统来说,这比听起来重要得多,因为健康的 run 几乎是它的全部。
无人值守的 pipeline 给你发现它在变差的机会非常少。它的绝大部分输出都被设计成可以被忽略的,而且它成功地做到了可以被忽略——直到它做不到的那一刻。它所需要的尝试次数,在它决定打印什么的那个瞬间,已经存在于内存中了。打印它们几乎不花任何代价。不打印它们,是一个等着以后被惊到的决定。