告别Selenium!Playwright无头模式内存优化70%,某新闻站持续采集30天
2026/7/25 13:27:20 网站建设 项目流程

一、前期背景与问题定位

做公开新闻数据采集的同行应该都有体感,浏览器自动化方案的内存问题是长期绕不开的痛点。早年团队一直用Selenium Grid做分布式采集,十几个实例跑起来服务器内存直接飙满,只能靠定时重启临时救场,既丢任务又增加运维成本。后来全面切换到Playwright,本以为能一劳永逸,结果默认配置跑了一周,容器还是接连OOM,内存每天稳步上涨几十兆,累积下来照样崩溃。

那段时间半夜经常收到内存告警,重启完没两天又告警,治标不治本。索性花了两周时间做专项优化,从浏览器启动参数、资源拦截、池化复用到主动回收,一层层抠内存开销,最终单实例稳态内存下降70%,在某头部新闻站点的持续采集任务中,稳定运行30天无异常重启,任务完成率稳定在99.2%。

1.1 Selenium方案的固有瓶颈

Selenium的内存问题不是配置能完全解决的,本质是架构层面的先天不足:

  • 驱动层中转开销大,每一次页面操作都要经过驱动进程转发,内存和CPU双重损耗
  • 页面销毁不彻底,DOM引用、JS句柄经常残留,累积下来就是不可逆的内存泄漏
  • 多实例管理粗糙,浏览器进程、驱动进程、调度进程三层叠加,资源利用率极低
  • 长期运行稳定性差,普遍12-24小时就需要重启一次实例,不适合无人值守的持续采集

1.2 切换Playwright后的新问题

刚切换Playwright的时候,初始内存表现确实比Selenium好一大截,单实例启动内存直接从380MB降到220MB。但跑了一周就发现问题:内存一直在缓慢上涨,日均涨幅4%左右,从来没有回落的迹象,跑到第8天容器内存就突破了1G,触发OOM。

很多人有个误区:觉得换了Playwright就天然解决内存问题。实际上它只是基础开销更低,泄漏问题依然存在,只是爆发周期更长。对于需要7×24小时运行的持续采集场景,默认配置远远不够。

1.3 内存开销拆解与排查思路

优化的第一步不是瞎改参数,而是先搞清楚内存都耗在了哪里。我们用Chrome DevTools的Memory面板结合容器监控,拆解出了五大开销来源:

45%25%15%10%5%Playwright无头模式内存占比页面资源(图片/广告/脚本)Chromium内核基础开销JS执行与DOM累积Playwright对象与连接缓存与存储累积

占比最大的其实是页面资源:新闻页的图片、广告、第三方统计脚本,占了将近一半的内存开销。其次是Chromium默认开启的大量无关特性,纯采集场景根本用不上。真正由业务逻辑导致的泄漏占比并不高,但长期累积下来就是压垮容器的最后一根稻草。

二、分步实操:五层内存优化落地

整个优化遵循「先砍大头、再控泄漏、最后做回收」的思路,从外到内分五层推进,每一层都有明确的收益和可落地的配置。

2.1 启动参数裁剪:砍掉非必要内核特性

Chromium默认是给普通用户浏览网页设计的,开启了大量和采集无关的特性:GPU加速、插件、扩展、动画、字体渲染等等。这些功能对采集毫无价值,却占了大量基础内存。

我们对启动参数做了一轮大刀阔斧的裁剪,核心配置如下:

constbrowser=awaitchromium.launch({headless:'new',args:['--disable-gpu','--disable-software-rasterizer','--disable-dev-shm-usage','--no-sandbox','--disable-extensions','--disable-plugins','--disable-images','--disable-audio','--disable-video','--mute-audio','--no-first-run','--no-default-browser-check','--disable-features=TranslateUI,BlinkGenPropertyTrees,IsolateOrigins']});

几个关键参数说明:

  • headless: 'new':采用新版无头模式,比旧版内存更低,行为也更接近真实浏览器
  • --disable-dev-shm-usage:Docker环境必加,避免共享内存不足导致崩溃
  • --disable-images:全局禁用图片加载,是内存收益最高的参数之一
  • 禁用各类特性和扩展,砍掉所有和页面渲染无关的功能模块

这一步做完,单实例启动内存直接从220MB降到了130MB,基础开销砍掉近40%。

2.2 路由资源拦截:过滤所有无关加载项

参数级的图片禁用比较粗暴,部分场景会影响内容渲染。更精细的做法是用路由拦截,按需过滤资源,这也是整体收益最高的一步优化。

核心拦截逻辑:

awaitpage.route('**/*',(route)=>{constresourceType=route.request().resourceType();consturl=route.request().url();// 直接拦截媒体、字体类资源if(['image','media','font','manifest'].includes(resourceType)){returnroute.abort();}// 拦截第三方统计、广告、监控域名constblockKeywords=['track','advert','monitor','log','analytics'];if(blockKeywords.some(key=>url.includes(key))){returnroute.abort();}route.continue();});

拦截规则可以根据站点灵活调整,核心原则是:和数据提取无关的资源一律不加载。做完这一步,单页面的内存增量从25MB降到了6MB左右,页面加载速度也从2.1秒缩短到0.8秒,内存和性能双重收益。

2.3 资源池化复用:减少创建销毁开销

很多人用Playwright有个坏习惯:来一个任务开一个页面甚至一个浏览器,用完就关。实际上Browser和BrowserContext的创建销毁开销极大,频繁创建不仅慢,还容易产生内存碎片。

我们采用三级池化方案:

  1. Browser级:单容器只维护1个Browser进程,全程复用,不频繁重启
  2. Context级:维护固定大小的BrowserContext池,按任务类型隔离,避免Cookie串扰
  3. Page级:每个Context下复用Page对象,用完重置状态后归还,不销毁重建

池化的核心不是永不销毁,而是控制销毁频率,用复用降低开销。同时给每个资源设置生命周期阈值,达到阈值后销毁重建,从源头避免累积泄漏。

2.4 分级主动回收:截断累积泄漏路径

无论怎么优化,Chromium长期运行总会有内存累积,这是内核层面的问题,无法完全消除。与其等它涨到OOM,不如主动做分级回收,用可控的重启替代不可控的崩溃。

我们设计了三级回收机制:

  • Page级:单个页面处理完50个任务后,跳转about:blank并强制清理缓存,超过100个任务则销毁重建
  • Context级:单个上下文处理完200个任务后,整体销毁重建,清空所有Cookie、存储和缓存
  • Browser级:当进程内存超过阈值(如180MB),或连续运行超过7天,优雅重启整个浏览器进程

不要迷信手动调用GC,V8的垃圾回收不受外部精准控制,主动调用效果非常有限。定期重建上下文和页面,是最简单也最有效的泄漏截断方案。

2.5 业务逻辑精简:消除隐性内存占用

很多内存问题其实是业务代码写得不规范导致的,几个常见的隐性坑:

  • 无脑等待load事件,其实只需要等目标元素出现即可,不用等所有资源加载完成
  • JSHandle、ElementHandle用完不释放,持有引用导致页面DOM无法回收
  • 页面内注入的脚本创建了定时器、闭包,页面跳转后没有清理
  • 大量使用page.evaluate返回复杂对象,底层序列化开销大

对应优化原则:用最小粒度的等待条件,用完的句柄及时调用dispose()释放,注入脚本做好清理工作,尽量减少跨进程的大数据传输。

三、工程化架构与运行效果

参数优化只是基础,要做到30天持续稳定运行,还要配套完整的工程化架构。

3.1 持续采集服务架构设计

我们最终落地的是四层架构,从任务调度到资源回收全链路闭环:

采集执行层

浏览器资源池层

任务调度层

监控回收层

内存实时监控

阈值触发回收

异常自动重启

运行指标上报

任务队列

任务分发器

失败重试机制

Browser实例管理

BrowserContext池

Page对象池

资源健康检查

路由拦截与加载

数据提取逻辑

结果持久化

几个关键设计点:

  • 按站点分Context池,不同站点资源隔离,避免Cookie和缓存串扰
  • 资源池支持弹性伸缩,高峰自动扩容,低谷自动回收空闲资源
  • 内存监控独立于业务逻辑,达到阈值优雅重启,不影响正在执行的任务
  • 全链路指标监控,内存、成功率、超时率实时上报,异常自动告警

3.2 优化前后核心指标对比

我们在相同硬件、相同任务量下做了横向对比,核心数据如下:

指标Selenium GridPlaywright 默认配置Playwright 优化后
单实例初始内存~380MB~220MB~85MB
单页面稳态内存增量~40MB/页~25MB/页~6MB/页
运行7天稳态内存>2GB崩溃~850MB~150MB
日均内存增长率~15%~4%<0.2%
平均无故障运行时长12-24小时5-7天>30天
单页面平均加载耗时~3.2s~2.1s~0.8s

整体稳态内存从850MB降到150MB,降幅超过82%;算上初始内存的综合降幅约70%,完全达到了预期目标。

3.3 30天线上运行数据

这套方案部署在某新闻站采集项目上,连续运行30天的核心表现:

  • 单容器配置2核2G,共部署4个实例,日均处理约1.2万条新闻数据
  • 内存稳定在120-180MB区间波动,从未触发OOM
  • 任务成功率99.2%,失败主要由网络波动导致,无浏览器层面的崩溃
  • 期间仅执行例行的Context重建和每周一次的Browser优雅重启,无异常重启记录
  • 服务器整体资源占用下降60%,相同配置下能承载的任务量翻了三倍

四、问题排查与踩坑实录

整个优化过程踩了不少实战坑,很多问题是文档里不会写的,这里整理几个最典型的。

4.1 新旧无头模式的兼容性差异

一开始图省事用了旧版headless: true,内存高、特征明显。换成新版headless: 'new'之后,内存降了一截,但部分站点的正文内容提取失败。排查后发现,新版无头模式的默认视口参数、渲染行为和旧版有差异,部分懒加载逻辑触发条件不一样。

解决方案:统一显式指定视口大小和设备缩放比,不要依赖默认值,确保渲染行为一致。

4.2 上下文复用的数据串扰问题

早期复用Context的时候,不同频道的采集任务出现了数据串扰,比如A频道的Cookie跑到了B频道的请求里,导致返回内容异常。原因是任务归还时没有彻底清理存储状态,残留的Cookie和localStorage带到了下一个任务。

解决方案:按任务类型分池,每个任务归还前强制清空所有存储,再跳转到about:blank等待状态完全重置,确认干净后再归还池。

4.3 资源拦截导致的内容丢失

一开始把所有图片都拦截了,结果部分新闻站的正文内容是靠图片懒加载触发渲染的,图片不加载,正文DOM就不会完整生成,导致提取到的内容缺斤短两。

解决方案:不搞一刀切拦截,改为先等待目标正文元素渲染完成,再拦截后续的图片资源;或者对关键站点放开首屏图片,只拦截非正文区域的媒体资源,在内存和数据完整性之间做平衡。

4.4 WebSocket长连接泄漏陷阱

排查内存缓慢上涨问题的时候,发现了一个很隐蔽的泄漏点:新闻站普遍有实时推送、评论刷新的WebSocket长连接,页面跳转后连接没有正常断开,一直挂在内存里,时间越久累积越多。

解决方案:路由拦截中加入WebSocket规则,非必要的长连接直接阻断;每次页面跳转前,主动执行脚本清理所有定时器和连接,避免残留。

4.5 Docker环境的共享内存坑

Docker默认的/dev/shm只有64MB,Chromium页面开多了很容易因为共享内存不足崩溃。一开始只加了--disable-dev-shm-usage参数,虽然能解决崩溃问题,但性能有损耗。

最终方案:启动容器时指定--shm-size=1g,分配足够的共享内存,比禁用共享内存的方案稳定性更好,性能也更高。

五、总结与经验沉淀

回头看整个优化过程,最大的感受是:Playwright不是银弹,它只是提供了更好的基础,真正要做到长期稳定运行,还是要靠精细化的配置和工程化的管理。

内存优化没有什么黑科技,核心思路非常朴素:先砍掉不必要的开销,再通过复用减少创建销毁的损耗,最后用主动回收机制截断累积泄漏。不要追求零泄漏,这不现实,要学会用可控、可预期的重启,替代不可控、随机的崩溃。

对于持续采集场景来说,稳定性永远是第一位的。再炫的技术,跑两天就崩也没有实用价值。把基础参数调对、把资源管好、把监控做全、把回收机制落地,才能真正做到无人值守、长期稳定运行。

后续我们还会继续优化两个方向:一是针对不同站点做差异化的拦截策略,进一步降低单页面开销;二是引入异常指纹识别,提前识别可能导致泄漏的页面,主动触发回收。优化是个持续迭代的过程,没有终点。

合规声明

本文所述技术仅用于合法的公开数据采集、安全研究与自有系统对接场景。任何技术都有其适用边界,读者在实际应用中请严格遵守《网络安全法》《数据安全法》《个人信息保护法》等相关法律法规,尊重平台方的服务协议与知识产权,不得用于非法数据抓取、恶意攻击等违规场景。技术本身是中性的,如何使用它,考验的是每个从业者的职业操守。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询