<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>SIGBUS on Zero Day Notes</title>
        <link>https://blog.5772447.xyz/tags/sigbus/</link>
        <description>Recent content in SIGBUS on Zero Day Notes</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh</language>
        <lastBuildDate>Tue, 20 Oct 2026 09:00:00 +0800</lastBuildDate><atom:link href="https://blog.5772447.xyz/tags/sigbus/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>容器共享内存默认 64MB：浏览器进程频繁崩溃</title>
            <link>https://blog.5772447.xyz/posts/9c7278d1/</link>
            <pubDate>Tue, 20 Oct 2026 09:00:00 +0800</pubDate>
            <guid>https://blog.5772447.xyz/posts/9c7278d1/</guid>
            <description>&lt;h2 id=&#34;问题背景&#34;&gt;&lt;a href=&#34;#%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;问题背景&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;报表中心有一套 PDF 生成服务：业务系统把报表模板和参数交给它，它用 headless Chromium 渲染成 PDF 返回。服务跑在 Docker 里，Node 进程启动 8 个渲染 worker，通过 Puppeteer 控制浏览器。日均生成 3000 余份报表，白天高峰集中在上午十点和下午三点。&lt;/p&gt;&#xA;&lt;p&gt;上线一年多一直稳定。上个月业务量上涨，我们把 worker 并发从 4 调到 10，容器规格没动：4 核、4GB 内存限制。扩容后吞吐确实上去了，但稳定性开始出问题——每天总有几次任务失败，重试往往又能成功，像随机故障。&lt;/p&gt;&#xA;&lt;h2 id=&#34;故障现象&#34;&gt;&lt;a href=&#34;#%e6%95%85%e9%9a%9c%e7%8e%b0%e8%b1%a1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;故障现象&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;失败任务的日志基本是 &lt;code&gt;Page crashed!&lt;/code&gt; 或 &lt;code&gt;Target closed&lt;/code&gt;，偶尔能看到 &lt;code&gt;Bus error (core dumped)&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;容器本身没有重启，Node 主进程一直活着，健康检查正常。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;docker stats&lt;/code&gt; 显示容器内存长期在 1.2–1.4GB，离 4GB 限制很远；宿主机也还有大量空闲内存。&lt;/li&gt;&#xA;&lt;li&gt;失败集中在含图表的复杂报表上；简单纯文字报表几乎不出问题。&lt;/li&gt;&#xA;&lt;li&gt;并发越高失败越频繁，单并发压测 500 次一次没挂。&lt;/li&gt;&#xA;&lt;li&gt;失败没有固定时间点，和流量高峰大致相关，但同一时刻也有大量任务正常完成。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;排查过程&#34;&gt;&lt;a href=&#34;#%e6%8e%92%e6%9f%a5%e8%bf%87%e7%a8%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;排查过程&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;第一步先排除 OOM。容器限制了 4GB，但实际用量只有 1.3GB 左右，看起来不像内存问题。查 cgroup 计数确认：&lt;code&gt;/sys/fs/cgroup/memory.events&lt;/code&gt; 里 &lt;code&gt;oom&lt;/code&gt; 和 &lt;code&gt;oom_kill&lt;/code&gt; 都是 0；&lt;code&gt;dmesg&lt;/code&gt; 里没有 oom-killer 记录；容器 &lt;code&gt;RestartCount&lt;/code&gt; 也是 0。主进程没被杀，说明不是容器级 OOM。&lt;/p&gt;&#xA;&lt;p&gt;第二步看失败形态。崩溃的不是 Node 主进程，而是 Chromium 的 renderer 子进程——Puppeteer 通过 DevTools 协议和浏览器通信，renderer 一死，页面就报 &lt;code&gt;Page crashed!&lt;/code&gt;。这解释了为什么容器健康检查看不出来：监控只看主进程和容器状态，子进程崩溃被完全遮住了。&lt;/p&gt;&#xA;&lt;p&gt;第三步对比变量。把并发降回 4，连续跑一天零失败；升到 10，失败立刻回来。失败页面又集中在图表多的模板上——图表渲染要传大量位图，正是 Chromium 多进程架构里共享内存的主要用途。怀疑点开始从“内存不够”转向“共享内存不够”。&lt;/p&gt;&#xA;&lt;p&gt;第四步验证假设。进容器执行 &lt;code&gt;df -h /dev/shm&lt;/code&gt;，输出是 64M、已用 100%、可用 0。再看 &lt;code&gt;docker inspect&lt;/code&gt; 的 &lt;code&gt;HostConfig.ShmSize&lt;/code&gt;，67108864 字节，正好 64MB。&lt;code&gt;ls /dev/shm&lt;/code&gt; 下面是一堆 &lt;code&gt;.org.chromium.Chromium.*&lt;/code&gt; 文件。也就是说，Chromium 的共享内存段已经被写满，renderer 再往 mmap 区域写就会收到 SIGBUS——日志里的 &lt;code&gt;Bus error&lt;/code&gt; 正是这个信号。&lt;/p&gt;&#xA;&lt;p&gt;第五步复现。写了个压测脚本，10 并发连续渲染图表报表，每秒采样 &lt;code&gt;/dev/shm&lt;/code&gt; 使用率：大约 20 秒后冲到 100%，随后必然出现 renderer 崩溃；同一脚本加上 &lt;code&gt;--disable-dev-shm-usage&lt;/code&gt; 参数后不再复现。&lt;/p&gt;&#xA;&lt;p&gt;再估算一下量级：单个复杂报表在渲染期间占用的共享内存可达数十 MB，10 个并发同时渲染，64MB 的池子连两个页面都撑不住。这也解释了为什么单并发永远测不出来——单页面峰值低于 64MB，池子够用；并发一上来，多个 renderer 的共享内存段叠加，才把上限顶穿。&lt;/p&gt;&#xA;&lt;h2 id=&#34;解决方案&#34;&gt;&lt;a href=&#34;#%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;解决方案&#xD;&#xA;&lt;/h2&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;调整共享内存&lt;/strong&gt;：容器启动参数加 &lt;code&gt;--shm-size=1g&lt;/code&gt;；docker-compose 里写 &lt;code&gt;shm_size: 1gb&lt;/code&gt;；K8s 则挂一个 &lt;code&gt;emptyDir: {medium: Memory, sizeLimit: 1Gi}&lt;/code&gt; 到 &lt;code&gt;/dev/shm&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;同步核对内存预算&lt;/strong&gt;：tmpfs 的页会计入容器 memory cgroup，1GB shm 不是“额外白送”的。当前 4GB 限制下，应用 1.4GB 加 shm 1GB 仍有充足余量，所以限制保持不变；如果容器规格更小，就要把 shm 和 limit 一起算。另外要明确：&lt;code&gt;shm-size&lt;/code&gt; 是上限而不是预分配，tmpfs 只按实际写入的页计费，调大到 1GB 不会立刻占掉 1GB 内存。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;保留兜底参数&lt;/strong&gt;：在镜像启动脚本里给 Chromium 加 &lt;code&gt;--disable-dev-shm-usage&lt;/code&gt; 作为应急开关，它让 Chrome 改用 &lt;code&gt;/tmp&lt;/code&gt; 存共享内存。代价是走磁盘 IO、且可能撑爆 &lt;code&gt;/tmp&lt;/code&gt;，只作为临时手段，不作为默认配置。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;加监控&lt;/strong&gt;：采集每个容器 &lt;code&gt;/dev/shm&lt;/code&gt; 使用率，80% 告警；同时监控 renderer 崩溃日志关键字，而不是只看容器是否重启。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;根因分析&#34;&gt;&lt;a href=&#34;#%e6%a0%b9%e5%9b%a0%e5%88%86%e6%9e%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;根因分析&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;直接根因是 &lt;strong&gt;Docker 默认给容器的 &lt;code&gt;/dev/shm&lt;/code&gt; 只有 64MB，高并发下 Chromium 的共享内存需求撞上硬顶，renderer 写入 mmap 区域时触发 SIGBUS 而崩溃&lt;/strong&gt;。64MB 是容器运行时沿用多年的保守默认值，为最小化共享内存占用而设，适合轻量进程，不适合浏览器这种重度依赖共享内存的多进程应用。同样的 64MB 默认值在 Kubernetes 里也存在，不显式挂载 &lt;code&gt;/dev/shm&lt;/code&gt; 的 Pod 一样会遇到这个问题。&lt;/p&gt;&#xA;&lt;p&gt;更深一层，问题被两层“看起来正常”掩盖：容器内存只用了三分之一，所以没人往内存方向想；崩溃发生在子进程，容器和主进程都健康，监控也不报警。并发从 4 提到 10 时，我们只评估了 CPU 和内存余量，没有评估共享内存这类“隐形资源”——它不在常规容量评估表里，却在默认值里埋着上限。&lt;/p&gt;&#xA;&lt;h2 id=&#34;预防措施&#34;&gt;&lt;a href=&#34;#%e9%a2%84%e9%98%b2%e6%8e%aa%e6%96%bd&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;预防措施&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;容器运行模板显式声明易被忽略的默认值：shm-size、ulimit nofile、pids-limit、stop grace period，不再依赖运行时默认。&lt;/li&gt;&#xA;&lt;li&gt;浏览器、桌面、机器学习这类多进程/共享内存型负载，上线前必须确认 &lt;code&gt;/dev/shm&lt;/code&gt; 容量，并写入部署清单。&lt;/li&gt;&#xA;&lt;li&gt;监控从“容器是否活着”下沉到“子进程是否健康”：采集关键子进程退出、SIGBUS/SIGSEGV 计数和业务失败率。&lt;/li&gt;&#xA;&lt;li&gt;扩容或调并发前，按目标并发做一次压测，压测指标包含 &lt;code&gt;/dev/shm&lt;/code&gt;、cgroup &lt;code&gt;memory.events&lt;/code&gt;，而不只是 CPU/内存。&lt;/li&gt;&#xA;&lt;li&gt;基础镜像或运行模板变更时，把 shm-size、内存限制、ulimit 纳入回归清单；新镜像上线前用 &lt;code&gt;docker run --rm &amp;lt;image&amp;gt; df -h /dev/shm&lt;/code&gt; 确认默认值，避免同类问题换个服务再犯一次。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;总结&#34;&gt;&lt;a href=&#34;#%e6%80%bb%e7%bb%93&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;总结&#xD;&#xA;&lt;/h2&gt;&lt;p&gt;这次故障的教训不是“该调大 shm”，而是“默认值也是容量决策”。容器的每一个默认参数，都是运行时替我们做的一个假设；负载轻时它沉默，负载上来它就变成天花板。&lt;code&gt;docker stats&lt;/code&gt; 里内存只用三分之一，和“内存没问题”是两回事——真正被耗尽的资源可能根本不在那张表上。把隐式默认值显式化，是容器化负载从“能跑”到“跑得稳”之间最便宜的一步。&lt;/p&gt;&#xA;&lt;h2 id=&#34;延伸阅读&#34;&gt;&lt;a href=&#34;#%e5%bb%b6%e4%bc%b8%e9%98%85%e8%af%bb&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;延伸阅读&#xD;&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/def9cf20/&#34; &gt;Docker 容器 OOM，服务雪崩是怎么炸开的？&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.5772447.xyz/posts/b1ca8efe/&#34; &gt;记一次 K8s Pod OOMKilled 触发的服务雪崩&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
        </item></channel>
</rss>
