转载

基于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实现的原理可以通过下面这张图。

基于LXCFS增强docker容器隔离性的分析 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文件计算并返回,其调用栈如下:

--lxcfs_read ----proc_read ------proc_meminfo_read  static int proc_meminfo_read(char *buf, size_t size, off_t offset,         struct fuse_file_info *fi) {    //获取fuse上下文的进程pid     struct fuse_context *fc = fuse_get_context();     struct file_info *d = (struct file_info *)fi->fh;     //找到对应进程的对应cgroup分组     nih_local char *cg = get_pid_cgroup(fc->pid, "memory");     nih_local char *memlimit_str = NULL, *memusage_str = NULL, *memstat_str = NULL;     unsigned long memlimit = 0, memusage = 0, cached = 0, hosttotal = 0;  ... if (!cg)         return read_file("/proc/meminfo", buf, size, d);     //从进程cgroup分组获取memory子系统相关信息并计算返回     if (!cgm_get_value("memory", cg, "memory.limit_in_bytes", &memlimit_str))         return 0;     if (!cgm_get_value("memory", cg, "memory.usage_in_bytes", &memusage_str))         return 0;     if (!cgm_get_value("memory", cg, "memory.stat", &memstat_str))         return 0;     memlimit = strtoul(memlimit_str, NULL, 10);     memusage = strtoul(memusage_str, NULL, 10);     memlimit /= 1024;     memusage /= 1024;     get_mem_cached(memstat_str, &cached);  ...  //计算并替换虚拟proc下的meminfo中数值 if (startswith(line, "MemTotal:")) {             sscanf(line+14, "%lu", &hosttotal);             if (hosttotal < memlimit)                 memlimit = hosttotal;             snprintf(lbuf, 100, "MemTotal:       %8lu kB/n", memlimit);             printme = lbuf; ... 

在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) 

并且在/vae/lib/lxcfs/目录下产生两个目录:

  • proc
  • cgroup

将这两个目录通过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/

正文到此结束
Loading...