使用 cgroups 控制容器资源
在容器化技术中,资源限制是一个关键功能。本文介绍如何在自研容器运行时中集成 cgroups 支持,实现对 CPU 和内存等资源的精确控制。
命令行参数扩展
首先为 run 命令新增资源限制选项:
var runCmd = cli.Command{
Name: "run",
Usage: "运行容器并应用资源限制",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "it",
Usage: "启用交互终端",
},
cli.StringFlag{
Name: "mem",
Usage: "内存上限,例如: 100m",
},
cli.IntFlag{
Name: "cpu-quota",
Usage: "CPU 配额百分比,例如: 50",
},
cli.StringFlag{
Name: "cpuset",
Usage: "绑定的 CPU 核心列表,例如: 0,1",
},
},
Action: func(ctx *cli.Context) error {
if ctx.NArg() < 1 {
return fmt.Errorf("缺少执行命令")
}
cmdList := ctx.Args()
ttyMode := ctx.Bool("it")
resources := &subsystems.Config{
MemLimit: ctx.String("mem"),
CPUQuota: ctx.Int("cpu-quota"),
CPUSet: ctx.String("cpuset"),
}
executeContainer(ttyMode, cmdList, resources)
return nil
},
}
cgroups 抽象层设计
定义通用接口来管理不同的控制器:
type Controller interface {
Name() string
Configure(groupPath string, cfg *Config) error
Attach(groupPath string, pid int) error
Cleanup(groupPath string) error
}
type Config struct {
MemLimit string
CPUQuota int
CPUSet string
}
控制器实现
以内存控制器为例展示实现方式:
type MemoryController struct{}
func (mc *MemoryController) Name() string {
return "memory"
}
func (mc *MemoryController) Configure(path string, cfg *Config) error {
if cfg.MemLimit == "" {
return nil
}
groupDir, err := getGroupPath(mc.Name(), path, true)
if err != nil {
return err
}
limitFile := filepath.Join(groupDir, "memory.limit_in_bytes")
return ioutil.WriteFile(limitFile, []byte(cfg.MemLimit), 0644)
}
func (mc *MemoryController) Attach(path string, pid int) error {
groupDir, err := getGroupPath(mc.Name(), path, false)
if err != nil {
return err
}
taskFile := filepath.Join(groupDir, "tasks")
return ioutil.WriteFile(taskFile, []byte(strconv.Itoa(pid)), 0644)
}
func (mc *MemoryController) Cleanup(path string) error {
groupDir, _ := getGroupPath(mc.Name(), path, false)
return os.RemoveAll(groupDir)
}
挂载点定位
查找特定控制器在文件系统中的挂载位置:
func getGroupPath(controller, groupPath string, create bool) (string, error) {
mountRoot := findMountPoint(controller)
fullPath := filepath.Join(mountRoot, groupPath)
if !create {
return fullPath, nil
}
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
return fullPath, os.MkdirAll(fullPath, 0755)
}
return fullPath, nil
}
func findMountPoint(controller string) string {
file, err := os.Open("/proc/self/mountinfo")
if err != nil {
return ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, " ")
options := strings.Split(parts[len(parts)-1], ",")
for _, opt := range options {
if opt == controller {
return parts[4] // 挂载点路径索引
}
}
}
return ""
}
资源组管理器
统一管理层协调各控制器工作:
type GroupManager struct {
Path string
Resource *Config
controllers []Controller
}
func NewGroupManager(path string) *GroupManager {
return &GroupManager{
Path: path,
controllers: []Controller{
&MemoryController{},
&CPUController{},
},
}
}
func (gm *GroupManager) ApplyLimits(cfg *Config) error {
for _, ctrl := range gm.controllers {
if err := ctrl.Configure(gm.Path, cfg); err != nil {
log.Printf("配置 %s 失败: %v", ctrl.Name(), err)
}
}
return nil
}
func (gm *GroupManager) BindProcess(pid int) error {
for _, ctrl := range gm.controllers {
if err := ctrl.Attach(gm.Path, pid); err != nil {
log.Printf("绑定进程到 %s 失败: %v", ctrl.Name(), err)
}
}
return nil
}
func (gm *GroupManager) Release() {
for _, ctrl := range gm.controllers {
ctrl.Cleanup(gm.Path)
}
}
集成到容器启动流程
修改容器创建逻辑以支持资源控制:
func executeContainer(tty bool, cmds []string, res *Config) {
parentProc, pipe := container.CreateParentProcess(tty)
if parentProc == nil {
log.Fatal("无法创建父进程")
}
if err := parentProc.Start(); err != nil {
log.Fatalf("启动失败: %v", err)
}
// 应用资源限制
manager := NewGroupManager("custom-container-group")
defer manager.Release()
manager.ApplyLimits(res)
manager.BindProcess(parentProc.Process.Pid)
sendCommands(cmds, pipe)
parentProc.Wait()
}
验证效果
构建内存压力测试程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MB (1024*1024)
int main() {
char *buffer;
int count = 0;
while(1) {
buffer = malloc(MB);
memset(buffer, 0, MB);
printf("已分配 %d MB\n", ++count);
sleep(1);
}
return 0;
}
编译并运行带内存限制的容器:
./mycontainer run -it -mem 50m /bin/bash
# 在容器内执行测试程序
./mem_test
观察程序会在分配约 50MB 后被系统终止。
CPU 限制可通过类似方法验证,启动高负载任务后检查其 CPU 使用率是否符合设定值。