远程发布反复中断的时候,最容易得到的结论是:共享浏览器不稳定。页面加载失败,命令静默退出,发布做到一半没有下文,所有现象都像是在指向浏览器本身。可这次我把机器上的状态摊开看,发现问题没有那么神秘。
当时是一台只有16GB内存的机器。浏览器相关进程大约有29个,总占用在4.2–4.7GB之间,其中19个是渲染进程,约占3.6GB。整机可用内存只剩约3.9GB,旁边还有几个日常应用在工作。更关键的是,任务空间一共有6个,其中4个对应的任务进程已经结束,却仍然留在浏览器里。
这件事让我先停下了“是不是浏览器坏了”的判断。**共享浏览器的内存问题,很多时候不是某个进程突然变坏,而是一次任务结束后,资源没有跟着任务一起结束。**如果不处理,下一次发布就会在上一轮残留的基础上继续开空间、起渲染进程,最后表现成随机退出,实际却是生命周期一直没有收口。
先看任务结束后留下了什么
我一开始也想过直接处理内存数字。浏览器占用高,就找出渲染进程;进程太多,就批量结束;扩展看起来不常用,也顺手清掉。这个办法的吸引力在于立刻见效,数字会下降,页面也可能暂时恢复。
但我重新对照任务空间后,发现这只是把结果往后推。发布前查重会临时打开一个空间,发布本身也会打开一个空间。调用完成后,如果没有明确的关闭动作,浏览器不会因为外层脚本退出就替我们判断“这个空间已经没人用了”。空间留着,相关页面和渲染进程就有机会继续留着。
这里最容易误判的是,把“启动任务的脚本结束”当成“任务资源已经释放”。两者不是一回事。任务空间是共享浏览器里的独立状态,调用方必须知道自己创建了什么,并在离开时负责收回。否则,内存增长不是一次异常,而是每次成功执行都会留下的一点债务。
**先治理任务空间的生命周期,是因为它处理的是泄漏源;直接杀进程只是在处理泄漏后的表象。**这个顺序也解释了为什么我没有先把浏览器重启一遍。重启或许能把内存清空,却不能保证下一次调用不再留下同样的空间。
让每次调用都有一条退路
真正的改动不是给某个脚本补一行关闭命令,而是把所有会创建任务空间的调用放到同一种管理方式里。调用开始时登记空间,正常结束时关闭,发生异常时也关闭;如果进程被终止信号打断,或者远程连接中断,也要进入同一条释放路径。
这一步看似是工程细节,实际是在回答一个很朴素的问题:谁拥有这个空间?没有登记,就无法知道空间是哪个任务开的;没有统一释放路径,就只能寄希望于每个分支都记得清理。发布、查重、列表、删除和修改等动作只要会打开空间,就不能靠“通常会走到最后”来保证回收。
我选择集中登记并覆盖正常、异常和中断退出,是因为共享环境里最危险的不是一次失败,而是失败之后还留下了无法解释的状态。正常路径能处理顺利完成,异常路径能处理代码报错,信号路径则处理用户停止、连接断开和进程被外部结束。只有这3类出口都覆盖,租约才算完整。
当时还遇到一个容易被忽略的边界:关闭空间本身也可能超时。这里不能因为清理失败就让主任务再次失败,更不能为了追求“绝对干净”卡住发布。比较稳妥的做法是记录关闭失败,保留后续孤儿清理的机会,让清理动作和业务结果分开。释放优先,但释放失败不应该把已经完成的发布改判为失败。
孤儿空间要清,但不能误伤正在使用的
补上退出清理后,历史上已经留下的空间仍然存在。这就需要第二层机制:判断一个空间是不是孤儿,而不是看到名字像临时空间就全部关闭。
这次的判断依据有两类。名字里带创建任务进程号的空间,如果对应进程已经不存在,可以进入回收名单;名字没有进程号的空间,则需要结合闲置时间和发布锁来判断,不能只凭名称猜测。正在被任务使用的空间必须保留,长驻的网页空间和登录空间也不能纳入普通清理。
自动清理设置成低频运行,每10分钟检查一次。它发现发布锁有人持有,或者仍有浏览器命令在运行,就跳过这一轮。这个限制很重要,因为“看起来暂时没有进程”不一定代表“现在可以动”。清理程序和发布程序如果同时操作同一个共享浏览器,清理本身就可能制造新的中断。
演练时识别出6个孤儿空间:5个来自发布前查重,1个来自发布。这个结果把最初的猜测变成了可核对的证据:主要泄漏确实来自查重空间结束后没有关闭,而不是某个用户应用偷偷占满了内存。自动回收的安全边界不是“尽量多关”,而是“只关能够证明已经失去所有者的空间”。
按机器规格设回收门槛
资源生命周期收住之后,仍然需要处理已经积累的渲染进程。原来的门槛是按大内存机器设置的:浏览器超过12GB,或渲染进程超过40个,或者整机可用内存低于8GB才触发。放到16GB机器上,这些条件几乎不会及时发生。浏览器已经占到4.2–4.7GB,机器却还没有达到旧规则的警戒线。
我选择改成按总内存比例计算,是因为固定数字把机器规格藏起来了。小于32GB的机器,浏览器占总内存超过30%,或整机可用内存低于总内存的15%且渲染进程偏多,就进入回收判断。对16GB机器来说,前一个门槛约为4.8GB,后一个门槛约为2.4GB。大内存机器继续保持原来的行为,避免为了照顾小机器而改变已经稳定的环境。
这并不意味着看到超标就立刻杀进程。发布前体检要先拿到写锁,再检查可用内存;不够时先清理已确认的孤儿空间,再回收闲置渲染进程;如果仍然紧张,就每30秒复查一次,最多等待10分钟。到时间仍不够,也要把低内存结果写入输出并继续按既定流程放行,而不是把“资源不足”伪装成一个无上下文的失败。
回收时保留真正的通信通道
这一步是整个治理里最需要克制的地方。最容易误判的错误捷径,就是批量杀掉所有渲染进程,或者把所有扩展进程都当成可有可无的垃圾。演练时我发现,扩展进程和浏览器自身的界面进程里,可能包含当前任务依赖的通信通道。它们看起来占内存,却不等于可以安全回收。
所以回收名单只包含浏览器自己、并且已经闲置超过5分钟的普通页面渲染进程;扩展进程、浏览器自身界面进程、主进程和服务进程都排除。其他应用也不动,只把它们的占用列出来,交给使用者决定。内存治理的目标是恢复共享资源的可用性,不是为了得到一个更小的进程列表而牺牲正在工作的通道。
最后还增加了空闲重启条件:没有发布锁、没有活跃任务空间,同时浏览器占用超过总内存的35%,才允许平稳退出并重新打开,而且最多每2小时触发一次。它不是定时重启,也不是遇到高占用就强行重启。只有在确认没有工作时,重启才是回收长期碎片和历史状态的手段。
下次我会怎么做
这次之后,我会把共享浏览器的内存问题按4层来处理。第一层看调用是否登记了自己创建的空间,正常、异常和中断是否都能释放;第二层看历史空间能否证明已经成为孤儿,并在锁和活跃命令存在时暂停清理;第三层按总内存比例设门槛,让16GB和64GB机器都在合理的时机发出信号;第四层才是回收闲置渲染进程和空闲重启。
如果再次遇到浏览器变慢或发布退出,我不会先问“能不能把所有进程关掉”,而会先问3件事:哪些空间由这次调用创建,哪些空间已经没有所有者,哪些进程虽然占内存却仍是通信必需。回答不清楚之前,批量清理只会扩大风险。
这件事让我重新意识到,资源治理本质上不是追求某个瞬时的内存数字,而是让每一份资源都能找到自己的开始、使用和结束。一个流程成熟的标志,不是它在空闲机器上跑通一次,而是它在失败、退出和并发时,仍然知道哪些东西可以收回,哪些东西必须留下。这样下一次排查,才不会又从“浏览器是不是坏了”开始。