容器虚拟化带来轻量高效,快速部署的同时,也因其隔离性不够彻底,给用户带来一定程度的使用不便。Docker容器由于Linux内核namespace本身还不够完善的现状(例如,没有cgroup namespace,user namespace也是在kernel高版本才开始支持, /dev设备不隔离等),因此docker容器在隔离性方面也存在一些缺陷。例如,在容器内部proc文件系统中可以看到Host宿主机上的proc信息(如:meminfo, cpuinfo,stat, uptime等)。本文基于开源项目LXCFS,尝试通过用户态文件系统实现docker容器内的虚拟proc文件系统,增强docker容器的隔离性,减少给用户带来的不便。
2.1 LXCFS介绍
LXCFS是一个开源的FUSE用户态文件系统,起初是为了更好的在Ubuntu上使用LXC容器而设计开发。LXCFS基于C语言开发,代码托管在github上,目前较多使用在LXC容器中。主要由Ubuntu的工程师提供维护和开发。
LXCFS主要提供两个基本功能点:
即在容器内部提供一个虚拟的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入口函数进行初始化,大致步骤:
创建运行时工作目录/run/lxcfs/controllers/
将tmpfs文件系统挂载在/run/lxcfs/controllers/
检查当前系统已挂载的所有cgroup子系统
将当前系统各个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文件计算并返回,其调用栈如下:
--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用户态文件系统实现。
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/目录下产生两个目录:
将这两个目录通过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文件系统。
本文通过分析容器内部/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/