Docker is weird

Docker is weird

I decided to make my own container runtime for fun, and even though I’m still early on, I’ve learned that Docker is weird. Here’s some weird things I learned about Docker.

我决定为了好玩而制作自己的容器运行时,尽管我才刚刚起步,但我已经了解到 Docker 很奇怪。以下是我了解到的关于 Docker 的一些奇怪之处。

Unlike VMs, containers are just processes with special restrictions on them. Your kernel is still running that process! And if you know where to look, you can even find that process in /proc.

与虚拟机不同,容器只是带有特殊限制的进程。你的内核仍在运行该进程!如果你知道去哪里找,甚至可以在 /proc 中找到该进程。

There’s not really a linux feature that’s just “containers” either. Instead, containers are really 5 different linux kernel features in trenchcoat.

实际上并没有一种名为“容器”的 Linux 功能。相反,容器本质上是披着外衣的 5 种不同的 Linux 内核功能。

Namespaces hide what the host and container can see, cgroups limit container resource usage, and there’s a whole bunch of networking magic I don’t understand that lets you isolate a container’s network.

命名空间(Namespaces)隐藏了主机和容器所能看到的内容,控制组(cgroups)限制了容器的资源使用,还有一大堆我不理解的网络魔法,可以让你隔离容器的网络。

Images are just folders that Docker puts somewhere and then calls chroot (or pivot_root) to make that folder your container process’s new root directory. So if you run docker run -it —rm ubuntu /bin/bash, Docker is going to:

镜像只是 Docker 放置在某处的文件夹,然后调用 chroot(或 pivot_root)将该文件夹设为容器进程的新根目录。因此,如果你运行 docker run -it --rm ubuntu /bin/bash,Docker 将会:

Docker’s container runtime, runc, is written in Go, and Go has some design choices that make it less suited for a container runtime. Go’s runtime is natively multi-threaded. That’s a really cool feature, but it leads to some challenges in runc.

Docker 的容器运行时 runc 是用 Go 编写的,而 Go 的一些设计选择使其不太适合作为容器运行时。Go 的运行时原生支持多线程。这是一个非常酷的功能,但它在 runc 中带来了一些挑战。

Many of the Linux syscalls a container runtime needs to make have to be done in a single-threaded environment, so runc has a crazy C module that runs before Go’s runtime initializes so it can run those syscalls.

容器运行时需要执行的许多 Linux 系统调用必须在单线程环境中完成,因此 runc 有一个疯狂的 C 模块,它在 Go 运行时初始化之前运行,以便执行这些系统调用。

Go also doesn’t have fork! Fork only forks the main thread, so it’s kind of a footgun in a runtime like Go’s. But a container runtime really wants to fork, set up a bunch of restrictions, and then exec the process you actually put in docker run. To get around this, runc execs /proc/self/exe with a special command line arguement that identifies it as a child process to approximate a fork.

Go 也没有 fork!Fork 只会克隆主线程,因此在像 Go 这样的运行时中,这很容易导致错误。但容器运行时确实需要 fork、设置一系列限制,然后执行你在 docker run 中实际指定的进程。为了绕过这个问题,runc 会执行 /proc/self/exe 并带上一个特殊的命令行参数,将其标识为子进程,从而模拟 fork。

This is pretty easy to miss if you, like me, run Docker in MacOS. Many of the Linux kernel APIs that container runtimes use require, or at least used to, to be run as root, so the Docker daemon usually runs as root. I can only imagine that container security is a very tough job.

如果你像我一样在 MacOS 上运行 Docker,这一点很容易被忽略。容器运行时使用的许多 Linux 内核 API 要求(或者至少曾经要求)以 root 身份运行,因此 Docker 守护进程通常以 root 身份运行。我只能想象容器安全是一项非常艰巨的工作。

This seems better these days. The big exception seems to be networking. If you’re running a rootless container runtime, it looks like it has to reimplement the whole networking stack in userspace to actually give you the same features!

如今这种情况似乎有所改善。最大的例外似乎是网络。如果你运行的是无根(rootless)容器运行时,它似乎必须在用户空间中重新实现整个网络栈,才能真正提供相同的功能!

I’ve only just started my own container runtime, and I already feel like I’ve learned so much about Docker. Writing your own version of a tool you use is a great way to learn about it!

我才刚刚开始编写自己的容器运行时,就已经感觉学到了很多关于 Docker 的知识。编写自己使用的工具的版本是了解它的绝佳方式!