在2020年12月最新的 Docker 20.10 版本中,其中两个关键的特性发布揭示了容器运行时技术发展一些新方向。
首先是 Cgroup V2 已经被正式支持,虽然这个功能对最终用户很多是无感的,但是会让容器运行时的开发更加简洁,有更多的控制力。
Rootless Container正式可用,允许Docker daemon在none-root用户状态运行,可以充分利用Linux操作系统提供的安全隔离性。更多技术信息可以参见Rootless Container初探,
通过cgroup v2,Rootless 模式开始支持资源限制,比如 docker run --cpus , docker run --memory , docker run --pids-limit, 等参数可以让Rootless容器和普通容器一样工作。此外,Docker还进一步提升了 Rootless 模式的容器存储性能。我们知道Docker利用OverlayFS实现分层存储,来将多个只读的镜像层和一个读写层构建成为一个容器的rootfs。但是除 Ubuntu/Debian 之外,其他主要的Linux发行版都不支持非root用户挂载 OverlayFS。在4.18 内核以上,操作系统上可以支持非root用户挂载FUSE。新的 FUSE-OverlayFS开始被rootless容器支持,与早期 VFS 实现相比,具备更高的文件性能。
此外Rootless Container与初期相比,整体安装和使用体验有了很大提升。下面我们将将动手操作来看看相关的技术细节
安装配置
首先创建一个 CentOS 8 虚拟机,在内核参数中开启CGroup V2,并重新启动
$ sudo yum update
$ sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"
$ sudo reboot
```
检查 CGroup V2是否已经开启
```
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
$ systemctl --version
systemd 239 (239-41.el8_3)
+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD +IDN2 -IDN +PCRE2 default-hierarchy=legacy
```
配置 CGroup V2 的用户委派策略。系统默认只有 memory 和 pids 控制器可以被 non-root 用户委派使用。我们可以添加 cpu 和 io 等控制器。由于 cpuset 控制器需要 Systemd 244 版本或者更高,我们不做配置
```
$ mkdir -p /etc/systemd/system/user@.service.d
$ cat > /etc/systemd/system/user@.service.d/delegate.conf << EOF
[Service]
Delegate=cpu io memory pids
EOF
$ systemctl daemon-reload
安装所需软件包
$ sudo dnf install -y iptables
$ sudo dnf install -y fuse-overlayfs
(可选)安装 slirp4netns,否则 Docker会使用VPNKit作为网络栈实现。 slirp4netns的性能更优
$ sudo dnf install -y slirp4netns
```
(可选)由于缺乏权限,无法从非root容器ping其他节点,可以执行如下命令开启
```
$ sudo sh -c "echo 0 2147483647 > /proc/sys/net/ipv4/ping_group_range"
创建测试用户
$ useradd moby
$ passwd moby
$ usermod -aG wheel moby
```
重新用 moby 用户登录
安装 Rootless Container
```
$ curl -fsSL https://get.docker.com/rootless | sh
Created symlink /home/moby/.config/systemd/user/default.target.wants/docker.service → /home/moby/.config/systemd/user/docker.service.
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger moby`
[INFO] Make sure the following environment variables are set (or add them to ~/.bashrc):
export PATH=/home/moby/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock
```
检查 Docker 版本信息
```
$ docker version
Client: Docker Engine - Community
Version: 20.10.1
API version: 1.41
Go version: go1.13.15
Git commit: 831ebea
Built: Tue Dec 15 04:28:35 2020
OS/Arch: linux/amd64
Context: default
Experimental: true
Server: Docker Engine - Community
Engine:
Version: 20.10.1
API version: 1.41 (minimum version 1.12)
Go version: go1.13.15
Git commit: f001486
Built: Tue Dec 15 04:32:28 2020
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v1.4.3
GitCommit: 269548fa27e0089a8b8278fc4fc781d7f65a939b
runc:
Version: 1.0.0-rc92
GitCommit: ff819c7e9184c13b7c2607fe6c30ae19403a7aff
docker-init:
Version: 0.19.0
GitCommit: de40ad0
$ docker info
Client:
Context: default
Debug Mode: false
Server:
Containers: 1
Running: 1
Paused: 0
Stopped: 0
Images: 3
Server Version: 20.10.1
Storage Driver: fuse-overlayfs
Logging Driver: json-file
Cgroup Driver: systemd
Cgroup Version: 2
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 269548fa27e0089a8b8278fc4fc781d7f65a939b
runc version: ff819c7e9184c13b7c2607fe6c30ae19403a7aff
init version: de40ad0
Security Options:
seccomp
Profile: default
rootless
cgroupns
Kernel Version: 4.18.0-240.1.1.el8_3.x86_64
Operating System: CentOS Linux 8
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 31.02GiB
Name: iZuf653ngm016izi2bcowoZ
ID: DKDI:QUTV:34VG:SK27:XPRP:TJMT:3YID:EBJW:SCIJ:P4BH:MKBR:UM46
Docker Root Dir: /home/moby/.local/share/docker
Debug Mode: false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
Product License: Community Engine
WARNING: No kernel memory TCP limit support
WARNING: No oom kill disable support
WARNING: No cpuset support
WARNING: Support for cgroup v2 is experimental
WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled
```
可以看到 cgroup v2已经开启,安全选项中出现了rootless字样。注意:如果出现 "WARNING: No cpu shares support" 等应该是由于没有正确配置 CGroup V2 的用户委派策略。
配置完成上述环境变量,就可以快乐地玩耍 Rootless Container了
## 测试
启动一个测试Web应用
```
$ docker run --name=rootlessweb -d --cpus=0.5 -p 8080:80 nginx
...
$ curl http://127.0.0.1:8080
...
Welcome to nginx!
...
```
在容器内部查看当前用户,会返回一个模拟的 ```root``` 用户,而在操作系统上我们可以清楚看到容器进程是以当前用户执行的。
```
$ docker exec rootlessweb whoami
root
$ ps -f --pid $(docker inspect -f '{{.State.Pid}}' rootlessweb)
UID PID PPID C STIME TTY TIME CMD
moby 5042 5022 0 18:50 ? 00:00:00 nginx: master process nginx -g daemon off;
参照 https://github.com/genuinetools/amicontained/releases,可以查看容器更多的运行时细节
# amicontained
Container Runtime: not-found
Has Namespaces:
pid: true
user: true
User Namespace Mappings:
Container -> 0 Host -> 1000 Range -> 1
Container -> 1 Host -> 100000 Range -> 65536
AppArmor Profile: unconfined
Capabilities:
BOUNDING -> chown dac_override fowner fsetid kill setgid setuid setpcap net_bind_service net_raw sys_chroot mknod audit_write setfcap
Seccomp: filtering
Blocked Syscalls (64):
MSGRCV SYSLOG SETSID USELIB USTAT SYSFS VHANGUP PIVOT_ROOT _SYSCTL ACCT SETTIMEOFDAY MOUNT UMOUNT2 SWAPON SWAPOFF REBOOT SETHOSTNAME SETDOMAINNAME IOPL IOPERM CREATE_MODULE INIT_MODULE DELETE_MODULE GET_KERNEL_SYMS QUERY_MODULE QUOTACTL NFSSERVCTL GETPMSG PUTPMSG AFS_SYSCALL TUXCALL SECURITY LOOKUP_DCOOKIE CLOCK_SETTIME VSERVER MBIND SET_MEMPOLICY GET_MEMPOLICY KEXEC_LOAD ADD_KEY REQUEST_KEY KEYCTL MIGRATE_PAGES FUTIMESAT UNSHARE MOVE_PAGES UTIMENSAT PERF_EVENT_OPEN FANOTIFY_INIT NAME_TO_HANDLE_AT OPEN_BY_HANDLE_AT SETNS PROCESS_VM_READV PROCESS_VM_WRITEV KCMP FINIT_MODULE KEXEC_FILE_LOAD BPF USERFAULTFD PKEY_MPROTECT PKEY_ALLOC PKEY_FREE IO_PGETEVENTS RSEQ
Looking for Docker.sock
我们可以在系统上通过 pstree 观察整个进程树关系,所有的容器引擎和容器应用均以当前非root用户执行。
$ pstree moby
sshd───bash───pstree
systemd─┬─(sd-pam)
├─containerd-shim───13*[{containerd-shim}]
├─containerd-shim─┬─nginx───nginx
│ └─13*[{containerd-shim}]
├─dbus-daemon
├─fuse-overlayfs
└─rootlesskit─┬─exe─┬─dockerd─┬─containerd───18*[{containerd}]
│ │ ├─rootlesskit-doc─┬─docker-proxy───7*[{docker-proxy}]
│ │ │ └─7*[{rootlesskit-doc}]
│ │ └─19*[{dockerd}]
│ └─8*[{exe}]
├─slirp4netns
└─9*[{rootlesskit}]
```
## 网络性能测试
由于Rootless容器采用slirp4netns进行网络转发,网络性能有明显的下降。
![image.png](https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/528c9df402cc708a374153b091fd24c9.png)
下面我们将利用 iperf3 进行网络性能测试,启动服务器端
```
$ docker run -it --rm --name=iperf3-server -p 5201:5201 networkstatic/iperf3 -s
再开启另外一个窗口来,测试容器之间的网络带宽
$ SERVER_IP=$(docker inspect --format "{{ .NetworkSettings.IPAddress }}" iperf3-server)
$ echo $SERVER_IP
172.17.0.2
$ docker run -it --rm networkstatic/iperf3 -c $SERVER_IP
Connecting to host 172.17.0.2, port 5201
[ 5] local 172.17.0.3 port 56516 connected to 172.17.0.2 port 5201
[ ID] Interval Transfer Bitrate Retr Cwnd
[ 5] 0.00-1.00 sec 8.80 GBytes 75.6 Gbits/sec 0 440 KBytes
[ 5] 1.00-2.00 sec 8.88 GBytes 76.3 Gbits/sec 0 440 KBytes
[ 5] 2.00-3.00 sec 8.95 GBytes 76.9 Gbits/sec 0 440 KBytes
[ 5] 3.00-4.00 sec 9.04 GBytes 77.6 Gbits/sec 0 440 KBytes
[ 5] 4.00-5.00 sec 9.04 GBytes 77.7 Gbits/sec 0 440 KBytes
[ 5] 5.00-6.00 sec 8.78 GBytes 75.4 Gbits/sec 0 440 KBytes
[ 5] 6.00-7.00 sec 8.80 GBytes 75.6 Gbits/sec 0 440 KBytes
[ 5] 7.00-8.00 sec 8.97 GBytes 77.0 Gbits/sec 0 440 KBytes
[ 5] 8.00-9.00 sec 8.81 GBytes 75.7 Gbits/sec 0 440 KBytes
[ 5] 9.00-10.00 sec 8.57 GBytes 73.6 Gbits/sec 0 440 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 88.7 GBytes 76.2 Gbits/sec 0 sender
[ 5] 0.00-10.03 sec 88.7 GBytes 75.9 Gbits/sec receiver
iperf Done.
测试容器到宿主机之间的网络带宽,跨网络空间
$ HOST_IP=$(hostname --ip-address)
$ echo $HOST_IP
10.1.0.198
$ docker run -it --rm networkstatic/iperf3 -c $HOST_IP
Connecting to host 10.1.0.198, port 5201
[ 5] local 172.17.0.3 port 36738 connected to 10.1.0.198 port 5201
[ ID] Interval Transfer Bitrate Retr Cwnd
[ 5] 0.00-1.00 sec 126 MBytes 1.06 Gbits/sec 0 124 KBytes
[ 5] 1.00-2.00 sec 121 MBytes 1.02 Gbits/sec 0 124 KBytes
[ 5] 2.00-3.00 sec 121 MBytes 1.02 Gbits/sec 0 124 KBytes
[ 5] 3.00-4.00 sec 120 MBytes 1.01 Gbits/sec 0 124 KBytes
[ 5] 4.00-5.00 sec 128 MBytes 1.07 Gbits/sec 0 124 KBytes
[ 5] 5.00-6.00 sec 122 MBytes 1.03 Gbits/sec 0 124 KBytes
[ 5] 6.00-7.00 sec 121 MBytes 1.02 Gbits/sec 0 124 KBytes
[ 5] 7.00-8.00 sec 125 MBytes 1.05 Gbits/sec 0 124 KBytes
[ 5] 8.00-9.00 sec 118 MBytes 986 Mbits/sec 0 124 KBytes
[ 5] 9.00-10.00 sec 120 MBytes 1.01 Gbits/sec 0 124 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 1.19 GBytes 1.03 Gbits/sec 0 sender
[ 5] 0.00-10.03 sec 1.19 GBytes 1.02 Gbits/sec receiver
iperf Done.
通过Slirp 网络转发后,网络带宽有较大的损耗。
局限性
- Slirp 网络性能下降非常明显,lxc-user-nic 利用 SETUID 创建veth设备,具备更出色的网络性能,但是setuid需要特权模式,同时也引入了新的安全风险。
- mount namespace在user namespace中目前不支持 NFS 和 块设备
- Rootless复用Linux内核成熟的用户权限体系让Docker daemon以非特权用户运行,极大降低了攻击面,但是并不能防止内核缺陷导致的安全漏洞。
总结
目前不同的容器运行时实现都开始提供对 Rootless Container 的支持,简单比较如下
Docker | Podman | LXC | |
---|---|---|---|
Networking | Slirp Lxc-user-nic VPNkit |
Slirp | Lxc-user-nic |
Storage | FUSE-OverlayFS | FUSE-OverlayFS | VFS |
Cgroups | cgroups v2 | cgroups v2 | PAM module |
Rootless容器在提升RunC容器的安全隔离性方面前进了一大步,社区还提供了无需特权用户的Kubernetes实验版本,比如 k3s 和 usernetes 。在2021,我们也能看到Kubernetes 提供对cgroup v2的支持。
随着操作系统内核和容器社区的共同努力,容器技术可以变得更加安全,可以适用于更多业务场景中。