SpeedyCloud李雨来:深入Docker的资源管理
2015-08-29 07:29:00Docker解决什么问题?
Docker如何实现隔离?
Docker如何实现资源管控?
Docker容器的资源使用情况怎么监控?
8月28日-29日,由InfoQ举办的CNUTCon全球容器顶级盛会在北京成功举办。大会吸引了国内外近千名对容器技术感兴趣的技术人员,共同探讨容器技术的应用场景、技术方案以及架构细节。
本文是SpeedyCloud首席架构师李雨来在CNUTCon上的分享,题目是《深入Docker的资源管理》,根据自身的经验解答了上述提出的问题。
Docker解决什么问题?
对于Docker来说,它解决了两大问题
- 容器级别的高性能虚拟化提供了资源的隔离和管控
- DockerHub+Aufs(当然还有其他驱动)提供的镜像存储和变更管理
通过解决这两个核心问题,Docker给了我们很大的想象力。我们可以在Docker提供的这两个基础功能上包装出更多更有趣的产品,同时这些生态环境中出现的项目也可以大大解放运维哥哥。
资源的隔离
Docker隔离怎么做
Docker的资源隔离使用了Linux Kernel中的Namespaces功能。Namespaces主要是通过Linux的clone内核调用来使用。根据传入的不同参数组合来启用对应的Namespace隔离。下面是目前Linux Kernel支持的Namespaces参数列表:
- CLONE_NEWNS (Kernel 2.4.19开始支持,负责挂载点的隔离)
- CLONE_NEWUTS (Kernel 2.6.19开始支持,负责主机名的隔离)
- CLONE_NEWIPC (Kernel 2.6.19开始支持,负责进程间通讯的隔离)
- CLONE_NEWPID (Kernel 2.6.24开始支持,负责进程ID的隔离)
- CLONE_NEWNET (Kernel 2.6.24开始支持,负责网络栈的隔离)
- CLONE_NEWUSER (Kernel 3.8开始支持,负责用户和组的隔离)
通过上面的列表看来,越新的Kernel对Namespaces的支持越好。不过这也预示着还在使用CentOS 5(Kernel版本为2.6.18)及以下版本的服务器来说可以宣判死刑了。CentOS 6的Kernel主要还是在2.6.32,所以支持还不错。
Namespace是怎么展现的
我们可以通过/proc
文件系统来找到一些Namespace的蛛丝马迹。在任何一个/proc/${PID}/ns
文件夹中我们都可以发现以下这么几个文件:
- ipc
- mnt
- net
- pid
- user
- uts
这几个文件对应了clone内核调用中所提供的Namespaces。如果我们在clone内核调用中不传入所有的CLONE_NEW*
参数,那么未传入的Namespace会从父进程继承。这种设计的灵活性在于它给了我们一种让不同的Container共享同一个Namespace的能力。比如说,让两个Container共享宿主机的网络。
如何操作Namespaces
对于Namespaces的操作,主要是3个内核调用:
- clone
- setns
- unshare
clone
之前已经提到了,专门用来创建Namespace的。setns
用来给调用该函数的进程替换到指定的Namespace上,它需要一个fd参数,而fd就是对应进程中/proc/${PID}/ns/xxx
中对应文件的描述符。unshare
有点类似clone
,不过它负责给调用它的进程创建一个指定的Namespace。
当然,除了内核调用之外,我们还有几个命令行工具来简单地操作Namespace:
- ip netns
- unshare
其中最有意思的就是ip netns
了,我们可以用它来创建一个只有网络隔离功能的Container。
通过Namespaces可以做什么
既然我们了解了Namespaces以及如何控制它,接下来我们就来看看如何通过对Namespaces的操控来Hack一下Docker的Container。注意,前方高能,直接上代码:
```
!/bin/bash
PID=docker inspect -f '{{.State.Pid}}' $1
ID=docker inspect -f '{{.Id}}' $1
ETHNAME=$2
mkdir -p /var/run/netns ln -s /proc/${PID}/ns/net /var/run/netns/${ID}
ip link add dev ${ETHNAME}.0 type vet peer name ${ETHNAME}.1 ip link set dev ${ETHNAME}.1 netns ${ID} ip link set dev ${ETHNAME}.0 up ip netns exec ${ID} ifconfig ${ETHNAME}.1 $3 up
rm -rf /var/run/netns/${ID} ```
上面这个脚本主要是通过ip netns来操作Net Namespace从而给一个Docker的Container增加一块网卡,并配置好IP地址。用法为:network.sh docker-test veth0 192.168.1.10/24
。
当命令执行完毕之后,在宿主机上可以看到一个veth0.0的网卡,剩下的可以根据个人喜好把网卡加入Linux Bridge还是OpenVSwitch。
该脚本主要的地方在于:ip link set dev ${ETHNAME}.1 netns ${ID}
,这行用来向一个网络的Namespace中加入一块网卡。而ip netns exec ${ID} ...
则用来在一个指定的网络Namespace中执行一条命令。
资源使用的管控
如果只是使用Namespaces来隔离的话,我们会遇到以下的一些问题:
- 不同Container之间抢夺CPU资源
- 不同Container之间抢夺IO资源
- Container的磁盘资源使用过多把硬盘写满了导致其他Contianer出现问题
Docker最开始的理想中包含了对groups封装以方便管理员更容易地操作Container,避免陷入groups的复杂操作。
想法很好,但当我们真正运维Docker时,我们还是无法逃脱cgroups的魔爪。
注:以下针对groups子系统的描述使用的是lxc-cgroup中对
state-object
参数的命名方式。
Docker的cgroups模型
Docker对cgroups的操作根据驱动的不用使用了不同的路径:
- libcontainer:
/sys/fs/cgroups/
/docker/ - Linux Container:
/sys/fs/cgroups/
/lxc/
CPU和内存的资源管控
cgroups中对CPU的资源管控主要通过两种方式来设置:
- cpu.shares
- cpuset.cpus
cpu子系统中的shares文件用来设置一个CPU使用优先级的相对比例值。通过对这个值的修改可以表实出不同Container在使用CPU时的优先级。越大的优先级越高。
cpuset子系统中的cpus则可以指定一个Container只使用指定的CPU Core。
对于内存的管控,cgoups可以通过以下两个参数来设置:
- memory.memsw.limitinbytes
- memory.limitinbytes
memory子系统中的limitinbytes和memsw.limitinbytes用来设置Container能使用的最大内存和swap。
对于swap来讲,可能很多人忽略了它的重要性。swap的存在很大程度上减少或者说减缓了OOM(Out of Memory)的触发。从而给了运维人员在业务进程被OOM杀掉之前介入处理的可能性。对于OOM来说,它的触发机制为:
- 内核申请分配内存
- 内核发现物理内存中没有可用内存可分配,则把冷数据换出到swap中
- 内核发现swap已满,无法换出数据
- 触发OOM
而OOM被触发只有,它会扫描所有进程并统计它们使用的内存,并把内存占用最多的进程杀掉。在统计进程使用的内存时,OOM会把子进程使用的内存累加到父进程。也就是说它会杀掉父进程而不是那个占用内存最多的子进程。
当我们知道这个特性之后,想想看如果服务器中运行了php-fpm,然后OOM被触发了......
那么有人会问了,有没有swap最终都会触发OOM啊!答案是没错,OOM确实都会触发。但当你使用swap时,你的业务处理的性能就会变得很慢,这时运维可以通过监控系统发现问题并借入。但没有swap的话,你的运维人员还没来得及介入,业务进程已经被OOM杀掉了。
为了业务系统的稳定性Contaier一定要设置swap!
为了业务系统的稳定性Contaier一定要设置swap!
为了业务系统的稳定性Contaier一定要设置swap!
重要的事情说三遍
如果发现/sys/fs/cgroups/memory/docker/CONTAINERID/memsw.limitin_bytes文件不存在,可以增加一个Kernel的启动参数来启用这个功能:
cgroups_enable=memory swapaccount=1
磁盘IO的资源管控
在Container使用磁盘IO时就跟Container使用CPU时是一样的。如果没有管控,那么大家就开始相互抢夺磁盘IO资源了。
对于磁盘IO资源的管控,cgroups提供了两种方法:
- 基于权重的IO调度
- 对IOPS和吞吐量进行精细的限制
如果要使用基于权重的IO调度的话,需要启用CFQ调度算法,这里我就不详解了。主要讲一下针对IOPS和吞吐量限制的方法:
- blkio.throttle.readbpsdevice
- blkio.throttle.writebpsdevice
- blkio.throttle.readiopsdevice
- blkio.throttle.writeiopsdevice
光看名字就能知道blkio子系统中不同文件所要达到的目的了。在这里我主要讲一下如何设置。
对于throttle.readbpsdevice这个文件,我们需要写入的格式为:
major
和minor
是磁盘的两个编号,我们可以通过/proc/partitions
文件中获得对应磁盘的major
和minor
编号。在这里提醒大家:限制的是块设备的IO不是分区的IO。也就是说minor
编号大部分情况都是0
。
Docker的磁盘容量的管控
默认的Docker Image的驱动使用的是Aufs。使用Aufs可以很方便快捷地创建一个Container的底层文件系统。但Aufs在Container磁盘容量管控中做得并不好。在磁盘容量管控上,为大家分享两种方法:
- 通过LVM方式创建一个卷,通过--volume参数来让Container访问
- 使用btrfs驱动,通过对sub volume设置quota参数来限制磁盘容量
对于第一种方法来说,有个前提条件是Container中运行的应用程序所进行的磁盘操作要落到--volume参数指定的文件夹中。否则就会占用Aufs所在盘的容量。这种方式适用于Container中运行的程序是自己可控的。
第二种方法相对简单一些,Docker的btrfs驱动是使用btrfs所提供的sub volume功能来实现的。一个Container会对应一个subvolume,针对这个subvolume启用quota并设置quota参数即可限制Container的磁盘容量:
btrfs qgroup limit -e 100G /var/lib/docker/btrfs/subvolumes/
对于btrfs来说还有很多很好地特性,比如说可以在线加入一块新的块设备来扩充整个文件系统的大小等等。有兴趣的同学可以自行研究一下。不过btrfs也有自己的问题,对于老的Kernel来说,其支持的功能和稳定性来讲并不适合生产环境。如果Kernel比较新,那么可以考虑部署在生产环境,毕竟已经有Facebook这样的大公司在生产环境上测试btrfs了。
资源的监控
对于Docker的Container所使用的资源进行监控也是运维同学最关心的内容。cgroups提供了很多接口来让我们看到Container对资源的使用情况。
- cpuacct.usage Container占用的CPU时间,单位为ns
- memory.usageinbytes Container占用的内存容量
- memory.memsw.usageinbytes Container占用内存和swap的总容量
- blkio.throttle.io_serviced Container在各个块设备上的IO操作数
- blkio.throttle.ioservicebytes Container在各个块设备商的IO吞吐量
对于网络带宽的监控可以通过/sys/class/net/
文件夹下的文件来获取。
Out Of Docker
最后提供2个和Docker类似的项目,他们都有一个统一的特点:用Bash实现