从1到10000 QPS:高并发爬虫优化的真相与反直觉发现

原创
alex 1天前 阅读数 8 #头条

从1到10000 QPS:高并发爬虫优化的真相与反直觉发现

问题诊断:从单线程爬虫到IP封禁的瓶颈

我写过太多爬虫,一开始都是图省事,一个循环套一个requests.get完事。那天照旧,10亿个URL,进度条走得很安详,我泡了杯茶回来一看,5.83it/s。按这个速度,10亿个请求要跑47662个小时,差不多五年半。这不是优化问题,这是方向问题。来源:HN原文

顺手加了个线程池,20个worker,总该快了吧。结果程序跑了几分钟,直接抛JSONDecodeError。我打日志一看,响应体是一张CAPTCHA页面,状态码清一色的403、429、400、412。服务器没打算跟我讲道理,它在门口竖了块牌子:你的IP被限了。加线程池没有解决问题,只是让封禁来得更快一些。来源:HN原文

代理池贵、不稳定、配置麻烦。云厂商IP、家庭宽带重拨、Tor,我挨个试过,要么成本兜不住,要么慢得没法用。IP层面被掐住脖子,什么并发模型都是白搭。这里得说句公道话,不是所有站点都值得这样折腾。HN上有人直接开怼,说别浪费服务器带宽和CPU去抓那些根本没法保存使用的东西,他运营的站点就因为这档子事多掏钱付带宽超限费,还得同时挡住真实用户。来源:HN评论 我承认他说得有一定道理,但我手头这批数据确实有分析价值,而且文章里的数据都做了匿名化处理,仅供学习和参考。另外,原文是中文,英文版是实验性的机器翻译,细节上可能有出入,我读的时候留了个心眼。来源:HN原文

优化路径:并发模型、连接复用与协议升级

先把requests换掉。aiohttp和httpx都试过,异步客户端配合连接池复用,省掉了每次请求都重新建立TCP和TLS握手的开销。这一步见效很快,QPS从个位数跳到了几百。接着处理DNS,阻塞式解析换成aiodns,事件循环不再被DNS查询堵死。再往后是multiprocessing,每个进程跑独立的事件循环,把Python的GIL甩到一边去。来源:HN原文

到这一步,依赖版本开始变得重要。Python 3.10+、httpx 0.23+、uvloop 0.17+,我照着原文的依赖清单配齐,一个都不能少。实测数字摆出来:单线程5.83it/s,asyncio约300QPS,asyncio加多进程八核跑到约2400QPS。最后的10000QPS不是一蹴而就,是压了很久才稳住的。来源:HN原文

这里有个容易踩的坑。异步客户端不是无限并发的,连接数有上限,服务器端也有限制。我一度以为多开几个进程就完事了,结果每个进程的事件循环在连接数逼近上限时照样卡住。连接池预热、请求级重试配指数退避加抖动、动态并发度调整,这三样组合起来,才让QPS稳定在万级。光有并发,没有对连接生命周期的管理,跑不起来。来源:HN原文

反直觉发现:asyncio+HTTP/2反而更慢,正确性优先于速度

最反直觉的事是HTTP/2。原以为多路复用能在长连接场景里占便宜,实测下来,同等并发下QPS反而降了15到20%。服务器端的流控和帧处理开销把多路复用的优势抵消得一干二净。我盯着数据看了半天,确认不是测量误差,是这条路本身不通。来源:HN原文

另一个反直觉的点:在IP没被限流的时候,多线程爬虫比asyncio快。Python线程切换的开销低于异步回调的上下文切换,而且requests的同步API更稳。我花了不少时间调优异步方案,回头一看,简单的东西在特定场景下就是更好使。来源:HN原文

还有连接保活的问题。抓包发现服务器端大约30秒就会主动关闭空闲连接,Keep-Alive timeout到了就掐。我不得不加定期心跳请求来维持连接活性,不然QPS会慢慢掉下去。并发度超过1000之后,QPS不再线性增长,DNS查询和本地文件I/O成了新瓶颈。写磁盘的代码我原本没太在意,后来发现它拖了后腿,把结果写入换成异步I/O才解决。来源:HN原文

写到最后作者自己说了句实在话:如果目标不是极限压榨,稳定采集控制在5000 QPS更合适。超过这个阈值,错误率和成本显著上升,被目标站点列为恶意流量的概率也更高。我读完这篇笔记,把压测目标调回到5000,留点余量给意外情况。10000 QPS是数字上的胜利,5000 QPS才是工程上的理性选择。来源:HN原文


原文:

High-Concurrency Crawler Optimization Notes – From 1 to 10000 QPS · 来源:Hacker News

参考来源:

作者:itfan123 值班编辑 · 发布于 2026年11月3日

版权声明

所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除