Linux 内核日志取证实战:dmesg 异常指纹、OOM 与磁盘 I/O 错误归因、重启前快照完整流程

为什么「服务器变快了要重启」是个常见的误判

运维现场经常出现这样一幕:同事说「最近是不是变快了?重启一下先看看」,或者反过来,「跑得好好的,重启一下应该就好了」。这两种说法本质上都是同一个误区——把内核日志当成「出问题才去翻的报警记录」,而不是「一台机器的体检档案」。

内核日志(dmesg 输出、 /var/log/kern.log、journald 的 kernel 字段)记录的是 CPU、内存、磁盘、网络驱动、文件系统、安全模块在硬件与内核交界处发生的全部事件。它有三个特点:第一,量大而噪杂,正常人不可能逐行去读;第二,同一个故障会在重启后静默消失,因为重启清空了环形缓冲区,也重置了所有累计计数器;第三,它记录的内容往往比应用层日志更早、更接近根因。

所以真正的问题不是「怎么读 dmesg」,而是「怎么把 dmesg 变成一个可以比对的基线」。本文讲的就是这件事:怎么把内核日志从一次性、易失的文本,做成一台机器可长期追踪的「异常指纹」,并在它开始漂移的时候最早发现。关键不在于记住几个 grep 关键词,而在于建立一套「基准 + 增量 + 归因」的流程。

先分清三类日志源,很多人只看了第一类

排查之前先明确去哪里拿数据,不同来源覆盖的层面不同,漏掉一个就可能得出错误结论。

1. 内核环形缓冲区(ring buffer)

dmesg 直接读的就是它。默认缓冲区大小在 x86_64 上通常是 128KB 左右,也就是几百到一两千行,高流量网关在几分钟内就能把它刷满,早期记录会被覆盖。查看当前与最大值:

dmesg | wc -l
cat /proc/sys/kernel/dmesg_restrict
cat /proc/sys/kernel/printk

kernel.dmesg_restrict=1 时普通用户读不到 dmesg,会返回 Operation not permitted,这不是故障,是安全策略。printk 那一行第一位是控制台日志级别,第四位是默认消息级别,值得记一下:7 表示 debug、6 info、4 warning、3 error。

2. 持久化的内核日志

在 Debian/Ubuntu 上通常是 /var/log/kern.log(rsyslog 写入),在 systemd 系统上是 journalctl -k。这两者与 ring buffer 的关键区别是跨重启保留,是唯一能做长期对比的来源。检查是否开启了持久化:

ls -d /var/log/journal 2>/dev/null || echo "journald 未持久化,重启后日志全丢"
grep -i '^Storage' /etc/systemd/journald.conf

如果 Storage 是 auto 且 /var/log/journal 不存在,journal 只存在于内存,重启即蒸发。这是个人站长最常踩的坑之一:明明「昨天还报过错」,重启后什么都查不到。

3. 硬件与固件事件

真正难查的磁盘问题往往不写在 kern.log 里,而在 smartctl 和 /sys 计数器中。内核日志只是线索的一个入口,不能当作唯一证据。

建立「异常指纹」:把日志变成可对比的特征向量

逐行读日志不可持续。可持续的做法是:把日志按「消息模板」聚合成计数,形成一台机器的特征向量,然后每天比一次。以下是可直接使用的提取脚本,按消息模板计数并排序,输出前十类:

#!/bin/bash
# kern-fingerprint.sh — 内核日志异常指纹提取
set -uo pipefail
OUT="${1:-/var/lib/kern-fp/$(date +%F).txt}"
mkdir -p "$(dirname "$OUT")"

# 取最近 24h,若 journald 不可用则回退到 kern.log
if journalctl -k --since "24 hours ago" >/dev/null 2>&1; then
    SRC=$(journalctl -k --since "24 hours ago" --no-pager -o cat 2>/dev/null)
else
    SRC=$(tail -n 200000 /var/log/kern.log 2>/dev/null)
fi

printf '%s\n' "$SRC" | sed -E \
  -e 's/^\[[^]]*\] *//' \
  -e 's/[0-9a-f]{8,}/HEX/g' \
  -e 's/([0-9]+\.){3}[0-9]+/IP/g' \
  -e 's/[0-9]+/N/g' \
  -e 's/ +/ /g' \
| sort | uniq -c | sort -rn | head -40 > "$OUT"

echo "写到 $OUT"
cat "$OUT" | head -10

注意 sed 里的三处归一化:把十六进制地址、IP、数字统统替换成占位符。这一步是整个方法的核心——只有把变量抽掉,模板才能对齐;只有模板对齐,两次快照才可比。直接 diff 原始日志是没有意义的,因为每行的 IP、PID、时间戳都不同。

每天固定时间跑一次(cron 或 systemd timer),然后对比今天和昨天同一模板的计数:

# 只看数量显著增长的模板(>3 倍且绝对值 >10)
diff <(awk '{c=$1;$1="";print $0"\t"c}' /var/lib/kern-fp/$(date -d yesterday +%F).txt | sort) \
     <(awk '{c=$1;$1="";print $0"\t"c}' /var/lib/kern-fp/$(date +%F).txt | sort) \
| grep '^>' | awk -F'\t' '{split($1,a,"\t");if(a[2]>10)print}'

这个「增量视图」的价值在于:它会自己告诉你要关注哪一行。一台健康机器的指纹在几周内几乎是稳定的,一旦某一类突然多出几十条,那就是一个真实的信号,而不是噪声。

五类必看的内核日志,以及它们各自意味着什么

一、OOM 与内存压力:被杀的到底是谁

OOM 相关日志的标准形态是 Out of memory: Killed process 12345 (php-fpm) total-vm:..., anon-rss:..., oom_score_adj:0。很多人只看到「php-fpm 被杀了」就去调 PHP 参数,这是典型的归因错误。被杀进程只是「最大的那个」,不一定是「吃内存的那个」。

# 完整的 OOM 事件上下文,包含任务列表与各进程内存
journalctl -k --since "3 days ago" | grep -A 40 -B 5 'Out of memory'
# 谁被杀的、当时内存多少
journalctl -k | grep -oE 'Killed process [0-9]+ \([^)]+\) total-vm:[0-9]+kB, anon-rss:[0-9]+kB' | tail -20

看 OOM 报告时要重点看两处:一是 Mem-Info 段里的 slab 与 page cache 数值,判断是真内存不足还是缓存策略问题;二是任务列表最后几行的 oom_score 排名,才能知道内核为什么选了这一个。另外 oom_score_adj 不等于 0 说明有人手工设过保护,这在 systemd 服务里很常见,值得追一下是谁设的。

二、文件系统与磁盘 I/O 错误:最容易被忽略的「静默故障」

这类日志包括 EXT4-fs error、XFS (...): metadata I/O error、blk_update_request: I/O error, dev sda, sector ...、ata1.00: failed command: READ FPDMA QUEUED。它们有一个共同特征:出现了也不影响服务立刻对外提供响应,但数据正在悄悄损坏。

# 三条最值得每天查的内核错误
journalctl -k --since "7 days ago" | grep -iE 'I/O error|EXT4-fs error|XFS .*error|remount.*read-only|medium error|uncorrectable'

看到 remounting filesystem read-only 就必须立刻处理:文件系统已经切到只读,任何写入都在失败,但应用可能只是「报了个保存失败」,没人注意到根因。这时候的正确动作是:

mount | grep ' ro,'
dmesg -T | grep -iE 'error|read-only' | tail -30
smartctl -a /dev/sda | grep -iE 'Reallocated|Pending|Uncorrect|Error_Log'

三个 SMART 属性的含义要记住:Reallocated_Sector_Ct 大于 0 说明盘已经替换过坏道;Current_Pending_Sector 大于 0 说明有扇区读不出来、正等重试,这是最危险的一档;Offline_Uncorrectable 大于 0 基本可以判定盘要换。此时该做的是立刻备份,而不是先调参数。

三、网卡与 TCP 事件:丢包到底丢在哪一层

常见形态有 eth0: link up/down、NIC Link is Down、TCP: request_sock_TCP: Possible SYN flooding on port 443、nf_conntrack: table full, dropping packet。最后一条尤其值得注意——它意味着 连接跟踪表满了,新连接被直接丢弃,现象是「时好时坏、莫名其妙连不上」。

cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
dmesg -T | grep -c 'table full, dropping packet'

如果 count 稳定贴着 max 的 80% 以上,就把 max 提高(同时相应调整 nf_conntrack_buckets),但更要查是谁在制造海量短连接:ss -s、ss -tan state time-wait | wc -l 是最快的两个入口。此外 link up/down 反复出现要怀疑网线/光模块/对端交换机,而不是内核。这类问题的判定标准是:如果 down 事件的时间间隔毫无规律且伴随大量重传,硬件层故障概率就很高。

四、CPU 层面的硬错误与 MCE

mce: [Hardware Error]、CPU: 3 PID: 1234 at ...、Machine check events logged 这类日志来自 CPU/内存控制器的硬件纠错机制。个人 VPS 上不常见,但一旦出现就意味着宿主机硬件可能有问题——这时候正确做法是收集证据并联系服务商,而不是自己在系统里折腾。

journalctl -k | grep -iE 'mce|hardware error|corrected error|EDAC' | tail -20
# 有 rasdaemon 的话,它能给出更结构化的统计
command -v ras-mc-ctl && ras-mc-ctl --errors

五、驱动与固件不匹配的告警

典型如 nvme nvme0: I/O ... timeout, aborting、ata: SError: { ... }、virtio_net: transmit queue timeout、BUG: soft lockup - CPU#1 stuck for 22s。soft lockup 是单核被内核态代码占死超过 20 秒,通常意味着驱动或内核 bug;hard lockup 则是连中断都进不去了,严重得多。

判断方法:如果同类告警在固件/内核升级后才出现,优先怀疑版本回归,考虑降级或换 LTS 内核,而不是去改业务代码。这一点对自建服务器的站长很实用——很多人一遇到卡顿就怀疑是 PHP 或 MySQL,实际上根因可能在内核更新带来的驱动变化。

把内核日志接进日常巡检:三条落地规则

规则一:每次重启前先存一份「重启前快照」

重启会清空 ring buffer,这是最不可逆的损失。用 systemd 的关机钩子把当前 dmesg 落盘:

# /etc/systemd/system/save-dmesg.service
[Unit]
Description=Save dmesg before shutdown
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target
RequiresMountsFor=/var/log

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'mkdir -p /var/log/dmesg-archive && dmesg -T > /var/log/dmesg-archive/$(date +%%Y%%m%%d-%%H%%M%%S).log && ls -1t /var/log/dmesg-archive/*.log | tail -n +21 | xargs -r rm -f'

[Install]
WantedBy=shutdown.target reboot.target halt.target
systemctl daemon-reload
systemctl enable save-dmesg.service
systemctl start save-dmesg.service   # 立刻验证一次能否写出文件
ls -la /var/log/dmesg-archive/

注意 %%Y 的双百分号——在 systemd unit 里 % 是占位符,必须转义,这是这类 unit 最常见的失败原因。另外务必先手动 start 一次确认能落盘,否则等到真关机时才发现没生效。

规则二:给日志来源加容量护栏

内核日志刷屏本身就会拖慢系统,尤其是磁盘 I/O 已经劣化的情况下——日志写入会进一步加剧 I/O 压力,形成正反馈。给 journald 设上限:

# /etc/systemd/journald.conf.d/limits.conf
[Journal]
SystemMaxUse=500M
SystemKeepFree=1G
SystemMaxFileSize=50M
RateLimitIntervalSec=30s
RateLimitBurst=2000
systemctl restart systemd-journald
journalctl --disk-usage

RateLimitBurst 默认 1000 条/30 秒,在高流量机器上很容易触发限流,导致「日志丢失」——而丢失的往往正是故障爆发那一刻的日志。所以排队型服务(尤其是 WAF、反代)要把这个值调高。

规则三:把「新出现的关键词」当作告警源

不要监控行数(噪声太大),而要监控「第一次出现的模板」。用一个小脚本维护历史模板库,出现新模板就发通知:

#!/bin/bash
# kern-new-template.sh — 只报「新出现」的内核消息模板
set -uo pipefail
LIB=/var/lib/kern-fp/known.txt
TODAY=$(mktemp)
journalctl -k --since "1 hour ago" --no-pager -o cat 2>/dev/null \
 | sed -E 's/[0-9a-f]{8,}/HEX/g; s/([0-9]+\.){3}[0-9]+/IP/g; s/[0-9]+/N/g' \
 | sort -u > "$TODAY"
touch "$LIB"
NEW=$(comm -23 "$TODAY" "$LIB")
if [ -n "$NEW" ]; then
    echo "发现新内核消息模板:"
    echo "$NEW" | head -20
    # printf '%s\n' "$NEW" >> "$LIB"   # 确认无害后再加入白名单
fi
rm -f "$TODAY"

这个思路的好处是:它把「我需要记住所有正常日志」这个不可能的负担,变成了「我只需要审一次新模板」的可管理动作。新模板确认无害后加入白名单,之后就不再打扰。

一次真实的归因流程:从告警到根因

把上面的东西串起来,实战中应该是这样的顺序,而不是随手 grep:

  1. 现象确认:先确认是人感受到的慢,还是监控指标异常,两者不一定同步。
  2. 时间锚定:拿到告警时间点,往前推 5 分钟作为检索起点。这个「提前量」很重要,因为内核报错往往比应用报错早几十秒到几分钟。
  3. 指纹对比:跑一次 kern-fingerprint.sh,与昨日/上周同时段对比,找出增量最大的模板。
  4. 上下文读取:用 journalctl -k --since ... --until ... 精确切片,配合 -o short-precise 拿到毫秒级时间戳,与其他服务的日志做时间对齐。
  5. 交叉验证:内核说 I/O 错误,就去 smartctl 看属性;内核说 conntrack full,就去 ss -s 看连接分布。单一来源不下结论。
  6. 写进变更记录:把「现象—证据—动作—结果」四段记一行,这才是下次能复用的资产。
# 精确切片 + 毫秒时间戳,用于和其他日志对齐
journalctl -k --since "2026-10-17 03:12:00" --until "2026-10-17 03:18:00" \
  --no-pager -o short-precise

这里有一个容易忽略的细节:journalctl 默认用本地时区显示,而很多应用日志用 UTC。对齐时间前先确认两边的时区,否则会得出「日志时间对不上,肯定是缓存」这类错误结论——这类误判在实际排查里出现的频率相当高。

几个反复出现的认知误区

误区一:dmesg 没报错就是硬件没问题。内核只记录它检测到的事件,很多磁盘劣化在 SMART 里已经报警,内核还没开始报错。所以 SMART 巡检是独立的一环,不能省。

误区二:清空 dmesg 可以「清理问题」。dmesg -c 只是清掉记录,问题本身不变,而且会毁掉唯一的证据。正确做法是归档后再清,或者干脆不清。

误区三:日志里的时间就是事件发生的时间。高负载时 printk 会排队,日志时间戳可能比真实事件晚若干秒。dmesg -T 用的是墙钟,和 [ 12345.678] 的单调时间不是一回事,两者不能混用来算间隔。

误区四:重启能修的就是小问题。恰恰相反,重启能「修好」的往往是状态类问题(内存泄漏、连接泄漏、句柄泄漏),这些才是最需要查的,因为它们的复发周期通常等于业务增长周期。重启只是把问题推迟到下次爆发。

小结:把内核日志当基线,而不是当报警器

内核日志真正的用法不是出事之后 grep 关键词,而是平时就有一份可以对比的指纹。三件事做到位,绝大多数「重启就好了」的玄学问题都能被提前定位:

  • 持久化:确认 /var/log/journal 存在,让日志跨重启存活,并在重启前自动归档 dmesg。
  • 归一化:用模板化把大量变量抽掉,让两次快照可比,只关注增量。
  • 交叉验证:内核日志给方向,SMART、ss、vmstat、应用日志给证据,单一来源不下结论。

对于个人站长来说,这套东西的成本很低——一个 cron、一个脚本、一个 500MB 的日志配额,就能把「服务器莫名其妙变慢」这类最耗时间的排查,压缩成几分钟的对比工作。更重要的是,它会让你在问题真正爆发之前就看见趋势:磁盘的 Pending Sector 从 0 变成 2,conntrack 使用率从 40% 爬到 85%,这些都不会立刻造成故障,但都指向同一个方向。看见了,就有时间从容处理。

Last modification:October 17th, 2026 at 12:27 pm

Leave a Comment