Linux容器核心技术之: Cgroups
文章目录Cgroups是什么?Cgroups中的三个组件cgroup 用于对进程分组, 分组后便于统一设置资源限制;subsystem 用于对资源做限制及监控;hierarchy三个组件的相互关系cgroup 文件系统(cgroupfs)介绍Cgroups使用示例在cgroup中添加和移动进程通过subsystem限制cgroup中进程的资源参考Cgroups是什么?查看linux man pag
文章目录
Cgroups是什么?
查看linux man page: https://man7.org/linux/man-pages/man7/cgroups.7.html 其中对Cgrpups的描述:
cgroups - Linux control groups
Control groups, usually referred to as cgroups, are a Linux
kernel feature which allow processes to be organized into
hierarchical groups whose usage of various types of resources can
then be limited and monitored. The kernel’s cgroup interface is
provided through a pseudo-filesystem called cgroupfs. Grouping
is implemented in the core cgroup kernel code, while resource
tracking and limits are implemented in a set of per-resource-type
subsystems (memory, CPU, and so on).
翻译过来大致是说:
cgroups是Linux内核的特性. 利用cgroups, 可以将多个进程分组, 这样这些进程使用的各种资源可以被限制及监控. 内核的cgroups接口是通过一个叫cgroupfs的伪文件系统来实现的.
小结: Cgroups可以用来限制及监控进程使用的资源.
Cgroups中的三个组件
cgroup 用于对进程分组, 分组后便于统一设置资源限制;
subsystem 用于对资源做限制及监控;
一般包含如下几项:
blkio
设置对块设备(比如硬盘)输入输出的访问控制cpu
设置 cgroup 中进程的 CPU 被调度的策略。cpuacct
可以统计 cgroup 中进程的 CPU 占用cpuset
在多核机器上设置 cgroup 中进程可以使用的 CPU 和内存(此处内存仅使用于
NUMA 架构)devices
控制 cgroup 中进程对设备的访问freezer
用于挂起( suspend )和恢复( resume) cgroup 中的进程memory
用于控制 cgroup 中进程的内存占用net_cls
用于将 cgroup 中进程产生的网络包分类,以便 Linux tc (traffic controller)可
以根据分类区分出来自某个 cgroup 的包并做限流或监控net_prio
设置 cgroup 中进程产生的网络流量的优先级ns
这个 subsystem 比较特殊,它的作用是使 cgroup 中的进程在新的 Namespace fork
新进程 (NEWNS)时,创建出一个新的 cgroup ,这个 cgroup 包含新的 Namespace中的进程
每个 subsystem 会关联到定义了相应限制的 cgroup 上,并对这个 cgroup 中的进程做相
应的限制和控制. 这些 subsystem 是逐步合并到内核中的. 可以通过lssubsys
命令查看内核支持的subsystem:
$ lssubsys
cpuset
cpu,cpuacct
memory
devices
freezer
net_cls
blkio
perf_event
hugetlb
pids
oom
hierarchy
hierarchy的功能是把cgroup 串成一个树状的结构, 一个这样的树便是一个hierarchy ,通过这树状结构, Cgroups 可以做到继承;
三个组件的相互关系
- 系统在创建了新的 hierarchy 之后,系统中所有的进程都会加入这个 hierarchy的cgroup根节点,这个 cgroup 根节点是 hierarchy 默认创建的;
- subsystem 只能附加到 hierarchy 上面;
- 一个hierarchy 可以附加多个 subsystem;
- 一个进程可以作为多个 cgroup 的成员,但是这些 cgroup 必须在不同的 hierarchy中;
- 一个进程 fork 出子进程时,子进程是和父进程在同一个 cgroup 中的,也可以根据需要
将其移动到其他 cgroup 中;
下面用一张图来简单说明下(其中的Process表示进程)
cgroup 文件系统(cgroupfs)介绍
通过前面关于Cgroups的介绍, 我们知道, Cgroups的接口, 是通过cgroupfs这个伪文件系统来实现的, 那来看看这个文件系统长啥样:
$ mkdir cgroup-test # 创建一个hierarchy挂载点
$ mount -t cgroup -o none,name=cg-root-1 cg-root-1 ./cgroup-test/ # 挂载一个hierarchy
$ mount # 查看, 发现我们的系统上多了一个名为cg-root-1, 类型为cgroup的文件系统(即cgroupfs), 挂载点为/data/test/cgroups/cgroup-test
# ...省略一些内容...
cg-root-1 on /data/test/cgroups/cgroup-test type cgroup (rw,relatime,name=cg-root-1)
$ ls ./cgroup-test/ # 挂载后我们就可以看到系统在这个目录下生成了一些默认文件
cgroup.clone_children cgroup.procs notify_on_release tasks
cgroup.event_control cgroup.sane_behavior release_agent
# 创建子cgroup: cg-1和cg-2
$ mkdir cg-1 cg-2
# 注意cg-1和cg-2下的文件也是自动生成的, 相当于继承了父cgroup(cgroup-test)的一些设置
$ tree
.
├── cg-1
│ ├── cgroup.clone_children
│ ├── cgroup.event_control
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cg-2
│ ├── cgroup.clone_children
│ ├── cgroup.event_control
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup.clone_children
├── cgroup.event_control
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks
注意, cgroup-test, cg-1, cg-2下面里边的文件都是自动生成的! 这些文件分别表示:
cgroup.clone_children
, cpuset subsystem 会读取这个配置文件,如果这个值是1(默认是0),子cgroup 才会继承父 cgroup cpuset 的配置;cgroup.procs
是hierarchy树中当前cgroup节点中的进程组 ID ,当前cgroup的位置是在hierarchy树的根节点,这个文件中会有现在系统中所有进程组的 IDnotify_on _release
和release agent
会一起使用。notify_on_release
标识当这个 cgroup最后一个进程退出的时候是否执行了release_agent
;release_ agent
则是 个路径,通常用作进程退出之后自动清理掉不再使用的 cgroup;tasks
标识 cgroup 下面的进程 ID ,如果把 个进程 ID 写到 tasks 文件中,便会将相应的进程加入到这个 cgroup; 注意: tasks有时也区分线程还是进程id, 具体看这里: https://man7.org/linux/man-pages/man7/cgroups.7.html#CGROUPS_VERSION_2_THREAD_MODE
Cgroups使用示例
如何使用Cgroups来限制进程使用的资源呢? 一起来看看
在cgroup中添加和移动进程
使用上面创建好的hierarchy(cgroup-test/{cg-1, cg-2}).
我们知道, tasks
文件中标识cgroup下面的进程ID, 那么将进程加入到cgroup中也很简单, 就是将进程的id写入到tasks文件中就行了.
$ cd cg-1 # 进入cgroup: cg-1
$ echo $$ # 查看当前shell进程id
1572
$ echo $$ > tasks # 将当前shell进程加入到cgroup: cg-1
$ cat /proc/$$/cgroup # 查看当前进程加入的cgroup
21:name=cg-root-1:/
# ... 省略一些内容
$ cd ../cg-2/
$ echo $$ > tasks # 将当前shell进程移动到cgroup: cg-2中
$ cat /proc/$$/cgroup
21:name=cg-root-1:/cg-2 # 注意这里, 只有cg-2
上述示例: 将当前shell进程加入到cg-2时, 会自动从cg-1中移除, 也说明了: 一个进程可以作为多个 cgroup 的成员,但是这些 cgroup 必须在不同的 hierarchy中;
通过subsystem限制cgroup中进程的资源
在上面创建 hierarchy 的时候,这个 hierarchy 并没有关联到任何的 subsystem ,所以没办法通过那个 hierarchy 中的 cgroup 节点限制进程的资源占用,其实系统默认已经为每个subsystem 创建了一个默认的 hierarchy ,比如 memory hierarchy:
$ mount | grep memory
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
可以看到, memory subsystem的hierarchy 挂载到了 /sys/fs/cgroup/memory , 我们就在这个hierarchy下创建cgroup, 限制进程占用的内存.
$ cd /sys/fs/cgroup/memory
$ mkdir test-memory-limit
$ cd test-memory-limit/
$ ls
cgroup.clone_children memory.kmem.tcp.max_usage_in_bytes
...... 省略一些内容
memory.kmem.tcp.limit_in_bytes memory.move_charge_at_immigrate tasks
其实还可以通过 cgcreate -g memory:test-memory-limit
来创建cgroup, 效果和上面操作后是一样的.
可以通过cgdelete -g memory:test-memory-limit
来删除cgroup;
创建好cgroup后, 我们来通过cgroup来限制下进程占用的内存(在test-memory-limit目录):
$ echo '100m' > memory.limit_in_bytes
$ cat memory.limit_in_bytes
104857600 # 100m=100 * 1024 * 1024
$ echo $$
16570
$ echo 16570 > tasks
$ stress --vm-bytes 128m --vm-keep -m 1 # 启动一个占用128m的进程报错了
stress: info: [335] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [335] (415) <-- worker 336 got signal 9
stress: WARN: [335] (417) now reaping child worker processes
stress: FAIL: [335] (451) failed run completed in 0s
$ stress --vm-bytes 99m --vm-keep -m 1 # 启动一个占用99m的进程成功了
stress: info: [351] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
Docker是如何使用Cgroups的
我们知道Docker的底层技术有使用到Cgroups, 结合一个实例来看下:
$ docker run -itd -m 128m ubuntu # -m 128m 限制容器使用的内存最大为128m
64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
$ docker ps # 确保容器正常运行
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
64b8f2e16f30 ubuntu "bash" 2 seconds ago Up 1 second stupefied_bhaskara
$ cd /sys/fs/cgroup/memory/docker
$ cd 64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
$ ls
... 省略一些内容
memory.failcnt memory.limit_in_bytes
memory.kmem.tcp.limit_in_bytes memory.move_charge_at_immigrate tasks
$ cat memory.limit_in_bytes # 查看容器内存限制
134217728 # 128m=134217728=128 * 1024 * 1024
$ cat memory.usage_in_bytes # 查看容器已经使用的内存
778240
可以看到, Docker 通过为每个容器创建 cgroup 并通过 cgroup 去配置资源限制和资源监控。
事实上, 每一个Docker容器, 在操作系统上是对应到进程, 可以这样看:
# 通过容器id查找相应的容器进程
$ ps -ef|grep -v grep | grep 64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
root 11313 6086 0 14:17 ? 00:00:00 containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc
$ pstree -pl | grep 11313 # 这里11313其实是containerd-shim进程, 真正运行的是bash(ubuntu镜像的入口是 CMD ["bash"])
|-containerd(6086)-+-containerd-shim(11313)-+-bash(11330)
$ ps -ef|grep -v grep|grep 11330
root 11330 11313 0 14:17 ? 00:00:00 bash
# 通过docker inspect <容器id> 也可以看到其进程id是11300(bash进程)
$ docker inspect 64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29 | grep -C 2 '"Pid"'
"OOMKilled": false,
"Dead": false,
"Pid": 11330,
"ExitCode": 0,
"Error": "",
$
$ cat /proc/11330/cgroup # 顺便看下容器进程关联的cgroup, 很快也就明白了docker容器是怎么使用cgroup的
12:hugetlb:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
11:cpuacct,cpu:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
10:cpuset:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
9:devices:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
8:freezer:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
7:perf_event:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
6:oom:/
5:blkio:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
4:memory:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
3:net_cls:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
2:pids:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
1:name=systemd:/docker/64b8f2e16f30c4cde22846dd07ef4c65cd272c3a482eb527c4cd8c90657dcd29
使用Go语言实现通过cgroup 限制容器的资源
memory-limit.go:
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
"syscall"
)
// 挂载了 memory subsystem hierarchy 的根目录位置
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
func main() {
fmt.Println("os args:", os.Args)
if os.Args[0] == "/proc/self/exe" {
// 容器进程
fmt.Printf("current pid:%v\n", syscall.Getpid())
cmd := exec.Command("sh", "-c", "stress --vm-bytes 200m --vm-keep -m 1")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
checkErr(err, "/proc/self/exe run")
}
cmd := exec.Command("/proc/self/exe")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
checkErr(err, "/proc/self/exe start")
// 得到 fork 出来进程映射在外部命名空间的 pid
fmt.Printf("%v\n", cmd.Process.Pid)
// 在系统默认创建挂载了 memory subsystem Hierarchy 上创建 cgroup
err = os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
checkErr(err, "Mkdir")
// 将容器进程加入到这个 cgroup
err = ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "tasks"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)
checkErr(err, "WriteFile tasks")
//g限制 cgroup 进程使用
err = ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes"), []byte("100m"), 0644)
checkErr(err, "WriteFile limit_in_bytes")
_, err = cmd.Process.Wait()
checkErr(err, "cmd.Process.Wait")
}
func checkErr(err error, reason string) {
if err != nil {
panic(fmt.Sprintf("err:%v, reason:%s", err, reason))
}
}
运行, 发现因内存限制而无法执行stress …:
$ rmdir /sys/fs/cgroup/memory/testmemorylimit
rmdir: failed to remove ‘/sys/fs/cgroup/memory/testmemorylimit’: No such file or directory
$ go run memory-limit.go
os args: [/tmp/go-build3880927588/b001/exe/memory-limit]
9238
os args: [/proc/self/exe]
current pid:1
stress: info: [6] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [6] (415) <-- worker 7 got signal 9
stress: WARN: [6] (417) now reaping child worker processes
stress: FAIL: [6] (421) kill error: No such process
stress: FAIL: [6] (451) failed run completed in 0s
panic: err:exit status 1, reason:/proc/self/exe run
...省略一些内容
上述 exec.Command("sh", "-c", "stress --vm-bytes 200m --vm-keep -m 1")
改为exec.Command("sh", "-c", "stress --vm-bytes 99m --vm-keep -m 1")
, 再运行发现可以成功:
$ rmdir /sys/fs/cgroup/memory/testmemorylimit
$ go run memory-limit.go
os args: [/tmp/go-build1141603810/b001/exe/memory-limit]
10400
os args: [/proc/self/exe]
current pid:1
stress: info: [6] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
^Csignal: interrupt
$
说明memory限制生效了
总结
- Cgroups的三大组件为: cgroup, hierarchy, subsystem;
- Docker容器其实是操作系统上的一个进程;
参考
(完)
更多推荐
所有评论(0)