Cgroup
cgroup和namespace类似,也是把进程进行分组,但是目的和namespace不一样,namespace是为了隔离进程组之间的资源.而cgroup是为了对一组进程进行统一的资源监控与限制.
cgroup技术把系统中所有的进程组织成一棵棵独立的树,每一棵树都有系统所有的进程
树的每个节点都是一个进程组.而每一棵树又于一个或者多个subsystem关联,树的作用是把进程分组,而subsystem作用是对这些组进行操作.
-
cgroup cgroup树的一个节点,相当于一个进程组.
-
subsystem 一个subsystem就是一个内核模块,他被关联到一颗cgroup树之后,就会在树的每个节点(进程组)上做具体的操作。subsystem经常被称作"resource controller",因为它主要被用来调度或者限制每个进程组的资源,但是这个说法不完全准确,因为有时我们将进程分组只是为了做一些监控,观察一下他们的状态,比如perf_event subsystem。到目前为止,Linux支持12种subsystem,比如限制CPU的使用时间,限制使用的内存,统计CPU的使用情况,冻结和恢复一组进程等,后续会对它们一一进行介绍。
-
hierarchy 一个hierarchy可以理解为一棵cgroup树,树的每个节点就是一个进程组,每棵树都会与零到多个subsystem关联。在一颗树里面,会包含Linux系统中的所有进程,但每个进程只能属于一个节点(进程组)。系统中可以有很多颗cgroup树,每棵树都和不同的subsystem关联,一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的subsystem关联。目前Linux支持12种subsystem,如果不考虑不与任何subsystem关联的情况(systemd就属于这种情况),Linux里面最多可以建12颗cgroup树,每棵树关联一个subsystem,当然也可以只建一棵树,然后让这棵树关联所有的subsystem。当一颗cgroup树不和任何subsystem关联的时候,意味着这棵树只是将进程进行分组,至于要在分组的基础上做些什么,将由应用程序自己决定,systemd就是一个这样的例子。
以上摘自
https://segmentfault.com/a/1190000006917884
定义Cgroups数据结构
package subsystems
type ResourceConfig struct {
MemoryLimit string
CpuShare string
CpuSet string
}
//Subsystem 接口,每个Subsystem可以实现下面四个接口:
//cgroup抽象为path,因为cgroup在hierarchy的路径便是虚拟文件系统中的虚拟路径
type Subsystem interface {
Name() string
Set(path string, res *ResourceConfig) error
Apply(path string, pid int) error
Remove(path string) error
}
var (
SubsystemsIns = []Subsystem{
&CpusetSubSystem{},
&MemorySubSystem{},
&CpuSubSystem{},
}
)
然后我们编写memory的subsystem操作
无非是apply,remove,set与name
package subsystems
import (
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
)
type MemorySubSystem struct {
}
/*
取得subsystem在当前虚拟文件系统中的路径
然后往这个路径的memory.limit_in_bytes 中设置允许的最大内存
*/
func (s *MemorySubSystem) Set(cgroupPath string, res *ResourceConfig) error {
if subsysCgroupPath, err := GetCgroupPath(s.Name(), cgroupPath, true); err == nil {
if res.MemoryLimit != "" {
if err := ioutil.WriteFile(path.Join(subsysCgroupPath, "memory.limit_in_bytes"), []byte(res.MemoryLimit), 0644); err != nil {
return fmt.Errorf("set cgroup memory fail %v", err)
}
}
return nil
} else {
return err
}
}
/*
删除cgroup path中的cgroup
*/
func (s *MemorySubSystem) Remove(cgroupPath string) error {
if subsysCgroupPath, err := GetCgroupPath(s.Name(), cgroupPath, false); err == nil {
return os.RemoveAll(subsysCgroupPath)
} else {
return err
}
}
/*
把一个进程加入到cgroupPath对应的cgroup当中
*/
func (s *MemorySubSystem) Apply(cgroupPath string, pid int) error {
if subsysCgroupPath, err := GetCgroupPath(s.Name(), cgroupPath, false); err == nil {
if err := ioutil.WriteFile(path.Join(subsysCgroupPath, "tasks"), []byte(strconv.Itoa(pid)), 0644); err != nil {
return fmt.Errorf("set cgroup proc fail %v", err)
}
return nil
} else {
return fmt.Errorf("get cgroup %s error: %v", cgroupPath, err)
}
}
func (s *MemorySubSystem) Name() string {
return "memory"
}
如何找到挂载了subsystem的hierarchy的挂载路径?
可以通过这个指令来完成.代表subsystem是读写的memory
- 首先根据上面指令结果,要找到挂载某个subsystem的hierarchy cgroup根节点所在的目录
//找出cgroup的挂载点
func FindCgroupMountpoint(subsystem string) string {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return ""
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
txt := scanner.Text()
fields := strings.Split(txt, " ")
//47 33 0:42 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,memory
for _, opt := range strings.Split(fields[len(fields)-1], ",") {
if opt == subsystem {
//47 33 0:42 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,memory
//fields[4] = /sys/fs/cgroup/memory
return fields[4]
}
}
}
if err := scanner.Err(); err != nil {
return ""
}
return ""
}
- 然后要找到这个subsystem的绝对路径
func GetCgroupPath(subsystem string, cgroupPath string, autoCreate bool) (string, error) {
cgroupRoot := FindCgroupMountpoint(subsystem)
if _, err := os.Stat(path.Join(cgroupRoot, cgroupPath)); err == nil || (autoCreate && os.IsNotExist(err)) {
if os.IsNotExist(err) {
if err := os.Mkdir(path.Join(cgroupRoot, cgroupPath), 0755); err == nil {
} else {
return "", fmt.Errorf("error create cgroup %v", err)
}
}
return path.Join(cgroupRoot, cgroupPath), nil
} else {
return "", fmt.Errorf("cgroup path error %v", err)
}
}
那么怎么把容器的进程移到每个subsystem创建的cgroup当中?
以及我们怎么样实际对subsystem的cgroup做资源的限制,从而实现容器进程的资源限制?
就要看以下函数了.
package cgroups
import (
"./subsystems"
"github.com/Sirupsen/logrus"
)
type CgroupManager struct {
// cgroup在hierarchy中的路径 相当于创建的cgroup目录相对于root cgroup目录的路径
Path string
// 资源配置
Resource *subsystems.ResourceConfig
}
func NewCgroupManager(path string) *CgroupManager {
return &CgroupManager{
Path: path,
}
}
// 将进程pid加入到这个cgroup中
func (c *CgroupManager) Apply(pid int) error {
for _, subSysIns := range(subsystems.SubsystemsIns) {
subSysIns.Apply(c.Path, pid)
}
return nil
}
// 设置cgroup资源限制
func (c *CgroupManager) Set(res *subsystems.ResourceConfig) error {
for _, subSysIns := range(subsystems.SubsystemsIns) {
subSysIns.Set(c.Path, res)
}
return nil
}
//释放cgroup
func (c *CgroupManager) Destroy() error {
for _, subSysIns := range(subsystems.SubsystemsIns) {
if err := subSysIns.Remove(c.Path); err != nil {
logrus.Warnf("remove cgroup fail %v", err)
}
}
return nil
}
总结一下:
最核心的几步可抽象为:
//初始化subsystem实例
cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
defer cgroupManager.Destroy()
//遍历调用各个subsystem实例的set方法,创建与配置不同subsystem挂载的cgroup
cgroupManager.Set(res)
//把当前容器进程ID遍历加入到那些cgroup
cgroupManager.Apply(parent.Process.Pid)
那么容器进程就会加入到这些subsystem所对应的cgroup(进程组中),subsystem通过对进程组做资源限制,从而实现了对容器进程的资源限制.