内容简介:正如Docker官方的口号:“Build once,Run anywhere,Configure once,Run anything”,Docker被贴上了如下标签:轻巧、秒级启动、版本管理、可移植性等等,这些优点让它出现之初就收到极大的关注。现在,Docker已经不仅仅是开发测试阶段使用的工具,大家已经在生产环境中大量使用。今天我们给大家介绍关于容器隔离性的一个“坑”。在此之前,我们先来回顾一下Docker容器的底层实现原理。我们都知道,虚拟机与容器的底层实现原理是不同的,正如下图对比:虚拟机实现资源隔
正如 Docker 官方的口号:“Build once,Run anywhere,Configure once,Run anything”,Docker被贴上了如下标签:轻巧、秒级启动、版本管理、可移植性等等,这些优点让它出现之初就收到极大的关注。现在,Docker已经不仅仅是开发测试阶段使用的工具,大家已经在生产环境中大量使用。今天我们给大家介绍关于容器隔离性的一个“坑”。在此之前,我们先来回顾一下Docker容器的底层实现原理。
容器底层实现
我们都知道,虚拟机与容器的底层实现原理是不同的,正如下图对比:
虚拟机实现资源隔离的方法是利用一个独立的Guest OS,并利用Hypervisor虚拟化CPU、内存、IO设备等实现的。例如,为了虚拟化内存,Hypervisor会创建一个shadow page table,正常情况下,一个page table可以用来实现从虚拟内存到物理内存的翻译。相比虚拟机实现资源和环境隔离的方案,Docker就显得简练很多,它不像虚拟机一样重新加载一个操作系统内核,引导、加载操作系统内核是一个比较耗时而又消耗资源的过程,Docker是利用 Linux 内核特性实现的隔离,运行容器的速度几乎等同于直接启动进程。
关于Docker实现原理,简单总结如下:
- 使用Namespaces实现了系统环境的隔离,Namespaces允许一个进程以及它的子进程从共享的宿主机内核资源(网络栈、进程列表、挂载点等)里获得一个仅自己可见的隔离区域,让同一个Namespace下的所有进程感知彼此变化,对外界进程一无所知,仿佛运行在一个独占的操作系统中;
- 使用CGroups限制这个环境的资源使用情况,比如一台16核32GB的机器上只让容器使用2核4GB。使用CGroups还可以为资源设置权重,计算使用量,操控任务(进程或线程)启停等;
- 使用镜像管理功能,利用Docker的镜像分层、写时复制、内容寻址、联合挂载技术实现了一套完整的容器文件系统及运行环境,再结合镜像仓库,镜像可以快速下载和共享,方便在多环境部署。
正因为Docker不像虚机虚拟化一个Guest OS,而是利用宿主机的资源,和宿主机共用一个内核,所以会存在下面问题:
注意:存在问题并不一定说就是安全隐患,Docker作为最重视安全的容器技术之一,在很多方面都提供了强安全性的默认配置,其中包括:容器root用户的 Capability 能力限制,Seccomp系统调用过滤,Apparmor的 MAC 访问控制,ulimit限制,pid-limits的支持,镜像签名机制等。
1、Docker是利用CGroups实现资源限制的,只能限制资源消耗的最大值,而不能隔绝其他程序占用自己的资源;
2、Namespace的6项隔离看似完整,实际上依旧没有完全隔离Linux资源,比如/proc 、/sys 、/dev/sd*等目录未完全隔离,SELinux、time、syslog等所有现有Namespace之外的信息都未隔离。
容器隔离性踩过的坑
在使用容器的时候,大家很可能遇到过这几个问题:
1、在Docker容器中执行 top、free 等命令,会发现看到的资源使用情况都是宿主机的资源情况,而我们需要的是这个容器被限制了多少CPU,内存,当前容器内的进程使用了多少;
2、在容器里修改/etc/sysctl.conf,会收到提示”sysctl: error setting key ‘net.ipv4….’: Read-only file system”;
3、程序运行在容器里面,调用API获取系统内存、CPU,取到的是宿主机的资源大小;
4、对于多进程程序,一般都可以将worker数量设置成auto,自适应系统CPU核数,但在容器里面这么设置,取到的CPU核数是不正确的,例如Nginx,其他应用取到的可能也不正确,需要进行测试。
这些问题的本质都一样,在Linux环境,很多命令都是通过读取 /proc 或者 /sys 目录下文件来计算资源使用情况,以free命令为例:
lynzabo@ubuntu:~$ strace free
execve("/usr/bin/free", ["free"], [/* 66 vars */]) = 0
...
statfs("/sys/fs/selinux", 0x7ffec90733a0) = -1 ENOENT (No such file or directory)
statfs("/selinux", 0x7ffec90733a0) = -1 ENOENT (No such file or directory)
open("/proc/filesystems", O_RDONLY) = 3
...
open("/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
...
open("/proc/meminfo", O_RDONLY) = 3
+++ exited with 0 +++
lynzabo@ubuntu:~$
包括各个语言,比如Java,NodeJS,这里以NodeJS为例:
const os = require('os');
const total = os.totalmem();
const free = os.freemem();
const usage = (free - total) / total * 100;
NodeJS的实现,也是通过读取 /proc/meminfo 文件获取内存信息。Java也是类似。
我们都知道,JVM默认的最大Heap大小是系统内存的1/4,假若物理机内存为10G,如果你不手动指定Heap大小,则JVM默认Heap大小就为2.5G。JavaSE8(<8u131) 版本前还没有针对在容器内执行高度受限的Linux进程进行优化,JDK1.9 以后开始正式支持容器环境中的CGroups内存限制,JDK1.10 这个功能已经默认开启,可以查看相关Issue (Issue地址: https://bugs.openjdk.java.net/browse/JDK-8146115 )。熟悉JVM内存结构的人都清楚,JVM Heap是一个只增不减的内存模型,Heap的内存只会往上涨,不会下降。在容器里面使用Java,如果为JVM未设置Heap大小,Heap取得的是宿主机的内存大小,当Heap的大小达到容器内存大小时候,就会触发系统对容器OOM,Java进程会异常退出。常见的系统日志打印如下:
memory: usage 2047696kB, limit 2047696kB, failcnt 23543 memory+swap: usage 2047696kB, limit 9007199254740991kB, failcnt 0 ...... Free swap = 0kB Total swap = 0kB ...... Memory cgroup out of memory: Kill process 18286 (java) score 933 or sacrifice child
对于 Java 应用,下面提供两个办法来设置Heap
1、对于 JavaSE8(<8u131) 版本,手动指定最大堆大小。
docker run的时候通过环境变量传参确切限制最大heap大小:
docker run -d -m 800M -e JAVA_OPTIONS='-Xmx300m' openjdk:8-jdk-alpine
2、对于JavaSE8(>8u131)版本,可以使用上面手动指定最大堆大小,也可以使用下面办法,设置自适应容器内存限制。
docker run的时候通过环境变量传参确切限制最大heap大小
docker run -d -m 800M -e JAVA_OPTIONS='-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1' openjdk:8-jdk-alpine
对比这两种方式,第一种方式缺乏灵活性,在确切知道内存限制大小的情况下可以使用,第二种方法必须在JavaSE8(>8u131)版本才能使用。
当你启动一个容器时候,Docker会调用libcontainer实现对容器的具体管理,包括创建UTS、IPS、Mount等Namespace实现容器之间的隔离和利用CGroups实现对容器的资源限制,在其中,Docker会将宿主机一些目录以只读方式挂载到容器中,其中包括/proc、/dev、/dev/shm、/sys目录,同时还会建立以下几个链接:
- /proc/self/fd->/dev/fd
- /proc/self/fd/0->/dev/stdin
- /proc/self/fd/1->/dev/stdout
- /proc/self/fd/2->/dev/stderr
保证系统IO不会出现问题,这也是为什么在容器里面取到的是宿主机资源原因。
了解了这些,那么我们在容器里该如何获取实例资源使用情况呢,下面介绍两个方法。
从CGroups中读取
Docker 在 1.8 版本以后会将分配给容器的CGroups信息挂载进容器内部,容器里面的程序可以通过解析CGroups信息获取到容器资源信息。
在容器里面可以运行mount 命令查看这些挂载记录
... cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset) cgroup on /sys/fs/cgroup/cpu type cgroup (ro,nosuid,nodev,noexec,relatime,cpu) cgroup on /sys/fs/cgroup/cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpuacct) cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory) cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices) cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer) cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio) cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event) cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,nosuid,nodev,noexec,relatime,hugetlb) ...
在这里我们不讲解CGroups对CPU和内存的限制都有哪些,只介绍基于Kubernetes编排引擎下的计算资源管理,对容器CGroups都做了哪些支持:
- 当为Pod指定了requests,其中 requests.cpu 会作为
--cpu-shares参数值传递给docker run命令,当一个宿主机上有多个容器发生CPU资源竞争时这个参数就会生效,参数值越大,越容易被分配到CPU,requests.memory 不会作为参数传递给Docker,这个参数在Kubernetes的资源QoS管理时使用; - 当为Pod指定了limits,其中 limits.cpu 会作为
--cpu-quota参数的值传递给docker run命令,docker run命令中另外一个参数--cpu-period默认设置为100000,通过这两个参数限制容器最多能够使用的CPU核数,limits.memory 会作为--memory参数传递给docker run 命令,用来限制容器内存,目前Kubernetes不支持限制Swap大小,建议在部署Kubernetes时候禁用Swap。
Kubernetes 1.10 以后支持为Pod指定固定CPU编号,我们在这里不详细介绍,就以常规的计算资源管理为主,简单讲一下以Kubernetes作为编排引擎,容器的CGroups资源限制情况:
1、读取容器CPU核数
这个值除以100000得到的就是容器核数
~ # cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
400000
2、获取容器内存使用情况(USAGE / LIMIT)
~ # cat /sys/fs/cgroup/memory/memory.usage_in_bytes 4289953792 ~ # cat /sys/fs/cgroup/memory/memory.limit_in_bytes 4294967296
将这两个值相除得到的就是内存使用百分比。
3、获取容器是否被设置了OOM,是否发生过OOM
~ # cat /sys/fs/cgroup/memory/memory.oom_control oom_kill_disable 0 under_oom 0 ~ #
这里需要解释一下:
- oom_kill_disable默认为0,表示打开了oom killer,就是当内存超时会触发kill进程。可以在使用docker run时候指定disable oom,将此值设置为1,关闭oom killer;
- under_oom 这个值仅仅是用来看的,表示当前的CGroups的状态是不是已经oom了,如果是,这个值将显示为1。
4、获取容器磁盘I/O
~ # cat /sys/fs/cgroup/blkio/blkio.throttle.io_service_bytes 253:16 Read 20015124480 253:16 Write 24235769856 253:16 Sync 0 253:16 Async 44250894336 253:16 Total 44250894336 Total 44250894336
5、获取容器虚拟网卡入/出流量
~ # cat /sys/class/net/eth0/statistics/rx_bytes 10167967741 ~ # cat /sys/class/net/eth0/statistics/tx_bytes 15139291335
如果你对从容器中读取CGroups感兴趣,可以了解 docker stats源码实现 。
使用LXCFS
由于习惯性等原因,在容器中使用top、free等命令仍然是一个较为普遍存在的需求,但是容器中的/proc、/sys目录等还是挂载的宿主机目录,有一个开源项目:LXCFS。LXCFS是基于FUSE实现的一套用户态文件系统,使用LXCFS,让你在容器里面继续使用free等命令变成了可能。注意,LXCFS目前只支持为容器生成下面文件:
/proc/cpuinfo /proc/diskstats /proc/meminfo /proc/stat /proc/swaps /proc/uptime
如果命令是通过解析这些文件实现,那么在容器里面可以继续使用,否则只能通过读取CGroups获取资源情况。
总结
容器给大家带来了很多便利,很多公司已经或正在把业务往容器上迁移。在迁移过程中,需要清楚上面介绍的这个问题是不是会影响应用的正常运行,并采取相应的办法绕过这个坑。
这篇文章的分享就到这里,希望对大家有所帮助,欢迎与小米生态云团队交流。
以上所述就是小编给大家介绍的《Docker容器实现原理及容器隔离性踩坑介绍》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Docker 容器实战(六):容器的隔离与限制
- 容器资源可视化隔离的实现方法
- SOFAArk 发布 v1.0.0 版本 | 蚂蚁金服开源类隔离容器
- SOFAArk 发布 v1.0.0 版本 | 蚂蚁金服开源类隔离容器
- MySQL -- RR隔离与RC隔离
- MySQL -- 事务隔离
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Is Parallel Programming Hard, And, If So, What Can You Do About
Paul E. McKenney
The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!