写点什么

自己动手写 Docker 系列 -- 3.2 增加容器资源限制

作者:
  • 2022 年 3 月 20 日
  • 本文字数:5239 字

    阅读完需:约 17 分钟

简介

继上篇的 Run 命令容器的实现,本章节中将实现增加容器资源限制,已内存限制为例,展示如果现在容器的内存使用

源码说明

同时放到了 Gitee 和 Github 上,都可进行获取



本章节对应的版本标签是:3.2,防止后面代码过多,不好查看,可切换到标签版本进行查看


本次只实现了内存的限制,CPU 的实现同理

结果演示

 进程号 USER      PR  NI    VIRT    RES    SHR    %CPU  %MEM     TIME+ COMMAND  46805 lw        20   0  208668 204988    336 R 100.0   0.6   2:20.89 stress  48004 root      20   0  208668  99520    272 D  37.9   0.3   0:04.68 stress
复制代码


如上所示,第一个是没有使用资源限制 100M 的 stress 命令: stress --vm-bytes 200m --vm-keep -m 1


第二个是使用自己的 docker 限制了 100M 内存的 stress 命令:./main run -ti -mem 100m stress --vm-bytes 200m --vm-keep -m 1


后者是前者的一半,可以看出了资源限制起到了作用

编码实现

这边的 Cgroup 卡了很久才解决,照抄代码有一定的风险,还是理解原理后,自己一步一步的实现最终才完成了

参数接收改造

我们需要使用 docker 运行 stress 命令:


上面的参数比较多,原本的是不支持的,我们需要进行改造,代码如下:


var RunCommand = cli.Command{  Name:  "run",  Usage: `Create a container with namespace and cgroups limit mydocker run -ti [command]`,  Flags: []cli.Flag{    cli.BoolFlag{      Name:  "ti",      Usage: "enable tty",    },    // 增加内存等限制参数    cli.StringFlag{      Name:  "mem",      Usage: "memory limit",    },    cli.StringFlag{      Name:  "cpuset",      Usage: "cpuset limit",    },    cli.StringFlag{      Name:  "cpushare",      Usage: "cpushare limit",    },  },  /*    这里是run命令执行的真正函数    1.判断参数是否包含command    2.获取用户指定的command    3.调用Run function 去准备启动容器  */  Action: func(context *cli.Context) error {    if len(context.Args()) < 1 {      return fmt.Errorf("missing container command")    }    // 这个是获取启动容器时的命令    // 如果本次中的 stress --vm-bytes 200m --vm-keep -m 1,空格分隔后会存储在下面的cmdArray中    var cmdArray []string    for _, arg := range context.Args() {      cmdArray = append(cmdArray, arg)    }    tty := context.Bool("ti")    resConfig := &subsystem.ResourceConfig{      MemoryLimit: context.String("mem"),      CpuShare:    context.String("cpuShare"),      CpuSet:      context.String("cpuSet"),    }    run.Run(tty, cmdArray, resConfig)    return nil  },}
复制代码


在 init 的时候也需要支持,在 init 命令执行的时候需要传入对应的参数


func NewParentProcess(tty bool, cmdArray []string) *exec.Cmd {  commands := strings.Join(cmdArray, " ")  log.Infof("conmand all is %s", commands)  // 传入,cmdArray[0]是启动的程序,比如本篇中的是stress,command是完整的命令:stress --vm-bytes 200m --vm-keep -m 1  cmd := exec.Command("/proc/self/exe", "init", cmdArray[0], commands)  cmd.SysProcAttr = &syscall.SysProcAttr{    Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,  }  if tty {    cmd.Stdin = os.Stdin    cmd.Stdout = os.Stdout    cmd.Stderr = os.Stderr  }  return cmd}
复制代码


init 函数是具体执行,我们进行完善,修复了之前挂载/proc 的 bug,并采用新的方式运行命令:


func RunContainerInitProcess(command string, args []string) error {  log.Infof("RunContainerInitProcess command %s, args %s", command, args)
// 上篇中没启动一次程序,就需要重新挂载一次/proc,很麻烦,添加下面的这个private方式挂载,即可解决这个问题 // private 方式挂载,不影响宿主机的挂载 err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "") if err != nil { log.Errorf("private 方式挂载 failed: %v", err) return err }
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV err = syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "") if err != nil { log.Errorf("proc挂载 failed: %v", err) return err }
// 同时,我们使用lookPath的方式去查找命令进行执行 path, err := exec.LookPath(command) if err != nil { log.Errorf("can't find exec path: %s %v", command, err) return err } log.Infof("find path: %s", path) if err := syscall.Exec(path, args, os.Environ()); err != nil { log.Errorf("syscall exec err: %v", err.Error()) } return nil}
复制代码


上面我们使用 private 挂载方式解决之前的挂载/proc 的 bug,具体原来目前自己还没琢磨透。。。。。。


但 LookPath 的方式比以前更好,是一直查找可执行程序的方式:


比如这次的 stress --vm-bytes 200m --vm-keep -m 1,如果我们用之前的有运行方式,前面的 stress 应该输入:/usr/sbin/stress 之类的完整路径。


而使用 LookPath,自动查找可执行程序,很好用

进程启动后,资源限制

在上篇中,我们当前进程下 fork 启动了一个进程,并进行 Namespace 的隔离


在本篇中,需要在进程启动后,使用 Cgroup 操作的相关知识,写入相关的文件,进行资源限制


代码如下:


func Run(tty bool, cmdArray []string, config *subsystem.ResourceConfig) {  // 这里进程已经启动完成  parent := container.NewParentProcess(tty, cmdArray)  if err := parent.Start(); err != nil {    log.Error(err)    return  }
// 下面是使用自定义的资源管理器进行资源限制 cgroupManager := cgroup.NewCgroupManager("mydocker-cgroup") // 进程退出时,重置 defer cgroupManager.Destroy() // 将当前的进程的PID加入资源限制列表,如果内存的就是将PID写入tasks文件中 if err := cgroupManager.Apply(parent.Process.Pid); err != nil { log.Errorf("cgroup apply err: %v", err) return } // 将PID加入Cgroup if err := cgroupManager.Set(config); err != nil { log.Errorf("cgoup set err: %v", err) return }
log.Infof("parent process run") _ = parent.Wait() os.Exit(-1)}
复制代码

资源抽象类和资源管理器类

下面基本都是书中的代码,定义了一个资源的接口类,后面的内存、CPU 等资源限制实现这个接口即可(go 的这个接口写法和 Java 还挺不一样)


package subsystem
// 资源配置:命令中对内存、CPU的具体限制type ResourceConfig struct { MemoryLimit string CpuShare string CpuSet string}
// 资源限制接口:// Name: 名称,如memory、cpuset、cpushare// Set: 写入配置文件,对资源进行限制// Apply: 将PID加入当前Cgroup// Remove: 将PID移出当前Cgrouptype Subsystem interface { Name() string Set(cgroupName string, res *ResourceConfig) error Apply(cgroupName string, pid int) error Remove(cgroupName string) error}
// 具体实现类:内存和CPUvar SubsystemIns = []Subsystem{ &CpuSetSubsystem{}, &CpuShareSubsystem{}, &MemorySubsystem{},}
复制代码


管理类的代码如下:遍历资源限制的具体实现类,进行资源的限制


// CgroupName 当前进程的Cgroup名称:在Cgroup下建立的子文件夹名称type CgroupManager struct {  CgroupName string  Resouce    *subsystem.ResourceConfig}
func NewCgroupManager(cgroupName string) *CgroupManager { return &CgroupManager{ CgroupName: cgroupName, }}
// Apply 将PID加入Cgroupfunc (c *CgroupManager) Apply(pid int) error { for _, ins := range subsystem.SubsystemIns { err := ins.Apply(c.CgroupName, pid) if err != nil { return err } } return nil}
// Set 设置限制func (c *CgroupManager) Set(res *subsystem.ResourceConfig) error { for _, ins := range subsystem.SubsystemIns { err := ins.Set(c.CgroupName, res) if err != nil { return err } } return nil}
// Destroy 释放 Cgroupfunc (c *CgroupManager) Destroy() error { for _, ins := range subsystem.SubsystemIns { err := ins.Remove(c.CgroupName) if err != nil { return err } } return nil}
复制代码

内存限制具体实现

下面是内存限制的具体实现:建议在宿主机上尝试操作下如果手动写配置文件进行内存限制后,即可明白下面的代码


基本上就是:


1.将进程 PID 加入当前 CPU2.配置当前 Cgroup3.移出当前 PID


最后两个函数是,查找各个资源限制根目录和自动创建文件夹的相关代码


type MemorySubsystem struct {}
func (m MemorySubsystem) Name() string { return "memory"}
func (m MemorySubsystem) Set(cgroupName string, res *ResourceConfig) error { // 获取自定义Cgroup的路径,没有则创建,如:/sys/fs/cgroup/memory/mydocker-cgroup cgroupPath, err := GetCgroupPath(m.Name(), cgroupName) if err != nil { return err } log.Infof("%s cgroup path: %s", m.Name(), cgroupPath) // 将资源限制写入 limitFilePath := path.Join(cgroupPath, "memory.limit_in_bytes") if err := ioutil.WriteFile(limitFilePath, []byte(res.MemoryLimit), 0644); err != nil { return fmt.Errorf("set memory cgroup failed: %v", err) } return nil}
func (m MemorySubsystem) Apply(cgroupName string, pid int) error { // 获取自定义Cgroup的路径,没有则创建,如:/sys/fs/cgroup/memory/mydocker-cgroup cgroupPath, err := GetCgroupPath(m.Name(), cgroupName) if err != nil { return err } log.Infof("%s cgroup path: %s", m.Name(), cgroupPath) // 将PID加入该cgroup limitFilePath := path.Join(cgroupPath, "tasks") if err := ioutil.WriteFile(limitFilePath, []byte(strconv.Itoa(pid)), 0644); err != nil { return fmt.Errorf("add pid to cgroup failed: %v", err) } return nil}
func (m MemorySubsystem) Remove(cgroupName string) error { // 获取自定义Cgroup的路径,没有则创建,如:/sys/fs/cgroup/memory/mydocker-cgroup cgroupPath, err := GetCgroupPath(m.Name(), cgroupName) if err != nil { return err } log.Infof("%s cgroup path: %s", m.Name(), cgroupPath)
return os.RemoveAll(cgroupPath)}
func FindCgroupMountPoint(subSystem string) (string, error) { f, err := os.Open("/proc/self/mountinfo") if err != nil { return "", fmt.Errorf("open /proc/self/mountinfo err: %v", err) } defer f.Close()
scanner := bufio.NewScanner(f) for scanner.Scan() { txt := scanner.Text() fields := strings.Split(txt, " ") log.Debugf("mount info txt fields: %s", fields) for _, opt := range strings.Split(fields[len(fields)-1], ",") { if opt == subSystem { return fields[4], nil } } } if err := scanner.Err(); err != nil { return "", fmt.Errorf("file scanner err: %v", err) } return "", fmt.Errorf("FindCgroupMountPoint is empty")}
func GetCgroupPath(subsystemName, cgroupName string) (string, error) { // 找到Cgroup的根目录,如:/sys/fs/cgroup/memory cgroupRoot, err := FindCgroupMountPoint(subsystemName) if err != nil { return "", err }
cgroupPath := path.Join(cgroupRoot, cgroupName) _, err = os.Stat(cgroupPath) if err != nil && !os.IsNotExist(err) { return "", fmt.Errorf("file stat err: %v", err) } if os.IsNotExist(err) { if err := os.Mkdir(cgroupPath, os.ModePerm); err != nil { return "", fmt.Errorf("mkdir err: %v", err) } } return cgroupPath, nil}
复制代码

总结

在原书中,只写出了具体的内存资源限制的相关代码,但如果是照着 github 抄的话,本人就出现了运行不起来的尴尬


还是需要把原理搞懂,才能跟着作者的思路写出来,本文代码和书中有些许差异,但如果完整照抄应该是能跑起来的

发布于: 4 小时前阅读数: 4
用户头像

关注

还未添加个人签名 2018.09.09 加入

代码是门手艺活,也是门艺术活

评论

发布
暂无评论
自己动手写Docker系列 -- 3.2增加容器资源限制_Docker_萧_InfoQ写作平台