简介
继上篇的 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移出当前Cgroup
type Subsystem interface {
Name() string
Set(cgroupName string, res *ResourceConfig) error
Apply(cgroupName string, pid int) error
Remove(cgroupName string) error
}
// 具体实现类:内存和CPU
var 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加入Cgroup
func (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 释放 Cgroup
func (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 抄的话,本人就出现了运行不起来的尴尬
还是需要把原理搞懂,才能跟着作者的思路写出来,本文代码和书中有些许差异,但如果完整照抄应该是能跑起来的
评论