那天处理多平台问答时,我原本以为问题很简单:把几个任务同时发出去,等结果回来,再继续下一个。可刚开始操作,事情就变得不再像“多开几个任务”那么直接。短时间提交太多会触发验证;有的平台有每日发送上限;同一页面到底能不能连续提交;已经发出去的任务,提交动作和等待结果是否应该绑在一起;长任务如果一直挂着,又该什么时候停止。
这些问题表面上互不相干,实际上都在问同一件事:系统当前到底处于什么状态。**并发不是把动作复制很多份,而是要让每一个动作都知道自己占用了什么、什么时候完成、什么时候应该停下来。**如果状态没有先理清,增加并发只会把不确定性一起放大。
一开始,我把并发想得太简单
最初的直觉是“一家平台开一个浏览器,再给每个平台安排一些任务”。这样看起来很清楚,也像是最容易扩展的办法:有多少任务就开多少执行单元,互不干扰,速度自然会上去。
但实际情况很快暴露出问题。有的平台使用的是工作任务入口,而不是普通的新对话入口。每次提问都被当成一个新的工作任务,几十次操作下来,页面里积累了大量任务。用户当时直接问,40 多个工作任务能不能批量清理。这个问题提醒我,任务数量不是纯粹的吞吐量,它也会变成页面状态和清理成本。
另一个误判是把“多个任务”理解成“多个浏览器”。用户指出,同一家平台其实可以在同一个网页、同一个标签页里连续提交,没有必要为了并发不断新开浏览器。**共享页面不是偷懒,而是在复用已经建立好的登录、验证和会话状态。**如果不处理这一点,系统会同时维护太多浏览器上下文,最终不是任务更多,而是资源争抢、状态漂移和故障定位更难。
验证和限额,说明系统已经在发信号
短时间提交过多后,平台触发了验证。这里最容易误判的是:看到提交失败,就以为额度用完了,或者以为代码没有发出去。实际上,用户已经说明,那次并不是没有额度,而是进入了验证流程。另一个平台则可能存在每天只能发送 20 次左右的限制,从那里取不回来,不一定是取回逻辑坏了,也可能是已经碰到了平台上限。
这两个信号很像,却不能用同一个办法处理。验证要求暂停或转入人工处理,限额要求记录用量、降低速度,甚至把剩余任务推迟。如果把它们都当成普通网络失败,系统就会自动重试;重试又会继续触发验证或消耗配额,最后把真正的原因覆盖掉。
如果不处理,后果不只是“慢一点”。系统可能把验证页面当成空结果,把配额耗尽误报成超时,还可能在后台持续提交,直到账号进入更严格的限制状态。那时再去看日志,只能看到一串互相覆盖的失败,已经无法判断最早的信号来自哪里。
我选择先把平台状态单独记录下来,是因为只有这样,提交、等待和失败处理才有机会分开。至少需要区分正常可提交、等待验证、达到限额、任务已提交但结果未回收、结果已完成和应该停止这几类状态。状态不一定要复杂,但必须能解释下一步为什么允许或禁止。
提交和等待,不应该绑成一个动作
这次改动中,一个关键转折是补齐多个平台的无等待提交能力。以前的调用习惯是:发出问题,然后一直等到回答回来。单任务时这样很自然,多任务时却会把浏览器页面、锁和等待过程绑在一起。
一旦提交函数占着页面资源等待很久,其他任务就算只想提交,也只能排队。于是我们看到的“并发不足”,可能不是平台不能并发,而是代码把提交阶段和等待阶段揉成了一个长动作。只要有一个任务生成较慢,后面的任务就被它拖住。
我把流程拆成了两个阶段:先提交并拿到明确回执,再在不占用提交通道的情况下等待或回收。回执至少要说明请求是否已经进入平台、对应的任务标识是什么、当前是否需要等待。这样,提交成功不再被误认为回答已经完成,等待失败也不会抹掉提交成功这个事实。
这里的“无等待”不是不关心结果,更不是把任务丢到后台就算结束。它只是把“我已经把事情交给平台”和“平台已经给我最终答案”分开。一个可靠的长任务系统,必须同时保存提交回执和最终结果,不能拿后者去否定前者。
这个拆分还带来一个很实际的好处:多个任务可以共用一个页面上下文,提交动作快速通过,等待和回收按照自己的节奏进行。页面不会因为某个慢任务一直被占住,系统也能在回收阶段判断是不是遇到了验证、限额或静默失败。
真正的资源边界,是页面而不是任务数
用户问“10 家是什么意思”时,实际上是在追问资源模型。我的理解后来发生了变化:平台数量决定需要多少类页面上下文,但任务数量不应该直接决定浏览器数量。对一家平台来说,一个可复用的页面上下文往往已经包含登录状态、会话状态和平台自己的验证状态。任务只是在这个上下文中排队提交或并行等待。
当然,共享页面也不是无限制的。页面可能有只能串行的操作,提交和回收可能需要不同的锁,验证状态也可能暂时阻断所有新任务。因此不能简单地把“同一个页面”理解成“所有事情都同时做”。更准确的边界是:提交入口共享,页面状态集中管理,能并行的等待过程与提交过程分离,不能并行的动作保持短暂串行。
这样看,资源边界就从“我开了几台浏览器”变成了“我占用了几个页面上下文、几把锁、多少平台配额”。这比单看任务数更接近真实成本。开更多浏览器只能增加外壳,不能自动消除会话冲突,也不能绕过验证和每日上限。
Canary 是日常流程的一部分
当用户要求把 canary 纳入日常流程,并在修改适配器后自动运行时,我意识到,验证不能只在大改动完成后做一次。多平台系统的问题往往不是“完全不能用”,而是某个平台的一个边界状态悄悄变了:发送还能成功,但回收失败;回答能回来,但验证页被当成正常文本;一个适配器改好了,却让另一个平台共享的调度逻辑失效。
Canary 的价值不在于覆盖所有功能,而在于用很小的真实动作尽早确认关键链路还活着。它应该检查最小提交、回执、回收和停止信号,并把失败标成具体阶段。提交阶段失败和回收阶段失败,处理方式不同;限额和验证也不应只显示成一句“任务超时”。
**一个流程成熟的标志,不是它能一次跑通,而是它能在状态变化时尽早告诉我哪里变了。**所以 canary 不是额外的仪式,而是每次适配器修改后的基本回声。没有这声回声,我只能等到真实任务堆积之后,才发现问题已经扩大。
最容易误判的,是把速度当成吞吐量
错误捷径很明确:遇到失败就加浏览器、加后台任务、加重试,把并发数继续往上推。这个办法偶尔会让某一轮看起来更快,却没有解决平台验证、用量上限、共享页面和长任务停止的问题。
我现在更愿意把吞吐量理解成“单位时间内成功完成并能解释的任务数”。一个任务虽然很快发出去,却因为回执丢失、平台进入验证、结果被静默吞掉,不能算真正完成。数量越多,越需要可观察的状态,否则系统只是更快地产生无法解释的积压。
如果必须降速,我也不把它看成退步。遇到验证信号就暂停新提交,遇到用量接近上限就限流,遇到回收异常就保留已提交记录并停止无意义重试。这些动作牺牲了短时间速度,却保护了后续还能继续工作的条件。速度应该建立在状态可解释的基础上,而不是用更多并发掩盖状态不清。
这个方法以后可以复用到任何有外部平台参与的自动化流程:先列出资源边界,再区分提交、等待、回收和停止;对平台反馈做成明确状态;给每个阶段保留最小回执;最后用 canary 检查最短链路。这样即使某个平台改了页面,或者某1天突然收紧限制,也能知道该降速、暂停还是调整回收,而不是从头猜一遍。
下次我会怎么做
下次遇到多平台并发任务,我会先问 4 个问题:每个平台到底复用什么页面状态?提交动作是否能立刻返回回执?平台有没有验证、限额和其他停止信号?长任务在什么条件下自动停止?这 4 个问题没有答案之前,我不会先增加并发数。
然后,我会为每个平台保留一个清晰的上下文边界,把提交与结果回收分开。提交成功就记录提交成功,回收失败就记录回收失败;验证和限额进入可解释的暂停状态,不让普通重试把它们改写成超时。对于同一页面可以承载的任务,我会复用页面,但把真正不能并行的操作缩短并保护起来。
最后,在每次修改适配器后自动跑最小 canary,确认提交、回执、回收和停止钩子都还正常。检查通过后再逐步增加任务量,并观察失败类型是否发生变化。真正要追求的不是“同时跑得更多”,而是“在更多任务同时运行时,仍然知道每一个任务为什么在这里、下一步会发生什么”。
这件事让我重新意识到,工具真正改变的不是速度,而是我们能不能把判断过程留下来。并发系统也是一样:先把状态说清楚,速度才有意义;先让系统知道什么时候该停,长期运行才不会变成失控。