如果您经常与容器打交道,那么您可能已经多次发布了端口。发布的典型需求是这样的:您正在本地开发一个web应用程序,但在一个容器中,您希望使用笔记本电脑的浏览器测试它。接下来要做的是运行命令 docker run -p 8080:80 app,
然后在浏览器中打开 localhost:8080。小菜一碟
但是你有没有想过当你要求Docker发布一个端口时会发生什么?
在本文中,我将尝试连接端口发布(Docker显然创造了这个术语)和一种更传统的网络技术(称为端口转发)之间的点。我还将了解不同的“单主机”容器运行时(Docker Engine、Docker Desktop、containerd、nerdclt和Lima),以比较端口发布实现和功能。
一如既往,最终目标是加深对该技术的理解,并接近成为容器的超级用户!
什么时端口转发或端口映射
当我们说端口时,通常指的是由一对IP地址和端口号定义的某个[互联网]socket地址。
socket用于表示网络数据交换的参与者。因此,端口转发或端口映射只是一种奇特的方式,可以说寻址到一个套接字的数据被中介(例如,通过网络路由器或代理程序)重定向到另一个套接字。如下图:
从技术上讲,端口转发是网络地址转换(NAT)的一种形式。
有两种常见的重定向网络数据的方法(实现端口转发):
- 通过偷偷修改数据包的目的地址。
- 通过在客户端和服务器之间显式地放置代理。、
在第一种情况下,最初发往某个<addr1:port1>的数据包在途中被修改(或翻译),然后被传送到<addr2:port2>。通常,这是由一个Linux内核组件完成的,比如配置了一堆iptables规则的netfilter。
在第二种情况下,面向客户端的socket实际上由L4+代理进程维护,该进程读取传入数据并将其重新发送到最终目的地。 例如,没有包被实际翻译,只有有效载荷数据被代理。
虽然这两种方法之间存在细微差别,但大多数时候,它们是可互换的:
容器和端口转发
通常,容器有自己的网络堆栈和IP地址。能够访问该IP地址允许您调用容器内部运行的任何服务(假设它们侦听容器的公共接口)。
例如,使用Linux上的Docker Engine(不要与自2022年5月以来也支持Linux的Docker Desktop混淆),您可以创建一个nginx容器并从curl它:
如果你有足够的勇气在主系统上运行容器,你甚至可以在你喜欢的浏览器中打开上面的地址。这是一种方便的功能,我在执行一些特殊任务时经常依赖它。但它真的有用吗?
嗯,我知道通过IP访问Docker容器至少有两个显著的“不便”:
- 容器IP是动态分配的,容器的重新启动(或重新创建)可能会导致其IP地址发生变化。
- 默认情况下,容器IP只能从主机内部路由,而不能从外部访问。
这就需要端口发布派上用场的地方!
在其简单的形式中,用 docker run --publish 8080:80 nginx 命令创建一个常规的端口转发,从主机的0.0.0.0:8080到容器的$CONT_IP:80。
由于映射的两端都驻留在一个Linux主机上,因此这样的转发可以通过几个iptables规则来实现:
因此,在Docker Engine的情况下,端口发布和良好的旧内核空间端口转发之间似乎没有太大区别。Docker在顶部添加的唯一“额外”功能是在容器IP地址更改的情况下自动更新映射的目标部分。
您可能知道docker run-publish允许显式指定主机的接口,但您是否也知道docker支持8080-8090语法的端口范围?
docker run --publish 127.0.0.1:8080-8090:80 nginx
端口发布和不同的网络驱动程序
上面一节中的实验是使用默认(网桥)网络驱动程序完成的。显然,基于网桥的网络支持并受益于端口发布。然而,Docker还有其他几种网络类型。用Docker自己的话说:
容器使用的网络类型,无论是网桥、覆盖层、macvlan网络还是自定义网络插件,在容器内部都是透明的。从容器的角度来看,它有一个带有IP地址、网关、路由表、DNS服务和其他网络细节的网络接口(假设容器没有使用无网络驱动程序)。
但网络类型是否会影响端口发布功能?
让我们逐一回顾每种网络类型:
- none 网络意味着没有外部网络接口(因此没有IP地址),容器中只有环回。没有IP地址-没有端口转发。
- host网络意味着容器没有自己的网络堆栈。相反,它重用了主机的一个。您仍然可以转发端口,但您必须自己完成(例如,通过手动创建iptables规则),因为从Docker的角度来看,几乎不需要它。
- container:<other>网络表示当前容器重用另一个容器的网络模式。特别是,它使容器继承已经发布的端口(如果有的话)。但不能在该模式下发布自己的端口。
- overlay网络似乎是Swarm时代的回声。显然,您可以在那里进行某种端口发布,但要针对“服务”而不是单个容器。对这种模式没有太多的经验,所以不要猜测。
- ipvlan and macvlan 网络模式不支持端口转发,因为它们可能假设分配给容器的IP地址是可直接路由的,不属于Docker的基本职责。
总而言之,您希望使用端口发布的唯一网络类型是网桥。但这是默认类型,可能是最广泛的类型,因此这种限制不会降低端口发布的价值。
⚠️ 注意,大多数时候,如果底层网络驱动程序不支持端口转发,Docker会默默忽略-p |--publish标志。
容器桌面和端口发布
令人惊讶的是, Docker Desktop在容器网络方面的表现与Docker Engine不同。以下是当我尝试通过MacBook上的IP地址访问容器时发生的情况:
$ docker run -d --name nginx-1 nginx
$ CONT_IP=$(
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx-1
)
$ ping $CONT_IP
PING 172.17.0.3 (172.17.0.3): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
^C
--- 172.17.0.3 ping statistics ---
3 packets transmitted, 0 packets received, 100.0% packet loss
嗯,这有点道理。Docker容器需要Linux内核。因此,Docker Desktop for Mac(可能也适用于Windows和Linux)悄悄启动了一个全面的虚拟机,该虚拟机运行预先安装了Docker Engine的定制Linux发行版。因此,上述172.17.0.3地址是来自仅存在于该虚拟机内部的网桥网络的地址。显然,它不能从主机系统(在我的例子中是macOS)路由。
但无论如何,端口发布在Docker Desktop中有效!
$ docker run -d -p 8080:80 --name nginx-1 nginx
$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
下图解释了Docker桌面网络的内部结构。特别是,它揭示了如何进行端口转发:
长话短说,以Docker Desktop为例,它是基于用户空间和传统iptables的端口转发的结合。当您启动一个发布端口的容器时,主机上的代理进程(例如,在macOS上)会使用指定的IP地址和端口打开一个真正的socket。您可以使用lsof精确定位此进程:
$ docker run -d -p 8080:80 --name nginx-1 nginx
$ sudo lsof -i -P | grep LISTEN | grep :8080
com.docke... 24294 iximiuz 76u IPv6 0x89404e558d90602b 0t0 TCP *:8080 (LISTEN)
$ ps 24294
PID TT STAT TIME COMMAND
24294 ?? S 38:09.83 /Applications/Docker.app/Contents/MacOS/com.docker.backend -watchdog -native-api
在VM内部,端口发布的实现似乎与本文Docker Engine一节中描述的没有什么不同。可以使用在VM的网络命名空间中运行的特权容器(--net host标志,因为对于Docker Engine,VM是主机)来验证:
$ docker run --privileged --net host -it --rm alpine
/ $# apk add iptables
/ $# iptables -t nat -L
<typical Docker Engine port forwarding iptables rules>
神秘的vpnkit桥(vpnkit项目的一部分)有助于跨越主机和虚拟机系统之间的边界。
总之,当您在Docker Desktop中发布端口时:
- com.docker.backend 这个主机系统上的后端进程开始充当用户空间代理,并使用指定的地址打开套接字。
- socket可以被主机上的任何人(或访问主机网络的人)用来向容器发送数据。
- 当数据出现在套接字上时,它会被vpnkit网桥转发到VM的外部网络接口。
- 在VM内部,容器的端口已经使用基于Docker Engine iptables的“标准”技术发布到该接口。
如何使用容器发布端口
Docker严重依赖containerd进行较低级别的容器管理,这已经不是什么秘密了。但担忧的确切分离可能并不总是明确的。例如,发布端口对我来说总是很低级。但事实证明containerd不能自己发布端口!
与 docker run --name nginx -1 nginx:latest 类似,您可以使用
ctr run docker.io/library/nginx:latest nginx-1 直接在containerd中运行容器。
因此,端口发布显然是Docker自己的职责之一。但还是有其他办法。。。
使用强大的nerdctl进行端口转发
nerdctl是containerd的流行CLI客户端。与极简主义的ctr不同,nerdctl尝试丰富功能并兼容Docker(这意味着它模仿Docker CLI命令)。但如果它与Docker兼容,那么它必须实现端口发布!
对nerdctl源代码的快速检查显示(cmd/nnerdctl/run.go,ocihook/ocihook.go),它依赖于OCI运行时生命周期钩子(准确地说是在“CreateRuntime”上)来为即将启动的容器设置一些额外的网络功能。但是nerdctl并不能自己进行实际的网络配置。相反,它使用了helper containerd/go cni包,该包特别允许配置端口映射(很可能还将实际作业委托给相应的端口映射cni插件)。
🤓 注意,上述机制仅用于端口转发等额外配置。容器的网络名称空间及其与网桥网络的连接的实际创建是由containerd自己完成的(尽管可能是通过运行底层容器运行时和/或相同的containerd/go cni助手包)。
一个验证上述发现的小实验表明,portmap CNI插件使用了已经熟悉的iptables技巧来实现端口转发。因此,事实上,nerdctl与Docker Engine在这种情况下的作用非常接近:
$ sudo nerdctl run -d --name nerd-nginx-1 -p 8080:80 nginx
$ sudo nerdctl inspect \
-f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
nerd-nginx-1
10.4.0.4
$ sudo iptables -t nat -L | grep '10.4.0.4'
CNI-6603d34f605da0cf8e0a0934 all -- 10.4.0.4 anywhere
DNAT tcp -- anywhere anywhere tcp dpt:http-alt to:10.4.0.4:80
Lima如何实现端口转发
Lima项目试图用nerdctl打包containerd,并使该包在macOS上轻松运行。这应该包括运行虚拟机:
Lima推出了具有自动文件共享和端口转发(类似于WSL2)的Linux虚拟机和containerd。
因此,在架构上,Lima与Docker Desktop有些相似。
$ limactl start
$ nerdctl.lima run -d --name lima-nginx-1 -p 8080:80 nginx
$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
OK,在Lima中发布端口是可以工作的,但它是如何实现的?
$ sudo lsof -i -P | grep LISTEN | grep :8080
ssh 86663 iximiuz 15u IPv4 0xcf4f91a867ed7809 0t0 TCP localhost:8080 (LISTEN)
$ ps 86663
PID TT STAT TIME COMMAND
86663 ?? Ss 0:00.02 ssh: /Users/iximiuz/.lima/default/ssh.sock [mux]
我非常喜欢我在那里发现的东西!Lima使用旧的SSH隧道在主机系统(macOS)和VM中的服务之间转发端口。
在VM内部,它只是一个常规容器(由nerdctl启动),其端口发布为0.0.0.0:
$ limactl shell default
$ nerdctl ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6f2aa0304e4c nginx:latest "/docker-entrypoint.…" 5 seconds ago Up 0.0.0.0:8080->80/tcp lima-nginx-1
代替结论
那么,端口发布和端口转发之间有什么真正的区别吗?还是端口发布只是Docker调用容器端口转发的一种方式?好吧,你告诉我😉
无论如何,了解发布容器的端口时实际发生的情况是有帮助的。例如,它允许您预测哪些用例在技术上是可行的,并允许实现一些相当高级的场景,如发布已运行容器的端口或访问侦听容器本地主机的服务。