尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Linux资源抢占排查与隔离:cgroup、Docker与systemd实战

Linux资源抢占排查与隔离:cgroup、Docker与systemd实战 好先说个真实发生过其实也很常见的运维故障场景一个服务项目代号暂定为“林俊旸”部署在上海机房平时各项指标都很正常。可一到业务高峰期这台机器上的其他任务突然开始大量抢占 CPU、内存和磁盘 I/O直接把“林俊旸”的服务给“抢”得响应超时、健康检查失败甚至被编排系统强制重启。越是抢时间上线越是在这种资源争抢问题上栽跟头网上资料又比较零散折腾了两天才定位到根因。这篇文章就围绕这个“资源被抢走”的问题展开梳理一套从现象定位到资源隔离再到监控加固的完整排障方案。内容覆盖 Linux 资源排查命令、cgroup 与 Docker 资源限制、systemd 配额配置、故障模拟与验证方式以及生产环境里常见的“坑”。无论你是后端开发、运维工程师还是自己搭服务器练手都能照着复现和落地。1. 资源抢占问题的背景与核心概念1.1 什么是资源抢占资源抢占Resource Contention是指运行在同一台物理机或同一虚拟化节点上的多个进程、容器或虚拟机因为共享 CPU、内存、存储 I/O、网络带宽等有限资源在负载升高时互相挤压导致部分进程性能下降甚至不可用。举个容易理解的例子一台 4 核 8G 的服务器上同时跑着 Web 服务、定时任务、日志采集器。平时大家都很“克制”CPU 使用率加起来不到 30%。但某个深夜批处理任务突然并发执行8 个线程同时打满计算Web 服务的请求线程等不到 CPU 时间片接口响应时间从 50ms 飙升到 5 秒这就是典型的 CPU 抢占。用专业一点的话说操作系统的进程调度器会按照优先级和时间片把 CPU 分配给各个进程。当有大量 CPU 密集型的任务同时存在时每个进程能分到的时间片都会缩水。内存方面则更严重如果某个进程快速申请内存Linux 内核可能触发 OOM Killer挑一个占用高或者优先级低的进程直接杀死。在容器和虚拟化环境下资源抢占还会来自同一个宿主机上的“邻居”也就是所谓的“吵闹邻居”Noisy Neighbor问题。1.2 为什么需要资源隔离资源隔离Resource Isolation的核心目标是让不同应用之间互不干扰保证核心服务具备稳定的性能上限。具体来说有几点价值避免单点故障扩散某个进程把内存耗尽不应该导致同一台机器上的其他核心服务全部不可用。保障服务质量QoS在流量高峰时核心业务应该能够获得足够的 CPU 和内存而不是和离线任务争抢。便于容量规划每个容器或服务有明确的资源上限扩容、缩容时更有依据。提升成本效率通过隔离可以让多个任务安全地共享同一批机器而不是为了稳定性搞“一台机器一个应用”的浪费式部署。1.3 本文适用的场景本文聚焦的是多进程、多容器共享一台 Linux 服务器时的资源争抢问题。例如一个 Spring Boot 服务和其他 Python 脚本、数据处理任务部署在同一台机器上Docker 容器跑在同一个宿主机的默认网络和默认资源限制下systemd 管理的多个服务之间互相干扰使用 Kubernetes 时节点上 Pod 的资源请求与限制配置不合理。虽然 Kubernetes 已经是主流但理解单机层面的 cgroup、Docker 和 systemd 资源限制仍然是做容器化、云原生排障的基础。后面很多“节点资源不足”“Pod 被打爆”的问题本质上都是同一套原理。2. 环境准备与版本说明2.1 操作系统与工具文章中的命令和配置示例基于常见的 Linux 环境包括 CentOS 7.9、CentOS 8、Ubuntu 20.04/22.04。版本不同时部分包名和命令输出会有差异但核心思路完全一致。建议准备一台测试用的 Linux 服务器或虚拟机最低配置 2 核 4G这样可以比较明显地复现资源争抢问题。如果使用云服务器直接创建一个按量付费的实例即可测试完销毁避免产生固定成本。需要安装的工具如下top、htop、pidstat、iostat、iotop用于查看系统负载和定位高占用进程cgroup-tools用于手动操作 cgroup v1 的接口docker-ce用于演示容器资源限制stress或stress-ng用于模拟 CPU、内存、磁盘 I/O 压力jq用于解析 JSON 输出方便在脚本中处理命令结果。下面给出 Ubuntu 和 CentOS 的安装命令。Ubuntu / Debiansudo apt update sudo apt install -y htop sysstat linux-tools-common cgroup-tools stress stress-ng docker.io jqCentOS / RHELsudo yum install -y epel-release sudo yum install -y htop sysstat cgroup-tools stress stress-ng docker-ce jq注意CentOS 7 的 systemd 版本是 219部分资源控制参数需要用systemctl set-property来设置Ubuntu 22.04 默认使用 cgroup v2与 CentOS 7 的 cgroup v1 有些差异后面会单独说明。2.2 确认 cgroup 版本cgroup 是 Linux 内核提供的资源隔离机制Docker 和 systemd 的资源限制最终都依赖它。不同发行版可能使用 cgroup v1 或 cgroup v2。执行下面命令查看当前 cgroup 版本mount | grep cgroup如果输出中包含类似cgroup2 on /sys/fs/cgroup type cgroup2说明系统使用 cgroup v2。如果输出的是cgroup on /sys/fs/cgroup type cgroup且包含多个cgroup挂载点说明是 cgroup v1。在 cgroup v1 环境下CPU、内存、I/O 分别由cpu、memory、blkio等子系统控制。在 cgroup v2 环境下所有控制器统一挂载在/sys/fs/cgroup下需要使用cgroup.controllers和cgroup.subtree_control等文件来管理。2.3 示例项目结构为了方便演示我准备用一个简单的脚本服务来充当“林俊旸”服务。它本身有一定 CPU 计算和内存占用方便观察资源限制前后的变化。demo-service/ ├── app.sh # 模拟业务服务做循环计算并定时输出心跳 ├── memory_hog.py # 模拟内存持续增长的应用用来触发 OOM 场景 ├── io_hog.sh # 模拟磁盘 I/O 密集任务 └── limit-test.sh # 一键测试脚本使用 cgroup 或 docker 限制资源这些脚本不是生产代码而是用于复现问题的最小实验程序。在实际项目中你只需要把对应进程 PID 替换成真实服务即可。3. 资源抢占排查的核心方法3.1 先看整体负载再找具体进程排查资源抢占问题时最忌讳的是直接登录服务器就执行kill -9。正确顺序是先看整体情况再逐步缩小范围。整体情况用uptime查看uptime输出示例14:32:10 up 12 days, 5:03, 2 users, load average: 6.81, 4.23, 3.10load average后面的三个数字分别表示过去 1 分钟、5 分钟、15 分钟的平均负载。如果你机器只有 4 核1 分钟负载已经接近 7说明系统处于过载状态确实存在资源争抢。然后使用top查看实时进程排序top在top界面中按P按 CPU 排序按M按内存排序按T按 CPU 时间排序。如果某个进程 CPU 占用一直很高它的 PID 就是下一步重点排查对象。更精确的 CPU 使用率可以使用pidstatpidstat -u -p PID 1 5这会每秒钟采样一次总共采样 5 次只输出指定 PID 的 CPU 使用情况。3.2 排查内存与 OOM内存争抢比 CPU 争抢更难察觉因为很多时候进程是在瞬间被 OOM Killer 杀掉的。查看系统内存整体状态free -h查看是否有进程处于D状态不可中断睡眠通常和磁盘 I/O 相关ps aux | awk $8 ~ /D/ {print}查看内核日志中的 OOM 记录dmesg -T | grep -i oom可能出现类似下面的日志[Thu Mar 16 10:32:11 2023] Out of memory: Killed process 12345 (java) total-vm:4096000kB, anon-rss:2048000kB, file-rss:124kB, oom_score_adj:0这表示 java 进程被内核 OOM Killer 杀掉。如果 OOM 记录里被杀掉的是你自己的服务那几乎可以断定是内存资源不足或内存限制配置有问题。3.3 排查磁盘 I/O磁盘 I/O 抢占经常出现在日志写入、定时备份与业务流量高峰重叠的时候。使用iostat查看整体磁盘使用率iostat -x 1 5重点关注%util、rkb/s、wkb/s和await几列。%util达到或接近 100%说明磁盘已经很繁忙。await表示 I/O 请求的平均等待时间数值越大表示排队越严重。要定位具体是哪个进程在大量读写磁盘可以安装iotop后执行sudo iotop -o-o参数表示只显示正在执行 I/O 的进程减少无关输出干扰。3.4 排查网络带宽与连接数网络抢占有两种常见情况一是带宽被占满二是连接数被打满。检查网络连接状态可以使用ssss -ant | awk {print $1} | sort | uniq -c输出示例10 ESTAB 3 LISTEN 80 SYN-SENT如果 SYN-SENT 数量特别多说明对外连接建立失败可能与本机或目标服务负载过高有关。查看网卡吞吐量sar -n DEV 1 5在没有sar的环境下也可以使用nload或iftop实时查看带宽占用sudo iftop -i eth0注意需要把eth0替换为实际的网卡名称可以通过ip addr查看。3.5 定位资源抢占源的排查清单在实际操作中建议按照下面的清单逐项排查排查步骤命令判断依据查看负载uptime1 分钟负载是否超过核数查看 CPU 热点top按 P 排序是否存在长期占用超过 100% 的进程查看内存热点top按 M 排序内存占用是否接近总量或限额查看 OOM 历史dmesg -T | grep -i oom是否出现已 kill 记录查看磁盘繁忙度iostat -x 1%util 是否接近 100await 是否偏高定位 I/O 进程sudo iotop -o哪个进程读写字节数最大查看 TCP 状态ss -ant是否存在大量非 ESTAB 状态连接查看进程 D 状态ps aux | awk $8 ~ /D/是否有进程长期处于 D 状态如果这一步发现了明显的“抢资源”进程一般有两个处理方向一是直接限制它的资源上限二是把它迁移到其他机器。接下来重点介绍资源限制的落地方法。4. 资源隔离与配额配置实战4.1 创建模拟业务服务先编写一个模拟“林俊旸”服务的脚本它会持续做计算并每 5 秒输出一行心跳日志#!/bin/bash # 文件路径demo-service/app.sh echo service start, pid $$ while true; do echo $(date %F %T) service heartbeat # 模拟 CPU 密集计算 for i in $(seq 1 100000); do echo $i /dev/null done sleep 5 done给脚本添加执行权限并启动chmod x demo-service/app.sh nohup ./demo-service/app.sh app.log 21 启动后通过top观察可以看到app.sh以及它派生的bash子进程会占用不少 CPU。记录下进程 PID后面用它来测试资源限制。4.2 使用 Docker 进行资源限制如果你的应用已经容器化Docker 是最直接的限制工具。启动一个容器限制只能使用 0.5 个 CPU 核心和 256MB 内存docker run -d --name lin-junyang-demo --cpus0.5 --memory256m \ -v $(pwd)/demo-service:/app \ busybox sh -c while true; do echo heartbeat; sleep 1; done参数说明--cpus0.5容器最多使用 0.5 个 CPU 核心时间也就是每秒钟最多使用 50% 的单个核心。--memory256m容器最多使用 256MB 内存超过后如果触达限制容器内进程可能被 OOM Killer 杀掉Docker 也会根据策略把容器重启或停止。-v把宿主机目录挂载到容器/app便于修改脚本。查看容器实际资源使用情况docker stats --no-stream输出示例CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % a1b2c3d4e5f6 lin-junyang-demo 49.50% 12.5MB / 256MiB 4.88%可以看到 CPU 被限制在 50% 左右。如果不加--cpus参数同一个容器在空闲机器上可能会打满所有核心。4.3 使用 cgroup 手动限制未容器化进程并不是所有应用都能马上容器化。对于直接跑在宿主机上的进程可以手动使用 cgroup v1 进行限制。以 CPU 限制为例创建新的 cgroup 并设置 CPU 配额# 创建 cgroup 目录 sudo mkdir -p /sys/fs/cgroup/cpu/test-limit sudo mkdir -p /sys/fs/cgroup/memory/test-limit # 设置 CPU 限制单核的 50% # cpu.cfs_period_us 默认 100000即 100ms # cpu.cfs_quota_us 设为 50000表示在 100ms 周期内最多运行 50ms echo 50000 | sudo tee /sys/fs/cgroup/cpu/test-limit/cpu.cfs_quota_us echo 100000 | sudo tee /sys/fs/cgroup/cpu/test-limit/cpu.cfs_period_us # 设置内存限制256MB echo 268435456 | sudo tee /sys/fs/cgroup/memory/test-limit/memory.limit_in_bytes然后把进程 PID 写入 cgroup 的tasks文件# 假设目标进程 PID 是 12345 echo 12345 | sudo tee /sys/fs/cgroup/cpu/test-limit/tasks echo 12345 | sudo tee /sys/fs/cgroup/memory/test-limit/tasks这样 PID 12345 以及它后续创建的子线程都会受到 cgroup 限制。验证方式cat /sys/fs/cgroup/cpu/test-limit/cpu.cfs_quota_us cat /sys/fs/cgroup/cpu/test-limit/cpu.statcgroup v2 环境下的限制方式稍有不同。在 v2 中需要先向父 cgroup 的cgroup.subtree_control写入要启用的控制器然后创建子目录再往子目录里的cpu.max和memory.max写入配置# 启用 cpu 和 memory 控制器 echo cpu memory | sudo tee /sys/fs/cgroup/cgroup.subtree_control # 创建子 cgroup sudo mkdir -p /sys/fs/cgroup/test-limit # CPU 限制50000/100000也就是 50% echo 50000 100000 | sudo tee /sys/fs/cgroup/test-limit/cpu.max # 内存限制256MB echo 268435456 | sudo tee /sys/fs/cgroup/test-limit/memory.max # 把进程 PID 移入 echo 12345 | sudo tee /sys/fs/cgroup/test-limit/cgroup.procs需要注意的是如果你使用的服务器上同时有 systemd 和 Docker它们可能已经在使用 cgroup 的某些控制器。手动干预时建议在/sys/fs/cgroup下创建自己特有的子目录不要直接修改系统服务的 cgroup 配置。4.4 使用 systemd 限制服务资源对于由 systemd 管理的服务更推荐直接写 unit 文件来配置资源限制。编辑服务文件# 文件路径/etc/systemd/system/lin-junyang-demo.service [Unit] DescriptionLin Junyang Demo Service Afternetwork.target [Service] ExecStart/opt/demo-service/app.sh Restartalways # 限制 CPU 使用率为 50% CPUQuota50% # 限制内存为 256MB MemoryMax256M # 限制磁盘读写速率 IOReadBandwidthMax/dev/vda1 10M IOWriteBandwidthMax/dev/vda1 10M [Install] WantedBymulti-user.target参数说明CPUQuota50%systemd 会换算成 cgroup 的cpu.cfs_quota_us限制该服务最多使用 50% 的 CPU。MemoryMax256M在 cgroup v2 中对应memory.max在 cgroup v1 中对应memory.limit_in_bytes。IOReadBandwidthMax/IOWriteBandwidthMax限制磁盘读写带宽后面的/dev/vda1要替换成实际的磁盘设备路径。重新加载并启动服务sudo systemctl daemon-reload sudo systemctl enable --now lin-junyang-demo sudo systemctl status lin-junyang-demo查看服务的实际资源占用sudo systemctl status lin-junyang-demo --no-pager如果服务超出资源限制systemd 会记录相关日志可以通过journalctl -u lin-junyang-demo查看。4.5 运行与验证为了更直观地看到限制效果我们可以用stress-ng压出大量 CPU 占用然后观察限制前后差异。启动 4 个 CPU 压力进程不限制的话会看到 CPU 使用率接近 400%4 核打满stress-ng --cpu 4 --timeout 60s如果同时只让它在 cgroup 内占用 50%可以这样验证# 先创建一个 cgroup sudo mkdir -p /sys/fs/cgroup/cpu/stress-limit echo 50000 | sudo tee /sys/fs/cgroup/cpu/stress-limit/cpu.cfs_quota_us echo 100000 | sudo tee /sys/fs/cgroup/cpu/stress-limit/cpu.cfs_period_us # 启动压力进程 stress-ng --cpu 4 --timeout 60s STRESS_PID$! # 把压力进程移入 cgroup echo $STRESS_PID | sudo tee /sys/fs/cgroup/cpu/stress-limit/tasks # 观察 top这时通过top可以看到 4 个进程的 CPU 相加大约为 50%而不会打满整机。再验证一下内存限制。编写一个简单的 Python 脚本持续往列表里塞数据# 文件路径demo-service/memory_hog.py import time data [] while True: data.append(x * 1024 * 1024) time.sleep(0.01)先不限制运行python3 memory_hog.py观察一段时间后如果系统内存不足可能会触发 OOM Killer。接着在 Docker 中运行同样功能并设置内存限制docker run -d --name memory-hog-demo --memory128m \ python:3.9-slim python -c a[]; [a.append(x*1024*1024) for _ in iter(int,1)]启动后查看容器状态docker ps -a大概率会看到容器处于Exited状态且退出码为 137表示被 OOM 杀死。查看日志docker logs memory-hog-demo这正好解释了生产环境中“服务突然消失”的常见原因不是代码 bug而是内存资源达到上限后被内核强制终止。5. 常见问题与排查思路5.1 容器反复重启问题现象常见原因解决思路容器启动几秒后自动退出退出码 137容器内进程触发 OOM被内核或 Docker 杀掉调大--memory或优化应用内存占用容器退出码 125Docker 启动参数错误检查docker run参数和镜像是否存在容器退出码 1应用自身启动失败查看docker logs输出排查代码或依赖问题CPU 限制生效但内存限制未生效cgroup v2 控制器未启用修改 systemd 配置以委托控制器或使用 Docker 的--memory参数5.2 cgroup 文件只读或写入失败手动操作 cgroup 时可能遇到Permission denied或Read-only file system的问题。原因通常是内核未开启对应控制器当前用户没有 root 权限在容器内操作/sys/fs/cgroup时容器没有对应的 capabilities系统使用 cgroup v2某个控制器尚未写入cgroup.subtree_control。排查步骤使用sudo重试查看内核启动参数是否包含cgroup_no_v1或cgroup_enablememory确认系统是 v1 还是 v2再选择对应的文件路径在容器内通过--privileged或挂载/sys/fs/cgroup来获得权限但生产环境不建议为了权限而轻易使用特权容器。5.3 限制后服务性能依然忽高忽低有些场景下虽然已经给容器或服务设置了--cpus或CPUQuota但性能仍然剧烈波动。可能原因是宿主机本身开启了超线程CPU 配额和物理核心换算关系复杂磁盘 I/O 和网络带宽也达到瓶颈单纯限制 CPU 不够页面换入换出频繁内存 swap 导致性能抖动应用内部线程池大小和数据库连接池配置不合理外部服务耗时高导致自身线程堆积。建议同时监控 CPU、内存、磁盘 I/O、网络四个维度并查看慢请求日志判断性能瓶颈到底在哪里。5.4 忘记限制会导致什么后果这是生产环境非常常见的“事故前兆”。如果服务 A 没有资源限制服务 B 在高峰期被打满那么两者会互相拖垮。更严重的是当内存不足时内核 OOM Killer 的评分逻辑会综合进程占用内存大小、进程优先级等因素有可能优先杀掉“看起来占用内存多但实际应该保留”的进程。因此即使暂时没有出现资源争抢也建议在所有关键服务上配置资源上限。这不是“限制业务能力”而是给整个系统加上保护线。6. 最佳实践与工程建议6.1 明确职责边界并写入配置部署一个服务或容器时必须明确它需要的资源下限和上限。在 Docker 层面可以使用--cpus、--memory、--pids-limit在 Kubernetes 层面使用requests和limits。配置里不要只写“多大内存”最好同步写清楚 CPU、内存、磁盘 I/O 和 PID 数限制。例如 Kubernetes 中resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1Girequests用于调度决策limits用于运行时限制。两者的关系是requests保证最少资源limits防止超出上限。如果只设置limits不设置requests调度器可能把多个大容器堆到同一节点导致实际运行时出现资源争抢。6.2 建立监控告警而不是事后救火资源抢占最理想的解决方式是提前发现趋势而不是等用户报障后才登录服务器。建议至少监控以下指标CPU 使用率、平均负载内存使用率、swap 使用量磁盘 I/O 的%util、await进程或容器的 OOM Kill 次数网络出入带宽。可以使用 Prometheus 加上node_exporter采集宿主机数据使用cadvisor采集容器数据。告警规则建议不要只看瞬时值因为瞬时抖动可能误报警。更合理的做法是持续 5 分钟超过阈值再触发告警。一个简单的 Prometheus 告警规则示例groups: - name: resource-alerts rules: - alert: HighLoad expr: node_load1 / count(node_cpu_seconds_total{modeidle}) 0.8 for: 5m labels: severity: warning annotations: summary: 服务器负载偏高6.3 定期做混沌测试和容量验证生产环境中资源争抢往往在流量高时才出现而流量高时又不敢轻易调整配置。所以更安全的做法是在测试环境定期做“混沌测试”人为制造 CPU 密集、内存压力、磁盘 I/O 压力验证核心服务是否能在资源受限的前提下保持可用。例如用stress-ng与业务服务跑在同一台测试机上观察核心服务响应时间是否在可接受范围触发 OOM 后编排系统是否能把服务快速拉起资源限制配置是否生效日志和监控是否能完整记录故障过程。6.4 注意安全边界与最小权限在操作服务器资源限制时有几点安全建议手动修改 cgroup 时尽量在测试环境操作避免直接改到生产服务的 cgroup不要随意给容器加--privileged权限防止容器逃逸风险运维脚本需要 root 权限时建议通过 sudo 的精细规则控制而不是让所有员工直接使用 root 账号限制磁盘读写时确认设备路径正确避免误把系统盘限死导致整机异常当需要释放资源或重启服务时优先通过编排系统或 systemd 操作不建议直接kill -9除非已经确认进程无法正常停止。6.5 容量规划要留缓冲生产环境的资源使用率如果长期超过 70%一旦出现突发流量或某个服务异常占用资源整个节点就会非常危险。容量规划时建议按“正常峰值的 1.5 到 2 倍”来申请资源同时在高可用架构上保持至少 N1 的冗余能力。这样即使一台机器出现资源抢占流量也能切换到其他副本而不是所有请求都积压在异常节点上。7. 总结与学习路线这篇文章从一个“服务资源被抢走”的排障场景出发梳理了资源抢占的常见表现和定位方法并给出了 cgroup、Docker、systemd 三种资源限制方式的完整配置示例。核心收获可以归纳为几点先学会用top、pidstat、iostat、dmesg定位“谁抢了资源”再用 cgroup 或 Docker 设置资源上限让每个应用都有自己的“安全边界”理解 cgroup v1 和 v2 的差异遇到只读或写入失败时知道从哪里排查生产环境不能只依赖手动排查必须配套监控告警和容量规划。如果之前不太熟悉 Linux 资源管理下一步建议按顺序深入学习这几个方向Linux 进程管理和信号理解kill和退出码cgroup v2 的内核文档以及 systemd 的资源控制属性Docker 的--cpus、--memory、--pids-limit参数和底层实现Kubernetes 的 requests/limits 与 QoS 等级Prometheus 监控与告警规则编写。如果本文对你有帮助可以收藏备用。后续如果再遇到类似“服务突然变慢”“容器反复重启”“负载莫名飙升”的问题建议先把资源排查清单过一遍再决定是否需要调整资源限制。实际操作时请注意任何生产环境的变更都要经过测试、备份和授权不要在状态未知的情况下贸然修改配置。
返回列表