跳到主要内容

那个悄然演变成延迟敏感型服务的夜间批处理作业

· 阅读需 11 分钟
Tian Pan
Software Engineer

这一切始于一个 cron 作业。每晚凌晨 2 点,一个脚本会被唤醒,拉取当天的记录,通过模型运行,将结果写入表中,然后继续休眠。这是解决该问题的最简单形态,而且在整整一年的时间里,它确实是最合适的形态。没有人去考虑它,因为没有人需要去考虑。

接着有人问结果能否在早上 8 点而不是中午准备好。然后有人问用户是否可以按需触发单条记录的运行。接着一位产品经理问是否可以让应用内的体验“感觉像是即时的”。每个请求都是合理的。每一次改动都很小。而且从始至终,没有人打开过一份名为“重新架构推理流水线”的文档,因为没有任何一次单一的改动让人觉得像是在重写。

18 个月后,你拥有了一个披着批处理作业外壳的延迟敏感型在线服务。它的 p99 无人衡量,队列无人清理,且存在一种失效模式:由于流水线被构建为重试整个批次,一条错误记录就会导致面向用户的请求停滞。这是 AI 系统中最常见的架构失效之一,而且它几乎从未作为一项决策出现,而是作为对一系列合理请求不断说“是”而产生的缓慢累积。

重写你架构的三个请求

架构漂移是指系统逐渐地、无意地偏离其初始设计的现象。危险之处在于,它是由局部合理的决策构成的。缩短 cron 间隔的工程师是在响应一个真实的抱怨。添加 HTTP 端点以触发单条记录运行的工程师是在疏通一个真实的功能。没有人做错任何事。但架构还是变了,因为单个决策会产生复合效应,而没有人去衡量这些决策对系统形态产生的总体影响。

看看这种下滑在实践中是如何发生的。请求一:“我们能早点拿到结果吗?”你将 cron 计划从每晚改为每小时。依然是批处理,没问题。请求二:“用户能为他们自己的记录触发它吗?”你将批处理函数封装在一个处理大小为 1 的批次的端点中。它在演示中运行良好。请求三:“它能感觉像是即时的吗?”现在你正盯着一个曾被调整为在 1 万条记录中摊销启动成本的函数,它在用户盯着旋转进度条时正带着一条记录被调用。

每一步都保留了之前的代码。这恰恰就是问题所在。批处理函数在每次调用时仍然将整个模型加载到内存中,因为这一成本过去分摊在 1 万行记录上。流水线在最后仍然通过一次事务写入结果,因为当消费者是下游报告时,部分写入并不重要。重试逻辑在发生任何故障时仍然会重新运行整个作业,因为对于每晚一次的批处理,在凌晨 2 点重试所有内容的成本为零。在线流量违反了所有这些假设,而这些假设是不可见的,因为它们从未被写下来。它们只是作为批处理作业成长起来的代码的自然形态。

批处理推理与在线推理优化的目标截然相反

你无法从一种模式平滑地滑向另一种模式的原因在于,它们并不是一个光谱上的两个点,而是针对存在直接冲突的目标进行优化的两种设计。

批处理推理优化的是单件成本。它允许速度慢。它可以等到积累了足够的工作量以高效利用 GPU,大批量运行所有内容,并将每一项固定成本——模型加载、连接建立、容器启动——分摊到数千条记录中。在 LLM 服务中,将请求分组可以将每个 token 的成本降低约 85%,而仅增加约 20% 的延迟。对于批处理作业来说,这种权衡无异于白捡。吞吐量是唯一重要的数字,重试是真的免费,因为没有人等待。

在线推理优化的是 p99 延迟。它不能等待积累工作,因为这项工作对应的是一位用户,而用户已经在等待了。它必须在以百毫秒计的预算内,通过网络完成检索上下文、运行模型、验证输出并返回结果。硬性指标不是平均值,而是长尾。延迟优先的设计会刻意让 GPU 在空闲期间处于低负载状态,以便突发流量不会冲垮 p99,并接受更高的单次请求成本作为可预测性的代价。

这些目标确实是冲突的。让批处理推理变得廉价的批处理技术,正是摧毁长尾延迟的元凶,因为交替处理分组请求的 prefill 和 decode 阶段,使得系统在结构上很难同时做到高速和高吞吐量。在批处理中免费的重试,在在线服务中意味着延迟预算翻倍。追求效率的举措与追求延迟的举措背道而驰。一个从未经过重新设计就从一种模式漂移到另一种模式的系统,现在正被要求通过一个仅为满足其中之一而构建的设计来同时满足这两个目标——而它将悄无声息地在那个它并非为此而生的目标上败下阵来。

批处理系统所做的假设与在线流量的冲突

有三个假设深深地植根于批处理管道中,以至于工程师们甚至不再将它们视为假设。然而,当实时流量到来时,每一个假设都会演变成生产事故。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates