手头这本讲后台服务稳定性的书,第18章讲崩溃处理,18.6节给了一个特别扎心的案例:崩溃转储丢失。看到这一节标题的时候,我第一反应就是上个月线上数据库那次事故。深夜两点,MySQL实例异常退出,告警把手机震亮,等我爬起来,进程早被守护脚本拉起来了,服务看起来恢复了,可天亮想复盘的时候,却连一个core文件都找不到,身边只剩一堆零散日志,全靠猜。
崩溃转储(core dump)说白了,就是程序崩溃那一瞬间内核把进程的内存现场快照写到磁盘,这份快照是事后还原问题最硬的证据,相当于飞机上的黑匣子。少了它,排查偶发崩溃就像没有监控盲猜嫌疑人。这一节正好把“core dump为什么会丢”和“丢了以后怎么查”两件事讲透了。这篇文章我把书里的分析思路搭配自己真实排障的过程一起整理出来,适合所有做服务端开发、运维、SRE的同行参考。
1. 崩溃转储到底是什么?丢了为什么让人抓狂
1.1 崩溃转储记录了什么
先把这个概念说清楚。当进程触发除零、空指针解引用、非法指令、断言失败这类信号时,Linux内核会介入,把进程当前的地址空间、寄存器上下文、调用栈信息、信号信息、内存映射和部分堆内容一并写入core文件。不是所有信号都产生core,像SIGKILL这种连通知都不给的信号,内核直接就杀掉进程了,根本不给你留转储的机会。
打个比方,进程崩溃就像出了交通事故,core文件就是行车记录仪最后几秒的视频。日志只是路过的行人描述的“大概情况”,而core文件是完整的现场录像,连司机当时踩没踩刹车、往哪个方向打了方向盘都能还原。
实际操作中,一个有效的core文件配合gdb能做很多事情。比如我写过一段有bug的C程序,对空指针调了成员函数,崩溃后拿到core文件,执行 gdb -c core,再敲一个 bt,直接就能看到 main.c:10 的调用栈,几秒钟定位问题。哪怕程序没有符号信息,函数名和模块信息也能抓到一部分。对有经验的工程师来说,这是一个可以让排查效率翻倍的工具。
1.2 一个core文件值多少钱
有人可能会想,丢了就丢了,反正有日志。但真正遇到偶发崩溃的人不会这么说。我做过一个C++服务,线上隔三差五报double free,几十个实例里只有一个偶发崩溃,日志打到最后一行的调用栈都是正常的,完全看不出哪里有问题。因为没有core文件,只能加日志、灰度、再复现,反反复复搞了好几周。
后来换了思路,把核心服务的core_pattern改成落盘,并且把服务从容器里挪到能稳定保存core的实例上,等下一次崩溃,用gdb打开core文件,一帧一帧回溯,问题浮出水面:一个对象在回调中被提前释放,但另一条异步路径还在使用它。没有core,这种内存释放顺序问题几乎不可能靠肉眼从日志里找出来。
所以说,转储丢失的直接后果不是“少一个文件”,而是排障路径从“精准定位”倒退成“大海捞针”。一个core文件可以把几周的排查压缩到几小时,它丢失的成本远比很多人想象中大得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 崩溃转储丢失的完整链路与六类真正原因
2.1 转储从触发到落盘要经过哪几道关卡
要搞懂core文件为什么会丢,先得知道它从内核到磁盘之间要过哪些环节。这个链路并不长,但每一环都有可能出问题。
第一关是信号触发。内核看到导致进程崩溃的信号后,会判断当前进程是否允许生成core。这里有一个进程资源限制叫 RLIMIT_CORE,如果这个值是0,内核直接跳过转储,进程退出干净利落。很多服务默认启动环境下这个限制就是0。
第二关是读取 /proc/sys/kernel/core_pattern。这个决定core文件写到哪里。如果core_pattern以 | 开头,内核会把core数据通过管道交给一个用户态程序去处理;如果不是管道,就是一个普通文件路径,内核自己负责落盘。
第三关是权限和空间。落盘路径的目录必须存在、必须可写,磁盘要有剩余空间,SELinux或AppArmor不能被拦截。只要是路径写错一步,内核写失败就放弃了。
第四关容易被忽略:如果是管道模式,还有一个 kernel.core_pipe_limit 参数。这个参数限制同一时间能有多少个core dump同时通过管道传输,超过限制的内核会直接跳过转储。服务集群同时崩溃时,这地方会悄悄丢数据。
理解这个链路之后你就会发现,core文件丢失往往不是某一个神秘原因,而是链路上某个环节“静默失败”,没有报错,也没有日志。
2.2 转储丢失的根因地图
我总结了线上最常见的六类转储丢失原因,每一类都有对应的特征和判断方式。
第一类,资源限制被归零。启动脚本里写了 ulimit -c 0,或者systemd服务单元里 LimitCORE=0,进程从生下来就没有生成core的权利。这种是最常见的,也是最好查的。
第二类,core_pattern指向了黑洞处理器。最典型的当属Ubuntu自带apport工具。apport原本的定位是桌面环境下的崩溃弹窗上报工具,在服务器上,没有GUI交互环境,它拿到core数据后往往判断“不是桌面场景,也没人看”,直接丢弃。最坑的是,core_pattern 里还写着apport,但apport服务本身是inactive,看起来就像没启用一样,内核却依然把core管道数据喂给它。
第三类,落盘路径无效。目录不存在、挂载点是只读、权限不够,内核写core失败后不会主动告警,只会悄悄放弃。
第四类,磁盘空间不足。core文件可能非常大,一个几GB内存的进程崩溃,core文件动辄数GB,磁盘满了之后文件写到一半被截断,或者根本创建不成功。
第五类,setuid与dumpable限制。如果 fs.suid_dumpable=0,带setuid的进程或者某些特殊权限进程不会产生core。
第六类,容器与宿主环境错位。容器内进程崩溃时,内核是宿主机的内核,core_pattern也是宿主机的配置。容器里以为core会写到容器内某个目录,但实际落盘路径在宿主机上,两边对不上,你怎么找都找不到。
另外再提一个容易误判的点:如果进程是被OOM killer杀掉的,内核发送的是SIGKILL,这种信号根本不会产生core。所以看到内存耗尽导致进程消失,就别再傻等core文件了,应该去看dmesg里的OOM记录。
3. 案例复盘:线上数据库崩溃后core文件为何凭空消失
3.1 现场:告警已恢复,但证据全无
这是我自己经历的一个典型case。环境是Ubuntu Server 20.04,MySQL 8.0,由systemd托管。故障发生在凌晨2点17分,mysqld进程异常退出,连接全部中断,但是几分钟后守护机制把它自动拉起来了,业务侧很快就恢复了。
天亮之后我开始查崩溃根因,查了一圈发现:/var/crash 是空的,/data 下没有core文件,/tmp 下也没有。崩溃的证据好像消失了一样。唯一剩下的线索是内核日志里的几行segfault记录。说明进程确实崩溃过,信号也是能触发core的种类,但core文件就是没有落盘。
这种状态是最磨人的,服务恢复了,说明影响不大,但没有core就没有根因,下一次再崩怎么办?只能硬着头皮排查。
3.2 第一步:查看core_pattern,发现真凶
排障第一步永远是看 /proc/sys/kernel/core_pattern。
bash复制cat /proc/sys/kernel/core_pattern
结果输出如下:
bash复制|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E
看到 apport 这三个字母,我基本就有数了。这是Ubuntu的崩溃报告工具,主要负责桌面版环境,本身设计目标是当用户双击一个程序导致崩溃时弹窗问“要不要上报”。在服务器上,没有这套交互逻辑,apport拿到core数据后,判断当前不是桌面会话,又找不到whoopsie上报服务,索性把数据扔了。
我马上做两个确认。先看apport服务状态:
bash复制systemctl status apport
结果它显示inactive,看起来已经禁用。再看 /var/crash 目录,里面没有任何新的崩溃报告文件。
这个状态非常有迷惑性:服务已经停了,但内核的 core_pattern 仍然指向它,所以core数据还是会通过管道送给这个程序的入口。程序启动后发现自己不该干活,直接中断。我把它叫作“黑洞处理器”——进程死了,core送了,处理器收了,然后丢了。
接着看dmesg确认崩溃信息:
bash复制dmesg -T | grep -i segfault
输出显示mysqld在某地址触发了segfault,IP、error code这些关键信息都有。至少能证明这一夜确实发生过段错误,而且bug的触发方向还能从内核日志里看出一点端倪,但完整的调用栈已经跟着core文件一起消失了。
3.3 第二步:排查limits与存储,排除其他嫌疑
为了防止误判,我没有直接下结论,继续排查另外两个关键项。
先看systemd服务单元里的资源限制。因为MySQL进程由systemd托管,进程的 RLIMIT_CORE 是从systemd那里继承的。执行:
bash复制systemctl show mysql | grep -i limit
结果里没有看到LimitCORE,说明没设置,默认值很可能很小甚至为0。再看MySQL的维护脚本,也没有主动执行 ulimit -c unlimited。也就是说,即使core_pattern不是apport,这个服务也可能因为LimitCORE为0而写不出core。
再看磁盘:
bash复制df -h /data /var /tmp
空间都充足,目录权限也正常,排除磁盘满的问题。至此可以确认:这次core丢失的主要原因是core_pattern指向apport这个黑洞,次要问题是systemd没有给服务进程放开RLIMIT_CORE。两条叠加,即使前一条不丢,后一条也会让你什么都拿不到。
3.4 第三步:修复方案与落地验证
修复思路是把core_pattern改成一个明确的落盘目录,同时把process limits放开。
先写内核参数。在 /etc/sysctl.d/99-coredump.conf 里加入:
bash复制kernel.core_pattern=/data/coredumps/core.%e.%p.%t
kernel.core_uses_pid=1
fs.suid_dumpable=1
创建目录并设置权限:
bash复制mkdir -p /data/coredumps
chmod 0700 /data/coredumps
然后重新加载sysctl配置:
bash复制sysctl --system
再改systemd服务单元,用覆盖文件方式添加limits:
bash复制systemctl edit mysql
写入:
ini复制[Service]
LimitCORE=infinity
完成后重启MySQL服务:
bash复制systemctl restart mysql
验证环节很重要,不验证等于白改。我用了最简单直接的办法,起一个sleep进程,手动发SIGSEGV:
bash复制sleep 111 &
kill -SEGV %1
ls -al /data/coredumps/
确认core文件出现在目录里之后,再用gdb打开它验证可用性:
bash复制gdb -c /data/coredumps/core.sleep.* /usr/bin/sleep
能打开、能看信号类型,说明整条链路已经通。之后我又对一个测试MySQL实例做了一遍同样的验证,确认不是只有sleep这种简单进程能生成core,数据库进程本身也能正常落盘。这套验证做完,我才敢说这次修复是真正生效的。
4. 防止转储再丢:可抄作业的配置与自检清单
4.1 开箱即用的配置模板
如果你不想经历一次线上事故才去补配置,可以直接照下面这套方案做。
方案A,内核直接落盘。优点是最简单、最直观,core文件就在指定目录,用gdb直接打开。缺点是文件可能很大,需要自己管清理。核心配置如下:
bash复制# /etc/sysctl.d/99-coredump.conf
kernel.core_pattern=/data/coredumps/core.%e.%p.%s.%t
kernel.core_uses_pid=1
同时确保服务的 RLIMIT_CORE 是unlimited。systemd托管就加 LimitCORE=infinity,SysV风格的初始化脚本就加 ulimit -c unlimited。
方案B,systemd-coredump托管。core_pattern换成管道方式交给systemd,由systemd负责压缩和存储,文件默认放在 /var/lib/systemd/coredump/。好处是自带压缩,省空间,而且能用 coredumpctl 统一查询。配置如下:
bash复制kernel.core_pattern=|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
查询命令:
bash复制coredumpctl list
coredumpctl info <pid>
两个方案对比可以参考下面这张表:
| 对比项 | 内核直接落盘 | systemd-coredump |
|---|---|---|
| 配置复杂度 | 低 | 中 |
| 文件压缩 | 无 | 默认支持压缩 |
| 查询工具 | 自己找文件 | coredumpctl统一查 |
| 大core落盘速度 | 快 | 经过管道,略慢 |
| 兼容性 | 任意Linux发行版 | 需要systemd版本支持 |
我个人的习惯是,核心数据库这类服务用方案A,因为落盘速度快、路径可控;普通业务服务用方案B,省心。
4.2 核心配置项逐个说
有几个参数容易混淆,我单独拎出来讲一下。
kernel.core_uses_pid:设置为1时,core文件名会追加进程PID,避免同名程序崩溃覆盖前一个core。
fs.suid_dumpable:这个参数控制setuid进程是否允许生成转储。默认为0会禁止,但很多数据库会以特殊权限运行,导致core静默丢失。生产环境如果确实需要这类进程的core,应该设置为1,但同时要清楚core文件包含内存敏感数据,必须限制目录权限。
kernel.core_pipe_limit:只有管道模式生效。多个进程同时崩溃时,超过这个数字的core会被内核直接丢弃。默认值0表示不受限制,但管道处理器处理不过来时,新的崩溃进程会被阻塞。建议配合systemd-coredump时把它设为4或8,遇到雪崩式故障时至少还能留住前几个现场。
RLIMIT_CORE:进程级限制,很多人会漏掉。systemd服务默认的LimitCORE在大部分Linux发行版上是0,这才是服务进程core丢失的真正元凶之一。检查方法很简单,对运行中的进程看 /proc/<pid>/limits,里面有 Max core file size 这一行。
4.3 自动清理:防止core反过来拖垮磁盘
core文件是事故资产,同时也是磁盘杀手。一个大内存实例崩溃,core文件轻松占用几十GB,如果没人清理,磁盘迟早被撑爆。我在生产环境见过磁盘被core文件占满导致的二次事故,这个坑不要踩。
给大家一个简单可用的清理方案。用cron每天凌晨跑一次:
bash复制find /data/coredumps -type f -mtime +7 -delete
如果想更稳妥,用logrotate管理,限制每个核心文件大小和保留份数:
bash复制/data/coredumps/core.* {
daily
rotate 7
size 10G
missingok
notifempty
compress
}
同时建议在监控系统里加一项:core目录下文件总大小超过阈值就告警。core本身就代表事故,有新的core文件生成就应该有人知道,而不是等到磁盘报警才被动处理。
5. 常见问题速查与真实排障心得
5.1 崩溃转储丢失排查速查表
遇到core文件丢失时,先别慌,按症状查这张表:
| 现象 | 排查动作 | 常见根因 |
|---|---|---|
| core文件完全没有 | cat /proc/sys/kernel/core_pattern、ulimit -c |
apport黑洞或RLIMIT_CORE为0 |
| 只有部分服务有core | 检查各服务的systemd单元 | LimitCORE设置不一致 |
| 目录里有文件但gdb打不开 | ls -lh、df -h |
core被磁盘空间截断 |
| systemd-coredump没写文件 | journalctl -u systemd-coredump* |
配置缺失或ExternalSize限制 |
| 容器内崩溃找不到core | 查看宿主机core_pattern | 容器与宿主机路径不一致 |
| 内存耗尽进程消失 | dmesg -T | grep -i oom |
OOM killer不产生转储 |
| crash目录空但dmesg有segfault | dmesg -T | grep -i segfault |
管道处理器丢数据 |
排查时记住一条主线:先确认core到底有没有生成,再确认生成之后写在哪个路径,最后确认写进去的文件能不能读。按这个顺序走,半小时内基本能定位。
5.2 几条用钱换不来的实操心得
最后分享几个我从踩坑里攒下的习惯。
第一,永远把崩溃转储当资产来运营。core文件的配置不要等事故发生时才想起来改,应该纳入发布基线。每次服务变更、升级内核版本、改系统参数之后,都应该顺手验证一次core能不能正常生成。可以用一个测试脚本定时执行,比如跑一个故意段错误的进程,看core_pattern和落盘是否正常。这比任何书上的理论都实在。
第二,dmesg的segfault行和core文件要配合着看。内核日志里那一行记录着崩溃时的指令指针、错误码和触发地址,即使没有core,也能判断出方向。比如error code为6,往往表示写操作触发了保护错误。这些细节配合core文件一起看,能让定位更快更准。
第三,看到core_pattern里有apport,直接换掉。不管apport服务状态是什么,它在服务器场景下就是一个“档案焚毁者”。换成systemd-coredump或直接落盘,两条路都行,就是别留着apport。
第四,不要在故障发生时第一次抓core。临时改配置、重启服务,等于一边救火一边学用灭火器。正确的做法是在测试环境把core链路完整验证过,包括权限、路径、SELinux、容器映射,全部摸清之后,再等生产坏消息。
这本书18.6节读完之后,我自己最大的变化就是:现在每次上线,我都会顺手检查一遍core配置。crash dump丢失,丢的不是一个文件,而是一次定位问题的最佳机会。这个观点我特别认同,也推荐你也把它写进自己的运维清单里。
