采集任务被 OOM 杀掉,上一个渠道的数据一直留在内存里
1800 字约 6 分钟
摘要Job Market Monitor 凌晨的浏览器采集被内核杀掉一个 Chromium 进程。服务器内存还有余量,爆掉的是采集容器自己的 1 GiB 上限。原因是校招采完后完整结果一直被变量引用,直到社招结束才释放。改为每个渠道独立进程加磁盘暂存,在不加内存的前提下完整跑通了近两万个岗位。
Job Market Monitor 每天凌晨 03:15 开始采集,近百个渠道,大部分走普通的 HTTP 请求,少数官网需要用浏览器渲染。采集跑在一个单独的容器里,我给它设了 1 GiB 的内存上限,并且没有开 swap。
9 月 16 日早上,我让助手顺手改一个模型并发的参数,发布时它发现凌晨的浏览器采集出过一次 OOM(内存溢出)。调度器已经恢复,但原因没有查。我让它把原因查清,并想清楚怎么彻底解决。
谁的内存满了
助手先对照了采集日志、内核日志和容器的内存限制。
结论是,满的不是整台服务器,而是这个容器自己的 1 GiB 上限。北京时间 03:36:41,容器达到上限,内核终止了一个 Chromium 的子进程。那一刻整台服务器的内存使用率约 71%,还有余量。当时某大厂的社招刚采到第 110 页左右,内核记录里的主要进程是这样的:
| 进程 | 内存 |
|---|---|
| 采集的 Python 进程 | 453 MiB |
| 被终止的浏览器渲染进程 | 294 MiB |
| Playwright 驱动 | 111 MiB |
| 另一个等待采集的 Python 进程 | 77 MiB |
这几个数字不能直接相加,因为 RSS 里含有共享内存,但足够说明一个问题:调度里原本就限制了同时只开一个浏览器,这并不能限制整个采集任务的内存。浏览器之外,Python 进程本身就占了将近一半。
什么是 OOMOOM 是 Out Of Memory 的缩写。容器设了内存上限之后,进程占用一旦超出,内核会挑一个进程直接杀掉,不会等它自己释放。所以采集任务不是报错退出,而是莫名其妙地少了一个进程。
一个一直没释放的变量
Python 进程为什么会占 453 MiB?助手在采集流程里找到一个很具体的原因:某个渠道的校招采完之后,完整的结果仍然被一个变量引用着,一直保留到同一家公司的社招也采完才释放。也就是说,校招的几千个岗位正文、社招正在累积的岗位正文,还有浏览器,是同时留在内存里的。
它用真实的采集入口配合模拟的连接器复现了这一点:旧结果在强制垃圾回收之后仍然存活。
这也解释了为什么之前的验收没发现问题。之前分别单独跑过校招和社招的实验都没有超限,但生产里是连着跑的,上一个渠道的数据还没放手,下一个渠道就开始了。单独跑每个渠道的实验,会系统性地低估连续运行的内存峰值。
通俗比喻:搬家时不肯清空上一个房间一家人先打扫了客厅,把客厅的东西都搬到了走廊里,说是等全部打扫完再统一处理。接着开始打扫卧室,卧室的东西也往走廊里放。
单独只打扫客厅或者只打扫卧室,走廊都放得下;两个房间连着打扫,走廊就堵死了。
先不加内存
我问助手,如果这样修,是不是就不用增加内存上限了?它的回答是先保持 1 GiB 的限制修,是否真的不用加,以后面的实测为准。提高上限只能增加余量,不能替代对内存生命周期的整理。修复分三部分:
- 每个渠道交给独立的工作进程。 校招采完进程就退出,社招使用全新的进程。调度器只保留进度和结果摘要。这样不仅旧数据被释放,Python 和浏览器驱动自身保留的内存也一起回收了。
- 大列表的岗位正文暂存到磁盘。 采集期间内存里只保留岗位索引和当前页面,岗位正文先写到数据盘上。最终仍然会读取全部岗位,做分页、唯一岗位集合和内容指纹的完整性检查,通过后才入库,不能靠少采数据来省内存。
- 启动新任务前先看内存余量。 余量不足就等待,并在分页的安全边界提前重建浏览器页面,再继续下一页。同时记录各渠道的内存峰值,方便以后判断容量。
在同样的条件下重现
验收的关键是条件要和出事的现场一样。助手在服务器上起了一个隔离的实验容器:
- 同样的 1 GiB 内存上限,不开 swap。
- 某大厂的校招和社招连续采集,同时并行运行另一家公司的校招。
- 采集的结果真正写进一个隔离的数据库,而不是只验证到采集这一步,免得内存问题被推迟到入库阶段才出现。
实验过程中,采集的 Python 进程在岗位正文落盘之后只占 100 MiB 出头,故障时是 453 MiB。校招结束、进程退出后,容器里的匿名内存降到约 23 MiB,之后才启动社招,说明上一个渠道的占用确实被释放了。社招跑到一万多个岗位时,内存也没有随岗位数量继续上涨。
全部跑完的结果:
| 项目 | 结果 |
|---|---|
| 采集并入库的岗位 | 共 19,955 个,包括校招 7,578、社招 11,285 和另一家公司的校招 1,092 |
| 完整性 | 全部字段在暂存到磁盘前后一致,数据库里的岗位集合和内容指纹与采集结果完全一致 |
| 官网抽查 | 校招、社招各抽样打开官网详情,标题、职责、要求和链接均一致 |
| 内存峰值 | 采样到的工作集峰值约 820 MiB,低于 1 GiB |
| 退出情况 | 退出码全为 0,内核的 OOM 计数为 0 |
所以结论是,在当前规模下,保持 1 GiB 的上限就够,不需要增加内存。生产上的采集容器上限、画像并发数和每天 03:15 的计划都没有改变。
记在文档里的验收要求
这次最大的教训,不是某个变量没释放,而是之前验收的方式有漏洞。以后涉及采集流程的改动,必须测试连续多个渠道、其他渠道并行、以及入库阶段,并且要检查内核的 OOM 计数,不能把“单个渠道能跑完”当成“整个定时任务稳定”。这些要求已经写进了项目文档。
稳定不等于永远不会再爆磁盘暂存是用少量的读写换内存,并不会让内存占用消失。浏览器自身的内存仍然有波动。数据规模增加,或者官网页面变得更重时,这个结论就要重新验证。新加的内存采样日志是为了让下一次判断有依据。