在 5.x 内核中,当内存使用水位持续非常高的场景下,相比 3.10 低版本内核 5.x 内核会更多的尝试去回收内存而不是尽早触发 oom,所以这种场景下回收内存行为有比较大的概率会导致磁盘压力升高,因为大量 cache 会落盘,但由于进程持续运行会将进程的二进制文件持续读取到内存中,最终导致的现象就是内存持续回收,进程持续读二进制数据到内存,CPU 忙于回收内存和读取数据,磁盘的读 IO 也持续跑满,GuestOS 出现夯机。接下来针对这个场景我们研究一下社区提供的用户态 oomkill 的方案 oomd。
复现代码
先来看下该问题如何复现,下面是我写的一个复现脚本,用于申请指定大小的内存,每次申请大小是4K。
import os
import sys
import gc
import time
def allocate_large_memory(size_in_mb):
chunk_size = 4 * 1024 # 4KB
total_size_in_bytes = size_in_mb * 1024 * 1024
large_memory_chunks = []
for _ in range(total_size_in_bytes // chunk_size):
chunk = bytearray(chunk_size)
large_memory_chunks.append(chunk)
remaining_bytes = total_size_in_bytes % chunk_size
if remaining_bytes > 0:
last_chunk = bytearray(remaining_bytes)
large_memory_chunks.append(last_chunk)
gc.disable()
return large_memory_chunks
def set_oom_adj(pid):
print("Setting oom_adj for pid %d..." % pid)
cmd = "echo -17 > /proc/%d/oom_adj" % pid
ret = os.system(cmd)
if ret != 0:
return ret
return 0
if __name__ == '__main__':
if len(sys.argv) != 2:
print("Usage: mem.py [mem_size(mb)].")
exit(0)
pid = os.getpid()
ret = set_oom_adj(pid)
if ret == 0:
size_in_mb = int(sys.argv[1])
large_memory_chunks = allocate_large_memory(size_in_mb)
total_allocated_size = sum(len(chunk) for chunk in large_memory_chunks) / (1024 * 1024)
print(f"Allocated {total_allocated_size:.2f}MB of memory using 4KB chunks.")
time.sleep(1000)
else:
print("Error setting oom_adj...")
复现步骤
环境:Ubuntu22.04 5.15 内核版本。
- 查看本机内存水位,available 是 7162,vm.min_free_kbytes 默认是 66 兆左右,那么我们申请 7100 兆之后肯定能踩到内存回收水线。
- 按这个思路执行
python3 mem.py 7100
申请 7100 兆内存 - 内存申请后数秒,可以看到机器已经出现夯机,监控查看磁盘此时读 IO 持续打满(这里为什么是读 IO 高,写 IO 不高?因为这个场景中基本上都是读操作,page 没有被改动,那么回收内存过程中因为 page 是 clean 状态,内核直接清理掉,而不会刷盘)。
在这种夯机的场景中,业务层面可能会更关注如何恢复,但夯机的时候业务可能并非完全不可用,可能导致部分探活的机制无法正常探测到服务异常,但实际业务已经出现问题。这个过程会导致业务恢复周期变长,基于这种考虑更多时候我们希望直接触发 oom kill 将进程 kill 掉,利用一些高可用的机制将业务重新拉起来,但遗憾的是高版本内核层面无法通过简单的配置实现 oom kill。 不过社区中针对这种场景有一些从用户态实现的 oom kill 能力,比如 Facebook 开源的 oomd方案,下面我们基于 oomd 这个方案做一些验证。
安装
- Debian 11+ or Ubuntu 20.04+
apt install oomd
sudo systemctl enable --now oomd.service
sudo systemctl start oomd.service
- CentOS Stream 8 及以上版本。
yum install oomd
sudo systemctl enable --now oomd.service
sudo systemctl start oomd.service
配置文件
oomd 提供了多种监控系统压力的方式,主要是内存和 IO 维度。详细插件的介绍可以参考社区文档:https://github.com/facebookincubator/oomd/blob/main/docs/core_plugins.md。
配置文件格式
ARG:
<string>: <string>
NAME:
<string>
PLUGIN:
{
"name": NAME,
"args": {
ARG[,ARG[,...]]
}
}
DETECTOR:
PLUGIN
DETECTOR_GROUP:
[ NAME, DETECTOR[,DETECTOR[,...]] ]
ACTION:
PLUGIN
DROPIN:
"disable-on-drop-in": <bool>,
"detectors": <bool>,
"actions": <bool>
SILENCE_LOGS:
"silence-logs": "NAME[,NAME[,...]]"
POST_ACTION_DELAY:
"post_action_delay": "<int>"
PREKILL_HOOK_TIMEOUT:
"prekill_hook_timeout": "<int>"
RULESET:
[
NAME,
DROPIN,
SILENCE_LOGS,
POST_ACTION_DELAY,
PREKILL_HOOK_TIMEOUT,
"detectors": [ [DETECTOR_GROUP[,DETECTOR_GROUP[,...]]] ],
"actions": [ [ACTION[,ACTION[,...]]] ],
]
ROOT:
{
"rulesets": [ RULESET[,RULESET[,...]] ],
"prekill_hooks": [ PLUGIN ]
}
插件
这里介绍其中几个主要使用到的插件:
- pressure_rising_beyond 该插件用于监控指定的 cgroup 中的压力,超过设定的阈值则执行对应的 action。
- dump_cgroup_overview 该插件主要用于打印指定 cgroup 的指标数据。
- kill_by_memory_size_or_growth 这是一个 action 用到的插件,用于根据内存的占用情况以及内存增长趋势进行判断应该 kill 什么进程。
下面是针对该场景的一个配置文件示例,可以作为参考:
{
"rulesets": [
{
"name": "memory pressure protection",
"detectors": [
[
"user is under pressure and system is under a lot of pressure",
{
"name": "pressure_rising_beyond",
"args": {
"cgroup": "user.slice",
"resource": "memory",
"threshold": "5",
"duration": "15"
}
},
{
"name": "pressure_rising_beyond",
"args": {
"cgroup": "system.slice",
"resource": "memory",
"threshold": "5",
"duration": "15"
}
}
],
[
"system is under a lot of pressure",
{
"name": "pressure_rising_beyond",
"args": {
"cgroup": "system.slice",
"resource": "memory",
"threshold": "10",
"duration": "30"
}
}
]
],
"actions": [
{
"name": "kill_by_memory_size_or_growth",
"args": {
"cgroup": "user.slice/*"
}
}
]
},
{
"name": "low swap protection",
"detectors": [
[
"swap is running low",
{
"name": "swap_free",
"args": {
"threshold_pct": "15"
}
}
]
],
"actions": [
{
"name": "kill_by_swap_usage",
"args": {
"cgroup": "system.slice/*,workload.slice/workload-wdb.slice/*,workload.slice/workload-tw.slice/*"
}
}
]
}
]
}
该配置针对 user.slice 和 system.slice 两个 cgroup 进行监控,满足下面的任意条件就会触发用户态 kill。
- system.slice 和 user.slice 两个 cgroup 的压力均超过 5,持续 15 秒。
- system.slice 的压力超过 10 持续 30 秒触发后会从 user.slice 这个 cgroup 选择进程进行 kill 。
测试效果
可以看到当系统压力上来之后,oomd 识别到了最占用内存的 cgroup 并成功执行了 kill 动作,提前从用户态触发 oom,避免整个 OS 出现夯机。