
一、前期背景与问题定位做公开新闻数据采集的同行应该都有体感浏览器自动化方案的内存问题是长期绕不开的痛点。早年团队一直用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加速、插件、扩展、动画、字体渲染等等。这些功能对采集毫无价值却占了大量基础内存。我们对启动参数做了一轮大刀阔斧的裁剪核心配置如下constbrowserawaitchromium.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-featuresTranslateUI,BlinkGenPropertyTrees,IsolateOrigins]});几个关键参数说明headless: new采用新版无头模式比旧版内存更低行为也更接近真实浏览器--disable-dev-shm-usageDocker环境必加避免共享内存不足导致崩溃--disable-images全局禁用图片加载是内存收益最高的参数之一禁用各类特性和扩展砍掉所有和页面渲染无关的功能模块这一步做完单实例启动内存直接从220MB降到了130MB基础开销砍掉近40%。2.2 路由资源拦截过滤所有无关加载项参数级的图片禁用比较粗暴部分场景会影响内容渲染。更精细的做法是用路由拦截按需过滤资源这也是整体收益最高的一步优化。核心拦截逻辑awaitpage.route(**/*,(route){constresourceTyperoute.request().resourceType();consturlroute.request().url();// 直接拦截媒体、字体类资源if([image,media,font,manifest].includes(resourceType)){returnroute.abort();}// 拦截第三方统计、广告、监控域名constblockKeywords[track,advert,monitor,log,analytics];if(blockKeywords.some(keyurl.includes(key))){returnroute.abort();}route.continue();});拦截规则可以根据站点灵活调整核心原则是和数据提取无关的资源一律不加载。做完这一步单页面的内存增量从25MB降到了6MB左右页面加载速度也从2.1秒缩短到0.8秒内存和性能双重收益。2.3 资源池化复用减少创建销毁开销很多人用Playwright有个坏习惯来一个任务开一个页面甚至一个浏览器用完就关。实际上Browser和BrowserContext的创建销毁开销极大频繁创建不仅慢还容易产生内存碎片。我们采用三级池化方案Browser级单容器只维护1个Browser进程全程复用不频繁重启Context级维护固定大小的BrowserContext池按任务类型隔离避免Cookie串扰Page级每个Context下复用Page对象用完重置状态后归还不销毁重建池化的核心不是永不销毁而是控制销毁频率用复用降低开销。同时给每个资源设置生命周期阈值达到阈值后销毁重建从源头避免累积泄漏。2.4 分级主动回收截断累积泄漏路径无论怎么优化Chromium长期运行总会有内存累积这是内核层面的问题无法完全消除。与其等它涨到OOM不如主动做分级回收用可控的重启替代不可控的崩溃。我们设计了三级回收机制Page级单个页面处理完50个任务后跳转about:blank并强制清理缓存超过100个任务则销毁重建Context级单个上下文处理完200个任务后整体销毁重建清空所有Cookie、存储和缓存Browser级当进程内存超过阈值如180MB或连续运行超过7天优雅重启整个浏览器进程不要迷信手动调用GCV8的垃圾回收不受外部精准控制主动调用效果非常有限。定期重建上下文和页面是最简单也最有效的泄漏截断方案。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只有64MBChromium页面开多了很容易因为共享内存不足崩溃。一开始只加了--disable-dev-shm-usage参数虽然能解决崩溃问题但性能有损耗。最终方案启动容器时指定--shm-size1g分配足够的共享内存比禁用共享内存的方案稳定性更好性能也更高。五、总结与经验沉淀回头看整个优化过程最大的感受是Playwright不是银弹它只是提供了更好的基础真正要做到长期稳定运行还是要靠精细化的配置和工程化的管理。内存优化没有什么黑科技核心思路非常朴素先砍掉不必要的开销再通过复用减少创建销毁的损耗最后用主动回收机制截断累积泄漏。不要追求零泄漏这不现实要学会用可控、可预期的重启替代不可控、随机的崩溃。对于持续采集场景来说稳定性永远是第一位的。再炫的技术跑两天就崩也没有实用价值。把基础参数调对、把资源管好、把监控做全、把回收机制落地才能真正做到无人值守、长期稳定运行。后续我们还会继续优化两个方向一是针对不同站点做差异化的拦截策略进一步降低单页面开销二是引入异常指纹识别提前识别可能导致泄漏的页面主动触发回收。优化是个持续迭代的过程没有终点。合规声明本文所述技术仅用于合法的公开数据采集、安全研究与自有系统对接场景。任何技术都有其适用边界读者在实际应用中请严格遵守《网络安全法》《数据安全法》《个人信息保护法》等相关法律法规尊重平台方的服务协议与知识产权不得用于非法数据抓取、恶意攻击等违规场景。技术本身是中性的如何使用它考验的是每个从业者的职业操守。