在Ubuntu服务器运维中,最让人头疼的问题往往不是CPU飙到100%,而是一个失控的进程拖垮整个系统,导致SSH连不上去,业务全部瘫痪。解决这个问题的核心手段,就是通过cgroup对系统资源进行强制隔离。Ubuntu 22.04及之后的版本默认使用cgroup v2,配置方式与旧版本有本质区别,直接用v1的思路去操作会处处碰壁。

确认当前cgroup版本与挂载状态

接手一台服务器,第一件事是搞清楚它跑的是cgroup v1还是v2。执行mount | grep cgroup,如果输出中包含cgroup2 on /sys/fs/cgroup type cgroup2,那就是v2环境。Ubuntu 22.04/24.04默认启用cgroup v2 unified hierarchy,所有控制器统一挂载在一个层级下。如果看到多个tmpfs挂载点分别对应cpu、memory等子系统,那是v1的混合模式。systemd在v2环境下会自动把服务放到对应的slice中,但手动精细控制仍需直接操作cgroup文件系统。

systemd的slice机制:第一层隔离

systemd默认将进程组织成三级结构:根slice(-.slice)下包含system.slice(系统服务)、user.slice(用户会话)和machine.slice(容器)。每个service文件都可以通过Slice=指令指定归属。执行systemd-cgls可以直观看到这个树状结构。比如把MySQL放到一个独立的mysql.slice里,它就不会和Nginx争抢system.slice的资源配额。创建自定义slice非常简单,在/etc/systemd/system/下新建一个mysql.slice文件,内容只需[Slice]段,然后让mysql.service里的Slice=mysql.slice指向它。重载systemd后,这个slice就会出现在/sys/fs/cgroup/system.slice/mysql.slice/下,所有子进程自动继承这个cgroup的约束。

CPU资源硬隔离:实时进程也不能越界

cgroup v2的CPU控制器用cpu.max文件来限制配额。格式是$MAX $PERIOD,单位微秒。比如echo "200000 1000000" > /sys/fs/cgroup/system.slice/mysql.slice/cpu.max,意思是每1秒周期内,这个cgroup最多使用0.2秒的CPU时间,相当于限制到单核的20%。如果要限制使用多个核心,把$MAX设为$PERIOD乘以核心数即可。更激进的做法是直接修改cpu.weight,这是v2的权重控制,默认100,范围1到10000。把某个批处理任务的weight调到10,即使CPU满载,它也只能抢到很少的时间片。对于需要绝对隔离的场景,cpuset控制器仍然可用,但需要先通过写cpuset.cpus和cpuset.mems来指定允许的CPU核心和内存节点。注意,在v2下启用cpuset需要先往父级cgroup的cgroup.subtree_control写入+cpuset,否则子cgroup里不会出现cpuset相关文件。

内存隔离:OOM不再殃及无辜

内存限制是防止单点故障扩散的关键。v2的内存控制器通过memory.max文件设置硬上限,单位字节。例如限制某个cgroup最多使用512MB内存:echo "536870912" > memory.max。当进程超过这个值,内核会触发OOM killer,但只杀这个cgroup内的进程,不会影响宿主机其他服务。比硬限制更灵活的是memory.high,这是一个软限制,超过后内核会开始节流和回收内存,但不会立即OOM,给应用一个缓冲时间。还有一个容易被忽略的参数是memory.swap.max,如果不显式设置,默认情况下swap使用量等于memory.max,意味着限制512MB内存的同时也允许512MB swap,总虚拟内存可达1GB。对于Redis这类纯内存应用,建议直接echo "0" > memory.swap.max禁止swap,避免性能抖动。

IO隔离:防止日志写盘拖慢数据库

IO控制器在v2下叫io,需要通过cgroup.subtree_control启用。启用后子cgroup中会出现io.max文件,格式为设备号 读写类型 限制值。设备号通过ls -l /dev/sda查看,比如8:0。限制某个cgroup对磁盘的写速度为10MB/s:echo "8:0 wbps=10485760" > io.max。wbps是每秒写入字节数,rbps是读取,wiops和riops是IO操作次数。对于数据库服务器,把备份脚本的写IO限制到很低,可以避免备份期间的IO争用。io.weight同样支持权重分配,默认100,范围1到10000,用于在多个cgroup之间按比例分配IO带宽。

动态调整与进程迁移

cgroup配置不是一锤子买卖。业务高峰时可以临时调高限制,凌晨再收紧。所有cgroup接口都是文件,脚本直接echo新值即可生效。把已经运行的进程移入cgroup,只需将其PID写入cgroup.procs文件。比如echo "12345" > /sys/fs/cgroup/system.slice/mysql.slice/cgroup.procs,进程12345及其所有线程都会立即受到该cgroup的约束。反过来,把进程移出cgroup就是写入目标cgroup的cgroup.procs,因为一个进程同时只能属于一个cgroup。systemd管理的服务,推荐用systemctl set-property来动态修改,例如systemctl set-property mysql.service MemoryMax=1G,这个命令会直接操作底层cgroup并持久化到systemd的配置中。

容器场景下的cgroup集成

如果服务器上跑了Docker或Podman,每个容器本身就是一个cgroup。Docker的--cpus、--memory参数最终都转化为cgroup v2的配置。但容器引擎的默认配置往往过于宽松,比如不限制IO。可以在容器启动时通过--device-write-bps来限制磁盘写入。对于Podman,它原生支持cgroup v2,可以直接用--cgroup-parent指定容器挂载到哪个自定义slice下,实现容器与宿主机服务的统一资源视图。Kubernetes的节点资源预留也是通过kube-reserved和system-reserved这两个cgroup来实现的,运维人员需要确保预留值合理,否则kubelet和系统服务可能被Pod挤占资源。

监控与告警:让cgroup可见

配置完cgroup后,必须建立监控。cgroup v2的统计信息都在对应目录下的memory.current、cpu.stat、io.stat等文件中。memory.current是当前内存使用量,cpu.stat里的usage_usec是累计CPU使用微秒数,io.stat按设备列出读写字节和操作数。写一个简单的采集脚本,每分钟读取这些文件并上报到时序数据库,就能绘制出每个cgroup的资源消耗曲线。推荐使用cAdvisor或node_exporter的cgroup collector来自动采集,前者适合容器环境,后者适合混合部署。告警规则要区分硬限制和软限制:memory.current接近memory.max时需要立即告警,因为即将触发OOM;而memory.high被超过时可以先发低优先级通知。

常见踩坑与排查思路

第一个坑是权限问题。直接echo写入cgroup文件需要root权限,但systemd的slice目录默认属主是root。如果想让非特权用户操作,需要借助systemd的Delegate=yes或者用setfacl设置ACL。第二个坑是cgroup.subtree_control的继承规则。v2要求父cgroup必须先启用某个控制器,子cgroup才能使用。如果你发现子cgroup里没有memory.max文件,检查父级的cgroup.subtree_control是否包含+memory。第三个坑是进程迁移时的竞争条件。把进程从一个cgroup移到另一个时,如果进程正在疯狂分配内存,可能在迁移瞬间突破新cgroup的限制,建议先设限制再移进程。第四个坑是systemd的自动清理。默认情况下,service停止后systemd会删除对应的cgroup,但自定义slice不会自动删除,需要手动清理或者设置RemoveOnStop=yes。

生产环境推荐配置示例

假设一台Ubuntu 24.04服务器同时运行Nginx、MySQL和一个日志分析脚本。先创建三个slice:web.slice、db.slice和batch.slice。在db.slice上设置CPU权重800、内存硬限制4GB、禁止swap;在web.slice上设置CPU权重600、内存软限制2GB;在batch.slice上设置CPU权重100、内存硬限制1GB、IO写速度限制5MB/s。对应的systemd命令:

systemctl set-property db.slice CPUWeight=800 MemoryMax=4G MemorySwapMax=0
systemctl set-property web.slice CPUWeight=600 MemoryHigh=2G
systemctl set-property batch.slice CPUWeight=100 MemoryMax=1G

然后修改各service文件的Slice=指向对应slice。IO限制需要直接操作cgroup文件:

echo "8:0 wbps=5242880" > /sys/fs/cgroup/system.slice/batch.slice/io.max

这套配置确保数据库获得最多CPU资源且不会被OOM,Web服务有弹性内存空间,批处理任务无论如何都不会影响在线业务。定期检查/sys/fs/cgroup下各slice的memory.current和cpu.stat,验证限制是否生效,根据实际负载微调权重和上限值。资源隔离的本质是把不确定性关进笼子里,cgroup就是这个笼子的建造工具,用好了它,一台物理机才能安全地混合运行多种负载。