通过Cgroups来限制容器资源
Cgroups的3个组件
cgroup用于分组管理进程,subsystem(cpu和memory等)用于资源控制。 一个cgroup包含一组进程,可以在一个cgroup上增加subsystem的参数配置,把一组进程和一组subsystem的参数关联起来。
多个cgroup串成一个树状结构,这样的树是一个hierarchy。
系统默认为每个subsystem创建一个默认的hierarchy,例如memory的是/sys/fs/cgroup/memory。
查看memory subsystem的挂载点:mount | grep memory
Cgroups弊端
Cgroups对资源的限制能力不完善,例如proc文件系统。在容器里执行top指令,显示的是宿主机的CPU和内存数据,不是当前容器的数据。原因是top数据来自proc文件系统,proc文件系统不知道Cgroups限制的存在。
lxcfs已解决上面的问题,lxcfs是一个FUSE文件系统,让proc感知到cgroup的存在。
lxcfs把cgroup中容器相关的信息读取出来,存储到lxcfs相关的目录下,通过文件挂载的方式,将lxcfs相关目录映射到容器内的/proc目录下,从而使得容器内执行top、free等命令时显示容器真实数据。
安装
yum install -y fuse fuse-lib fuse-devel
git clone https://gitee.com/mirrors/lxcfs.git
git checkout stable-4.0
cd lxcfs
yum -y install libtool
./bootstrap.sh
./configure
make
make install
确认安装成功
lxcfs -h
运行lxcfs
lxcfs /var/lib/lxcfs &
启动容器
docker run -it --rm -m 256m --cpus 1 \
-v /var/lib/lxcfs/proc/cpuinfo:/proc/cpuinfo:rw \
-v /var/lib/lxcfs/proc/meminfo:/proc/meminfo:rw \
centos:latest bash
确认显示的容器资源正确
OOM处理机制
分配内存机制
和CPU不同,Linux允许进程申请超过实际物理内存上限的内存。
申请成功后,程序没有得到真正的物理内存。物理内存只有程序真的往这个地址写入数据的时候,才会分配给程序。这种内存申请模式的好处是,有效提高系统的内存利用率。
OOM行为控制
memory.oom_control
对于under_oom,表示是否处于OOM状态,内存耗尽时值为1,否则为0。
对于oom_kill_disable,值为0时表示开启oom killer,值为1时表示关闭。
$ sysctl -a | grep oom
# 除0以外的值表示OOM Killer运行时输出进程的列表信息
vm.oom_dump_tasks = 1
# 内存不足时,0是选择消耗内存最多的进程,1是选择触发内存不足的进程
vm.oom_kill_allocating_task = 0
# 内存不足时,0是通过杀进程来恢复,1是重启系统
vm.panic_on_oom = 0
多层嵌套cgroup
以父cgroup限制为准
group1里的memory.limit_in_bytes值是200MB,它的子控制组group3里memory.limit_in_bytes值是500MB。在group3里所有进程使用的内存总值就不能超过200MB。
oom_score计算方法
每个进程的oom_score_adj值,来自/proc/进程号/oom_score_adj,取值范围是-1000到1000,默认值是0。oom_score_adj值设置成-1000,会使OOM killer对当前进程失效;oom_score_adj值设置成1000,会使当前进程首当其冲被杀掉。
控制组中总的可用页面数乘以oom_score_adj的千分比,加上进程已经使用的物理页面数,计算出来的oom_score值最大的进程,会被系统选中杀死。
Docker配置Cgroups
$ docker run -itd -m 100m nginx:latest
700c59e5b38207399138144ffe44b5879c8776e9acf6234bdc821b9c68ce111d
# Docker为每个容器创建cgroup
# 在挂载了memory subsystem的hierarchy的根目录下
$ find /sys/fs/cgroup/memory -name docker-700c59e*.scope
/sys/fs/cgroup/memory/system.slice/docker-700c59e5b38207399138144ffe44b5879c8776e9acf6234bdc821b9c68ce111d.scope
# 查看容器进程id
$ docker inspect 700c59e | grep -i pid
"Pid": 1790,
"PidMode": "",
"PidsLimit": 0,
# 查看cgroup管理的进程
$ cat /sys/fs/cgroup/memory/system.slice/docker-700c59e5b38207399138144ffe44b5879c8776e9acf6234bdc821b9c68ce111d.scope/tasks
1790
1837
# 查看容器内的进程
$ ps -ef | grep 1790
root 1790 1771 0 21:03 pts/0 00:00:00 nginx: master process nginx -g daemon off;
101 1837 1790 0 21:03 pts/0 00:00:00 nginx: worker process
root 1898 1498 0 21:07 pts/0 00:00:00 grep --color=auto 1790
$ ps -ef | grep 1837
101 1837 1790 0 21:03 pts/0 00:00:00 nginx: worker process
root 1901 1498 0 21:07 pts/0 00:00:00 grep --color=auto 1837
# 查看容器内存限制(100M)
$ cat /sys/fs/cgroup/memory/system.slice/docker-700c59e5b38207399138144ffe44b5879c8776e9acf6234bdc821b9c68ce111d.scope/memory.limit_in_bytes
104857600
Go限制容器资源
代码
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
"syscall"
)
// 挂载了memory subsystem的hierarchy的根目录位置
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
func main() {
if os.Args[0] == "/proc/self/exe" {
// 7.打印容器进程在当前PID命名空间下的PID
fmt.Printf("current pid %d", syscall.Getpid())
fmt.Println()
// 子进程默认加入到父进程的cgroup里面
cmd := exec.Command("sh", "-c", `stress --vm-bytes 200m --vm-keep -m 1`)
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
// 1.准备执行命令
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
// 2.启动命令的执行
if err := cmd.Start(); err != nil {
fmt.Println("ERROR ", err)
os.Exit(1)
} else {
// 3.得到fork出来的进程映射在外部命名空间的pid
fmt.Printf("container pid is %v\n", cmd.Process.Pid)
// 4.在cgroupMemoryHierarchyMount上创建cgroup
os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
// 5.将容器进程加入到这个cgroup中
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "tasks"),
[]byte(strconv.Itoa(cmd.Process.Pid)), 0644)
// 6.限制cgroup进程使用内存(如果是100m,那么stress进程会被杀掉)
ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit",
"memory.limit_in_bytes"), []byte("300m"), 0644)
fmt.Println("prepare to execute stress --vm-bytes 200m --vm-keep -m 1")
}
}
步骤
1 创建执行/proc/self/exe命令的容器进程。
2 在/sys/fs/cgroup/memory下创建子目录testmemorylimit,作为cgroup节点。
3 把容器进程ID加入这个cgroup的tasks文件里面。
4 设置cgroup的内存限制。
5 基于容器进程来创建stress进程,stress进程创建一个worker进程。
执行结果
当限制内存为300m时,stress正常运行。
当限制内存为100m时,stress会被OOM kill。
# ./main
container pid is 2014
prepare to execute stress --vm-bytes 200m --vm-keep -m 1
current pid 1
stress: info: [4] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [4] (415) <-- worker 5 got signal 9
stress: WARN: [4] (417) now reaping child worker processes
stress: FAIL: [4] (421) kill error: No such process
stress: FAIL: [4] (451) failed run completed in 0s
exit status 1
参考资料
《自己动手写Docker》