正文
再结合没有产生 core-dump 文件的现象,如果是内存耗尽导致进程被 oom-killer 进程杀死是说得通的。因为oom-killer 进程使用
SIGKILL
信号强制杀死进程,查看 Linux 信号手册,根据 POSIX.1-1990 标准,
SIGKILL
信号意味着进程被强制结束并且不进行核心转储。
Signal Standard Action Comment
────────────────────────────────────────────────────────────────────────
...
SIGIOT - Core IOT trap. A synonym for SIGABRT
SIGKILL P1990 Term Kill signal
SIGLOST - Term File lock lost (unused)
...
瞌睡遇上枕头,刚好发现监控平台上线了单机秒级监控(感谢平台工具给力),再找到发生 crash 的机器和时间点,发现推测是对的,在几秒内其中一个 worker 进程内存占用飙升,从几百 MB 一路暴涨到十几 GB,在 8C16G 规格的机器上很快就因为内存耗尽被内核杀掉。
问题查到这里,好消息是排查方向总算对了,坏消息是 OOM 进程闪退只是问题的表现,而导致内存飙升的根本原因还是没什么头绪。
怀疑有异常的攻击流量,然而查看闪退前后该机器的网络流量,inbytes 和 outbytes 并没有波动,所以也基本排除了被突发流量攻击;怀疑是网关上 ip geo 信息查询的二分查找逻辑有死循环,经过代码检查和测试也没发现这里有问题;甚至怀疑系统跑久了有内存碎片,但经过排查也排除了这种可能,今年之前也没出现这种问题。
所以目前的情况就是在没有任何外部攻击的情况下,系统内存突然就爆了。这还真是见了鬼,排查了这么多问题,如此诡异的情况也是少见。
core-dump 文件是进程闪退前最后的“遗照”,类似尸检之于法医,对于查问题能提供非常多线索。拿不到转储文件真是两眼一抹黑 —— 全靠猜。所以痛定思痛,决定想办法把 core-dump 文件拿到。
既然闪退是因为内存占用过高,而被 oom-killer 杀死又不会进行核心转储,总不能到 Linux 内核里修改 oom-killer 发送的信号。在跟师兄讨论时,师兄提出一个思路:在用户态实现一个 oom-killer(青春版),当然没有复杂的打分逻辑,只需要检测目标进程的内存用量,到阈值之后发送一个可以产生内核转储行为的信号来杀死进程,通过主动杀死进程的方式产生 core-dump 文件。
后面师兄抽空帮忙写了一个 nginx 辅助进程,逻辑是每秒检测一次所有 worker 进程的内存用量,如果超过一定阈值就发送
SIGABORT
信号主动杀死对应进程。当然为了防止工具误杀导致更严重的问题,限制在应用重启后的生命周期内最多只会触发一次。
找了部分机器部署之后,还真给拿到了 core-dump 文件,不过还有个小插曲,第一次拿到的文件过大发生了截断,后续又将单进程的内存阈值从 8GB 调整为 4GB,终于拿到了完整可用的 core-dump!
如上图可以看到程序的堆栈,这种通过自杀产生的内核堆栈不像内存越界的堆栈直接指向了程序崩溃点,当前的堆栈只能反应程序在异常时执行的代码,不一定是准确的问题点,但也能提供非常多的线索。从堆栈结合代码可以看出程序正在进行数据攒批写出, 再往前是 schema 埋点数据的拆分,攒批写出时恰好在调用 ngx_pcalloc 函数从内存池中申请内存,所以很可能是在 schema 埋点数据拆分之后的攒批发送时内存分配出了问题。