• Containers from Scratch从零开始实现容器


    转载自 ericchiang-Containers from Scratch

    2017/01/07,这篇文章是为了我在 CAT BarCmp 的演讲而写,演讲的起由是我的自我挑战——“在没有Docker或rkt的情况下介绍Docker”

    容器(containers)通常被认为是廉价的虚拟机,仅仅是单个主机(host)上的隔离的进程组。这种隔离利用了linux内核中内置的几种底层技术:namespaces、cgroups、chroots以及许多你之前可能已经听过的术语。

    所以,让我们玩得开心,并使用这些底层技术构建我们自己的容器。

    Container file systems

    容器镜像(images),即你从互联网上下载下来的东西,实际上只是压缩文件(tarball),容器中最不神奇的就是你可以与之交互的文件。

    这篇文章里,我通过剥离Docker image构建了一个简单的tarball,它看起来像Debian文件系统,并将成为隔离进程的游乐场(playground)。

    $ wget https://github.com/ericchiang/containers-from-scratch/releases/download/v0.1.0/rootfs.tar.gz
    $ sha256sum rootfs.tar.gz 
    c79bfb46b9cf842055761a49161831aee8f4e667ad9e84ab57ab324a49bc828c  rootfs.tar.gz
    

    首先,解压文件并查看

    $ # tar needs sudo to create /dev files and setup file ownership
    $ sudo tar -zxf rootfs.tar.gz
    $ ls rootfs
    bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
    boot  etc  lib   media  opt  root  sbin  sys  usr
    $ ls -al rootfs/bin/ls
    -rwxr-xr-x. 1 root root 118280 Mar 14  2015 rootfs/bin/ls
    
    

    生成的目录看起来非常像linux系统,一个bin目录包含可执行文件,一个etc目录包含系统配置,一个lib目录包含共享库,等等。

    实际上构建这个tarball是一个非常有趣的话题,但我们将略过这里,总而言之,我非常推荐我同事Brian Redbeard的精彩演讲"Minimal Containers".

    chroot

    我们将使用的第一个工具是chroot,它是一个有类似名字的syscall的轻薄包装器,可用于限制一个进程的文件系统视图。在本例中,我们将把进程限制在"rootfs"目录中,然后执行一个shell

    一旦我们到了这里,我们就可以到处乱逛(poke around),运行命令,做一些经典的shell所做的事情

    $ sudo chroot rootfs /bin/bash
    root@localhost:/# ls /
    bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
    boot  etc  lib   media  opt  root  sbin  sys  usr
    root@localhost:/# which python
    /usr/bin/python
    root@localhost:/# /usr/local/bin/python -c 'print "Hello, container world!"'
    Hello, container world!
    root@localhost:/# 
    

    值得注意的是,这是因为所有的东西都拷贝进了tarball。当我们执行Python解释器,我们将执行 rootfs/usr/local/bin/python,而不是宿主机的Python。该解释器依赖于特意打包进归档(archive)中的共享库(shared libraries)和设备文件(device files)。

    我们还可以在chroot中运行应用程序,不只是shell,例如开启一个文件服务器

    $ sudo chroot rootfs python -m SimpleHTTPServer
    Serving HTTP on 0.0.0.0 port 8000 ...
    

    Creating namespaces with unshare

    怎么隔离这个 chrooted process,让我们在宿主机的另外一个终端运行一个命令:

    $ # outside of the chroot
    $ top
    

    不出所料,我们可以在chroot内部看到宿主机的 top 系统调用

    $ sudo chroot rootfs /bin/bash
    root@localhost:/# mount -t proc proc /proc
    root@localhost:/# ps aux | grep top
    1000     24753  0.1  0.0 156636  4404 ?        S+   22:28   0:00 top
    root     24764  0.0  0.0  11132   948 ?        S+   22:29   0:00 grep top
    

    更有甚者,我们的 chroot是以root运行的,因此它可以杀死 top 进程,xs

    root@localhost:/# pkill top
    

    就这种隔离程度??

    是时候谈到namespaces了,namespace允许我们创建受限的系统视图,包括进程树、网络接口、挂载等

    创建namespace非常简单,只需要带有一个参数的系统调用 unshare,一个同名的命令行工具unshare 为我们很好的包装了这个系统调用,让我们可以手动设置namespace。在本例中,我们将为shell创建一个 PID namespace,然后像上面一样执行chroot。

    $ sudo unshare -p -f --mount-proc=$PWD/rootfs/proc \
        chroot rootfs /bin/bash
    root@localhost:/# ps aux
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root         1  0.0  0.0  20268  3240 ?        S    22:34   0:00 /bin/bash
    root         2  0.0  0.0  17504  2096 ?        R+   22:34   0:00 ps aux
    root@localhost:/#
    

    我们可以发现,我们的shell认为自己的PID是1,且不再能看到主机的进程树了。

    Entering namespaces with nsenter

    namespaces 一个强大的地方在于其可组合性,进程可以选择隔离一些namespace但是共享其他namespace。例如,两个进程可以具有独立的PID namespace,但共享同一个network namespace(例如Kubernetes Pods),这就引出了 setns 系统调用和nsenter命令行工具。

    首先,让我们找到上一个例子中,运行在chroot中的shell

    $ # From the host, not the chroot.
    $ ps aux | grep /bin/bash | grep root
    ...
    root     29840  0.0  0.0  20272  3064 pts/5    S+   17:25   0:00 /bin/bash
    

    内核将namespace以文件的形式暴露到 /proc/(PID)/ns 目录下。在本例中,/proc/29840/ns/pid 是shell进程的命名空间

    $ sudo ls -l /proc/29840/ns
    total 0
    lrwxrwxrwx. 1 root root 0 Oct 15 17:31 ipc -> 'ipc:[4026531839]'
    lrwxrwxrwx. 1 root root 0 Oct 15 17:31 mnt -> 'mnt:[4026532434]'
    lrwxrwxrwx. 1 root root 0 Oct 15 17:31 net -> 'net:[4026531969]'
    lrwxrwxrwx. 1 root root 0 Oct 15 17:31 pid -> 'pid:[4026532446]'
    lrwxrwxrwx. 1 root root 0 Oct 15 17:31 user -> 'user:[4026531837]'
    lrwxrwxrwx. 1 root root 0 Oct 15 17:31 uts -> 'uts:[4026531838]'
    
    

    nsenter 命令行工具是 setns 系统调用的包装器,我们将提供namespace文件,然后运行 unshare 重新挂载 /proc 、运行chroot 重新设置chroot。这次,我们的shell不是创建一个新的namespace,而是连接一个已经存在的。

    $ sudo nsenter --pid=/proc/29840/ns/pid \
        unshare -f --mount-proc=$PWD/rootfs/proc \
        chroot rootfs /bin/bash
    root@localhost:/# ps aux
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root         1  0.0  0.0  20272  3064 ?        S+   00:25   0:00 /bin/bash
    root         5  0.0  0.0  20276  3248 ?        S    00:29   0:00 /bin/bash
    root         6  0.0  0.0  17504  1984 ?        R+   00:30   0:00 ps aux
    

    我们已经成功的进入了namespace,当我们在第二个shell(PID=5)中执行 ps 的时候可以看到第一个shell(PID=1),这是因为两个shell共用的同一个PID namespace,但是都是与宿主机隔离的啦

    Getting around chroot with mounts

    当部署一个不可变的(immutable)的容器时,添加一个文件或目录进入chroot有时很重要,为了存储或者配合。例如,我们在宿主机上创建一些文件,然后通过 mount 以只读的方式暴露给chrooted shell.

    首先,在宿主机上创建一个文件,它将以只读的方式挂载到chroot

    $ sudo mkdir readonlyfiles
    $ echo "hello" > readonlyfiles/hi.txt
    

    接下来,在我们的容器中创建一个目标文件夹,然后使用 -o ro 参数挂载以使其只读。如果你从不了解mount,你可以将其视为一个符号链接

    $ sudo mkdir -p rootfs/var/readonlyfiles
    $ sudo mount --bind -o ro $PWD/readonlyfiles $PWD/rootfs/var/readonlyfiles
    

    现在chrooted process可以看到挂载进来的文件了

     $ sudo chroot rootfs /bin/bash
    root@localhost:/# cat /var/readonlyfiles/hi.txt
    hello
    

    但是不能写入

    root@localhost:/# echo "bye" > /var/readonlyfiles/hi.txt
    bash: /var/readonlyfiles/hi.txt: Read-only file system
    

    符合我们的预期!! 尽管这是一个非常简单的例子,但是很容易扩展到NFS或基于内存的文件系统,只需要切换mount的参数

    使用 unmount 可以移除挂载(rm 不生效)

    $ sudo umount $PWD/rootfs/var/readonlyfiles
    

    cgroups

    cgroups,是控制组(control groups)的简写,允许内核对内存和CPU等资源进行强制隔离。毕竟,隔离进程有什么意义呢?它们仍可以通过占用内存杀死邻居。

    内核在 /sys/fs/cgroup 目录中暴露 cgroups,如果你的机器上没有,你需要挂载一个cgroup 才能进行接下来的操作。

    $ ls /sys/fs/cgroup/
    blkio  cpuacct      cpuset   freezer  memory   net_cls,net_prio  perf_event  systemd
    cpu    cpu,cpuacct  devices  hugetlb  net_cls  net_prio          pids
    

    在这个例子中,我们将创建一个cgroup来限制进程的内存。创建一个cgroup非常简单,只需要创建一个目录。我们将创建一个名为“demo”的 memory cgroup,一旦创建,内核就会自动用cgroup配置文件自动填充这个目录。

    $ sudo su
    # mkdir /sys/fs/cgroup/memory/demo
    # ls /sys/fs/cgroup/memory/demo/
    cgroup.clone_children               memory.memsw.failcnt
    cgroup.event_control                memory.memsw.limit_in_bytes
    cgroup.procs                        memory.memsw.max_usage_in_bytes
    memory.failcnt                      memory.memsw.usage_in_bytes
    memory.force_empty                  memory.move_charge_at_immigrate
    memory.kmem.failcnt                 memory.numa_stat
    memory.kmem.limit_in_bytes          memory.oom_control
    memory.kmem.max_usage_in_bytes      memory.pressure_level
    memory.kmem.slabinfo                memory.soft_limit_in_bytes
    memory.kmem.tcp.failcnt             memory.stat
    memory.kmem.tcp.limit_in_bytes      memory.swappiness
    memory.kmem.tcp.max_usage_in_bytes  memory.usage_in_bytes
    memory.kmem.tcp.usage_in_bytes      memory.use_hierarchy
    memory.kmem.usage_in_bytes          notify_on_release
    memory.limit_in_bytes               tasks
    memory.max_usage_in_bytes
    

    如果要调整,我们只需要向相应的文件写入值,例如我们限制cgroup只有100M内存和关闭swap

    # echo "100000000" > /sys/fs/cgroup/memory/demo/memory.limit_in_bytes
    # echo "0" > /sys/fs/cgroup/memory/demo/memory.swappiness
    

    task 文件是特殊的,它包含分配给这个cgroup的进程列表。将我们自己的PID添加到cgroup中。

    # echo $$ > /sys/fs/cgroup/memory/demo/tasks
    

    最后,我们写一个消耗内存的程序

    f = open("/dev/urandom", "r")
    data = ""
    
    i=0
    while True:
        data += f.read(10000000) # 10mb
        i += 1
        print "%dmb" % (i*10,)
    

    如果cgroup正确发挥作用,这个程序将不会使你电脑奔溃

    # python hungry.py
    10mb
    20mb
    30mb
    40mb
    50mb
    60mb
    70mb
    80mb
    Killed
    

    cgroups不能被移除直到 tasks 文件中的每个进程已经退出或者被再次赋给其他cgroup。退出shell并使用 rmdir 删除目录(不要使用 rm -r

    # exit
    exit
    $ sudo rmdir /sys/fs/cgroup/memory/demo
    

    Container security and capabilities

    容器是一种用root直接运行从互联网上下载的任意代码的有效方式,这是因为容器只会带来较低的开销。容器比VM更容易被打破,因此,许多用于提高容器安全性的技术(如SELinux、seccomp)和能力都涉及到限制以root的身份运行的进程的能力。

    这部分我们将探索 Linux capabilities.

    看下面的GO程序,它试图在端口80上进行监听:

    package main
    
    import (
        "fmt"
        "net"
        "os"
    )
    
    func main() {
        if _, err := net.Listen("tcp", ":80"); err != nil {
            fmt.Fprintln(os.Stdout, err)
            os.Exit(2)
        }
        fmt.Println("success")
    }
    

    当我们编译后运行它会发生什么呢?

    $ go build -o listen listen.go
    $ ./listen
    listen tcp :80: bind: permission denied
    

    可以预见,程序会运行失败,我们没有监听80端口的权限。当然,我们可以使用 sudo ,但是我们仅仅想给二进制程序一个监听较低端口的权限,不想给全部的sudo 权限。

    Capabilities是一系列离散的能力,包含root可以做的所有事情,这包括系统时钟、终止任意程序。在这里,CAP_NET_BIND_SERVICE 将允许可执行文件监听较低的端口。

    我们可以使用 setcap 命令授权可执行文件 CAP_NET_BIND_SERVICE

    $ sudo setcap cap_net_bind_service=+ep listen
    $ getcap listen
    listen = cap_net_bind_service+ep
    $ ./listen
    success
    

    我们更感兴趣的是取消能力而不是赋予,首先,让我们看看root shell有什么功能:

    $ sudo su
    # capsh --print
    Current: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,37+ep
    Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,37
    Securebits: 00/0x0/1'b0
     secure-noroot: no (unlocked)
     secure-no-suid-fixup: no (unlocked)
     secure-keep-caps: no (unlocked)
    uid=0(root)
    gid=0(root)
    groups=0(root)
    

    耶!这有非常多的功能。

    例如,我们将使用 capsh 去掉一些能力,包括 CAP_CHOWN ,如果工作符合预期,我们的shell尽管是root也不能修改文件的所有权。

    $ sudo capsh --drop=cap_chown,cap_setpcap,cap_setfcap,cap_sys_admin --chroot=$PWD/rootfs --
    root@localhost:/# whoami
    root
    root@localhost:/# chown nobody /bin/ls
    chown: changing ownership of '/bin/ls': Operation not permitted
    

    传统的观念仍然认为,当运行不受信任的代码时,VMs 是强制隔离的。但安全特性(例如capabilities)是重要的对于防止在容器中运行被hack的程序。

    除了更复杂的工具像SELinux, seccomp和capabilities之外,在容器中运行的程序还受益于,程序在容器外运行时,相同的最佳实践(也就是说在容器外怎么用比较好,在容器内也同样使用)。

    Conclusion

    容器不是魔法,每个拥有Linux机器的人都可以玩容器,像Docker和rkt仅仅是对现代内核已有的一些东西的打包。不,你可能不应该去实现一个自己的container runtime,但是有一个对这些底层技术更好的理解将帮助我们更好地使用高层工具(尤其是Debug)

    还有大量的话题我今天没有讲,网络和copy-on-write文件系统可能是最大的两个。然而,我希望这是一个良好的起点对于任何想动手实践的人,Happy hacking!

  • 相关阅读:
    hdu 4114 Disney's FastPass 状压dp
    lightoj 1381
    bzoj 2428: [HAOI2006]均分数据 随机化
    bzoj 3969: [WF2013]Low Power 二分
    套题:wf2013 (1/8)
    hdu 4119 Isabella's Message 模拟题
    hdu 4118 Holiday's Accommodation 树形dp
    UESTC 2015dp专题 N 导弹拦截 dp
    UESTC 2015dp专题 j 男神的约会 bfs
    UESTC 2015dp专题 H 邱老师选妹子 数位dp
  • 原文地址:https://www.cnblogs.com/lfri/p/15820394.html
Copyright © 2020-2023  润新知