容器技术的理解

容器技术的理解

马草原 735 2021-11-20

容器技术的理解

近些年来基于Docker的容器技术非常火🔥,本文浅谈一下我对容器的理解。

我们为什么需要容器?

容器技术并不是凭空出现的,它来源自时代发展中人们对于如何更高效地利用计算机资源的思考和工程实践,容器并不是凭空出现的是根据以下发展轴逐渐形成的。

1. 独立服务(裸机)时代

互联网服务早期,想要架设网络服务器,就需要租用某个地方的服务器设备,运行程序代码。只要有充足且称职的人员维护,就能最大限度的发挥服务器性能。

裸机时代的问题在于扩展服务极度缺乏灵活性:如果想要添加设备,就需要找服务器供应商(Dell 或 IBM)购买新的物理设备,并指派专业人员进行安装,调试,启动,这大概需要一两个月的时间。

并且,当部署好一个服务器集群,操作系统与驱动的升级,硬件的替换与维修,网络问题的修复,线材的整理,机房管理权限的设置,数据中心温度的控制以及电费与 IPS 费用的支付…等等这些都需要专业的团队去处理。

2. 虚拟机时代

于是我们进入了虚拟机时代,虚拟机是介于用户与硬件设备之间的一层抽象。一开始,相较于裸机时代,一台计算机服务于单一的用户主体,现在一台计算机允许多个用户主体登录,使用计算资源运行彼此的服务。只要设备性能充足,用户便可以在需要时快速添加新服务。这使得我们获得了一些服务扩展的灵活性。

但在这种模式下存在着一些问题:

  1. 任何用户都有权限获取其他用户服务存储的数据;
  2. 用户可以通过投放 Fork Bomb(见下方说明) 的方式,掠夺服务器资源;
  3. 一台物理设备上的任何租户都可能无意间使整个服务器崩溃;

为了解决这一问题,出现了虚拟机技术:即当用户创建服务时,在计算机的主操作系统上安装新的操作系统调度硬件资源以达成数据隔离的目标。并且当一个服务崩溃时,最多导致服务所属的操作系统崩溃,服务器设备上的其余租户将不受影响。

虚拟机技术的弊端在于在主操作系统中运行其他操作系统所带来的性能损耗。但只要计算机拥有充足的算力和内存,这些性能损耗都可以被接受。

Fork Bomb 是一种通过不断生成子进程,以达到占用大量系统资源的目的,从而导致系统无法正常工作,甚至停止响应的攻击手段。它通常通过在操作系统中创建大量进程,以消耗系统内存和处理器资源,并导致系统崩溃。

3. 公有云时代

通过微软云、亚马逊云、阿里云、腾讯云等公有云服务提供商提供的虚拟机服务,用户不再需要管理昂贵且复杂的数据中心,只需要管理自己的应用程序。云服务厂商虽然不会帮助用户更新操作系统,但是会定期更新服务器设备。

但在这种模式下,虚拟机提供商向用户提供的,本质上仍然是计算机的硬件设备(CPU 和内存),用户仍然需要支付调度,维护整个操作系统的开销(例如网络管理,安装与更新软件等),这又需要专业的技术人员负责。

如果能够帮助用户节省掉维护操作系统的开销,让应用程序直接运行,那就太棒了。这种需求催生了下个时代的到来。

4. 容器时代

容器技术为用户提供了许多虚拟机安全和资源管理的功能,但节省掉了运行整个操作系统的成本。

在Linux系统中,通过以下三个命令将进程组之间彼此隔离:

  • chroot:实现目录级别的资源隔离;
  • unshare:实现进程级别的资源隔离;
  • cgroup:限制隔离环境中可调度的资源大小;

实现容器技术的三个关键 Linux 命令

1. chroot命令

chroot是一个 Linux 命令,允许为一个新进程创建根目录。当为一个容器设置一个新目录后,容器内的进程将无法访问到任何根目录外的数据,这就消除了数据泄露的安全隐患。

运行以下命令开始实践:

  1. docker run -it --name docker-host --rm --privileged ubuntu:bionic

命令解析:

  • - docker run:在容器中运行一些命令;-it:令 shell 保持可交互状态;
  1. 创建新目录并进入: mkdir /my-new-root && cd $_
  2. 创建一些秘密文件:echo "my super secret thing" >> /my-new-root/secret.txt
  3. 运行命令:chroot /my-new-root bash

此时,程序会报错:

chroot: failed to run command 'bash': No such file or directory

这是因为新的根目录 /my-new-root 内并未包含 bash程序,执行以下命令修复:

  1. mkdir /my-new-root/bin
  2. cp /bin/bash /bin/ls /my-new-root/bin/
  3. chroot /my-new-root bash

此时程序依然会报错,因为我们尚未安装 bash的依赖。(通过 ldd命令可查看):

linux-vdso.so.1 (0x00007ffe5705a000)
libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007fb89f047000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb89ee43000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb89ea52000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb89f58b000)

接着运行以下命令:

  1. mkdir /my-new-root/lib{,64}
  2. 将 bash 的依赖项拷贝至新建的根目录:
cp /lib/x86_64-linux-gnu/libtinfo.so.5 /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libc.so.6 /my-new-root/lib

cp /lib64/ld-linux-x86-64.so.2 /my-new-root/lib64
  1. ls的依赖项如法炮制的安装:cp /lib/x86_64-linux-gnu/libselinux.so.1 /lib/x86_64-linux-gnu/libpcre.so.3 /lib/x86_64-linux-gnu/libpthread.so.0 /my-new-root/lib

此时,运行 chroot /my-new-root bash命令将成功运行。在 bash shell 中使用 pwd命令可见,当前根目录为 /。至此,我们完成了目录级别的资源隔离。

2. unshare命令

chroot命令使操作系统可以使用户彼此无法访问目录下的文件,但用户依然可以通过查看系统进程了解计算机的运行情况。通过杀死进程,卸载文件系统等手段,恶意用户依然会对计算机的安全造成威胁。

chroot 命令的问题

  1. 开启一个新的终端,并运行 docker exec -it docker-host bash 命令进入操作系统;
  2. 运行 tail -f /my-new-root/secret.txt & 命令,持久化一个后台进程;
  3. 运行 ps 命令查看进程 ID(PID);
  4. 在原先的终端中执行 kill <PID> 命令,可见终端 2 的持久化进程已经被杀死了;

由此可见,仅仅做到文件系统的隔离是不够的,因此需要通过 unshare命令,隐藏进程,让进程之间彼此不透明。

unshare 命令

unshare命令将从父进程中创建一个独立的命名空间。代码操作如下:

exit # from our chroot'd environment if you're still running it, if not skip this

# install debootstrap
apt-get update -y
apt-get install debootstrap -y
debootstrap --variant=minbase bionic /better-root

# head into the new namespace'd, chroot'd environment
unshare --mount --uts --ipc --net --pid --fork --user --map-root-user chroot /better-root bash # this also chroot's for us
mount -t proc none /proc # process namespace
mount -t sysfs none /sys # filesystem
mount -t tmpfs none /tmp # filesystem

再重复一次我们刚才的实验会发现,此时终端 #1 已经无法再访问和杀死终端 #2 的持久化进程了。

3. cgroups命令

即使通过 chroot 命令隔离文件系统,通过 unshare隔离进程,每个隔离环境依然可以访问服务器的所有物理资源,这使得当服务器中的一个租户运行大量计算占满计算资源时,其他租户的服务将无以为继。

这时候就需要用到 cgroups(control groups) 命令。它使得每个隔离单元只能够有限地使用系统资源。

具体操作如下:

# outside of unshare'd environment get the tools we'll need here
apt-get install -y cgroup-tools htop

# create new cgroups
cgcreate -g cpu,memory,blkio,devices,freezer:/sandbox

# add our unshare'd env to our cgroup
ps aux # grab the bash PID that's right after the unshare one
cgclassify -g cpu,memory,blkio,devices,freezer:sandbox <PID>

# list tasks associated to the sandbox cpu group, we should see the above PID
cat /sys/fs/cgroup/cpu/sandbox/tasks

# show the cpu share of the sandbox cpu group, this is the number that determines priority between competing resources, higher is is higher priority
cat /sys/fs/cgroup/cpu/sandbox/cpu.shares

# kill all of sandbox's processes if you need it
# kill -9 $(cat /sys/fs/cgroup/cpu/sandbox/tasks)

# Limit usage at 5% for a multi core system
cgset -r cpu.cfs_period_us=100000 -r cpu.cfs_quota_us=$[ 5000 * $(getconf _NPROCESSORS_ONLN) ] sandbox

# Set a limit of 80M
cgset -r memory.limit_in_bytes=80M sandbox
# Get memory stats used by the cgroup
cgget -r memory.stat sandbox

# in terminal session #2, outside of the unshare'd env
htop # will allow us to see resources being used with a nice visualizer

# in terminal session #1, inside unshared'd env
yes > /dev/null # this will instantly consume one core's worth of CPU power
# notice it's only taking 5% of the CPU, like we set
# if you want, run the docker exec from above to get a third session to see the above command take 100% of the available resources
# CTRL+C stops the above any time

# in terminal session #1, inside unshare'd env
yes | tr \n x | head -c 1048576000 | grep n # this will ramp up to consume ~1GB of RAM
# notice in htop it'll keep the memory closer to 80MB due to our cgroup
# as above, connect with a third terminal to see it work outside of a cgroup