直接用celery跑爬虫大概率出问题,因其默认多进程模式与requests等库存在ssl初始化冲突、资源泄漏,且i/o密集型任务用多进程低效;须改用gevent协程池、httpx同步客户端、redis令牌桶限流,并严格配置worker参数。

为什么直接用 celery 跑爬虫任务大概率会出问题?
因为 celery 默认使用多进程(prefork)工作模式,而大多数爬虫库(如 requests、scrapy)在子进程中可能触发 SSL 上下文重复初始化、事件循环冲突或资源句柄泄漏。更关键的是:爬虫本质是 I/O 密集型任务,用多进程反而浪费内存、启动慢、并发上限低。
真正可行的路径是强制 celery 切到单线程 + 异步 IO 模式:
- Broker 必须选
redis或rabbitmq(memory不可用于生产) - Worker 启动时加
--pool=gevent或--pool=eventlet - 对应安装
gevent并 patch 标准库:from gevent import monkey; monkey.patch_all() - 所有 HTTP 请求必须走异步友好的库(如
httpx同步/异步双模,或aiohttp)
celery 任务函数里怎么安全发起 HTTP 请求?
别用 requests.get() —— 它在 gevent 下会阻塞整个协程。必须用原生支持协程的客户端,且显式指定超时和重试逻辑,否则一个卡死请求会让整个 worker 协程挂住。
推荐写法(以 httpx 为例):
@app.task(bind=True, max_retries=3)
def fetch_url(self, url: str):
try:
# httpx.AsyncClient 在 gevent 下可安全同步调用
with httpx.Client(timeout=10.0, follow_redirects=True) as client:
resp = client.get(url, headers={"User-Agent": "Mozilla/5.0"})
return {"status": resp.status_code, "len": len(resp.content)}
except httpx.TimeoutException:
raise self.retry(countdown=2 ** self.request.retries)
except httpx.RequestError:
raise self.retry(countdown=2 ** self.request.retries)
注意点:
- 不要在任务里用
async def+await,celery 当前稳定版不支持原生 async task -
httpx.Client是同步接口但底层兼容 gevent;若硬要用httpx.AsyncClient,得配合asyncio.run(),但会破坏 gevent 协程调度 - 务必设
timeout,否则 DNS 卡住或服务无响应会拖垮整个 worker
如何避免多个爬虫任务互相干扰或被封 IP?
分布式环境下,不同 worker 可能同时请求同一域名,触发反爬限流。Celery 本身不提供请求节流能力,得自己加控制层。
实用方案是结合 redis 实现简单令牌桶:
- 每个域名配独立 key,如
rate_limit:example.com - 任务执行前用
redis.eval()原子脚本判断是否允许请求(Lua 脚本防竞态) - 失败则
self.retry(countdown=1),而不是立刻报错 - 别依赖
celery.rate_limit—— 它只限制任务入队频率,不管下游 HTTP 请求
示例 Lua 脚本逻辑(存在 Redis 中,由 Python 调用):
local key = KEYS[1]
local window = tonumber(ARGV[1]) -- 时间窗口秒数
local limit = tonumber(ARGV[2]) -- 最大请求数
local now = tonumber(ARGV[3])
local count = redis.call("ZCOUNT", key, now - window, now)
if count <h3>部署时 <code>celery worker</code> 启动命令容易漏的关键参数</h3><p>光写 <code>celery -A tasks worker</code> 肯定崩。真实环境必须显式约束资源与行为:</p>
-
--concurrency=20:设成 CPU 核数 × 2~4,别盲目设高——gevent 协程太多反而因调度开销降低吞吐 -
--max-tasks-per-child=1000:防止内存缓慢泄漏(尤其解析 HTML 多的场景) -
--heartbeat-interval=10:让监控系统能及时发现 worker 挂掉 -
--loglevel=INFO:DEBUG 日志在爬虫场景下会产生海量输出,磁盘和 IO 都扛不住 - 必须加
--pool=gevent,且确保gevent已安装并 patch 完毕,否则还是走 prefork
最终典型命令:
celery -A tasks worker --loglevel=INFO --concurrency=32 --pool=gevent --max-tasks-per-child=500 --heartbeat-interval=10
真正麻烦的不是写代码,而是每个 worker 进程的 gevent 环境一致性、HTTP 客户端的阻塞边界、以及 Redis 限流脚本的原子性——这些地方一漏,跑两天后才突然大面积失败。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











