从「收录」到「收崩」:亿级网页索引标记差点把爬虫集群整崩

发布时间:2026/8/20 23:47:44
从「收录」到「收崩」:亿级网页索引标记差点把爬虫集群整崩 部分情节为虚构演绎仅供参考说实话我们团队维护着一个站内搜索引擎爬虫集群每天要抓取上亿个网页。每个网页在抓取流水线里要打一堆布尔标记有没有被抓取过、有没有被索引、内容有没有更新、有没有被 robots 协议禁止、是不是死链、需不需要重新抓取。这些标记本质上就是一堆 True 和 False。听着挺简单对吧不就是给每个 URL 贴几个布尔标签嘛能有多难但你猜怎么着现实啪啪打脸网页量一涨这堆布尔标记直接把爬虫调度器整崩了——不是收录的「收」是崩溃的「崩」。抓取队列积压、重复抓取风暴、索引更新延迟整个搜索结果新鲜度从小时级退化到天级。越想精准收录越把集群收崩。这大概是我做搜索引擎以来最反直觉的一段经历明明每一步都在往「更省内存、更快调度」的方向走结果却是一步一个坑。直到我放弃自己造轮子才发现这个问题的正确解法。2. 从 set 到位图数据库五种方案轮番翻车2.1 用 set 存已抓取 URL内存直接爆最开始用最朴素的方案拿 set 存已抓取和待抓取的 URLcrawled_urlsset()indexed_urlsset()dead_linksset()need_recrawlset()单条 URL 平均长度 60 字节进了 set 加上哈希表开销至少 150 字节。1 亿个 URL 光一个 crawled set 就要 15GB四个 set 就是 60GB。而且每次调度器要查「哪些 URL 已抓取但未索引」得做集合差运算几亿条数据的 set 差集慢得要死。2.2 用 list[bool] 按 URL 序号标记指针的狂欢后来给每个 URL 分配一个整数 ID用 list 存布尔标记is_crawled[False]*500_000_000# 5亿网页is_indexed[False]*500_000_000Python list 存的是指向 PyObject 的指针每个指针 8 字节5 亿元素光指针就 4GB一个标记 4GB四个标记 16GB。服务器内存直接 OOM。2.3 用 bytearray省了内存但调度慢换成 bytearray每个标记 1 字节is_crawledbytearray(500_000_000)# 500MBis_indexedbytearray(500_000_000)# 又500MB四个标记 2GB还是太大。而且每次调度器要找「已抓取但未索引且非死链」的 URL得同时遍历三个 bytearray 做与运算Python 层循环慢得要死一次调度扫描要好几分钟抓取队列早就空了。2.4 用 numpy 矩阵调度快但动态扩容灾难换成 numpy 把所有标记排成矩阵importnumpyasnp# 5亿网页 × 6个标记位 30亿字节 ≈ 3GBflagsnp.zeros((500_000_000,6),dtypenp.bool_)向量化调度确实快一次与运算几秒钟。但新 URL 不断被发现numpy 定长数组扩容要全量拷贝 3GB。更要命的是大部分网页已经稳定不再变化只有不到 5% 需要重新抓取numpy 不管稀疏密集照单全收3GB 里 95% 都是 False纯浪费。2.5 自己写混合标记数组踩坑十二天前面的方案都不满意我决定自己写一个混合数组——活跃 URL 用位图稳定 URL 只存需要重抓的下标。听起来很美好然后就踩了十二天坑。2.6 小结方案5亿网页6标记内存调度扫描新增URL稀疏适配set 存URL60GB慢快天然稀疏list[bool]16GB慢快无bytearray3GB慢快无numpy 矩阵3GB快灾难无自写混合数组理论几十MB自己写自己写有五条路走下来内存墙、调度墙、维护成本墙三面夹击。3. 破局思路给索引系统装个自动变速箱3.1 内存墙省内存为什么等于快调度很多人以为省内存只是省钱。但在爬虫调度器里省内存直接决定调度速度。每次调度要扫描「已抓取但未索引的网页」如果标记数据在 CPU 缓存里一次扫描就是毫秒级如果在主存里就是百毫秒级如果还要跨网络去 Redis 查就是秒级。数据量越大缓存命中率越低调度越慢。省内存的本质是让更多数据塞进 CPU 缓存减少主存访问次数。时间和空间不是守恒关系省内存恰恰能同时提速。3.2 自动变速箱构想盯着这个问题想了好几天我突然想到为什么不能让布尔数组像汽车变速箱一样自动切换抓取活跃的时候比如新站上线大量页面待抓用位图紧凑存储调度快抓取稳定的时候大部分页面已收录不再变化只存需要重抓的下标省内存活跃度变化时自动换挡——但换挡只在两个时机发生创建数组时和调用 optimize() 时。平时标记 URL 都不换挡避免来回抖动。高 大量待抓低 稳定收录URL发现抓取活跃度位图模式 1bit/URL下标模式 只存待重抓爬虫调度器索引更新我越想越觉得靠谱当晚就开干。然后就踩了十二天坑。4. 自己写踩了十二天坑第一天写了个能跑的混合标记类稀疏场景内存只要几 MB觉得自己是天才。第二天加了密集模式阈值写死 10%活跃度在阈值附近波动时疯狂来回切换调度性能比不切还差。第三天加了滞回区间防抖结果阈值判断和实际存储对不上标记写串了已索引的被当成未索引重复抓。第四天稀疏区用 array(‘I’) 存 URL 下标下标越界不报错静默溢出排查了一整天。第五天批量标记接口写完发现「按位置标记」和「按值过滤标记」两个语义写串了。第六天按位取反已索引翻转成未索引写完count(True) 数字对不上——稀疏区取反后忘了翻转特殊值。第七天支持 in 运算符判断某 URL 是否已抓取结果每次全量扫描5 亿 URL 查一次好几秒。第八天统计待抓取数量的方法数字忽大忽小——缓存了统计结果但标记变更时缓存没失效。第九天自动换挡函数写完换挡瞬间全量重建内部结构大批量新 URL 涌入时卡了几百毫秒调度超时。第十天支持 pickle 序列化内部结构太复杂存进去读出来数据全乱。第十一天查找第一个待索引 URL 的位置稀疏区返回的是下标表位置而不是真实 URL ID差了好几个量级。第十二天盯着 2000 多行代码发现多线程安全、内存对齐、GC 压力全没处理心态崩了。最崩溃的是第十三天早上我意识到自己犯了一个根本性错误我把换挡做成了每次标记变化都可能触发的高频动作结果活跃度一波动就疯狂重建内部结构。正确做法是换挡只在创建时和 optimize() 时发生平时操作只在当前挡位内进行。从零实现一个生产可用的混合布尔数组真不是一个人两个月能干完的事。我决定去社区求助。5. 转机发帖求助评论区集体推荐同一个库我把踩坑经历整理成帖子发到技术社区标题是「5 亿网页的抓取索引标记set 爆内存、numpy 爆拷贝、Redis 爆延迟怎么办」评论区画风出奇一致所有人都在推荐同一个库bool-hybrid-array。其中一条评论直接点醒了我「你那个自动变速箱构想bool-hybrid-array 早就实现了。换挡只在创建时和调用 optimize() 时发生平时插入查询都不换挡所以不会抖。你之前的问题是把换挡做成了高频动作。」对啊换挡本来就该是低频的创建时根据初始数据定好挡位平时就在这个挡位里干活只有活跃度发生大变化时才手动调一次 optimize()。这才是自动变速箱的正确打开方式。评论区还提到「直接 pip install bool-hybrid-array你这个场景它天生就是为这个设计的。」「我用 numpy 存 URL 标记内存爆了换它之后稀疏场景内存降了 90% 以上。」「memory_usage(detailTrue) 可以看详细内存占用数字不会骗人。」「我生产环境跑了半年爬虫调度标记就是它的主场稳得很。」「密集区用位图、稀疏区只存下标两边都是成熟方案不是野路子。」「月下载量过万迭代了 100 多个版本不是课程作业。」「支持 numpy 直接转换np.array(arr) 一行接进现有数据管道。」「MIT 协议商用随便用。」「Python 3.9 到 3.14 全支持PyPy 也没问题。」「find 和 rindex 在稀疏区返回真实位置不是下标表位置。」我动手验了一下frombool_hybrid_arrayimportBoolHybridArr# 5亿网页只有5%需要重新抓取need_recrawlBoolHybridArr(i%200foriinrange(500_000_000))print(need_recrawl.memory_usage(detailTrue))跑出来的数字稀疏场景下 5 亿布尔值只占十几 MB比 numpy 的 500MB 省了 90% 以上。我用 tracemalloc 独立验证过误差在 1% 以内。但 memory_usage 是库自己算的不是第三方审计的。我只能保证我这边对得上你那边请自己测。别信我也别信它信你自己的测量。6. 同类方案横向对比6.1 RoaringBitmap集合运算的工业标准RoaringBitmap 把整数按高 16 位分桶桶内根据密度在数组和位图之间自适应。它在 URL ID 去重、已抓取集合等场景下是工业标配集合运算并交差极快。但它不是数组没有 arr[i] 按位置访问的语义不支持 append/pop不保留数组长度和顺序。如果你的需求是「维护一个完整的、按 URL ID 排列的布尔标记序列」它的集合语义就不对味了。6.2 bitarray 和 pyarrowbitarray 把每个布尔值压成 1bit5 亿元素约 62.5MB保留数组语义但定长且无稀疏优化。pyarrow.BooleanArray 同样位压缩强在列式存储和跨语言但数组不可变每次修改都要重建。6.3 对比表方案5亿 bool 内存5% 稀疏数组语义动态追加稀疏自适应集合运算典型场景list[bool]4GB有有无无小规模原型numpy500MB有无无向量化密集定长数值计算bitarray62.5MB有麻烦无位运算密集位压缩pyarrow62.5MB有无无有列式存储跨语言RoaringBitmap约 25MB无集合语义add/remove有极强URL ID集合运算bool-hybrid-array约 25MB有有有有但非主场动态布尔数组稀疏密集自适应6.4 缺点与适用边界第一optimize() 是低频操作频繁手动调用会导致全量重建抖动问题会回来。第二换挡瞬间是 O(n) 全量拷贝大规模数据可能上百毫秒。第三非线程安全多线程要自己加锁。第四生态年轻没有 RoaringBitmap 十年工业验证。第五均匀分布 50/50 时和 numpy 打平没有优势。第六memory_usage 是自报数据生产前请用 tracemalloc 自己验。适用场景稀疏动态更新单线程数组语义四个条件同时满足时最优。纯集合运算用 RoaringBitmap均匀定长用 numpy。bool-hybrid-array 的作者承诺现有公开接口不会删除no removal policy但行为细节可能随版本变化上生产前务必在自己的数据上验证。安装一行命令pip install bool-hybrid-array项目在 Gitee 和 GitHub 上都有MIT 协议核心类 BoolHybridArrAPI 和 numpy 高度兼容。别信我信你自己的测量。