apscheduler不能直接用于分布式定时调度,因其默认内存存储导致多实例重复执行任务,缺乏去重、状态同步与失败协调能力;即使使用sqlalchemyjobstore也仅解决持久化,无法避免并发抢占,真正需中心化队列+幂等增量标识+节点只消费不决策。

为什么不能直接用 APScheduler 做分布式定时调度
因为 APScheduler 默认使用内存存储 job,多个进程或机器启动后会各自触发同一任务,导致重复抓取;它也不自带任务去重、状态同步或失败重试协调能力。哪怕加了 SQLAlchemyJobStore,也仅解决持久化,不解决并发抢占(比如两个 worker 同时捞到同一条待爬任务)。
真正需要的是:一个中心化的任务队列 + 可幂等的增量标识 + 爬虫节点只消费、不决策。
- 增量依据必须落到数据源可验证的字段上,例如数据库的
updated_at、API 的last_modified时间戳,或自增id—— 不能依赖本地文件记录“上次爬到哪”,否则节点间无法对齐 - 定时行为应由单一调度器触发(如
celery beat或airflow),而非每个爬虫实例自己time.sleep()或启独立APScheduler - 任务入队前需带明确的分片键(如
site_id或category_hash),避免不同节点争抢同一逻辑分区
用 celery + redis 实现带增量上下文的任务分发
核心是把“该爬什么”和“怎么爬”解耦:celery beat 定期生成参数化任务,celery worker 拿到后查增量边界再执行真实 HTTP 请求。
示例:每 5 分钟触发一次针对某站点的增量检查
from celery import Celery
<p>app = Celery('crawler')
app.conf.broker_url = 'redis://localhost:6379/0'
app.conf.result_backend = 'redis://localhost:6379/1'</p><p>@app.task(bind=True, autoretry_for=(Exception,), retry_kwargs={'max_retries': 3})
def crawl_site_incremental(self, site_id: str):</p><h1>1. 从 redis 读上次最大时间戳</h1><pre class="brush:python;toolbar:false;">last_ts = app.backend.get(f'last_crawl_ts:{site_id}') or '1970-01-01T00:00:00Z'
# 2. 调用 API 获取 last_modified > last_ts 的条目
resp = requests.get(f'https://api.example.com/items?since={last_ts}')
items = resp.json()
# 3. 存储并更新时间戳(原子操作!)
if items:
save_items(items)
app.backend.set(f'last_crawl_ts:{site_id}', items[-1]['updated_at'])
- 务必用
app.backend.set而非普通redis.set,确保与结果后端一致,避免多 worker 写冲突 -
autoretry_for对网络抖动友好,但需配合幂等保存逻辑(如用INSERT ... ON CONFLICT DO NOTHING) - 时间戳格式必须和服务端 API 严格一致,常见坑是 Python
datetime.isoformat()默认带微秒,而某些 API 只认秒级
如何让每次增量请求真正“只拿新数据”
关键不在爬虫代码多聪明,而在上游数据源是否提供可靠、单调递增的变更标记。如果只有 created_at,且允许后台补录旧数据,那它就不适合做增量依据。
- 优先选服务端提供的
cursor或page_token参数,它天然支持断点续爬,比时间戳更准 - 若只能用时间戳,必须在拉取后校验响应头
Last-Modified或响应体中的max_updated_at字段,防止服务端返回乱序数据 - 对数据库直连场景,用
SELECT ... WHERE updated_at > %s ORDER BY updated_at LIMIT 1000,并强制updated_at有索引,否则分页会越来越慢 - 不要在应用层做“取全部再过滤”,尤其当全量数据达百万级时,网络和内存开销远超必要
节点扩容时最容易被忽略的三个点
加机器不等于吞吐线性上涨,瓶颈常藏在共享资源或隐式依赖里。
-
redis连接数打满:每个celery worker默认建多个连接,10 个 worker × 10 连接 = 100+ 连接,redis.conf的maxclients需提前调高 - 数据库写入锁表:所有 worker 并发 INSERT 同一张表,InnoDB 行锁可能升级为表锁,改用
INSERT IGNORE或按site_id分表能缓解 - 时间戳精度丢失:多节点系统时钟未用
ntpd或chrony同步,导致 A 节点写入的时间戳比 B 节点“更旧”,漏爬数据
分布式爬虫的复杂度不在 HTTP 请求本身,而在于各节点对“当前全局状态”的认知是否一致。时间戳、游标、版本号这些看似简单的字段,一旦跨节点异步更新,就容易变成幽灵 bug 的温床。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











