Backend

采集任务被 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 进程本身就占了将近一半。

什么是 OOM

OOM 是 Out Of Memory 的缩写。容器设了内存上限之后,进程占用一旦超出,内核会挑一个进程直接杀掉,不会等它自己释放。所以采集任务不是报错退出,而是莫名其妙地少了一个进程。

一个一直没释放的变量

Python 进程为什么会占 453 MiB?助手在采集流程里找到一个很具体的原因:某个渠道的校招采完之后,完整的结果仍然被一个变量引用着,一直保留到同一家公司的社招也采完才释放。也就是说,校招的几千个岗位正文、社招正在累积的岗位正文,还有浏览器,是同时留在内存里的。

它用真实的采集入口配合模拟的连接器复现了这一点:旧结果在强制垃圾回收之后仍然存活。

这也解释了为什么之前的验收没发现问题。之前分别单独跑过校招和社招的实验都没有超限,但生产里是连着跑的,上一个渠道的数据还没放手,下一个渠道就开始了。单独跑每个渠道的实验,会系统性地低估连续运行的内存峰值。

通俗比喻:搬家时不肯清空上一个房间

一家人先打扫了客厅,把客厅的东西都搬到了走廊里,说是等全部打扫完再统一处理。接着开始打扫卧室,卧室的东西也往走廊里放。

单独只打扫客厅或者只打扫卧室,走廊都放得下;两个房间连着打扫,走廊就堵死了。

先不加内存

我问助手,如果这样修,是不是就不用增加内存上限了?它的回答是先保持 1 GiB 的限制修,是否真的不用加,以后面的实测为准。提高上限只能增加余量,不能替代对内存生命周期的整理。修复分三部分:

  1. 每个渠道交给独立的工作进程。 校招采完进程就退出,社招使用全新的进程。调度器只保留进度和结果摘要。这样不仅旧数据被释放,Python 和浏览器驱动自身保留的内存也一起回收了。
  2. 大列表的岗位正文暂存到磁盘。 采集期间内存里只保留岗位索引和当前页面,岗位正文先写到数据盘上。最终仍然会读取全部岗位,做分页、唯一岗位集合和内容指纹的完整性检查,通过后才入库,不能靠少采数据来省内存。
  3. 启动新任务前先看内存余量。 余量不足就等待,并在分页的安全边界提前重建浏览器页面,再继续下一页。同时记录各渠道的内存峰值,方便以后判断容量。

在同样的条件下重现

验收的关键是条件要和出事的现场一样。助手在服务器上起了一个隔离的实验容器:

  • 同样的 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 计数,不能把“单个渠道能跑完”当成“整个定时任务稳定”。这些要求已经写进了项目文档。

稳定不等于永远不会再爆

磁盘暂存是用少量的读写换内存,并不会让内存占用消失。浏览器自身的内存仍然有波动。数据规模增加,或者官网页面变得更重时,这个结论就要重新验证。新加的内存采样日志是为了让下一次判断有依据。

评论