你好!大约一年前,我对 Docker 容器的启动时间很生气。这是因为我正在构建一个nginx 操场,我在每个 HTTP 请求上都启动一个新的“容器”,因此为了让它感觉相当敏捷,nginx 需要快速启动。
另外,我在一个非常小的云机器(256MB RAM)和一个小 CPU 上运行这个项目,所以我真的想避免不必要的开销。
从那时起,我一直在寻找一种更快地运行容器的方法,但直到上周我发现了bubblewrap时才找到!它非常快,我认为它非常酷,但我也遇到了一堆有趣的问题,我想为未来的自己写下来。
一些免责声明
- 我不确定我在这篇文章中使用bubblewrap 的方式是否可能不是它的预期使用方式
- 以这种方式使用bubblewrap时有很多锋利的边缘,您需要考虑很多关于Linux命名空间以及容器如何工作的问题
- bubblewrap 是一个安全工具,但我不是安全人员,我只是为奇怪的小项目做这个。你绝对不应该听我的安全建议。
好的,说了这么多,让我们谈谈我正在尝试使用bubblewrap 以相对安全的方式快速运行容器:)
Docker 容器在我的机器上启动大约需要 300 毫秒
我运行了一个快速基准测试来查看 Docker 容器运行一个简单命令 ( ls
) 需要多长时间。对于 Docker 和 Podman,大约是 300 毫秒。
$ time docker run --network none -it ubuntu:20.04 ls / > /dev/null Executed in 378.42 millis $ time podman run --network none -it ubuntu:20.04 ls / > /dev/null Executed in 279.27 millis
几乎所有这些时间都是 docker 和 podman 的开销——仅运行ls
本身就需要大约 3ms:
$ time ls / > /dev/null Executed in 2.96 millis
我想强调一点,虽然我不确定 Docker 和 podman 启动时间中最慢的部分到底是什么(我花了 5 分钟试图分析它们并放弃了),但我 100% 确定这很重要。
我们将使用bubblewrap 更快地运行容器的方式有很多限制,而且它是一个较低级别的接口,使用起来非常棘手。
目标 1:快速启动的容器
我觉得应该有可能让容器基本上立即启动或至少在 5 毫秒内启动。我的思考过程:
- 使用
unshare
创建一个新的命名空间基本上是即时的 - 容器基本上只是一堆命名空间
- 有什么问题?
容器启动时间(通常)并不那么重要
大多数时候,当人们使用容器时,他们会在容器内运行一些长时间运行的进程,比如网络服务器,因此启动是否需要 300 毫秒并不重要。
所以对我来说,没有很多容器工具可以优化启动时间是有道理的。但我仍然想优化启动时间:)
目标 2:以非特权用户身份运行容器
我的另一个目标是能够以非特权用户而不是 root 用户身份运行我的容器。
当我第一次得知 Docker 实际上以 root 身份运行容器时,我感到很惊讶——即使我以非特权用户 ( bork
) 身份运行 docker docker run ubuntu:20.04
,该消息实际上被发送到以 root 身份运行的守护进程和 Docker 容器进程它本身也以root
身份运行(尽管它被剥夺了所有功能)。
这对 Docker 来说很好(他们有很多非常聪明的人确保他们做对了!),但是如果我要在不使用 Docker 的情况下做容器的事情(出于上面提到的速度原因),我宁愿不这样做它以root身份使一切更加安全。
podman 可以以非 root 用户身份运行容器
在我们开始讨论如何用bubblewrap 做一些奇怪的事情之前,我想快速谈谈一个更普通的运行容器的工具:podman!
与 Docker 不同,Podman 可以以非特权用户身份运行容器!
如果我从普通用户那里运行它:
$ podman run -it ubuntu:20.04 ls
它不会在幕后以 root 身份秘密运行!它只是以我的普通用户身份启动容器,然后使用称为“用户名称空间”的东西,以便在容器内我看起来是 root。
podman 的另一个很酷的地方是它具有与 Docker 完全相同的界面,因此您只需使用 Docker 命令并将docker
替换为podman
,它就会正常工作。我发现有时我需要做一些额外的工作才能让 podman 在实践中工作,但它具有相同的命令行界面仍然非常好。
这种“以非 root 用户身份运行容器”功能通常称为“无根容器”。 (我觉得这个名字有点违反直觉,但这就是人们所说的)
尝试失败 1:使用runc
编写我自己的工具
我知道 Docker 和 podman 在后台使用runc ,所以我想——好吧,也许我可以直接使用runc
来制作我自己的工具,它可以比 Docker 更快地启动容器!
我在 6 个月前尝试这样做,但我不记得大部分细节,但基本上我花了 8 个小时来完成它,因为我无法得到任何工作而感到沮丧,然后放弃了。
我记得挣扎的一个具体细节是设置一个工作/dev
供我的程序使用。
进入气泡膜
好的,这是一个很长的序言,所以让我们进入正题!上周,我发现了一个名为bubblewrap
的工具,它基本上正是我在失败的尝试中尝试使用runc
构建的东西,除了它确实有效并且具有更多功能并且它是由了解安全性的人构建的!万岁!
bubblewrap 的接口与 Docker 的接口非常不同——它的级别要低得多。没有容器镜像的概念——而是将主机上的一堆目录映射到容器中的目录。
例如,以下是如何运行与主机操作系统具有相同根目录的容器,但对该根目录仅具有读取权限,并且仅对/tmp
具有写入权限。
bwrap \ --ro-bind / / \ --bind /tmp /tmp \ --proc /proc --dev /dev \ --unshare-pid \ --unshare-net \ bash
例如,您可以想象以这种方式在 bubblewrap 下运行一些不受信任的进程,然后将进程要访问的所有文件放在/tmp
中。
bubblewrap 以非特权(非 root)用户身份运行容器
与 podman 一样,bubblewrap 使用用户命名空间以非 root 用户身份运行容器。它也可以以 root 身份运行容器,但在这篇文章中,我们将讨论以非特权用户身份使用它。
泡泡纸很快
让我们看看在气泡包装容器中运行ls
需要多长时间!
$ time bwrap --ro-bind / / --proc /proc --dev /dev --unshare-pid ls / Executed in 8.04 millis
这是一个很大的区别! 8ms 比 279ms 快很多。
当然,就像我们之前说的,bubblewrap 更快的原因是它做的更少。所以让我们来谈谈bubblewrap不做的一些事情。
有些事情bubblewrap不做
以下是 Docker/podman 做而 bubblewrap 不做的一些事情:
- 为您设置 overlayfs 挂载,以便您对文件系统的更改不会影响基础映像
- 设置网络桥,以便您可以连接到容器内的网络服务器
- 可能还有很多我没想到的东西
一般来说,bubblewrap 是一个比 Docker 之类的工具低得多的工具。
此外,bubblewrap 的目标似乎与 Docker 完全不同——自述文件似乎说它旨在作为沙盒桌面软件的工具(我认为它来自flatpak )。
使用 bubblewrap 运行容器映像
我找不到使用bubblewrap 运行Docker 容器映像的说明,所以在这里。基本上我只是使用 Docker 下载容器映像并将其放入目录中,然后使用bwrap
运行它:
还有一个名为bwrap-oci的工具,它看起来很酷,但我无法编译它。
mkdir rootfs docker export $(docker create frapsoft/fish) | tar -C rootfs -xf - bwrap \ --bind $PWD/rootfs / \ --proc /proc --dev /dev \ --uid 0 \ --unshare-pid \ --unshare-net \ fish
需要注意的重要一点是,这不会为容器的文件写入创建临时覆盖文件系统,因此它会让容器编辑映像中的文件。
我写了一篇关于覆盖文件系统的文章,如果你想看看你自己如何做到这一点。
使用bubblewrap 运行“容器”与使用podman 不同
我只是举了一个如何用bubblewrap“运行容器”的例子,你可能会想“很酷,这就像podman,但速度更快!”。事实并非如此,它实际上与使用 podman 的方式比我预期的还要多。
我将“容器”放在引号中,因为有两种定义“容器”的方法:
- 实现OCI 运行时规范的东西
- 以某种方式与主机系统隔离的任何方式运行进程
bubblewrap 是第二种意义上的“容器”工具。它确实提供了隔离,并且它使用与 Docker 相同的特性——Linux 命名空间——来做到这一点。
但它不是第一种意义上的容器工具。而且它是一个较低级别的工具,因此您可能会进入一堆奇怪的状态,并且您确实需要考虑容器在使用时如何工作的所有奇怪细节。
在本文的其余部分,我将讨论一些使用bubblewrap 可能发生的奇怪事情,而podman/Docker 不会发生这些事情。
奇怪的事情1:不存在的进程
这是我使用气泡膜时遇到的一个奇怪情况的示例,这让我困惑了一分钟:
$ bwrap --ro-bind / / --unshare-all bash $ ps aux ... some processes root 390073 0.0 0.0 2848 124 pts/9 S 14:28 0:00 bwrap --ro-bind / / --unshare-all --uid 0 bash ... some other processes $ kill 390073 bash: kill: (390073) - No such process $ ps aux | grep 390073 root 390073 0.0 0.0 2848 124 pts/9 S 14:28 0:00 bwrap --ro-bind / / --unshare-all --uid 0 bash
这就是发生的事情
- 我在bubblewrap 内启动了一个bash shell
- 我跑了
ps aux
,看到了一个 PID390073
的进程 - 我试图杀死这个过程。它失败并出现错误
no such process
。什么? - 我运行了
ps aux
,仍然看到 PID390073
的进程
这是怎么回事?为什么进程390073
不存在,即使ps
说它存在?那不是不可能吗?
好吧,问题在于ps
实际上并没有列出当前 PID 命名空间中的所有进程。相反,它会遍历/proc
中的所有条目并将其打印出来。通常, /proc
中的内容实际上与系统上的进程相同。
但是对于 Linux 容器,这些事情可能会不同步。在这个例子中发生的事情是我们有来自主机 PID 命名空间的/proc
,但这些实际上并不是我们在 PID 命名空间中可以访问的进程。
将--proc /proc
传递给 bwrap 可以解决问题 – ps
然后实际上列出了正确的进程。
$ bwrap --ro-bind / / --unshare-all --dev /dev --proc /proc ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND bork 1 0.0 0.0 3644 136 ? S+ 16:21 0:00 bwrap --ro-bind / / --unshare-all --dev /dev --proc /proc ps au bork 2 0.0 0.0 21324 1552 ? R+ 16:21 0:00 ps aux
只有2个过程!一切正常!
奇怪的事情 2:试图监听 80 端口
将--uid 0
传递给 bubblewrap 使用户进入容器root
。您可能认为这意味着 root 用户在容器内具有管理权限,但事实并非如此!
例如,让我们尝试监听 80 端口:
$ bwrap --ro-bind / / --unshare-all --uid 0 nc -l 80 nc: Permission denied
这里发生的情况是,新的root用户实际上没有监听80端口所需的能力。(你需要特殊权限才能监听小于1024的端口,而80小于1024)
实际上有一种专门用于侦听特权端口的功能,称为CAP_NET_BIND_SERVICE
。
所以要解决这个问题,我们需要做的就是告诉bubblewrap 为我们的用户提供该功能。
$ bwrap --ro-bind / / --unshare-all --uid 0 --cap-add cap_net_bind_service nc -l 80 (no output, success!!!)
这行得通!万岁!
找到合适的功能很烦人
bubblewrap 默认情况下不提供任何功能,我发现找出所有正确的功能并手动添加它们有点烦人。基本上我的过程是
- 运行这个东西
- 看看什么失败了
- 阅读
man capabilities
以找出我缺少的能力 - 使用
--cap-add
功能 - 重复直到一切正常
但我猜这就是我希望事情变得快而付出的代价:)
奇怪的事情 2b: --dev /dev
使监听特权端口不起作用
另一件奇怪的事情是,如果我使用上面完全相同的命令(有效!)并添加--dev /dev
(设置/dev/
目录),它会导致它不再工作:
$ bwrap --ro-bind / / --dev /dev --unshare-all --uid 0 --cap-add cap_net_bind_service nc -l 80 nc: Permission denied
我认为这可能是bubblewrap 中的一个错误,但我还没有鼓起勇气深入研究bubblewrap 代码并开始调查。或者也许有一些明显的东西我错过了!
奇怪的事情 3:UID 映射
另一个有点奇怪的事情是——我试图在一个bubblewrap Ubuntu容器中运行apt-get update
,但一切都很糟糕。
以下是我在 Ubuntu 容器中运行apt-get update
的方式:
mkdir rootfs docker export $(docker create ubuntu:20.04) | tar -C rootfs -xf - bwrap \ --bind $PWD/rootfs / \ --proc /proc\ --uid 0 \ --unshare-pid \ apt-get update
以下是错误消息:
E: setgroups 65534 failed - setgroups (1: Operation not permitted) E: setegid 65534 failed - setegid (22: Invalid argument) E: seteuid 100 failed - seteuid (22: Invalid argument) E: setgroups 0 failed - setgroups (1: Operation not permitted) .... lots more similar errors
起初我想“好吧,这是一个功能问题,我需要设置CAP_SETGID
或其他东西来授予容器更改组的权限。但我这样做了,它根本没有帮助!
我认为这里发生的事情是 UID 地图的问题。什么是 UID 映射?好吧,每次您使用“用户命名空间”(podman 正在这样做)运行容器时,它都会创建容器内的 UID 到主机上的 UID 的映射。
让我们看看 UID 映射!这是如何做到的:
““ root@kiwi:/# cat /proc/self/uid_map 0 1000 1 root@kiwi:/# cat /proc/self/gid_map 1000 1000 1
This is saying that user 0 in the container is mapped to user 1000 on in the host, and group 1000 is mapped to group 1000. (My normal user's UID/GID is 1000, so this makes sense). You can find out about this `uid_map` file in `man user_namespaces`. All other users/groups that aren't 1000 are mapped to user 65534 by default, according to `man user_namespaces`. ### what's going on: non-mapped users can't be used The only users and groups that have been mapped are `0` and `1000`. But `man user_namespaces` says: > After the uid_map and gid_map files have been written, only the mapped values may be used in system calls that change user and group IDs. `apt` is trying to use users 100 and 65534. Those aren't on the list of mapped users! So they can't be used! This works fine in podman, because podman sets up its UID and GID mappings differently:
$ podman run -it ubuntu:20.04 bash root@793d03a4d773:/# cat /proc/self/uid_map 0 1000 1 1 100000 65536 root@793d03a4d773:/# cat /proc/self/gid_map 0 1000 1 1 100000 65536 “`
所有用户都得到了映射,而不仅仅是 1000 个。
我不太清楚如何解决这个问题,但我认为在bubblewrap 中可能可以像podman 一样设置uid 映射——这里有一个问题链接到解决方法。
但这不是我试图解决的实际问题,所以我没有深入研究它。
它工作得非常好!
我已经讨论了很多问题,但是我在bubblewrap 中尝试做的事情非常受限制,而且实际上非常简单。例如,我正在做一个 git 项目,我真的只想在容器中运行git
并从主机映射一个 git 存储库。
使用bubblewrap 非常简单!基本没有什么奇怪的问题!真的很快!
所以我对这个工具非常兴奋,将来我可能会用它来做更多的事情。
原文: https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap/