基于LXCFS增强docker容器隔离性的分析

1. 背景

容器虚拟化带来轻量高效,快速部署的同时,也因其隔离性不够彻底,给用户带来一定程度的使用不便。Docker容器由于Linux内核namespace本身还不够完善的现状(例如,没有cgroup namespace,user namespace也是在kernel高版本才开始支持, /dev设备不隔离等),因此docker容器在隔离性方面也存在一些缺陷。例如,在容器内部proc文件系统中可以看到Host宿主机上的proc信息(如:meminfo, cpuinfo,stat, uptime等)。本文基于开源项目LXCFS,尝试通过用户态文件系统实现docker容器内的虚拟proc文件系统,增强docker容器的隔离性,减少给用户带来的不便。

2. LXCFS简介及原理

2.1 LXCFS介绍

LXCFS是一个开源的FUSE用户态文件系统,起初是为了更好的在Ubuntu上使用LXC容器而设计开发。LXCFS基于C语言开发,代码托管在github上,目前较多使用在LXC容器中。主要由Ubuntu的工程师提供维护和开发。

LXCFS主要提供两个基本功能点:

  • a cgroupfs compatible view for unprivileged containers
  • a set of cgroup-aware files: 
    /proc/meminfo 
    /proc/cpuinfo 
    /proc/stat 
    /proc/uptime
即在容器内部提供一个虚拟的proc文件系统和容器自身的cgroup的目录树。

2.2 LXCFS实现原理

LXCFS是基于FUSE实现而成的一套用户态文件系统,和其他文件系统最本质的区别在于,文件系统通过用户态程序和内核FUSE模块交互完成。Linux内核从2.6.14版本开始通过FUSE模块支持在用户空间实现文件系统。通过LXCFS的源码可以看到,LXCFS主要通过调用底层fuse的lib库libfuse和内核模块fuse交互实现成一个用户态的文件系统。此外,LXCFS涉及到对cgroup文件系统的管理则是通过cgmanager用户态程序实现(为了提升性能,从0.11版本开始,LXCFS自身实现了cgfs用以替换第三方的cgroup manager,目前已经合入upstream)。

2.2.1 LXCFS主进程

LXCFS实现的main函数非常简单。在其main函数中可以看到,运行lxcfs时,首先会通过cgfs_setup_controllers入口函数进行初始化,大致步骤:

  1. 创建运行时工作目录/run/lxcfs/controllers/

  2. 将tmpfs文件系统挂载在/run/lxcfs/controllers/

  3. 检查当前系统已挂载的所有cgroup子系统

  4. 将当前系统各个cgroup子系统重新挂载在/run/lxcfs/controllers/目录下然后调用libfuse库主函数fuse_main,指定一个用户态文件系统挂载的目标目录(例如:/var/lib/lxcfs/),并传递如下参数:

    -s is required to turn off multi-threading as libnih-dbus isn't thread safe.

    -f is to keep lxcfs running in the foreground

    -o allow_other is required to have non-root user be able to access the filesystem

进入到libfuse之后,就是在fuse_loop()中接受内核态FUSE的请求。接下来就是频繁的完成用户态文件系统和内核FUSE交互,完成用户态文件系统操作。LXCFS文件系统具备编码实现可见struct fuse_operations定义的ops函数:

const struct fuse_operations lxcfs_ops = {
    .getattr = lxcfs_getattr,
    .readlink = NULL,
    .getdir = NULL,
    .mknod = NULL,
    .mkdir = lxcfs_mkdir,
    .unlink = NULL,
    .rmdir = lxcfs_rmdir,
    .symlink = NULL,
    .rename = NULL,
    .link = NULL,
    .chmod = lxcfs_chmod,
    .chown = lxcfs_chown,
    .truncate = lxcfs_truncate,
    .utime = NULL,

    .open = lxcfs_open,
    .read = lxcfs_read,
    .release = lxcfs_release,
    .write = lxcfs_write,

    .statfs = NULL,
    .flush = lxcfs_flush,
    .fsync = lxcfs_fsync,

    .setxattr = NULL,
    .getxattr = NULL,
    .listxattr = NULL,
    .removexattr = NULL,

    .opendir = lxcfs_opendir,
    .readdir = lxcfs_readdir,
    .releasedir = lxcfs_releasedir,

    .fsyncdir = NULL,
    .init = NULL,
    .destroy = NULL,
    .access = NULL,
    .create = NULL,
    .ftruncate = NULL,
    .fgetattr = NULL,
};

其中lxcfs_opendir等就是用户态文件系统的具体实现。

2.2.2 Fuse介绍

Fuse是指在用户态实现的文件系统,是文件系统完全在用户态的一种实现方式。目前Linux通过内核模块FUSE进行支持。libfuse是用户空间的fuse库,可以被非特权用户访问。通常对文件系统内的文件进行操作,首先会通过内核VFS接口,转发至各个具体文件系统实现操作最终返回用户态。这里同样,如果是FUSE文件系统,VFS调用FUSE接口,FUSE最终将操作请求返回给用户态文件系统实现具体的操作。关于FUSE实现的原理可以通过下面这张图。



3. docker容器虚拟proc文件系统实现

Linux系统proc文件系统是一个特殊的文件系统,通常存储当前系统内核运行状态的一系列特殊文件,包括系统进程信息,系统资源消耗信息,系统中断信息等。这里以meminfo内存相关统计信息为例,讲诉如何通过LXCFS用户态文件系统实现docker容器内的虚拟proc文件系统。

3.1 挂载虚拟proc文件系统到docker容器

通过 docker --volumns /var/lib/lxcfs/proc/:/docker/proc/ ,将宿主机上/var/lib/lxcfs/proc/挂载到docker容器内部的虚拟proc文件系统目录下/docker/proc/。此时在容器内部/docker/proc/目录下可以看到,一些列proc文件,其中包括meminfo。


3.2 cat /proc/meminfo

用户在容器内读取/proc/meminfo时,实际上是读取宿主机上的/var/lib/lxcfs/proc/meminfo挂载到容器内部的meminfo文件,fuse文件系统将读取meminfo的进程pid传给lxcfs, lxcfs通过get_pid_cgroup获取读取meminfo的进程所属的cgroup分组。在host的/cgroup目录下找到对应进程的cgroup子系统信息,并通过系统调用再次进入内核文件系统读取/cgroup/memory/PID/meminfo最终返回。

以上过程FUSE内核态最终通过用户态LXCFS实现。在LXCFS用户态文件系统中,执行读取meminfo的操纵在proc_meminfo_read实现。

从FUSE内核态返回到用户态libfuse并最终进去LXCFS用户态其调用栈如下:

Breakpoint 3, lxcfs_read (path=0x7ffff0000af0 “/proc/meminfo”, buf=0x7ffff0000c50 “\004”, size=65536, offset=0, fi=0x7ffff73bbcf0) at lxcfs.c:2834
2834    {
(gdb) bt
#0  lxcfs_read (path=0x7ffff0000af0 “/proc/meminfo”, buf=0x7ffff0000c50 “\004”, size=65536, offset=0, fi=0x7ffff73bbcf0) at lxcfs.c:2834
#1  0x00007ffff7bab597 in fuse_fs_read_buf () from /lib64/libfuse.so.2
#2  0x00007ffff7bab772 in fuse_lib_read () from /lib64/libfuse.so.2
#3  0x00007ffff7bb414e in do_read () from /lib64/libfuse.so.2
#4  0x00007ffff7bb4beb in fuse_ll_process_buf () from /lib64/libfuse.so.2
#5  0x00007ffff7bb1481 in fuse_do_work () from /lib64/libfuse.so.2
#6  0x00007ffff7989df5 in start_thread () from /lib64/libpthread.so.0
#7  0x00007ffff76b71ad in clone () from /lib64/libc.so.6

在LXCFS用户态文件系统中,最终会通过proc_meminfo_read读取/var/lib/lxcfs/proc/meminfo文件计算并返回,其调用栈如下:

在fuse文件系统上下文,找到当前进程所属哪个容器,最终在容器对应的cgroup分组中获取/cgroup/memmory/containerID/memory.limitinbytes, memory.usageinbytes和memory.stat,然后将/var/lib/lxcfs/proc/meminfo中的信息替换成容器对应cgroup分组中的资源信息,这样使容器看到的是自己的meminfo信息。这个文件的读取,写入就是通过fuse用户态文件系统实现。

4. 实现测试

centos7 + docker v1.7.1 + lxcfs 0.11

通过以下命令行启动lxcfs:

lxcfs -s -f -o allow_other /var/lib/lxcfs启动成功后,查看宿主机上mount信息:

 
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)
gvfsd-fuse on /run/user/1000/gvfs type fuse.gvfsd-fuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)
/dev/sr0 on /run/media/meifeng/CentOS 7 x86_64 type iso9660 (ro,nosuid,nodev,relatime,uid=1000,gid=1000,iocharset=utf8,mode=0400,dmode=0500,uhelper=udisks2)
/dev/sdb1 on /home/sdb type ext4 (rw,relatime,seclabel,data=ordered)
binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,relatime)
tmpfs on /run/lxcfs/controllers type tmpfs (rw,relatime,seclabel,size=100k,mode=700)
name=systemd on /run/lxcfs/controllers/name=systemd type cgroup (rw,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cpuset on /run/lxcfs/controllers/cpuset type cgroup (rw,relatime,cpuset)
cpuacct,cpu on /run/lxcfs/controllers/cpuacct,cpu type cgroup (rw,relatime,cpuacct,cpu)
memory on /run/lxcfs/controllers/memory type cgroup (rw,relatime,memory)
devices on /run/lxcfs/controllers/devices type cgroup (rw,relatime,devices)
freezer on /run/lxcfs/controllers/freezer type cgroup (rw,relatime,freezer)
net_cls on /run/lxcfs/controllers/net_cls type cgroup (rw,relatime,net_cls)
blkio on /run/lxcfs/controllers/blkio type cgroup (rw,relatime,blkio)
perf_event on /run/lxcfs/controllers/perf_event type cgroup (rw,relatime,perf_event)
hugetlb on /run/lxcfs/controllers/hugetlb type cgroup (rw,relatime,hugetlb)
lxcfs on /var/lib/lxcfs type fuse.lxcfs (rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other)

将这两个目录通过docker -v的形式挂载到容器内部:

docker run -ti -d —privileged  -v /var/lib/lxcfs/cgroup/:/docker/cgroup/:rw -v /var/lib/lxcfs/proc/:/docker/proc/:rw ubuntu:14.04

注意: docker 1.7.1版本不支持将外部目录再挂载到容器内部的/proc目录下,因此这里将/proc挂载在/docker/proc/下,实现容器虚拟proc文件系统。

5. 总结

本文通过分析容器内部/docker/proc/memnifo的具体实现,讲述了如何借助FUSE用户态文件系统在容器内部实现虚拟proc文件系统,以弥补docker容器因隔离性不够完善造成的使用不便。尽管lxcfs用户态文件系统实现的功能比较有限,但是借助用户态文件系统从容器对应的cgroup分组获取容器本身的资源信息,最终反馈在虚拟proc文件系统或者反馈给容器内部的监控agent,也不失为一种不错的研究方向。

参考:

https://github.com/lxc/lxcfs

https://linuxcontainers.org/lxcfs/getting-started/ https://insights.ubuntu.com/2015/03/02/introducing-lxcfs/ http://manpag.es/ubuntu1410/8+cgmanager http://fuse.sourceforge.net/ http://www.cnblogs.com/wzh206/archive/2010/05/13/1734901.html

https://linuxcontainers.org/lxcfs/


阅读更多
换一批

没有更多推荐了,返回首页