服务半夜凭空消失日志却很干净:一次 Linux 内存与 OOM Killer 排查复盘

一个 Java 服务半夜进程凭空消失,端口也没了,应用日志结尾却平静得反常、没有半个字的报错。排查梳理:凶手不在应用日志在内核日志、dmesg 里找 OOM Killer、free 看 available 不看 free、buff/cache 是内核借用的缓存、swap 是内存的备胎与变慢预警、OOM Killer 挑占内存最多的进程下手、overcommit 与 swappiness 两个内核参数,以及一套内存与 OOM 排查纪律。

2024 年,我们一个 Java 服务开始闹鬼。它会在半夜某个时刻突然消失——进程没了,端口也没了,监控发来告警。我第一时间扒应用日志,日志结尾平平静静,最后几行是再正常不过的业务输出,没有任何异常、没有任何报错、没有任何"我要退出了"的迹象,就像一个人说着话突然被人从中间掐断。重启,服务又活蹦乱跳;过几天,又在半夜悄无声息地没了。一个进程,既没崩溃报错,也没收到我发的关闭信号,却凭空蒸发——这事透着一股说不出的诡异。我盯着那截戛然而止的日志看了很久,始终想不通:到底是谁,在半夜杀了我的进程,还杀得这么干净、不留一句话?后来才发现,凶手根本不在我的应用日志里,而在内核日志里——是 Linux 内核的 OOM Killer。这件事逼着我把 Linux 的内存管理、freeswap、OOM Killer 这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,8G 内存,跑着一个 Java 服务和若干杂项
事故现象:
- Java 服务半夜进程凭空消失,端口也没了
- 应用日志结尾完全正常,无任何报错、无退出日志
- 重启后正常,过几天又在半夜消失

现场排查:
# 1. 应用日志结尾,平静得反常
$ tail -20 /var/log/app/app.log
... 处理订单 ORD2024xxx 完成
... 处理订单 ORD2024yyy 完成
(日志到此为止,没有任何异常或退出信息)
# ★ 进程不是自己退的,是被【外力】终止的

# 2. ★ 关键命令:看内核日志 dmesg
$ dmesg -T | grep -i 'killed process\|out of memory'
[Sun May 19 03:14:22 2024] Out of memory: Kill process 8123 (java)
                            score 879 or sacrifice child
[Sun May 19 03:14:22 2024] Killed process 8123 (java)
                            total-vm:9123456kB, anon-rss:6234567kB
# ★ 真相大白:是内核的 OOM Killer 杀了 java 进程!

# 3. 看内存到底紧张到什么程度
$ free -h
              total   used   free   shared  buff/cache  available
Mem:           7.6G   6.9G   180M     12M        520M       210M
Swap:            0B     0B     0B
#                                                          ^^^^^^
# ★ available 只剩 210M;而且 Swap 是 0 —— 一点缓冲都没有

根因(后来想清楚的):
1. ★ 物理内存被吃满:Java 堆 + 其他进程,把 8G 几乎用尽,
   available 只剩两百多兆。
2. ★ 这台机器【没有配置 swap】(Swap: 0B)。内存一旦告急,
   连"把冷数据换到磁盘救急"这条退路都没有。
3. ★ 当某个时刻内存彻底不够分,内核为了自保,会启动
   【OOM Killer】—— 挑一个"罪魁祸首"进程直接杀掉,
   释放内存,保住整个系统不崩。
4. 它的打分规则,倾向于杀【占内存最多】的进程 ——
   于是吃内存大户 Java 进程,成了首选目标。
5. ★ OOM Killer 是【内核】干的,用的是 SIGKILL,
   进程没有一丝机会留下遗言 —— 这就是应用日志为何那么"干净"。
进程半夜凭空消失,先去 dmesg 里找 OOM Killer。

修复 1:free 命令——内存到底用掉了多少

# === ★ 排查内存,第一个命令永远是 free ===
$ free -h                               # -h = 人类可读单位
              total   used   free   shared  buff/cache  available
Mem:           7.6G   6.9G   180M     12M        520M       210M
Swap:            0B     0B     0B

# === 逐列看懂(★ 每一列都有人理解错)===
# total      物理内存总量
# used       已被【进程】真正使用的内存
# free       完全【空闲、谁也没碰】的内存
# shared     多进程共享的内存(tmpfs 等)
# buff/cache 内核拿去做【缓冲和缓存】的内存(下一节专讲)
# available  ★ 真正重要的一列:估算【还能给新进程用】多少

# === ★ 最大的误区:盯着 free 那一列,以为它就是"可用内存" ===
# free = 纯空闲。但 buff/cache 那部分,内存一紧张【随时能还】。
# ★ 所以"还能用多少",看的是 available,不是 free!
# 这台机器 free 只有 180M 看着吓人,
# 但真正的紧张程度由 available(210M)说了算 —— 同样很危险。

# === 看更细的内存分布 ===
$ cat /proc/meminfo                     # 内核给出的完整内存信息
$ cat /proc/meminfo | grep -iE 'MemTotal|MemFree|MemAvailable'

# === 看【哪个进程】吃内存最多 —— 揪出大户 ===
$ ps aux --sort=-%mem | head -6
USER  PID  %MEM   RSS      COMMAND
root  8123 78.2  6234567   java -Xmx6g ...      # ★ 一个进程吃了 78%
# RSS = 该进程实际占用的物理内存(单位 KB)。
$ top -o %MEM                           # top 里按内存排序,动态看

# === ★ 一个常被忽略的点:看 RSS 也要看进程自己的"上限" ===
# 比如 Java,-Xmx6g 就给了它 6G 堆 —— 它本来就奔着吃 6G 去的。
# 一台 8G 的机器塞一个 -Xmx6g 的 Java,本身就埋了雷。

修复 2:buff/cache 不是"被占用"——available 才是真相

# === ★ 这是关于 Linux 内存,最该破除的一个误解 ===

# === 现象:很多人第一次看 free,会被 free 列吓到 ===
$ free -h
              total   used   free   buff/cache  available
Mem:           7.6G   2.1G   200M       5.3G       5.0G
# "free 只剩 200M 了!内存要爆了!" —— ★ 这是错的判断。

# === buff/cache 到底是什么 ===
# Linux 有个理念:【空闲的内存就是浪费的内存】。
# 所以内核会主动把暂时没人用的内存,拿去缓存最近读过的
# 文件、磁盘块 —— 这就是 buff/cache。目的是【下次读得更快】。
# ★ 关键:这部分内存是"借用"。一旦有进程真的需要内存,
#   内核会立刻把 buff/cache 释放还回去。

# === ★ 所以判断内存够不够,只看一列:available ===
# available = free + 【可以被回收的】buff/cache 等
#           = 内核估算的"现在还能给新进程多少内存"
# 上面例子:free 才 200M,但 available 有 5.0G ——
#   真实情况是【内存很宽裕】,那 5.3G cache 随叫随还。

# === ★ 反过来,真正危险的信号是这个 ===
# available 很低(比如只剩几百兆)+ free 也低
# -> 这才是内存真的快不够了。我这次的事故,正是这种:
#    available 只剩 210M,cache 也压到很低,没东西可还了。

# === 手动释放 cache(★ 一般【不需要】,生产慎用)===
$ sync && echo 3 > /proc/sys/vm/drop_caches
# ★ 这只是"看着舒服"—— cache 本来就会自动释放,
#   手动 drop 反而丢了缓存、让后续读变慢。除非排查需要,别动。

# === 一句话纪律 ===
# ★ 看内存够不够,看 available;别被 free 那一列吓住。
# buff/cache 高,通常是好事,说明内核在好好利用内存。

修复 3:swap 是什么——内存的"备胎"

# === ★ 这次事故的一个加重因素:这台机器 Swap 是 0 ===

# === swap 是什么 ===
# swap(交换空间)是【磁盘上】划出的一块区域,
# 当物理内存吃紧时,内核会把一些【不活跃】的内存页
# (一阵子没被访问的)挪到 swap 里,腾出物理内存救急。
# ★ 它是内存的"备胎"——慢,但能在内存告急时多撑一会儿。

# === 看当前 swap 情况 ===
$ free -h | grep Swap
Swap:            0B     0B     0B        # ★ 这台机器没配 swap
$ swapon --show                          # 看有哪些 swap 设备/文件
# 没输出 = 一个 swap 都没有。

# === ★ 没有 swap 意味着什么 ===
# 内存一旦不够,内核【没有缓冲余地】——
# 不能"挪点冷数据去磁盘撑一下",只能立刻走极端:OOM Killer。
# 有 swap 时,内存紧张会先表现为"变慢"(在 swap 里换进换出),
# 给你留出发现问题、处理问题的时间窗口。

# === 给机器加一块 swap(用文件做,简单)===
$ dd if=/dev/zero of=/swapfile bs=1M count=4096   # 建 4G 文件
$ chmod 600 /swapfile                    # ★ 权限必须 600
$ mkswap /swapfile                       # 格式化成 swap
$ swapon /swapfile                       # 启用
$ free -h | grep Swap                    # 确认生效
Swap:          4.0G     0B   4.0G

# === ★ 让 swap 开机自动生效:写进 /etc/fstab ===
$ echo '/swapfile none swap defaults 0 0' >> /etc/fstab
# 不写这行,重启后 swap 就没了。

# === ★ swap 不是万能药,要清醒认识 ===
# - swap 在磁盘上,比内存慢几个数量级。
#   服务大量用 swap = 性能雪崩,本质还是内存不够。
# - swap 的价值是【缓冲和兜底】,不是"内存的廉价替代品"。
# - 真正的解法永远是:加物理内存 / 减少程序内存占用。
#   swap 只是让你在出事前,多一个"变慢"的预警信号。

修复 4:OOM Killer——谁在半夜杀了我的进程

# === ★ 这次的真凶:OOM Killer(Out Of Memory Killer)===

# === 它是什么、为什么存在 ===
# 当内核发现"内存彻底不够分了,再不处理整个系统都要卡死",
# 它会启动 OOM Killer:【挑一个进程杀掉】,释放内存,
# 保住整个系统不崩。这是内核的【自保机制】。
# ★ 它用的是 SIGKILL —— 被选中的进程没有一丝机会清理、
#   没有一丝机会留下日志。这就是应用日志为何那么"干净"。

# === ★ 怎么确认进程是被 OOM Killer 杀的 ===
$ dmesg -T | grep -i 'out of memory\|killed process'
[Sun May 19 03:14:22 2024] Out of memory: Kill process 8123 (java)
[Sun May 19 03:14:22 2024] Killed process 8123 (java) total-vm:...
# 或从 systemd 日志看:
$ journalctl -k | grep -i 'oom\|killed process'
$ journalctl --since '2024-05-19 03:00' | grep -i oom
# ★ 应用日志里找不到的死因,内核日志 dmesg 里写得清清楚楚。

# === OOM Killer 怎么挑"该杀谁" —— oom_score ===
# 内核给每个进程算一个 oom_score,分越高越容易被杀。
# 算分主要看:进程占了多少内存(占得越多分越高)。
$ cat /proc/8123/oom_score               # 看某进程的当前得分
# ★ 所以"吃内存大户"永远是 OOM Killer 的首选目标 ——
#   我那个 -Xmx6g 的 Java,几乎是必选项。

# === ★ 保护关键进程:调 oom_score_adj ===
# oom_score_adj 范围 -1000 ~ 1000,负数 = 降低被杀概率。
$ echo -500 > /proc/8123/oom_score_adj    # 让 8123 不那么容易被杀
$ echo -1000 > /proc/8123/oom_score_adj   # -1000 = 基本豁免
# ★ 但这只是"换个进程杀"——内存的总账不平,迟早还是要出事。
# systemd 服务里也能配:[Service] OOMScoreAdjust=-500

# === ★ 别忘了:被杀的不一定"该死" ===
# OOM Killer 杀的是"占内存最多的",未必是"真正泄漏的那个"。
# 你的核心服务因为内存大,可能替真正的内存元凶背了锅。
# 所以查到 OOM,别只盯着被杀的进程,要查【整机内存为何耗尽】。

修复 5:overcommit 与 swappiness——两个相关内核参数

# === ★ 围绕 OOM,有两个内核参数值得了解 ===

# === 参数 1:vm.overcommit_memory —— 内核"超额承诺"内存 ===
$ sysctl vm.overcommit_memory
vm.overcommit_memory = 0                  # 默认值
# Linux 默认允许"超额承诺":进程申请内存(malloc)时,
# 内核往往【先答应下来】,赌"你不会真的全用上"。
# - 0:启发式判断,适度超额(默认,绝大多数场景用这个)
# - 1:无脑答应所有申请(★ 危险,极易触发 OOM)
# - 2:严格模式,按 swap + 物理内存×比例 算总额,不超额
# ★ 一般【不要乱改】这个值,默认的 0 适合绝大多数情况。
# 它解释了一个现象:为什么进程 malloc 时没报错,
#   等真正去【写】那块内存时,反而触发了 OOM。

# === 参数 2:vm.swappiness —— 内核多积极地用 swap ===
$ sysctl vm.swappiness
vm.swappiness = 30                        # 0~100,越大越爱用 swap
# - 值大:内存还没很紧张,内核就开始往 swap 挪页
# - 值小:尽量用物理内存,实在不行才动 swap
# 服务器一般设小一点(10~30),减少不必要的换页、保性能:
$ vi /etc/sysctl.conf
vm.swappiness = 10
$ sysctl -p
# ★ 注意:swappiness=0 不等于"禁用 swap"——
#   它只是"尽量不用",内存真到绝境照样会用 swap。

# === ★ 比调参数更重要的:从根上解决内存不够 ===
# 这次事故,真正该做的三件事,没有一件是调上面的参数:
# 1. ★ 把 Java 的 -Xmx 调到和物理内存匹配
#    (8G 的机器别给 -Xmx6g,留足内核和其他进程的份)
# 2. ★ 加 swap,哪怕只是为了把"猝死"变成"变慢预警"
# 3. ★ 上内存监控告警:available 低于阈值就提前报警,
#    别等 OOM Killer 替你"发现"问题。
# 内核参数是微调;内存的总账,得靠容量和程序本身去平。

修复 6:内存与 OOM 排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ 进程凭空消失、应用日志无报错,先查 dmesg ===
$ dmesg -T | grep -i 'out of memory\|killed process'
# 进程被 OOM Killer / 其他外力杀掉,内核日志里有记录。

# === 2. ★ 看内存够不够,看 available,不看 free ===
$ free -h
# buff/cache 高是好事;available 低才是真危险。

# === 3. 揪内存大户用 ps 按 %mem 排序 ===
$ ps aux --sort=-%mem | head

# === 4. ★ 服务器配一块 swap,把"猝死"变成"变慢预警" ===
$ swapon --show                           # 先看有没有
# 没有就建一块,并写进 /etc/fstab。

# === 5. OOM 杀的是占内存最多的,未必是真凶 ===
# 查到 OOM 别只盯被杀进程,要查整机内存为何耗尽。

# === 6. 进程内存上限要和物理内存匹配 ===
# Java 的 -Xmx、各种缓冲池,别配得超过机器扛得住的量。

# === 7. 排查内存问题的命令链 ===
$ free -h                                 # ① 整机内存,看 available
$ dmesg -T | grep -i oom                  # ② 有没有发生过 OOM
$ ps aux --sort=-%mem | head              # ③ 谁是内存大户
$ cat /proc/meminfo                       # ④ 内核级内存明细
$ swapon --show                           # ⑤ 有没有 swap 兜底
$ cat /proc//status | grep -i vm     # ⑥ 单个进程内存细节
# 按这个顺序,内存与 OOM 问题基本能定位。

命令速查

需求                        命令
=============================================================
看整机内存(看 available)   free -h
看内核内存明细              cat /proc/meminfo
按内存占用排序进程          ps aux --sort=-%mem | head
top 里按内存排序            top -o %MEM
确认是否发生过 OOM          dmesg -T | grep -i 'out of memory'
从 systemd 日志看 OOM       journalctl -k | grep -i oom
看有没有 swap               swapon --show
建并启用一块 swap 文件      mkswap /swapfile && swapon /swapfile
看某进程的 OOM 得分         cat /proc//oom_score
保护进程不被 OOM 杀         echo -500 > /proc//oom_score_adj

口诀:进程凭空消失先查 dmesg 找 OOM Killer
      内存够不够看 available 不看 free

避坑清单

  1. 进程凭空消失且应用日志无报错,先 dmesg 查是不是被 OOM Killer 杀的
  2. 判断内存够不够看 available 那一列,不要被 free 列的小数字吓到
  3. buff/cache 是内核借用的缓存,内存紧张随时归还,不算"被占用"
  4. OOM Killer 是内核自保机制,用 SIGKILL,进程没机会留任何日志
  5. OOM Killer 倾向杀占内存最多的进程,未必是真正泄漏的元凶
  6. 没有 swap 时内存告急会直接 OOM,有 swap 会先变慢给出预警窗口
  7. swap 在磁盘上很慢,是缓冲兜底不是内存替代,大量用 swap 等于内存不够
  8. swap 文件要 chmod 600,并写进 /etc/fstab 才能开机生效
  9. oom_score_adj 只是换个进程杀,不解决内存总账不平的根本问题
  10. 进程内存上限(如 Java -Xmx)要和物理内存匹配,留足内核和其他进程

总结

这次"进程半夜凭空消失"的事故,纠正了我一个埋藏得很深的盲区——我排查问题的目光,长期以来只会落在"我的应用"这一个圈子里。在这次之前,我潜意识里有一个从未质疑过的前提:一个进程的死,一定会在【它自己的日志里】留下死因。它要么是抛了未捕获的异常,要么是某段逻辑走进了死胡同,要么是收到了关闭信号体面退出——无论哪种,日志里总该有迹可循。所以当那个 Java 服务半夜消失、而应用日志的结尾却平静得反常——最后几行是再正常不过的业务输出,没有异常、没有报错、没有半个字的"我要走了"——我陷入了彻底的困惑。一个会自己崩溃的进程,日志里会有惨叫;一个收到关闭信号的进程,日志里会有遗言。可眼前这个进程,既没惨叫也没遗言,它就是在说着话的中途,被人从句子中间硬生生掐断了。我对着那截戛然而止的日志想了很久,始终绕不出那个怪圈:凶手不在日志里,可我又坚信凶手一定在日志里。复盘到根上,我才终于意识到,我一直忽略了一个根本的事实——我的应用,从来不是这台机器的主人,它只是内核统治下的一个"租户"。这台机器上真正说了算的,是 Linux 内核。当整台机器的物理内存被耗尽、内核发现自己再不出手整个系统都要卡死时,它会启动一个我此前闻所未闻的自保机制:OOM Killer。OOM Killer 的逻辑冷酷而清晰——它会从所有进程里挑一个"垫背的",一刀杀掉,用它释放出的内存保住整个系统的性命。而它挑选的标准,简单粗暴:谁占的内存最多,谁的嫌疑就最大。我那个被 -Xmx6g 喂得膀大腰圆的 Java 进程,在一台只有 8G 内存的机器上,几乎是 OOM Killer 名单上不容错过的头号目标。更关键的是,OOM Killer 是【内核】亲自动的手,它用的是 SIGKILL——那个不可捕获、不可阻挡的信号。这就解释了那截"干净得反常"的日志:我的进程根本没有收到任何可以反应的信号,它是在毫无预兆的情况下被内核从内存里瞬间抹除的,它当然来不及、也根本没有机会写下任何一个字的遗言。死因不在应用日志里,是因为杀它的根本不是应用层的任何东西——死因写在内核的日志里,写在 dmesg 里,那里白纸黑字躺着一行 Out of memory: Kill process 8123 (java)。这次事故还顺带捅破了我对 Linux 内存的另一个误解。我过去看 free,总是死死盯着 free 那一列,看到它只剩几百兆就心惊肉跳。可实际上,真正该看的是 available 那一列——因为 Linux 会主动把暂时空闲的内存拿去做文件缓存(buff/cache),这部分内存是"借用"的,任何进程一旦真需要,内核立刻归还。free 小不代表危险,available 小才是真的危险。而我这台出事的机器,available 确实已经低到只剩两百多兆,雪上加霜的是它连一块 swap 都没配——没有 swap,内存告急时内核连"把冷数据挪去磁盘撑一会儿"的退路都没有,只能从"内存紧张"这个状态,一步跨进 OOM Killer 这个终局。这次从一截"干净得反常"的日志出发,我最大的收获,是把排查的视野从"我的应用"这个小圈子里挣脱出来,郑重地补上了"内核"这一层。一个进程的命运,从来不只由它自己的代码决定;它脚下那个沉默的内核,握有在资源耗尽时决定"杀谁、留谁"的生杀大权。当一个进程死得没有遗言,不要再徒劳地翻它自己的日志了——它的死讯,记在它那位沉默房东的账本上,记在 dmesg 里。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

手动能跑放进 crontab 就失踪:一次 Linux 定时任务排查复盘

2026-5-20 19:31:53

Linux教程

A 说没收到 B 说发了:一次 Linux tcpdump 抓包定位丢包复盘

2026-5-20 19:38:25

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索