这次要生成的是一组图片,数量并不算夸张:总共10张。最开始的直觉很简单,既然每张图都是一个结果,那就一张一张提交,哪张完成了就先取哪张。这个做法看起来稳妥,实际上很快把问题推向了另一个方向:提交次数越来越多,会话额度被迅速占满,后面的任务反而无法继续。
后来复盘才发现,真正需要理解的不是“我要生成10张图”,而是“这项服务一次能够承接多少张,以及提交之后怎样把结果取回来”。服务单次最多处理8张,追加任务还必须沿用原任务 ID取回结果。于是,这个任务根本不应该被拆成10次单张冷启动,而应该先按容量分成2组,再分别提交和回收。
**批量生成的基本单位不是一张图,而是一次能被服务完整承接的任务。**如果不先看清这个单位,越是努力地逐张提交,越可能把时间和会话资源消耗在重复启动上。
看起来只是慢,实际是任务模型错了
当时最容易观察到的现象是速度慢。每张图都要经历一次提交、等待和取回,前一张没有结束,下一张就接着排进去。表面上,这是一个需要优化等待时间的问题;但如果只盯着等待,很容易继续给单张流程加重试、加轮询,最后只是让错误的模型变得更复杂。
问题的关键在于,生成服务把请求组织成任务,而不是把每张图片当成完全独立的临时动作。一个任务可以承接多张图,任务创建之后还有自己的身份,结果也要通过这个身份继续取回。忽略这层关系,就会误以为“再发一次”与“继续取同一个任务”没有区别。
如果不处理这个误解,后面会出现两种损失。第一种是时间损失:10张图要重复经历10次启动流程。第二种是资源损失:大量单张任务迅速占用会话上限,原本还可以继续处理的工作被挡在门外。更糟的是,已经提交出去的任务并不会因为新任务失败而自动消失,人工很难从一堆独立请求里判断哪些结果属于同一组。
先把容量当成约束读取
我一开始也倾向于把“批量”理解成把很多请求快速发出去。后来我判断,批量的第一步不应该是加快提交,而应该是读取服务容量和账号配额。原因很直接:如果不知道单次容量,上限附近的任务就没有可靠的拆分依据;如果不知道当前会话还能承接多少,计划出来的组数也可能只是纸面上的安排。
这次已知的单次容量上限是8张,所以10张任务最自然的拆法是第1组8张、第2组2张。这个拆分不是追求形式上的整齐,而是让第1组尽量接近安全上限,同时给第2组保留清楚的边界。拆组的依据应该是服务能稳定承接的容量,而不是操作者觉得哪种提交方式顺手。
这里还要区分单次容量和会话上限。单次容量回答的是“一次任务最多装多少张”;会话上限回答的是“当前还能不能继续创建或追加任务”。前者影响拆组,后者影响是否应该现在提交。把两者混为一谈,就可能在单次任务合法的情况下,仍然因为会话已经饱和而失败。
所以,提交前的检查至少要回答3个问题:单次最多能放多少张,当前还能承接几组,已经存在的任务是否还有结果待回收。检查结果如果说明额度不足,我不会把它伪装成一个暂时性网络问题。它是流程的前置条件没有满足,应该尽早停下来并明确暴露原因。
任务 ID是结果回收的锚点
容量拆好之后,还有一个更容易被漏掉的细节:追加任务必须从原任务 ID取回结果。这个要求改变了“提交”和“取回”的关系。提交不是发完就结束,任务 ID也不是日志里可有可无的一串字符,而是后续识别结果、继续处理和避免串组的锚点。
我会把每一组的输入范围和任务 ID放在一起保存。第1组对应一个 ID,第2组对应另一个 ID;等待时按 ID检查状态,取回时也按同一个 ID确认结果。这样即使不同任务的完成时间不同,也不会因为先完成哪一组而改变归属。
最容易误判的错误捷径,是每次想取结果时重新创建一个新任务。这样做似乎省去了保存和追踪的麻烦,实际上等于把原本可以继续取回的工作重新启动一遍。新任务可能没有原任务的上下文,也可能继续消耗会话额度;最后即便拿到了图片,也很难确认它是不是原来那一组的结果。
**任务 ID的价值不在于记录提交成功,而在于让提交、等待和取回仍然指向同一件事。**这条规则也适用于其他异步任务:只要结果不会在提交瞬间返回,就应该把任务身份当作流程状态的一部分,而不是把它留在人的记忆里。
提交和取回可以并行
另一个重要判断,是不同任务不必串成单张流水线。第1组提交后,不需要等8张全部取回,才开始准备第2组;第2组也不需要排在每一张图片后面。只要服务允许不同任务同时处理,就可以分别提交、分别保存 ID,再并行等待各自的结果。
这里的“并行”不是无条件地把请求全部打满。它仍然要受到会话上限和服务容量的约束。我的做法是先按最大安全批量拆组,再在允许的范围内同时等待不同任务。这样减少的是无意义的串行等待,不是用更多请求去碰运气。
如果把整个流程写成单张流水线,任何一张图的延迟都会阻塞后面所有图片;一组任务出了问题,也可能让人误以为整批都失败了。按任务 ID分开回收之后,等待对象变成了2组任务,异常也能落到具体组别上。某一组失败时,可以明确知道该组需要处理什么,而不是从10次单张提交里猜测状态。
我选择“提交与取回分离”,是因为这更接近服务真实的工作方式。提交阶段只负责建立任务和保存身份,取回阶段只负责按身份等待、检查和收集结果。职责分开之后,流程才有机会并行;如果把提交、等待、取回硬绑在一张图的循环里,任何并发都很难自然出现。
失败时不要删历史换假成功
遇到会话上限时,一个很诱人的做法是先删除历史会话,给新任务腾位置。这样可能让当前提交暂时通过,却掩盖了真正的问题:账号或会话的可用容量已经不足。更重要的是,删除会话属于受保护的人工动作,不应该被自动流程当成普通的清理步骤。
**资源不足时,正确的失败是停在前置检查,而不是删除历史状态制造“还能继续”的假象。**历史任务可能仍然需要取回,删除它们会让已经完成的工作失去追踪依据;即便某次删除没有立刻造成损失,也会让下一次排查缺少证据。
因此,零成本门应该放在付出提交成本之前。先读取容量、会话上限和待回收任务,再决定是否分组提交。如果检查发现额度不够,就输出清楚的阶段名和原因,停止新增任务,保留现有历史。缺图、任务状态异常或取回失败,也要在对应组别上明确报错,不能用一张“看起来有结果”的图片代替完整交付。
这件事让我重新意识到,自动化流程最危险的地方,往往不是一次明确的失败,而是用不可逆的动作把失败擦掉。删除历史会话可能让界面重新变空,却不会让资源约束消失。只有留下状态,才能判断下一步是等待、补交,还是调整任务规模。
把批量任务变成可复用流程
这次的流程可以压缩成一条很具体的规则:先查容量,再按容量拆组;每组只提交一次,保存任务 ID;提交完成后分开等待,按 ID回收结果;任何上限、缺图和异常都 fail-loud,不删除历史会话换取表面成功。
这套方法的价值不只在于把10张图从10次提交变成2次提交。它还把任务从“我记得发过哪些”变成了可以检查的状态:每组有自己的输入范围,有自己的任务 ID,有自己的完成结果,也有自己的失败原因。处理批量任务时,清晰的状态比更快地点击提交更重要。
方法也可以迁移到其他异步工作。凡是一次能承接多个对象、提交后需要等待、结果还要按身份取回的服务,都应该先找出单次容量,再设计任务边界。容量决定怎么拆,任务 ID决定怎么认,等待方式决定怎么提高整体吞吐,前置检查则决定什么时候应该拒绝开始。
一个流程成熟的标志,不是它在额度充足、任务顺利时跑得多快,而是它在上限和异常出现时仍然保留判断依据。真正可复用的批量能力,不是把更多请求同时发出去,而是让每个请求都有边界、有身份、有回收路径。
下次我会怎么做
下次遇到批量出图,我会先确认服务的单次容量和当前会话可用额度,再计算最少需要创建多少组任务。对于10张这类任务,如果上限仍是8张,就按8张和2张拆开,不把每张图当成一次独立冷启动。
提交时,我会为每组保存输入范围和任务 ID,提交完成后立即进入状态等待,不靠页面顺序或人的记忆判断结果属于哪一组。不同任务允许同时处理时,我会并行等待;如果会话上限不允许,就在提交前停止,而不是先发出去再想办法补救。
最后,我会把容量读取、配额检查和缺图检查放在零成本门里。检查不通过,就保留历史状态并给出明确原因;追加结果时,始终沿用原任务 ID。下一次真正要优化的不是“每张图发得多快”,而是“每组任务能不能一次发对、按身份收全,并在不能继续时诚实停下”。