1. 程式人生 > >基於LXCFS增強docker容器隔離性的分析

基於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/