容器新体验 - Rootless Container + cgroup V2

容器新体验 - Rootless Container + cgroup V2

在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 网络转发后,网络带宽有较大的损耗。

局限性

  1. Slirp 网络性能下降非常明显,lxc-user-nic 利用 SETUID 创建veth设备,具备更出色的网络性能,但是setuid需要特权模式,同时也引入了新的安全风险。
  2. mount namespace在user namespace中目前不支持 NFS 和 块设备
  3. 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实验版本,比如 k3susernetes 。在2021,我们也能看到Kubernetes 提供对cgroup v2的支持。

随着操作系统内核和容器社区的共同努力,容器技术可以变得更加安全,可以适用于更多业务场景中。

参考

上一篇:初探阿里云存储网关(多图慎入)


下一篇:亚信产业互联网生态亮相2016南京软博会