容器资源隔离CPU与内存

容器技术提供了不同于传统虚拟机技术的资源隔离方式,容器的打包和启动速度得到提高,但是却降低了容器的隔离强度,这里就有一个资源视图的隔离问题,容器可以通过cgroup的方式对资源的使用情况进行限制,包括内存、cpu等,但是如果一些进程使用一些常用的监控命令,例如free top等命令,其实看到的还是物理机的数据,而非容器的数据,这是由于容器并没有做到对/proc,/sys等文件的资源视图隔离,我们知道容器中看到的/proc伪文件系统的信息是宿主的/proc,没有隔离/proc 意味着获取不到容器中进程相关的proc信息。另外,一些需要读取proc信息的应用,会获取到错误的数据。 常用的proc伪文件系统包括:/proc/meminfo,/proc/cpuinfo, /proc/stat, /proc/uptime, /proc/loadavg等,举一个Java开发者常遇到的问题,当我们在启动JVM时,通常需要给容器设置JVM的内存参数,才能正确使用容器内存。

解决资源视图隔离问题的必要性

  1. 从容器的视角来看,通常一些业务开发者已经习惯了传统的物理机、虚拟机,在这些强隔离的宿主机上可以使用top、free等命令查看系统的资源使用情况,但是在容器内无法做到这一点,“越界”查看到了宿主机的信息
  2. 从应用程序的角度,在容器内运行进程和在物理机运行进程,其实运行环境是不同的,并且可能存在一些安全隐患
    2.1 例如java的JVM会使用free 查看内存,并尝试使用这个值用于设置JVM内存大小,这里很容易出现OOM
    2.2 例如golang程序中,使用runtime.NumCPU获得CPU数量,并用这个值启动相应数量的进程

FUSE文件系统

FUSE(用户态文件系统)是一个实现在用户空间的文件系统框架,通过FUSE内核模块的支持,使用者只需要根据fuse提供的接口实现具体的文件操作就可以实现一个文件系统。在fuse出现以前,Linux中的文件系统都是完全实现在内核态,编写一个特定功能的文件系统,不管是代码编写还是调试都不太方便,就算是仅仅在现有传统文件系统上添加一个小小的功能,因为是在内核中实现仍需要做很大的工作量。在用户态文件系统FUSE出现后(2.6内核以后都支持fuse),就会大大的减少工作量,也会很方便的进行调试。编写FUSE文件系统时,只需要内核加载了fuse内核模块即可,不需要重新编译内核。

完整的fuse功能包括a)用户态customize文件系统b)用户态fuse库(libfuse)c)内核支持(fs/fuse/*),共3层结构协作完成。

avatar

以用户态内核态看FUSE

avatar

FUSE的工作原理如上图所示。假设基于FUSE的用户态文件系统hello挂载在/tmp/fuse目录下。当应用层程序要访问/tmp/fuse下的文件时,通过glibc中的函数进行系统调用,处理这些系统调用的VFS中的函数会调用FUSE在内核中的文件系统;内核中的FUSE文件系统将用户的请求,发送给用户态文件系统hello;用户态文件系统收到请求后,进行处理,将结果返回给内核中的FUSE文件系统;最后,内核中的FUSE文件系统将数据返回给用户态程序。

fuse举例

这里引用知乎上的一个文章,如果已经使用fuse开发了一套文件系统,例如叫zfuse,这里首先将zfuse挂载到了/mnt/fuse 目录,如果我们要创建/mnt/fuse/my.log文件,会发生什么呢?

avatar

  1. 此时 open系统调用进入kernel space,vfs层根据挂载点文件的操作函数对应到fuse_create_open,此时已经工作在内核态,但是在fuse_create_open内会创建一个包含FUSE_CREATE操作数的消息
  2. 将FUSE_CREATE的消息通过管道文件发送给管道另一端的用户态接收进程,即我们自定义的zfuse文件系统进行处理,处理这个消息的进程在ZFUSE挂载时由libfuse库代码中创建,作用是读取管道文件消息并根据消息的操作数来执行对应操作,在这里解析到的是FUSE_CREATE操作数,对应libfuse库中的fuse_lib_create函数。
  3. zfuse的作用,libfuse只是将操作接口与内核VFS做到一一对接,而真正完成操作的还是ZFUSE,在fuse_lib_create中会调用ZFUSE内定义的create函数
  4. libfuse接口的作用,解耦合,简化ZFUSE开发。假设ZFUSE不需要lseek操作,那么就不需要实现lseek操作,而我们无法控制用户的行为,这里用户是指ZFUSE文件系统的使用者。假设用户在使用ZFUSE时进行了lseek操作,那么会先由libfuse提供的fuse_lib_lseek接口处理,libfuse发现ZFUSE没有实现lseek,就直接返回不支持此操作。

具体的实现流程

avatar

  1. ZFUSE挂载到/mnt/fuse,libfuse会fork出后台进程,用于读取管道文件消息。
  2. 用户使用ZFUSE文件系统,创建文件my.log
  3. 调用系统调用
  4. 经VFS交由fuse处理
  5. fuse下的create处理,向管道发送带创建操作(FUSE_CREATE)的消息,当前进程A加入等待队列
  6. libfuse下创建的后台进程读取到消息,解析操作数为FUSE_CREATE,对应到fuse_lib_create,即low level层接口。
  7. fuse_lib_create中调用ZFUSE的上层接口zfuse.create,由ZFUSE来实现创建操作
  8. 完成创建后,通过libfuse中的fuse_reply_create向管道发送完成消息从而唤醒之前加入等待队列的进程A
  9. 进程A得到创建成功的消息,系统调用结束,/mnt/fuse/my.log文件创建成功

用libfuse创建FUSE文件系统

比较详细的讲解libfuse使用

LXCFS

LXCFS是一个简单的用户文件系统,用于解决当前linux kernel的一些局限性,让容器更能感觉为是一个独立的系统,使用libfuse库基于C开发完成,主要提供两个方面

  1. 提供一系列可以绑定的文件,从而使cgroup能够感知
  2. 一个容器可以感知的cgroupfs-like tree

lxcfs 是一个开源的fuse文件系统,项目地址,用于让linux 容器更像虚拟机,让容器内的应用在读取内存和 CPU 信息的时候通过 lxcfs 的映射,转到自己的通过对 cgroup 中容器相关定义信息读取的虚拟数据上。将lxcfs的文件挂载到容器内后,容器内的进程在基于/proc 获取容器的CPU和内存时,即读取这些文件时,lxcfs文件系统会获取该容器的1号进程的宿主机ID,将这个读取操作转换为读取宿主机对应容器cgrroup配置文件的信息,从而实现获取正确的容器CPU和内存。

原来lxcfs是LXC容器的一个辅助项目,但是现在可以被任意的runtim使用,lxcfs通过用户态文件系统,在容器内提供以下文件

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

如下图所示,把宿主机的 /var/lib/lxcfs/proc/memoinfo 文件挂载到Docker容器的/proc/meminfo位置后。容器中进程读取相应文件内容时,LXCFS的FUSE实现会从容器对应的Cgroup文件中读取正确的内存限制。从而使得应用获得正确的资源约束设定。

avatar

详细讲解lxcfs,源码分析

LXCFS for Kubernetes

1
2
3
4
5
6
7
8
9
yum install fuse fuse-lib fuse-devel
git clone https://github.com/denverdino/lxcfs-admission-webhook.git
cd lxcfs-admission-webhook
bash deployment/install.sh
kubectl apply -f deployment/lxcfs-daemonset.yaml

设置namespace内开启lxcfs注解的设置,这里一定不要为lxcfs-daemonset所在的命名空间开启,否则导致启动失败
也可以不在namespace设置,只需要在创建的Pod中设置就可以
kubectl label namespace default lxcfs-admission-webhook=enabled

测试效果如下所示

Pod的limit参数配置了8个CPU,24G 内存

avatar

在容器内只能查看到25G内存

avatar

在容器内只能查看到6个CPU

avatar

这种交由k8s进行管理,估计会有些风险,将这个服务交由systemd 进行管理,运行在宿主机上,可能更安全一些。

lxcfs服务重启

lxcfs服务重启,会导致原正常使用的容器无法查看cpu、内存,这是因为 lxcfs重启后,/var/lib/lxcfs会删除再重建,inode变了,会出现如下错误

avatar

avatar

当lxcfs服务重启后,需要对容器挂载的响应目录进行重新挂载,lxcfs issue 列表中对这个问题进行了讨论,issue 193github上有人整理的remount的脚本代码

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
#! /bin/bash

PATH=$PATH:/bin
LXCFS="/var/lib/lxc/lxcfs"
LXCFS_ROOT_PATH="/var/lib/lxc"

containers=$(docker ps | grep -v pause | grep -v calico | awk '{print $1}' | grep -v CONTAINE)

#-v /var/lib/lxc/lxcfs/proc/cpuinfo:/proc/cpuinfo:rw
#-v /var/lib/lxc/lxcfs/proc/diskstats:/proc/diskstats:rw
#-v /var/lib/lxc/lxcfs/proc/meminfo:/proc/meminfo:rw
#-v /var/lib/lxc/lxcfs/proc/stat:/proc/stat:rw
#-v /var/lib/lxc/lxcfs/proc/swaps:/proc/swaps:rw
#-v /var/lib/lxc/lxcfs/proc/uptime:/proc/uptime:rw
#-v /var/lib/lxc/lxcfs/proc/loadavg:/proc/loadavg:rw
#-v /var/lib/lxc/lxcfs/sys/devices/system/cpu/online:/sys/devices/system/cpu/online:rw
for container in $containers;do
mountpoint=$(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/lib/lxc" }}{{ .Source }}{{ end }}{{ end }}' $container)
if [ "$mountpoint" = "$LXCFS_ROOT_PATH" ];then
echo "remount $container"
PID=$(docker inspect --format '{{.State.Pid}}' $container)
# mount /proc
for file in meminfo cpuinfo loadavg stat diskstats swaps uptime;do
echo nsenter --target $PID --mount -- mount -B "$LXCFS/proc/$file" "/proc/$file"
nsenter --target $PID --mount -- mount -B "$LXCFS/proc/$file" "/proc/$file"
done
# mount /sys
for file in online;do
echo nsenter --target $PID --mount -- mount -B "$LXCFS/sys/devices/system/cpu/$file" "/sys/devices/system/cpu/$file"
nsenter --target $PID --mount -- mount -B "$LXCFS/sys/devices/system/cpu/$file" "/sys/devices/system/cpu/$file"
done
fi
done

但是仍然存在一个问题,如何感知到lxcfs什么时候重启,因为只有知道了lxcfs进行了重启,我们才能对容器进行remount操作,这里有提供一个(方案)[https://github.com/alibaba/pouch/issues/140]

  1. 将以下卷挂载到容器,除了挂载proc相关的卷,这里增加了/var/lib/lxc/:/var/lib/lxc/:shared
1
2
3
4
5
6
7
-v /var/lib/lxc/:/var/lib/lxc/:shared  \
-v /var/lib/lxc/lxcfs/proc/uptime:/proc/uptime \
-v /var/lib/lxc/lxcfs/proc/swaps:/proc/swaps \
-v /var/lib/lxc/lxcfs/proc/stat:/proc/stat \
-v /var/lib/lxc/lxcfs/proc/diskstats:/proc/diskstats \
-v /var/lib/lxc/lxcfs/proc/meminfo:/proc/meminfo \
-v /var/lib/lxc/lxcfs/proc/cpuinfo:/proc/cpuinfo

如果是k8s pod 时,建议以以下的方式进行挂载
avatar

avatar

  1. 使用systemd 管理lxcfs服务,在service 文件中增加重启或者服务恢复时的重新remount操作
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
首先下载lxcfs的源码,编译安装
yum install fuse fuse-lib fuse-devel
yum -y install fuse-devel pam-devel wget install gcc automake autoconf libtool make
需要使用3.1.2 版本
git clone git://github.com/lxc/lxcfs
cd lxcfs
./bootstrap.sh
./configure
make
make install

cat /etc/systemd/system/multi-user.target.wants/lxcfs.service

[Unit]
Description=FUSE filesystem for LXC
ConditionVirtualization=!container
Before=lxc.service
Documentation=man:lxcfs(1)

[Service]
ExecStart=/usr/bin/lxcfs /var/lib/lxc/lxcfs/
KillMode=process
Restart= always
ExecStopPost=-/bin/fusermount -u /var/lib/lxc/lxcfs
Delegate=yes

# add remount script
ExecStartPost=/usr/local/bin/container_remount_lxcfs.sh

[Install]
WantedBy=multi-user.target
`
  1. 当lxcfs服务重启时,会调用/usr/local/bin/container_remount_lxcfs.sh 脚本对全部容器进行重新remount