这两天一台 VPS 上的 MySQL 总是自己宕机,查看了一下日志发现是因为 OOM ,内存不足被系统 Kill 了。
❯ sudo /etc/init.d/mysqld status
× mysqld.service - LSB: start and stop MySQL
Loaded: loaded (/etc/init.d/mysqld; generated)
Active: failed (Result: oom-kill) since Mon 2024-05-27 22:23:15 CST; 12h ago
Docs: man:systemd-sysv-generator(8)
Process: 2419860 ExecStop=/etc/init.d/mysqld stop (code=exited, status=0/SUCCESS)
CPU: 1h 11min 53.021s
Notice: journal has been rotated since unit was started, output may be incomplete.
网上稍微查了一下,可以使用修改 syetemd service 的方式来阻止 Linux 因为内存不足而杀死 MySQL。
做法
编辑
sudo systemctl edit mysqld.service
然后在配置文件中添加
[Service]
OOMScoreAdjust=-1000
保存配置文件之后,重启 MySQL
sudo systemctl restart mysqld.service
可以检查配置是否生效,记得替换 MySQL 的 PID。
cat /proc/$(pidof mysqld)/oom_score_adj
原因分析
MySQL 宕机并出现 “Out of memory” 问题,通常是由于短时间内应用程序大量请求导致系统内存不足,从而触发了 Linux 内核中的 Out of Memory (OOM) killer 机制。OOM killer 会终止某个进程以释放内存给系统使用。
通过检查相关日志文件(/var/log/),可以看到类似的 Out of memory: Kill process 信息:
May 25 19:06:53 wh kernel: [5351539.967422] php-fpm invoked oom-killer: gfp_mask=0x1100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
May 25 19:06:53 wh kernel: [5351539.967429] CPU: 1 PID: 2384831 Comm: php-fpm Not tainted 5.15.0-84-generic #93-Ubuntu
Linux 内核根据应用程序的需求分配内存,通常应用程序分配了内存但未全部实际使用。为提高性能,这部分未用的内存可以被其它进程利用。这种内存归属于每个进程,内核直接回收利用较为复杂,因此采用了内存过度分配(over-commit memory)的策略,以间接提高内存使用效率。一般情况下,这种策略是有效的,但当大多数应用程序同时消耗内存时,问题就出现了。此时,所有应用程序的内存需求加起来超出了物理内存(包括 swap)的容量,内核必须通过 OOM killer 终止一些进程来释放内存,保障系统正常运行。可以通过银行的例子来理解:当部分人取钱时银行能够应对,但如果全国人民同时取钱且都想取完自己的钱,银行实际上无法满足。
内核检测到内存不足并选择终止进程的过程,可以参考内核源代码 linux/mm/oom_kill.c。当系统内存不足时,会触发 out_of_memory()
,然后调用 select_bad_process()
选择一个 “bad” 进程进行终止。选择 “bad” 进程的过程由 oom_badness()
决定,其算法主要根据进程占用的内存量来判断:
/**
* oom_badness - heuristic function to determine which candidate task to kill
* @p: task struct of which task we should calculate
* @totalpages: total present RAM allowed for page allocation
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg, const nodemask_t *nodemask, unsigned long totalpages)
{
long points;
long adj;
if (oom_unkillable_task(p, memcg, nodemask))
return 0;
p = find_lock_task_mm(p);
if (!p)
return 0;
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN) {
task_unlock(p);
return 0;
}
points = get_mm_rss(p->mm) + p->mm->nr_ptes + get_mm_counter(p->mm, MM_SWAPENTS);
task_unlock(p);
if (has_capability_noaudit(p, CAP_SYS_ADMIN))
adj -= 30;
adj *= totalpages / 1000;
points += adj;
return points > 0 ? points : 1;
}
理解了这个算法,我们就理解了为什么 MySQL 总是被首当其冲地终止,因为它的内存占用最大。解决这个问题的最简单方法是增加内存,或优化 MySQL 使其占用更少的内存。此外,还可以优化系统以减少内存占用,使应用程序(如 MySQL)能够使用更多内存。一个临时解决方案是调整内核参数,使 MySQL 进程不易被 OOM killer 选中。
配置 OOM killer
可以通过调整内核参数来改变 OOM killer 的行为,避免频繁终止进程。例如,可以在触发 OOM 后立即触发 kernel panic,并在 10 秒后自动重启系统:
# sysctl -w vm.panic_on_oom=1
vm.panic_on_oom = 1
# sysctl -w kernel.panic=10
kernel.panic = 10
# echo "vm.panic_on_oom=1" >> /etc/sysctl.conf
# echo "kernel.panic=10" >> /etc/sysctl.conf
从上面的 oom_kill.c 代码可以看到,oom_badness()
会为每个进程打分,根据分数决定终止哪个进程。可以通过调整进程的 oom_score_adj
参数来控制哪些进程不易被选中终止。例如,如果不希望 MySQL 进程被轻易终止,可以找到 MySQL 的进程号,并将其 oom_score_adj
设置为 -15:
# ps aux | grep mysqld
mysql 2196 1.6 2.1 623800 44876 ? Ssl 09:42 0:00 /usr/sbin/mysqld
# cat /proc/2196/oom_score_adj
0
# echo -15 > /proc/2196/oom_score_adj
当然,如果需要的话,还可以完全禁用 OOM killer(不推荐在生产环境中使用):
# sysctl -w vm.overcommit_memory=2
# echo "vm.overcommit_memory=2" >> /etc/sysctl.conf