Linux中使用oomd实现用户态OOM Kill

计算弹性计算技术服务知识库
场景介绍

在 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 出现夯机。 图片

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论