简介
继上篇的 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 抄的话,本人就出现了运行不起来的尴尬
还是需要把原理搞懂,才能跟着作者的思路写出来,本文代码和书中有些许差异,但如果完整照抄应该是能跑起来的
评论