Docker 之十三资源隔离与资源限制介绍

虚拟机与容器底层实现的对比

vm-vs-docker

虚拟机与容器的底层实现原理是不同的,正如上图片的对比。虚拟机实现资源隔离的方法是利用一个独立的 Guest OS,并利用 Hypervisor 虚拟化 CPU、内存、IO 设备等实现的。例如,为了虚拟化内存,Hypervisor 会创建一个 shadow page table,正常情况下,一个 page table 可以用来实现从虚拟内存到物理内存的翻译。相比虚拟机实现资源和环境隔离的方案,Docker 就显得简练很多,它不像虚拟机一样重新加载一个操作系统内核,引导、加载操作系统内核是一个比较耗时而又消耗资源的过程,Docker 是利用 Linux 内核特性(LXC)实现的隔离,运行容器的速度几乎等同于直接启动进程。

Linux Namespace 的六大类型

linux-six-namespace

Docker 资源隔离与资源限制的实现原理

  1. 使用 Namespace 实现了系统环境的隔离, Namespace 允许一个进程以及它的子进程从共享的宿主机内核资源(Uts、Ipc、、Network、Pid、Mount、User 等)里获得一个仅自己可见的隔离区域,让同一个 Namespace 下的所有进程感知彼此变化,对外界进程一无所知,仿佛运行在一个独占的操作系统中。
  2. 使用 CGroups 限制这个环境的资源使用情况,比如一台 16 核 32GB 的机器上只让容器使用 2 核 4GB。使用 CGroups 还可以为资源设置权重,计算使用量,操控任务(进程或线程)启停等。
  3. 使用镜像管理功能,利用 Docker 的镜像分层、写时复制、内容寻址、联合挂载技术实现了一套完整的容器文件系统及运行环境,再结合镜像仓库,镜像可以快速下载和共享,方便在多环境部署。

Docker 隔离性的陷阱

  1. Docker 是利用 CGroups 实现资源限制的,只能限制资源消耗的最大值,而不能隔绝其他程序占用自己的资源。
  2. Namespace 的 6 项隔离看似完整,实际上依旧没有完全隔离 Linux 资源,比如 /proc 、/sys 、/dev/sd * 等目录未完全隔离,SELinux、time、syslog 等所有现有 Namespace 之外的信息都未隔离。
  3. 由于 /proc 、/sys 、/dev/sd * 等目录未完全隔离,如果在 Docker 容器中执行 top、free 等命令,会发现看到的资源使用情况都是宿主机的资源情况,而很多情况下需要知道的是这个容器被限制了多少 CPU、内存、当前容器内的进程使用了多少。这就导致程序运行在容器里面,调用 API 获取系统内存、CPU,取到的是宿主机的资源大小。同时对于多进程程序,一般都可以将 worker 数量设置成 auto,自适应系统 CPU 核数,但在容器里面这么设置,取到的 CPU 核数是不正确的,例如 Nginx,其他应用取到的可能也不正确,需要进一步测试。

Docker 隔离性陷阱原因分析

当启动一个容器时候,Docker 会调用 libcontainer 实现对容器的具体管理,包括创建 Uts、Ipc、、Net、Pid、Mount、User 等 Namespace 实现容器之间的隔离和利用 CGroups 实现对容器的资源限制。在其中,Docker 会将宿主机一些目录以只读方式挂载到容器中,其中包括 /proc、/dev、/dev/shm、/sys 目录,同时还会建立以下几个链接,保证系统 IO 不会出现问题,这也是为什么在容器里面取到的是宿主机资源原因。

1
2
3
4
/proc/self/fd->/dev/fd
/proc/self/fd/0->/dev/stdin
/proc/self/fd/1->/dev/stdout
/proc/self/fd/2->/dev/stderr

Docker 容器内,JVM 堆内存陷阱

JVM 默认的最大 Heap 大小是系统内存的 1/4,假若物理机内存为 10G,如果你不手动指定 Heap 大小,则 JVM 默认 Heap 大小就为 2.5G。JavaSE8 (<8u131) 版本前还没有针对在容器内执行高度受限的 Linux 进程进行优化,JDK1.9 以后开始正式支持容器环境中的 CGroups 内存限制,JDK1.10 这个功能已经默认开启,可以查看相关 Issue。熟悉 JVM 内存结构的人都清楚,JVM Heap 是一个只增不减的内存模型,Heap 的内存只会往上涨,不会下降。在容器里面使用 Java,如果为 JVM 未设置 Heap 大小,Heap 取得的是宿主机的内存大小,当 Heap 的大小达到容器内存大小时候,就会触发系统对容器 OOM,Java 进程会异常退出。常见的系统日志打印如下:

1
2
3
4
5
6
7
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 ...

Docker 容器内,手动设置 Java 应用的堆内存大小

1
2
3
4
5
# 对于JavaSE8(<8u131)版本,可以在创佳并启动容器的时候,通过环境变量传参确切限制最大Heap大小
# docker run -d -m 800M -e JAVA_OPTIONS='-Xmx300m' openjdk:8-jdk-alpine

# 对于JavaSE8(>8u131)版本,可以使用上面手动指定最大堆大小,也可以使用下面办法,设置自适应容器内存限制
# docker run -d -m 800M -e JAVA_OPTIONS='-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1' openjdk:8-jdk-alpine

从 CGroups 中正确读取容器资源信息

Docker 在 1.8 版本以后会将分配给容器的 CGroups 信息挂载进容器内部,容器里面的程序可以通过解析 CGroups 信息获取到容器资源信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 查看容器内的挂载记录
# cat /etc/mtab
cgroup /sys/fs/cgroup/systemd cgroup ro,seclabel,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd 0 0
cgroup /sys/fs/cgroup/net_cls,net_prio cgroup ro,seclabel,nosuid,nodev,noexec,relatime,net_prio,net_cls 0 0
cgroup /sys/fs/cgroup/cpuset cgroup ro,seclabel,nosuid,nodev,noexec,relatime,cpuset 0 0
cgroup /sys/fs/cgroup/cpu,cpuacct cgroup ro,seclabel,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0
cgroup /sys/fs/cgroup/perf_event cgroup ro,seclabel,nosuid,nodev,noexec,relatime,perf_event 0 0
cgroup /sys/fs/cgroup/memory cgroup ro,seclabel,nosuid,nodev,noexec,relatime,memory 0 0
cgroup /sys/fs/cgroup/hugetlb cgroup ro,seclabel,nosuid,nodev,noexec,relatime,hugetlb 0 0
cgroup /sys/fs/cgroup/freezer cgroup ro,seclabel,nosuid,nodev,noexec,relatime,freezer 0 0
cgroup /sys/fs/cgroup/blkio cgroup ro,seclabel,nosuid,nodev,noexec,relatime,blkio 0 0
cgroup /sys/fs/cgroup/pids cgroup ro,seclabel,nosuid,nodev,noexec,relatime,pids 0 0
cgroup /sys/fs/cgroup/devices cgroup ro,seclabel,nosuid,nodev,noexec,relatime,devices 0 0

# 获取已使用内存的大小(USAGE)
# cat /sys/fs/cgroup/memory/memory.usage_in_bytes
4289953792

# 获取内存限制的大小(LIMIT)
# cat /sys/fs/cgroup/memory/memory.limit_in_bytes
4294967296

# 获取磁盘I/O状况
# cat /sys/fs/cgroup/blkio/blkio.throttle.io_service_bytes
7:0 Read 12288
7:0 Write 0
7:0 Sync 0
7:0 Async 12288
7:0 Total 12288
......

# 获取容器虚拟网卡入口流量
# cat /sys/class/net/eth0/statistics/rx_bytes
10167967741

# 获取容器虚拟网卡出口流量
# cat /sys/class/net/eth0/statistics/tx_bytes
15139291335

# 获取容器内是否被设置了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。

LXCFS 使用

由于习惯性等原因,在容器中使用 top、free 等命令仍然是一个较为普遍存在的需求,但是容器中的 /proc、/sys 目录等还是挂载的宿主机目录。开源项目 LXCFS 是基于 FUSE 实现的一套用户态文件系统,使用 LXCFS 可以实现在容器内继续使用 free 等命令。但是 LXCFS 目前只支持为容器生成下面列表中的文件,如果命令是通过解析这些文件实现的,那么在容器里面可以继续使用,否则只能通过读取 CGroups 获取资源情况。如果对从容器中如何读取 CGroups 信息感兴趣,可以了解 docker stats 的源码实现。

1
2
3
4
5
6
/proc/cpuinfo
/proc/diskstats
/proc/meminfo
/proc/stat
/proc/swaps
/proc/uptime

Docker 资源限制配置

https://blog.csdn.net/candcplusplus/article/details/53728507