前段时间碰到一个很烦的事:同一个 ES 集群里,4 台物理机几乎同时挂了,3 台自动重启,1 台直接起不来,还得手动介入。
这事最开始看起来很像“机器突然抽风”,但我后面拿到两份 vmcore 去看,发现主线其实很清楚:不是 ES 进程自己崩了,也不是普通的内存不够,而是 dentry 缓存堆得太离谱,回收时又卡太久,最后把内核直接拖进了 panic。
更坑的是,这类问题特别容易被人用一种很糙的方式去“解决”:
sync && echo 2 > /proc/sys/vm/drop_caches
这招我不喜欢。它看着像止血,实际上更像拿榔头敲缓存,不但治不了根,还很容易顺手把性能也敲掉。下面把整个链路捋一下。
先说现象
这个问题不是第一次出现。
更早之前,单机房环境里的 ES 物理机就出现过批量 hang,当时已经怀疑过 dentry 缓存过高。只是那次还没来得及把这个点彻底收住,后面同一集群里又出了更狠的一次,4 台机器一起倒。
我这边能拿到的是两份 vmcore。虽然机器挂的方式不完全一样,但最后指向的是同一条线:
- 系统里有海量
dentry - 某个时刻内核开始回收这些
dentry - 回收过程太慢,CPU 长时间出不来
- watchdog 判定软锁死
- 由于
kernel.softlockup_panic=1,软锁死直接升级成内核恐慌 - 机器重启或者彻底挂死
如果只看最后一跳,很容易把注意力放在“为什么 panic”上。但那不是真问题。真正的问题是,前面的 dentry 为什么会堆到这么夸张。
先确认是不是它
我不太喜欢一上来就讲概念,先看数。
当时系统里这样算 dentry 占用:
cat /sys/kernel/slab/dentry/total_objects | awk '{print $1*204/1024/1024/1024,"G"}'
222.886 G
这个值已经不是“有点高”了,而是离谱。
再看整机内存:
$ free -h
total used free shared buff/cache available
Mem: 503G 247G 16G 21M 239G 256G
Swap: 0B 0B 0B
buff/cache 239G,和上面算出来的 dentry 体量基本对得上。
看到这里,方向其实已经很明确了:不是用户态进程把匿名内存吃光了,而是内核 slab 里塞了太多目录项缓存。机器总内存看起来还有“available”,但这不代表系统就安全。因为你真正撞上的不是“有没有剩余内存”这么简单,而是“内核清这些东西时要花多大代价”。
这事为什么会落到 ES 头上
根子大概率还是 ES 自己的文件模型。
ES 尤其是分片多、segment 多的时候,磁盘上会有大量小文件。文件一多,路径查找对应的 dentry 就会跟着涨。平时这些缓存存在内存里,查路径很快,系统看起来也不一定有明显异常,所以这个问题很容易拖很久。
真正麻烦的点在于,它不是那种“立刻把内存打满然后 OOM”的问题,而是会先在系统里攒一堆债。等某次回收开始了,债主一起上门。
这也是为什么很多人看到 free -h 里还有不少 available,第一反应会觉得“还行啊”。我一开始也不是没往这个方向想过,但后来看 vmcore 才意识到,主线根本不在“剩多少”,而在“回收能不能收得动”。
机器是怎么一步步挂掉的
把这次事故压缩一下,大概就是这么一条链:
1. dentry 一直涨
ES 产生了海量小文件,对应的 dentry 也一路累积。这个阶段机器未必会立刻报警,很多时候只是 slab 慢慢变大。
2. 某次事件触发回收
从 vmcore 看,触发点和 udevd 重新读取磁盘分区有关。这里不一定每次都得是 udevd,内存压力也可能触发类似路径,但这次卡死点确实落在这里。
3. 回收太慢
dentry 数量已经大到夸张,内核遍历和回收时花了太久,CPU 长时间被占住。
4. 软锁死
单核持续跑不出来,超过 watchdog 阈值后,就会被判定为 soft lockup。
5. panic
偏偏机器上还配了:
sysctl kernel.softlockup_panic
kernel.softlockup_panic = 1
所以这次不是“记一条告警日志然后继续跑”,而是直接 panic。某种意义上,真正把机器送走的最后一脚,不是 dentry 本身,而是“海量 dentry 回收过慢 + soft lockup + panic 开启”这个组合拳。
为什么我不认可定时 drop_caches
出了这种事后,最容易冒出来的方案就是配个定时任务,半夜跑一把:
sync && echo 2 > /proc/sys/vm/drop_caches
这个方案表面上很勤快,实际上问题不少。
第一,它解决不了来源。dentry 不是凭空长出来的,源头还是 ES 的文件数量和生命周期管理。你凌晨清一次,白天它照样继续涨。只要增长速度够快,两次定时任务之间照样能把你送走。
第二,它完全不看场合。真正常规一点的做法,至少也该有阈值、有判断,而不是每天到点就硬清。机器本来没到危险区,你也给它来一下,这就很蠢。
第三,副作用不小。sync 会把脏页往盘上刷,drop_caches 又会把页缓存、dentry、inode 一起清。ES 这种本来就很吃缓存命中率的服务,刚清完那一阵,性能抖一下几乎是必然的。
所以这东西最多只能算紧急时候的人工操作,而且还得知道自己在干什么。把它包装成“长期解决方案”,我觉得不靠谱。
真正该做的,还是两层一起收
先治源头
这类问题如果只盯着内核参数调,最后大概率还是会回来。核心还是要把文件数量压下去。
对 ES 来说,重点就几个:
- 看分片是不是太碎了
- 看 segment 文件是不是太多了
- 该归档、关闭、删除的索引别一直挂着
- 用好 ILM,必要时做
force_merge
这里面我最看重的还是两件事:别过度分片,以及别让历史索引长期保持一种“文件多但业务价值不高”的状态。很多时候不是机器不够大,而是文件组织方式太差。
再做内核侧兜底
内核参数不能治本,但可以帮你把坑边缘往后挪一点。
一个比较直接的参数是:
sysctl -w vm.vfs_cache_pressure=200
它的意思不是“立刻删缓存”,而是让内核在回收时,对 dentry 和 inode 更不客气一点。默认值是 100,往上调之后,至少不会让这类缓存无限舒服地堆着。
另一个参数是:
sysctl -w kernel.softlockup_panic=0
这个改动我会把它定义成止损,不是修复。
把它设成 0 之后,遇到软锁死时,内核至少不会第一时间把整机打崩。日志还在,告警还在,机器可能还是会卡,但不至于直接陪葬整个业务。
当然,这个参数也不是改完就能高枕无忧。如果根因还在,机器只是从“立刻死”变成“卡着不死”,一样难受。
监控别等出事了才补
这种故障有个很烦的点:它前期并不吵。
所以监控必须提前做,不然你平时看 CPU、load、用户态内存,全都可能没那么刺眼,等看到的时候已经很难看了。
至少这几个东西该盯起来:
/sys/kernel/slab/dentry/total_objects
/proc/slabinfo
slabtop -o
我的建议不是只盯“有没有涨”,而是盯增长速度、总量、以及它在总内存里的占比。因为不同规格机器,危险线不一样,告警阈值最好结合历史数据来定,不要拍脑袋写个固定值。
最后收一下
这次问题如果只用一句话概括,就是:
不是 ES 把内存简单吃满了,而是 ES 产生的海量小文件把 dentry 缓存推到了危险区,等内核真的开始大规模回收时,回收路径又慢到触发 soft lockup,最后因为 kernel.softlockup_panic=1,机器被直接送进了 panic。
所以这事不能靠定时清缓存糊弄过去。真正有用的动作,还是两件事:
- 从 ES 侧把文件数量降下来
- 从内核侧把回收策略和
soft lockup的处置方式调到更合理
不然的话,今天你半夜 drop_caches 一次,明天它还是会长回来。区别只是在于,下一次它打你,是白天还是凌晨。