bigywater.tech
爬虫工程 · 35

优雅停止:收到 SIGINT 时在途请求怎么收尾

爬虫要停的时候,在途的请求怎么办?直接按 Ctrl-C,已经发出的请求会被取消,结果丢了;容器发 SIGTERM,默认行为更直接。这篇让真实的爬虫进程收到不同的信号,看每种情况下在途请求、队列里剩余的 URL 和退出时间。

8 对 0
Ctrl-C 默认行为取消的在途请求数,对优雅停止取消的在途请求数
-15
没有处理器时,SIGTERM 让爬虫进程的退出码——没有机会输出任何收尾信息
2025 ms
服务器每个请求 2 秒时,优雅停止从发信号到退出的用时

默认行为

实测 · 本机执行输出
  退出码 0(脚本里捕获了 KeyboardInterrupt,所以是 0),从发信号到退出 19 ms
  进程最后的输出:{'done': 0, 'started': 8, 'left_in_queue': 192, 'log': ['取消:0', '取消:1', '取消:2'], 'nlog': 8}
  进程最后的输出:{'keyboardinterrupt': True}
  服务器共收到 8 个请求;asyncio.run 在收到 SIGINT 时取消了主任务,在途的请求被取消(连接被关闭),队列里剩下的 URL 没有被处理

asyncio.run 收到 SIGINT 会取消主任务,在途的请求一起被取消,进程以 KeyboardInterrupt 收尾。8 个已发出的请求全部白费,已经开始的工作没有保存。

优雅停止

实测 · 本机执行输出
  退出码 0,发信号后 313 ms 退出(服务器每个请求 0.3 秒);完成 8 个,开始过 8 个,队列里还剩 192 个没开始;被取消的请求 0 个
  服务器共收到 8 个请求;完成数 = 开始数 → 没有请求被半路丢掉;剩下的 192 个 URL 还留在队列里,可以保存下来下次再抓

在事件循环里用 loop.add_signal_handler(signal.SIGINT, stop.set) 设一个标志,worker 在取下一个 URL 之前检查它;在途的请求自己做完(8 个全完成、0 个取消),队列里剩下的 192 个 URL 没有被丢弃,可以写进状态文件下次再抓(上一篇的做法)。

SIGTERM 与 SIGKILL

实测 · 本机执行输出
  不处理信号时收到 SIGTERM:退出码 -15(负数表示被该信号杀死),2 ms 退出;进程有没有机会输出收尾信息:False
  不处理信号时收到 SIGKILL:退出码 -9(负数表示被该信号杀死),5 ms 退出;进程有没有机会输出收尾信息:False
  只给 SIGINT 装了处理器时收到 SIGTERM:退出码 -15,2 ms 退出;进程有没有机会输出收尾信息:False

容器、systemd、进程管理器通常先发 SIGTERM 再发 SIGKILL。没有为 SIGTERM 装处理器,进程直接以 -15 退出;SIGKILL 任何代码都拦不住。所以优雅停止的处理器要同时装给 SIGINT 和 SIGTERM。

等多久

实测 · 本机执行输出
  服务器每个请求 0.3 秒:发信号后 322 ms 退出;完成 8 个,队列里剩 192 个
  服务器每个请求 1.0 秒:发信号后 1023 ms 退出;完成 8 个,队列里剩 192 个
  服务器每个请求 2.0 秒:发信号后 2025 ms 退出;完成 8 个,队列里剩 192 个

优雅停止的时间等于在途请求剩下的时间:0.3 秒的请求 322 ms,2 秒的请求 2025 ms。生产里要配一个超时(比如超过 N 秒就取消剩下的),避免一个卡住的请求让进程永远停不下来。

分步演示

优雅停止
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。爬虫是独立的 Python 子进程(aiohttp,8 个 worker 共 200 个 URL,asyncio.Queue);父进程在本机服务器收到第一批 8 个请求(都在途)后立刻用 Popen.send_signal 发信号;“优雅停止”用 loop.add_signal_handler 设 asyncio.Event,worker 取任务前检查;服务器每个请求停 0.3 秒(最后一节试了 1.0、2.0 秒)
    • 没有验证:Windows 上的信号语义(没有 add_signal_handler);给 SIGINT 与 SIGTERM 都装处理器的效果;第二次 Ctrl-C 强制退出的做法;worker 卡在 CPU 计算(而不是等网络)时信号的响应;多进程爬虫里信号如何传给子进程;容器里 PID 1 的信号特殊性(Docker 系列有专门文章,这里没有测)
    京ICP备2026067502号-1京公网安备11010502063487号