介绍
cgroups 是Linux内核提供的一种可以限制单个进程或者多个进程所使用资源的机制,可以对 cpu,内存等资源实现精确的控制,Docker 就使用了 cgroups 提供的资源限制能力来完成cpu,内存等部分的资源控制。
cgroups 的全称是control groups,cgroups为每种可以控制的资源定义了一个子系统。典型的子系统介绍如下:
cpu 子系统,主要限制进程的 cpu 使用率。
cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
cpuset 子系统,可以为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。
memory 子系统,可以限制进程的 memory 使用量。
blkio 子系统,可以限制进程的块设备 io。
devices 子系统,可以控制进程能够访问某些设备。
net_cls 子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
net_prio 子系统, 限制任务中网络流量的优先级.
pids 子系统, 限制控制组中进程可以派生出的进程数量。
freezer 子系统,可以挂起或者恢复 cgroups 中的进程。 namespace。
cgroup和namespace类似,也是将进程进行分组,但它的目的和namespace不一样,namespace是为了隔离进程组之间的资源,而cgroup是为了对一组进程进行统一的资源监控和限制。
cgroup分v1和v2两个版本,v1实现较早,最初版本是在 linux kernel 2.6.24,随着时间的推移,添加了越来越多的controller,而这些controller又各自独立开发,导致了controller之间很大的不一致,功能比较多,但是由于它里面的功能都是零零散散的实现的,所以规划的不是很好,导致了一些使用和维护上的不便,v2的出现就是为了解决v1中这方面的问题,随着v2一起引入内核的还有cgroup namespace。Linux 3.10 提供了作为试验特性的 cgroups v2, 到了 linux kernel 4.5 后 cgroups v2 才成为正式特性。
cgroup提供了一个原生接口并通过cgroupfs提供(从这句话我们可以知道cgroupfs就是cgroup的一个接口的封装)。类似于procfs和sysfs,是一种虚拟文件系统。并且cgroupfs是可以挂载的,默认情况下挂载在/sys/fs/cgroup目录。使用 mount 挂载 cgroup 文件系统就可以使用配置这些controller了,系统通常已经挂载好了。Linux 通过虚拟文件系统的方式将功能和配置暴露给用户,这得益于 linux 的虚拟文件系统(vfs),vfs 将具体文件系统的细节隐藏起来,给用户态一个统一的问题件系统 api 接口。systemd也是对于cgroup接口的一个封装。systemd以pid1的形式在系统启动的时候运行,并提供了一套系统管理守护程序、库和实用程序,用来控制、管理linux计算机操作系统资源。通过systemd-cgls命令我们可以看到systemd工作的进程pid是1,而目录/sys/fs/cgroup/systemd是systemd维护的自己使用的非subsystem的cgroups层级结构。也就是说使用cgroup有两种方式,第一种就是 cgroupfs,第二种就是,systemd。
使用cgroups限制CPU使用
测试系统: CentOS7 ,内核版本 3.10.0-1062.18.1.el7.x86_64,cgroup v1
首先进入cpu子系统对应的层级路径下:cd /sys/fs/cgroup/cpu 通过新建文件夹创建一个cpu控制族群:mkdir test1,即新建了一个cpu控制族群:test1 新建test1之后,可以看到目录下自动建立了相关的文件,这些文件是伪文件。我们的测试示例主要用到cpu.cfs_period_us和cpu.cfs_quota_us 、cgroup.procs 3个文件。 cgroup.procs 记录受限PID cpu.cfs_period_us:cpu分配的周期(微秒),默认为100000。 cpu.cfs_quota_us:表示该control group限制占用的时间(微秒),默认为-1,表示不限制。
如果要限制CPU 使用5% ,cpu.cfs_quota_us 写入5000。
echo 5000 > /sys/fs/cgroup/cpu/test1/cpu.cfs_quota_us
然后,把受限进程pid写入cgroup.procs文件中,即可完成对该进程限制CPU 使用5%,当进程CPU使用超过%5就会被系统kill。当进程退出,cgroup.procs以及tasks会清除相关pid的记录。
echo 1234 > /sys/fs/cgroup/cpu/test1/cgroup.procs
使用cgroups限制内存使用
测试系统: CentOS7 ,内核版本 3.10.0-1062.18.1.el7.x86_64,cgroup v1
首先进入cpu子系统对应的层级路径下:cd /sys/fs/cgroup/memory 通过新建文件夹创建一个内存控制族群:mkdir test1,即新建了一个内存控制族群:test1 新建test1之后,可以看到目录下自动建立了相关的文件,这些文件是伪文件。
$ ls -la /sys/fs/cgroup/memory/test1 cgroup.clone_children memory.kmem.limit_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.max_usage_in_bytes memory.soft_limit_in_bytes tasks cgroup.event_control memory.kmem.max_usage_in_bytes memory.kmem.usage_in_bytes memory.memsw.usage_in_bytes memory.stat cgroup.procs memory.kmem.slabinfo memory.limit_in_bytes memory.move_charge_at_immigrate memory.swappiness memory.failcnt memory.kmem.tcp.failcnt memory.max_usage_in_bytes memory.numa_stat memory.usage_in_bytes memory.force_empty memory.kmem.tcp.limit_in_bytes memory.memsw.failcnt memory.oom_control memory.use_hierarchy memory.kmem.failcnt memory.kmem.tcp.max_usage_in_bytes memory.memsw.limit_in_bytes memory.pressure_level notify_on_release
主要配置含义:
cgroup.procs: 使用该组配置的进程列表。
memory.limit_in_bytes:内存使用限制。
memory.memsw.limit_in_bytes:内存和交换分区总计限制。
memory.swappiness: 交换分区使用比例。
memory.usage_in_bytes: 当前进程内存使用量。
memory.stat: 内存使用统计信息。
memory.oom_control: OOM 控制参数。
假设有进程 pid 1234,希望设置内存限制为 10MB,我们可以这样操作:
limit_in_bytes 设置为 10MB
echo "10*1024*1024" | bc > /sys/fs/cgroup/memory/test1/memory.limit_in_bytes
swappiness 设置为 0,表示禁用交换分区,实际生产中可以配置合适的比例。
echo 0 > /sys/fs/cgroup/memory/test1/memory.swappiness
添加控制进程pid,当进程 1234 使用内存超过 10MB 的时候,默认进程 1234 会触发 OOM,被系统 Kill 掉。
echo 1234 > /sys/fs/cgroup/memory/test1/cgroup.procs
使用cgroups限制HIDS-Agent的cpu和内存
使用 cgroups + etcd + kafka 开发而成的hids的架构,agent 部分使用go 开发而成, 会把采集的数据写入到kafka里面,由后端的规则引擎(go开发而成)消费,配置部分以及agent存活使用etcd。
agent 支持安装成系统服务,直接亮代码:
test1-master.go
package main import ( "fmt" "log" "os" "os/signal" "syscall" "github.com/takama/daemon" "path/filepath" "io/ioutil" "os/exec" ) const ( name = "test1" description = "test1 service" procsFile = "cgroup.procs" memoryLimitFile = "memory.limit_in_bytes" swapLimitFile = "memory.swappiness" cpuLimitFile = "cpu.cfs_quota_us" Name = "Pagent" memoLimit = 50 // 50M mcgroupRoot = "/sys/fs/cgroup/memory/"+Name cpuLimit = 5 // 5% cpucgroupRoot = "/sys/fs/cgroup/cpu/"+Name ) var stdlog, errlog *log.Logger type Service struct { daemon.Daemon } func (service *Service) Manage() (string, error) { usage := "Usage: ./test1-master install | remove | start | stop | status" if len(os.Args) > 1 { command := os.Args[1] switch command { case "install": exist, _ := PathExists(mcgroupRoot) if exist { fmt.Printf("has dir![%v]\n", mcgroupRoot) } else { err := os.Mkdir(mcgroupRoot, os.ModePerm) if err != nil { fmt.Printf("mkdir failed![%v]\n", err) } else { fmt.Printf("mkdir success!\n") } } exist, _ = PathExists(cpucgroupRoot) if exist { fmt.Printf("has dir![%v]\n", cpucgroupRoot) } else { err := os.Mkdir(cpucgroupRoot, os.ModePerm) if err != nil { fmt.Printf("mkdir failed![%v]\n", err) } else { fmt.Printf("mkdir success!\n") } } mPath := filepath.Join(mcgroupRoot, memoryLimitFile) writeFile(mPath, memoLimit*1024*1024) sPath := filepath.Join(mcgroupRoot, swapLimitFile) writeFile(sPath, 0) cPath := filepath.Join(cpucgroupRoot, cpuLimitFile) writeFile(cPath, cpuLimit*1000) return service.Install() case "remove": return service.Remove() case "start": return service.Start() case "stop": return service.Stop() case "status": return service.Status() default: return usage, nil } } go startCmd("/usr/local/test1/test1-agent") interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt, os.Kill, syscall.SIGTERM) for { select { case killSignal := <-interrupt: stdlog.Println("Got signal:", killSignal) if killSignal == os.Interrupt { return "Daemon was interruped by system signal", nil } return "Daemon was killed", nil } } return usage, nil } func init() { stdlog = log.New(os.Stdout, "", log.Ldate|log.Ltime) errlog = log.New(os.Stderr, "", log.Ldate|log.Ltime) } func main() { srv, err := daemon.New(name, description, daemon.SystemDaemon,"nil") if err != nil { errlog.Println("Error: ", err) os.Exit(1) } service := &Service{srv} status, err := service.Manage() if err != nil { errlog.Println(status, "\nError: ", err) os.Exit(1) } fmt.Println(status) } func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err } func writeFile(path string, value int) { if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", value)), 0755); err != nil { log.Panic(err) } } type ExitStatus struct { Signal os.Signal Code int } func startCmd(command string) { restart := make(chan ExitStatus, 1) runner := func() { cmd := exec.Cmd{ Path: command, } cmd.Stdout = os.Stdout if err := cmd.Start(); err != nil { log.Panic(err) } fmt.Println("add pid", cmd.Process.Pid, "to file cgroup.procs") mPath := filepath.Join(mcgroupRoot, procsFile) writeFile(mPath, cmd.Process.Pid) cpuPath := filepath.Join(cpucgroupRoot, procsFile) writeFile(cpuPath, cmd.Process.Pid) if err := cmd.Wait(); err != nil { fmt.Println("cmd return with error:", err) } status := cmd.ProcessState.Sys().(syscall.WaitStatus) options := ExitStatus{ Code: status.ExitStatus(), } if status.Signaled() { options.Signal = status.Signal() } cmd.Process.Kill() restart <- options } go runner() for { status := <-restart switch status.Signal { case os.Kill: fmt.Println("app is killed by system") default: fmt.Println("app exit with code:", status.Code) return } fmt.Println("restart app..") go runner() } }
test1-agent
package main import ( "test1/app" ) func main() { var agent app.Agent agent.Run() }
cgroups 挂载问题
测试发现很多Ubuntu 机器开机没有挂载cgroupfs,下面提供一个挂载脚本。
#!/bin/sh set -e if grep -v '^#' /etc/fstab | grep -q cgroup; then echo "cgroups mounted from fstab, not mounting /sys/fs/cgroup" exit 0 fi if [ ! -e /proc/cgroups ]; then exit 0 fi mountpoint -q /sys/fs/cgroup || mount -t tmpfs -o uid=0,gid=0,mode=0755 cgroup /sys/fs/cgroup for d in `tail -n +2 /proc/cgroups | awk '{ if ($2 == 0) print $1 else if (a[$2]) a[$2] = a[$2]","$1 else a[$2]=$1 };END{ for(i in a) { print a[i] } }'`; do mkdir -p /sys/fs/cgroup/$d mountpoint -q /sys/fs/cgroup/$d || (mount -n -t cgroup -o $d cgroup /sys/fs/cgroup/$d || rmdir /sys/fs/cgroup/$d || true) done dir=/sys/fs/cgroup/systemd if [ ! -d "${dir}" ]; then mkdir "${dir}" mount -n -t cgroup -o none,name=systemd name=systemd "${dir}" || rmdir "${dir}" || true fi echo "Cgroupfs successfully mounted" exit 0