Skip to content
个人学习网站
4 分钟阅读

加了队列,高峰不再被拒绝——但我发现自己做的还是"派活"

Agent 沙盒系列第五篇。用一个队列把"排队"这件事显式化,用户从"被拒绝"变成"排在第 7 位"。写完回头看代码才发现:我以为自己做成了"窗口自己叫号",实际写的还是"大堂经理挨个问哪个窗口空着"。

上一篇结束时留了两个洞:没人记着还有哪些活没干完,以及池子满了只会说“没有空闲资源”。

这两个洞是同一个东西——系统里缺一张“等办事项清单”。

银行叫号机

线下办事早就解决过这个问题:进门先取号,坐着等叫。取号机不关心哪个窗口空着,它只负责把你排进队伍。

代码上就是加一个队列。分配器的活一下子变得极简单——收下任务,丢进队列,告诉用户排第几:

r.lpush(QUEUE_NAME, json.dumps(task_data))   # 丢进队列,就这一句

return {
    "message": "任务已排队",
    "position_in_queue": r.llen(QUEUE_NAME), # 你前面还有几个
}

它不再需要知道有几个容器、谁忙谁闲。高峰时用户看到的不再是“没有空闲资源”,而是“你排在第 7 位”——同样是等,体验完全不同:一个是被拒之门外,一个是已经在队伍里了。

另一头,有个调度进程一直盯着队列:

while True:
    if r.llen(QUEUE_NAME) > 0:            # 队列里有活
        worker_name = await find_idle_worker()   # 找个空闲的容器
        if worker_name:
            task = r.rpop(QUEUE_NAME)     # 取出一个任务
            await dispatch_task(worker_name, task)   # 派给它
    await asyncio.sleep(1)

跑起来是对的:任务不会丢,高峰会排队,用户拿到的是位置而不是错误。

然后我回头看了一眼这段代码

我原本以为这一步做的是从“派活”改成“领活”——教科书上的说法是从 push 改成 pull:不再由中心把任务塞给指定的人,而是让空下来的人自己去队列里拿。

可上面那段代码写的是什么?find_idle_worker() 挨个问哪个容器空着,然后 dispatch_task() 把活送过去。

这还是派活。我只是把派活的人从“前台”挪到了“后台”。

对照银行的比方:我想做的是“窗口忙完自己按下一位”,实际做的是“大堂经理挨个问窗口忙不忙,然后把客人领过去”。区别不是风格问题:

大堂经理派人(我写的) 窗口自己叫号(本该做的)
会不会两人领同一个活 会——问和派之间有空档 不会——队列取出这个动作本身是独占的
要不要挨个问 要,容器越多越慢 不用,谁空谁自己来
中心要不要知道谁闲着 要 不用,中心只管队列

也就是说,第三篇里那两个毛病——问和派之间的竞态、挨个问的开销——这一版一个都没解决,只是从用户眼前挪到了后台。用户不再看见报错了,问题还在原地。

真正的 pull 只要 worker 自己一句阻塞式取任务(Redis 里就是 BRPOP):谁空了谁自己去拿,取到就是自己的,别人拿不到同一个。不需要锁,不需要“谁闲着”这张表,也不需要中心去问。

我把这一段写进文章而不是悄悄改掉,是因为它是这个系列最有价值的一次翻车:“引入了队列”和“改成了拉模型”是两件事,前者只是有了缓冲,后者才消灭了调度冲突。中间那步我当时以为自己做了。

还有一个没实现的东西

提交任务的接口收了一个字段:

task_data = {"task_id": task_id, "pages": pages, "is_vip": is_vip}

is_vip 一路存进队列,然后——没有然后。调度那边只有一条先进先出的队列,谁先来谁先办。优先级这件事,字段收下了,逻辑没写。

这种“接口先留着”的写法本身不算错,但如果不说,它看起来就像功能已经有了。真要做,通常是两条队列(VIP 一条、普通一条),先看 VIP 队列再看普通队列;再讲究一点还要防止普通队列被饿死。

这一步定下来的事

拿到手的是真东西:高峰从“拒绝”变成“排队”,任务不再丢,而且分配器和干活的容器彻底解耦了——分配器现在完全不知道有几个容器。

没拿到的是:调度冲突还在,只是藏起来了。

下一篇是最后一篇:把前面所有部件合起来压一压,看哪些是真的稳,哪些只是没被压到;也说清楚“硬隔离”这一步为什么我停在了理论上。

一句话版本:队列给的是缓冲,不是无锁;把“谁来干”的决定权交给边缘,才是那件真正省事的事。

代码:agent-sandbox-oss/lab5。