爬虫的任务队列放在 Redis 里,worker 崩溃了正在处理的任务去哪了?这篇在本机一个独立的 redis-server 上,用 kill -9 逐个试 BRPOP、BLMOVE 加 ack、租约三种取任务方式,以及租约、去重、毒任务这些配套的坑。
200 个任务,3 个 worker(其中 ['w2'] 被信号 [9] 杀掉):完成 199 个,队列里剩 0 个,丢失(既不在队列里也没完成)1 个 ['t50'];t50 被执行了 1 次
用 BRPOP 取任务,任务一出队就只存在于 worker 的内存里。worker 被 kill -9,这个任务就丢了(实验里 200 个任务丢 1 个)。
处理 t50 中途被杀:worker 结束后(死掉的是 ['w2']) 完成 199 个,队列剩 0 个,死掉的 worker 的"正在处理"列表里留着 ['t50']
放回队列、w3 接手后:完成 200 个;t50 被执行 2 次;全部任务共执行 201 次(去重后 200 个任务)
t50 处理完、ack 之前被杀:worker 结束后(死掉的是 ['w2']) 完成 200 个,队列剩 0 个,死掉的 worker 的"正在处理"列表里留着 ['t50']
放回队列、w3 接手后:完成 200 个;t50 被执行 2 次;全部任务共执行 201 次(去重后 200 个任务)把任务移进“正在处理”列表,做完再删除,崩溃时任务还在那个列表里,可以放回队列。代价是可能重复:无法区分“没做完”和“做完了没来得及 ack”,两种情况下实验里都是 201 次开始处理。
(a) 某个 worker 处理 t5 时被杀:最终完成 10/10,丢失 0;t5 两次开始执行相隔 0.66 秒(租约 0.5 秒,监控每 0.1 秒扫一次) (b) 租约 0.5 秒,t0 要处理 0.8 秒,不续约,只让 worker 跑 4 秒:t0 被执行 5 次;监控把到期的任务放回队列 5 次,进了 dead 列表 [];t0 的事件顺序 ['w1:start', 'w1:finish', 'w1:start', 'w1:finish', 'w1:start', 'w1:finish', 'w1:start', 'w1:finish', 'w1:start'] (c) 同上,但处理期间每 0.2 秒续约一次:t0 被执行 1 次;监控把到期的任务放回队列 0 次,进了 dead 列表 [];t0 的事件顺序 ['w1:start', 'w1:finish'] (d) 同 (b),但监控最多把同一个任务放回 3 次,再多就放进 dead 列表:t0 被执行 3 次;监控把到期的任务放回队列 2 次,进了 dead 列表 ['t0'];t0 的事件顺序 ['w0:start', 'w0:finish', 'w0:start', 'w0:finish', 'w0:start', 'w0:finish']
取任务时记一个到期时间,监控把到期的任务放回队列,就不用知道哪个 worker 死了。恢复至少要等一个租约(实验里 0.66 秒,租约 0.5 秒)。
两步取任务,在 RPOP 之后、ZADD 之前崩溃:最终完成 9/10,丢失 1 个 ['t3'](任务不在队列里、也没有租约,监控找不到它) Lua 脚本一次完成"取出 + 记租约",在脚本返回之后(开始处理之前)崩溃:最终完成 10/10,丢失 0 个;t3 靠租约到期被放回(监控放回 1 次)
租约比任务耗时短又不续约,任务做完之前就被放回,每次都被重新取走,没有尽头(实验里 4 秒内被处理 5 次)。worker 处理期间定期续约,或者限制最多放回次数,都能收住。
任务执行次数 9(6 个任务,t0 被执行 4 次);不去重的副作用计数 8;用"已处理集合"做 SADD 去重后的副作用计数 6(= 不同任务数 6)
取任务和记租约分两个命令,中间崩溃任务就丢了(实验里注入崩溃,丢 t3)。用 Lua 脚本在 Redis 里一次做完。
没有重试上限:4.4 秒内 worker 因 t7 崩溃 7 次;t7 被执行 7 次;其他任务完成 9 个;dead 列表 [];队列剩 0 个 最多 3 次(超过就放进 dead 列表):2.6 秒内 worker 因 t7 崩溃 3 次;t7 被执行 3 次;其他任务完成 9 个;dead 列表 ['t7'];队列剩 0 个
至少一次投递会重复处理,副作用要按任务 id 幂等(实验里去重后副作用数等于不同任务数)。处理时必然崩溃的毒任务没有重试上限就会不断杀死 worker(4 秒内 7 次),要有上限并放进 dead 列表。