1. 崩溃转储丢失:最让人抓狂的“案发现场被抹掉”
读《第18章 崩溃》读到18.6这一节时,我差点从椅子上弹起来。“案例:崩溃转储丢失”,书里用三页纸讲了一个很典型的故障:程序crash了,内核该把core dump落盘,可磁盘上就是找不到转储文件。配置翻了一遍,全对,文件系统干干净净。我看了这段,第一反应是:这不就是我线上踩过的坑吗。
崩溃转储(core dump)是程序崩溃瞬间留下的内存快照,进程的堆、栈、寄存器、打开的文件描述符信息都包含在里面。没有它,很多段错误只能靠猜,或者靠反复压测去复现。这篇文章我用一次真实案例,把“崩溃转储怎么会丢”和“怎么让它不丢”从头到尾盘一遍。适合后端开发、SRE、运维同学,尤其是还在用裸奔配置跑C/C++、Go、Rust服务的人。
1.1 崩溃转储为什么是“命根子”
我见过太多因为缺少转储而“死无对证”的事故。某个服务每过几小时挂一次,重启后像没事人一样。没有core dump,你只能翻日志、看监控、比对流量,一点一点缩小范围。日志要是恰好没打到关键分支,排查时间就从小时变成几天。而拿到core dump以后,gdb加载,backtrace一下,崩溃栈清清楚楚,经常几分钟就能把问题钉死。所以说,丢转储不是丢一个文件,是丢了整个案发现场。
有个真实案例我记得很清楚:一个后台任务在凌晨3点段错误退出,业务方很早就反馈过,但我们一直没抓到规律。因为没有core dump,每次只能靠增加日志再上线,反反复复折腾了两周。后来偶然发现,原来进程的工作目录是只读挂载,内核一直想把core写到当前目录,结果每次失败,而日志里连一条内核提示都没有。这种问题,如果没有崩溃转储机制的基础知识,根本想不到。
1.2 一次让我熬夜到凌晨的“丢尸”现场
那次是线上支付网关崩溃,凌晨收到告警,值班的人重启后服务恢复,没有保留现场。第二天早上我想分析core dump,结果/data/coredumps下空空如也,整个系统里find了一下,连名为core的文件都没有。当时第一反应是:肯定是core_pattern配的和实际不一致。
查了/proc/sys/kernel/core_pattern,指向的是systemd-coredump,不是裸路径。也就是说,内核没有直接把文件写到磁盘,而是把崩溃镜像通过管道喂给了systemd-coredump服务。既然这样,去journal里翻systemd-coredump的日志,看到一条类似“Failed to save coredump”的记录。展开以后才发现,它默认的存储目录/var/lib/systemd/coredump根本没建出来,而且服务运行身份对这个目录没有写权限。表面看配置没问题,实际上整条链路断在最后一公里。
这就是典型的“该有却没有”,书里的案例和我经历的场景几乎一样。所以与其等出事再翻,不如先把机制搞清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 崩溃转储到底是怎么产生的,为什么“该有却没有”
要排查转储丢失,不能靠瞎猜。崩溃转储从内核触发到最终落盘,中间要经过多个环节,任何一个环节出问题,结果都是“没有core文件”。我先把这条链路讲清楚。
2.1 从一次崩溃到core文件落盘的完整路径
当Linux进程收到SIGSEGV、SIGABRT这类信号,并且信号没有被进程捕获时,内核会尝试生成core dump。第一步看进程的RLIMIT_CORE资源限制,也就是core file size。这个值如果是0,内核会直接放弃,连后续动作都没有。很多人明明看core_pattern配置正常,但文件就是没有,多半是ulimit -c 0。
如果限制不为0,内核读取/proc/sys/kernel/core_pattern。这个配置决定转储去哪里。它有两种形态:普通文件和管道。
- 普通文件路径:比如/var/crash/core.%e.%p。内核直接以崩溃进程的凭据,在指定路径创建文件。
- 管道命令:比如|/usr/lib/systemd/systemd-coredump %P %u %g。内核启动一个用户态程序,把转储内容通过标准输入喂给它,由它负责存储或转发。
如果配置的是普通路径,还要看崩溃进程的工作目录是否可写、文件系统剩余空间是否充足、SELinux或AppArmor是否允许写、目录权限是否匹配。如果是管道,还得看那个接收程序有没有正常运行、有没有写权限、目标目录是否存在。任何一个条件不满足,转储都可能静默失败。
另外,内核还有一个安全机制:以setuid/setgid身份运行的进程,或者进程的dumpable属性被清除,一般不会生成core dump。这个细节在书中没细说,但线上很常见,比如程序调用了prctl(PR_SET_DUMPABLE, 0),或者通过sudo、setcap启动,都可能导致内核直接放弃转储。
2.2 让转储消失的六个“隐形杀手”
我把自己遇到过的、以及网上同行反馈过的典型原因整理了一下,常见的有这些:
| 故障点 | 现象 | 排查方向 |
|---|---|---|
| RLIMIT_CORE为0 | 没任何core文件,内核日志也可能没有 | ulimit -c、服务单元文件的LimitCORE |
| core_pattern路径错误 | 目录不存在导致写入失败 | cat /proc/sys/kernel/core_pattern |
| 磁盘空间不足 | 写入中断,可能留下0字节文件 | df -h,查看core所在分区 |
| 目录权限不够 | dump生成失败,日志无关 | 看目录属主、strace内核行为 |
| AppArmor/SELinux限制 | 内核拒绝创建文件 | dmesg、audit日志 |
| systemd-coredump服务故障 | 管道下游异常,转储被丢弃 | systemctl status systemd-coredump、coredumpctl |
| 容器环境隔离 | 进程在容器内崩溃,落盘位置受宿主影响 | 先看宿主core_pattern,再确认容器挂载 |
这张表只是索引,具体到每个点都要有验证方法。比如RLIMIT_CORE,你用ulimit -c命令查的是当前shell的限制,不一定等于服务进程的真实限制。systemd管理的服务要看单元文件里的LimitCORE=,docker容器还要看--ulimit core=设置。只看shell是会被骗的。
2.3 书里这一节没有展开的三个细节
书里18.6主要讲了配置参数,但我认为有三个细节没讲透,这也是转储丢失事故里面最隐蔽的部分。
第一,core_pattern管道形式下的“哑火”。很多现代Linux发行版默认把core_pattern设置为|/usr/lib/systemd/systemd-coredump,而不是一个文件路径。如果你拿“文件路径是否存在”那一套去查,永远查不出问题。必须看journal里对应用户的日志,或者用coredumpctl list看有没有索引记录。
第二,内核在写普通core文件时,并不总是给足权限。它按崩溃进程的有效用户ID创建文件,文件权限默认可能是0600。如果你用root去分析,readable没问题,但如果你用普通用户跑gdb,就会遇到Permission denied,看起来好像转储又丢了,其实文件在,只是没权限读。
第三,fs.suid_dumpable这个参数。很多安全基线会把/proc/sys/fs/suid_dumpable设成0,意味着setuid程序崩溃时不转储。如果线上服务用sudo或setcap启动,转储就可能被内核安全策略吞掉。这个参数在书里连提都没提,但它可以解释一类“别的进程都能dump,只有我的服务不dump”的怪现象。
3. 案例复盘:我是怎么一步步找到转储丢失根因的
前面讲了一堆原理,现在把完整的排查过程拆开。这个案例是最常见的那种:配置看起来没问题,转储却没有生成。我尽量还原当时的思路和命令,让有类似问题的人可以直接照着做。
3.1 现场证据:只有一条日志
事故发生时,服务进程在凌晨4点17分退出,监控告警只显示进程消失,应用日志最后一行是“开始处理订单编号10086”。没有堆栈,没有信号信息。我登上去以后,先用find找了一圈转储文件:
bash复制find / -xdev -name 'core*' -mmin -120 2>/dev/null
没结果。又试了lsof | grep core,也什么都没有。这时我意识到,转储文件可能压根没生成,或者被重定向到了别的地方。
接下来做的是看内核日志:
bash复制dmesg -T | grep -Ei 'segfault|trap|core'
日志里能看到类似segfault at 0x... ip 0x... sp 0x... error 14的记录,说明进程确实是因为段错误崩溃的。这段信息很关键,它告诉我内核已经触发了core流程,但不确定后续是否成功。
3.2 排查顺序:先看内核,再看文件系统
先看当前生效的core_pattern:
bash复制cat /proc/sys/kernel/core_pattern
输出是:
code复制|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
我第一反应是,转储交给systemd-coredump了,不一定以普通文件形式落在磁盘。于是立刻用coredumpctl list查看有没有记录,结果列表里确实有崩溃进程,状态却显示“failed”。再查看具体日志:
bash复制journalctl -u 'systemd-coredump@*' --since '2024-xx-xx 04:17' --no-pager
日志里一行Failed to write core dump跳出来,后面跟着存储路径。问题基本锁定:systemd-coredump想把转储写到/var/lib/systemd/coredump/,但这个目录因为某种原因不存在,或者当前用户没有写权限。
这时候还要确认是不是文件系统的问题。我执行:
bash复制df -h /var/lib/systemd
分区剩余空间有几十GB,排除磁盘满。再ls -ld /var/lib/systemd/coredump,结果显示目录不存在。这就奇怪了,systemd-coredump包安装后应该会创建这个目录,为什么会消失?查了之后发现,运维同事清理过/var/lib/systemd下的旧日志,可能不小心删掉了整个coredump目录,而systemd-coredump服务并不会自动重建它。
3.3 从日志到文件系统,根因终于落定
补上目录并调整权限:
bash复制mkdir -p /var/lib/systemd/coredump
chown root:systemd-coredump /var/lib/systemd/coredump
chmod 2775 /var/lib/systemd/coredump
然后用coredumpctl测试存储是否正常。这里多说一句,chmod 2775的setgid位是有讲究的。目录加上setgid后,任何用户在这个目录下新建的文件都会继承目录所属组,这样systemd-coredump组里的分析工具就能统一读取,不会因为创建者不同导致权限分裂。如果没有setgid,root创建的core是root:root,普通用户创建的core是user:user,后续统一分析很容易卡权限。
测试时直接触发一个崩溃进程:
bash复制bash -c 'kill -SEGV $$'
执行完再看coredumpctl list,新增了一条记录,存储正常。然后我按书里的思路补查了ulimit -c,发现systemd服务单元文件里没有显式设置LimitCORE,默认是inherit,而继承自systemd的默认值,通常是infinity。这里没有踩坑,但很多服务会在脚本里写ulimit -c 0,导致服务起来后根本没有core权限,后续会重点提。
3.4 给所有人提个醒:转储文件“生成”和“能用”是两码事
文件生成成功,不代表分析时能用。案例修复后,我顺手做了一次验证:试着用gdb加载生成的core文件。做法是把崩溃可执行文件和core文件放到一起,执行:
bash复制gdb /opt/app/bin/payment-gateway /var/lib/systemd/coredump/core.payment-gateway.xxxx
然后输入bt。如果栈帧显示的全是??,或者提示no symbol table,说明二进制是strip过的,或者core文件不完整。这件事要提前验证,不能等到真出事才发现符号对不上。许多团队把core dump配置完成后就不再管,等到要分析才发现生产二进制是release版,符号文件没归档,这种“有尸体但没法尸检”的情况,比转储丢失更尴尬。
4. 加固方案:把“留尸体”变成稳定产物
案例修好只是第一步,接下来要做的,是让崩溃转储变成一个稳定的监控产物,而不是随缘生成的东西。我按线上实践给出了下面这套方案,已经稳定运行大半年。
4.1 一套可靠的core dump落盘配置
我推荐用固定目录 + systemd-coredump的组合,这样既有普通文件便于查看,又有元数据便于检索。核心配置文件放在/etc/sysctl.d/50-coredump.conf:
ini复制kernel.core_pattern = |/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
kernel.core_uses_pid = 1
fs.suid_dumpable = 0
注意,core_pattern前面那个竖线不是装饰,是管道符。它告诉内核不要直接写文件,而是把转储数据通过stdin交给指定程序处理。systemd-coredump会自己决定存储路径,默认存在/var/lib/systemd/coredump/。如果你更想看到普通文件,也可以写成固定路径,但我不建议在安全要求高的环境里使用裸路径,因为文件名很容易冲突,而且权限控制不如systemd管理那么细。
改完配置要立刻生效,执行:
bash复制sysctl --system
然后确认:
bash复制sysctl kernel.core_pattern
这里容易踩一个坑:部分发行版的/etc/sysctl.conf里也可能有一行kernel.core_pattern,两个文件同时存在时以最后一个生效为准。查配置时要看实际运行值,而不是只盯着自己改的文件。
4.2 让systemd-coredump更“听话”的三个参数
systemd-coredump的行为可以通过/etc/systemd/coredump.conf控制,比较常用的有三个:
ini复制[Core]
Storage=external
Compress=yes
ProcessSizeMax=2G
Storage=external表示核心文件单独保存,而不是只存journal摘要。默认值可能是external,但如果你发现core文件全被塞进了journald,就要检查这个参数。Compress=yes会压缩存储,节省空间。代价是分析时要用工具能识别的解压方式,一般coredumpctl会处理,问题不大。ProcessSizeMax控制单个进程体积上限,防止某个进程把内存撑爆后core文件又把磁盘写满。设为2G意味着超过2G的进程崩溃时,系统只保存元数据不保存完整镜像。这个要根据服务实际内存设置,不能拍脑袋。
如果服务是由systemd托管的,还应该在对应的service单元文件里加上:
ini复制LimitCORE=infinity
不加这一行,哪怕全局core_pattern配得再好,systemd也可能在启动进程时把core限制设为0。这是生产环境里最容易被忽略的地方。
4.3 监控和演练:让崩溃转储成为可观测资产
配置完之后,我强烈建议做一次“崩溃演习”。我自己的做法是写一个临时脚本,故意触发SIGSEGV和SIGABRT,分别验证两种信号的转储结果。然后用coredumpctl list检查,并实际用gdb加载一次。整个流程走通以后,再做一次清理,确认没有配错路径。
还要加监控。最粗暴但有效的方法,是定时统计core文件数量。我写了个cron任务,每天扫描/var/lib/systemd/coredump,如果当天没有新的core文件但服务发生过crash,就报警。另外还要监控磁盘使用率,core文件容易体积很大,一旦服务频繁崩溃,转储可能几分钟内填满磁盘。解决办法是给core文件配置专门的日志轮转,比如logrotate对/var/lib/systemd/coredump/*.core按大小或时间轮转,保留最近3天。
容器的场景单独说:容器内无法修改宿主机的/proc/sys/kernel/core_pattern,因为这是内核参数,属于宿主管控。多数情况下容器里崩溃进程的core会落到宿主机,按宿主配置处理。如果想让容器内崩溃转储和宿主机服务分开,可以在容器启动脚本里设置ulimit -c unlimited,但最终落盘位置还是受宿主core_pattern约束。比较成熟的方案是把core_pattern指向一个专门脚本,通过脚本里的环境变量或者进程信息判断是哪个容器,再重定向到不同目录。这个改造相对复杂,在容器规模不大的时候,先做到宿主机统一收集就够了。
5. 从“丢转储”延伸到崩溃分析的几个操作细节
排查转储丢失的过程中,你其实已经在做崩溃分析了。这一章的内容还可以继续往外延,我补充几个和定位真实bug强相关的细节。
5.1 拿到core文件之后的五分钟定位法
很多人拿到了core文件,却不知道怎么下手。我在读这一章时,反复提醒自己,转储的价值不在文件本身,而在“你能从里面挖出什么”。我的习惯是打开gdb后按这个顺序操作:
gdb复制bt
info registers
frame 1
info locals
x/32gx $rsp
bt看完整调用栈,info registers看关键寄存器,frame 1切到崩溃位置附近,info locals看局部变量,x/看栈内存。大部分段错误和空指针,在这一步就能定位,甚至不需要看汇编。如果是优化过的release二进制,建议把符号文件提前归档,或者打包时用-g保留但在发布前strip,并把symbol文件存到独立仓库。这样线上即使没有调试符号,也能用symbol server加载。
5.2 一个容易被忽略的信号:核心文件的时间戳
排查崩溃转储丢失时,很多人会忽略时间戳。比如ls -l看到core文件的时间是进程崩溃后几分钟,说明某个定时任务或者重启脚本可能重新生成了它,未必是原始转储。我遇到过一种情况:服务启动脚本里自带ulimit -c unlimited,崩溃后生成一个core文件,但随后服务自动重启,启动过程中又把自己的core文件当成垃圾清理了。从结果看,转储确实“丢”了,但它其实存在过几秒。这个问题只能通过文件系统审计日志,或者auditd规则去还原。
如果要在生产环境长期收集转储,可以考虑把core文件发到对象存储。Linux内核的core_pattern支持管道到脚本,脚本里可以调用对象存储工具,比如先压缩再传到S3或OSS。这样即使本地磁盘被清空,远端仍有据可查。整个过程要注意脚本本身的稳定性,如果脚本崩溃,转储就真的丢了。
5.3 跨平台对照:Windows和Android上的“崩溃转储”
既然是读书笔记,我还是想把话题拉回共性上。Windows上对应的是minidump,默认存在%LocalAppData%\CrashDumps,由WER(Windows Error Reporting)接管。Android上对应的是/data/tombstones下的tombstone文件,包含崩溃线程的调用栈和寄存器,由debuggerd生成。它们的核心逻辑一样:都是把崩溃现场留证。排查“转储丢失”的思路也通用:先确认崩溃信号有没有被捕获、负责生成转储的组件有没有运行、存储路径有没有权限、磁盘空间是否充足。这些套路换系统不换思想。
6. 踩坑记录与排查速查表
最后一部分是我自己踩过的坑。与其一条条讲理论,不如直接把这些坑摆出来,说不定哪个就是你正在遇到的问题。
6.1 坑一:core_pattern指向了不存在的目录
有段时间我给一个服务单独配置了kernel.core_pattern = /opt/coredump/core_%e_%p,自以为很完美。结果忘了先创建/opt/coredump。内核尝试写文件时发现父目录不存在,写失败,但看起来没有日志,实际上dmesg会有failed to create core file之类的提示,只是容易被忽略。后来我把配置脚本改成了自动创建目录并及时检查,这个问题才绝迹。
6.2 坑二:只改了sysctl,忘了重启相关服务
我在调systemd-coredump时,改了coredump.conf里的Storage=external,但没重启systemd-coredump.socket和systemd-coredump.service。当时测试崩溃,发现core并没有生成,排查了很久才发现配置没被服务重新加载。改完配置后一定要执行systemctl daemon-reload,然后重启相关服务,再用coredumpctl验证一遍。
6.3 坑三:容器里调core_pattern,宿主机和容器互相干扰
有次在容器里用sysctl -w kernel.core_pattern=/tmp/core,改的时候没报错,但容器重启后配置又变回去。后来才明白,容器访问的是宿主机的内核参数,不是独立的。容器里改kernel.core_pattern实际上会影响宿主机,这个操作非常危险。生产环境一定要在容器编排里明确禁止容器修改内核参数,或者给容器跑在非特权模式。如果你想为一类容器单独指定转储路径,最好在core_pattern里用%e之类的变量配合脚本分发。
6.4 坑四:ulimit -c 0写进了启动脚本
线上有个Java服务,我们在启动脚本里设置了ulimit -c 0,本意是防止Java进程崩溃时产生巨型core文件。后来排查一个C++扩展崩溃,才发现这个设置对同一进程组里所有子进程也生效,导致C++模块崩溃后没有转储。教训是:限制core大小要按服务单独设置,不要图省事在公共脚本里一刀切。如果只是不想让Java产生core,应该在JVM参数里设置专门的选项,或者控制工作目录,而不是直接限制整个进程的core权限。
6.5 坑五:core文件生成却读不了
还有一次,core文件明明生成了,权限是-rw-------,属主是崩溃进程的用户。我用root去读没问题,但团队其他同学用普通账号登录后,gdb直接报Permission denied。大家以为转储丢了,其实只是没权限。我后来在coredump.conf里调整了目录属组,并给core文件读取权限留了组读位。这个细节在日常不太起眼,一旦多人协作分析就会卡壳。
6.6 排查速查表:崩溃转储丢失的十步检查
把前面的内容压缩成一张速查表,适合贴在工位上:
| 步骤 | 命令/操作 | 预期结果 |
|---|---|---|
| 1 | cat /proc/sys/kernel/core_pattern |
明确是路径还是管道 |
| 2 | ulimit -c |
非0 |
| 3 | systemctl status systemd-coredump.socket |
running |
| 4 | coredumpctl list |
有对应记录 |
| 5 | journalctl -u 'systemd-coredump@*' |
无写失败 |
| 6 | df -h |
空间足够 |
| 7 | ls -ld /var/lib/systemd/coredump |
目录存在且可写 |
| 8 | dmesg -T | grep -i core |
无拒绝记录 |
| 9 | auditctl -l 查SELinux/audit |
无拦截事件 |
| 10 | 实际触发一次崩溃并分析 | 文件生成且可加载 |
按这个表走一遍,90%的“崩溃转储丢失”问题都能定位。剩下10%多半涉及文件系统或者安全模块的深层策略,需要结合具体环境再查。
写到这里,我最大的体会是:崩溃转储这件事,只要理解内核和用户态之间的协作方式,排查起来并不难。但恰恰因为平时不出事,很多人从没验证过自己系统的转储链路是否真的通。我现在每上线一套新环境,都会主动触发一次崩溃,确认core文件生成、读取、分析三个环节都正常,再让服务跑起来。这个习惯救过我很多次,也建议你从今天开始试试。
