asyncio+httpx爬虫在DigitalOcean VPS处理多关键词时挂死
问题现象
基于asyncio、httpx开发的Google广告爬虫,本地环境运行完全正常;部署至配置为8GB内存、4核AMD vCPU、25GB磁盘、Ubuntu 20.04系统的DigitalOcean VPS后,处理50个以上关键词时会无响应挂死,无任何错误日志输出。用户通过POST接口上传关键词CSV即可触发爬虫任务。
现有技术栈与配置
- 后端框架:FastAPI,采用Redis Queue(RQ)将爬虫任务放入后台执行以规避请求超时,已配置RQ队列
default_timeout=-1 - 爬虫逻辑:通过ScraperAPI代理请求Google Shopping搜索结果,解析广告落地页域名后导出CSV文件
- 服务部署:Web服务使用Gunicorn+UvicornWorker部署,已配置服务超时时间为0
- 请求配置:代码中已设置httpx请求
timeout=None,内置3次请求重试逻辑
已完成排查
- 排除API层问题:在VPS上单独直接运行爬虫脚本,可复现相同挂死问题
- 现象确认:爬虫固定在处理最后几个关键词时挂起数小时无进展,无报错输出
排查思路与解决方案
快速定位手段
先不要盲目改配置,第一时间定位卡点:
- 执行
pip install py-spy安装采样工具 - 等爬虫挂死时,执行
ps aux | grep 爬虫脚本名拿到进程PID - 执行
py-spy dump --pid <PID>直接打印所有协程、线程的调用栈,可直接看到代码具体卡在哪一行,是等待网络响应、锁竞争还是阻塞IO。
高频根因与修复
1. 无限超时配置导致半开连接永久卡死
timeout=None是当前配置里最高危的问题:
- VPS到代理服务器的公网链路存在丢包概率,代理侧触发风控、长连接静默断开时,TCP连接不会主动发送断开标记,客户端会永久等待对端返回数据,既不报错也不触发重试,刚好对应无日志挂死的现象
- 修复方案:
- 移除
timeout=None配置,给所有请求设置合理超时阈值,参考配置:timeout=httpx.Timeout(20.0, connect=10.0) - 为TCP连接开启keepalive探测,及时断开死连接,参考配置:
import socket import httpx transport = httpx.AsyncHTTPTransport( socket_options=[ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60), (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10), (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3), ] ) # 全局复用这一个client实例,不要每个请求新建client client = httpx.AsyncClient(transport=transport, timeout=httpx.Timeout(20.0))
- 移除
2. 无限制并发触发连接池耗尽/句柄泄漏
- 本地网络到代理延迟低,连接周转快,50个并发可以正常跑;VPS公网延迟高,连接占用时间长,httpx默认连接池大小、系统默认文件描述符上限很容易被打满,后续请求会永久等待可用连接,表现为挂死
- 如果每个请求都新建一个AsyncClient实例、请求结束后没有显式关闭响应,会进一步加剧连接泄漏
- 修复方案:
- 全局复用同一个AsyncClient实例,不要为单个请求新建客户端
- 用
asyncio.Semaphore限制爬虫最大并发数,根据代理QPS限制设置为10-20即可,不要用asyncio.gather一次性扔出所有请求 - 调整httpx连接池配置适配并发量:初始化AsyncClient时传入
limits=httpx.Limits(max_connections=50, max_keepalive_connections=20) - 临时执行
ulimit -n 65535调高系统文件描述符上限,同时将配置写入/etc/security/limits.conf持久化,避免高并发下句柄耗尽
3. 同步代码阻塞asyncio事件循环
- 如果在协程里直接跑同步IO操作(比如同步写CSV、用同步版BeautifulSoup做解析、文件读写),会阻塞整个事件循环,所有协程都无法调度
- 本地测试数据量小、执行快,阻塞感知不明显;VPS上资源调度延迟高,最后几个任务刚好卡到阻塞点就会完全停住
- 修复方案:所有同步阻塞逻辑用
await asyncio.to_thread(同步函数, 参数)扔到线程池执行,不要直接在协程上下文里调用同步方法。
4. 系统层异常无感知
- VPS内存不足时OOM Killer会静默杀掉进程,RQ fork模式运行任务时也可能出现子进程僵死、事件循环锁死的问题
- 排查方式:执行
dmesg | grep -i oom查看系统日志,确认有没有进程被OOM强杀的记录 - 修复方案:RQ worker启动时加上
--worker-class rq.worker.SimpleWorker参数,禁用fork模式跑任务,规避fork导致的asyncio事件循环异常。
内容的提问来源于stack exchange,提问作者dougj
相关产品推荐
相关产品推荐

