通常本地运行服务的时候,我们都会监听127.0.0.1,比如下面这样(需要安装python3)
$ python3 -m http.server --bind 127.0.0.1
Serving HTTP on 127.0.0.1 port 8000 (http://127.0.0.1:8000/) ...
然后在浏览器中就可以输入http://127.0.0.1:8000,浏览器就会展示服务运行的当前的目录结构了。但是呢,当你在container中运行的时候,再通过此链接访问你就会遇到 connection refused 或者 connection reset 的问题。
$ docker run -p 8000:8000 -itd python:3.7-slim python3 -m http.server --bind 127.0.0.1
c2e94f44dc86dc48b9b4d03cc547c265134d070c55c9c144e5d0adcfd0da85a9
$ curl 127.0.0.1:8000
curl: (56) Recv failure: 连接被对方重设
这个是为什么呢?为了知道如何解决这个为题,我们需要最小程度的了解Docker网络是如何运行的。这边文章将会覆盖如下三方面:
- 网络的
namespaces,以及Docker是如何使用它们的 docker run -p 5000:5000到底做了什么,以及为什么上述的例子不能正常访问- 如何修复
image可以使得服务可以正常的访问
不使用Docker时候的网络
我们从第一个场景出发:直接在操作系统内运行一个服务,然后直接访问这个服务。操作系统有多个网络的interfaces。例如我的电脑就有如下的网络接口:
$ ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.0.101
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1
我们来一起看看这三个网络接口:
- 暂时忽略
docker0这个网络接口 lo是一个回环的网络接口,其IPv4的地址127.0.0.1。这个咱的电脑,不需要通过任何网络硬件仅仅在内存中就可进行寻址ens33是我的WiFi地址,其IPv4的地址:192.168.0.101。当电脑需要链接互联网的时候,发送的包都通过这个interface
第一个场景起一个服务监听127.0.0.1,然后本地直接访问这个服务,其可视化的图如下:
网络的 namespaces
你可以注意到上图中是一个Default network namespace,这个是什么?
Docker 是一个运行容器的系统: 一种让进程间互相隔离的方式。这个特性是基于一系列 Linux 内核特性构建起来的,其中一个就是 network namespaces —— 一种使得不同的进程拥有不同的网络设备,IPs,防护墙规则等。
默认情况下,每个由 Docker 运行的容器都有自己的 network namespace,和自己的IP:
$ docker run --rm -it busybox
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
所以这个容器有两个 interface,eth0和lo每个都有自己的IP地址。但是由于这个是另外一个network namespace,所以上面的Default network namespace是不同的。
为了让上面的表述更加清晰,我们在容器中运行http.server
$ docker run -itd python:3.7-slim python3 -m http.server --bind 127.0.0.1
其网络结构如下图:
现在我们知道连接为什么会被拒绝了:服务监听的是容器network namespace中的127.0.0.1。而浏览器访问的是Default network namespace中的127.0.0.1。这是两个不同的interface,所以是不能建立链接的。
那我们应该怎样在两个network namespace建立连接呢?可以使用 Docker 的port-forwarding。
Docker run port-forwarding (is not enough)
当我们使用参数-p 5000:5000运行容器的时候,会转发Docker daemon运行所在network namespace中所有interfaces的端口5000的流量到容器network namespace的外部interface的IP的5000端口。使用参数-p 8080:80则会转发Docker daemon运行所在的network namespace中所有8080端口的流量到容器network namespace的外部interface的IP的80端口。
让我们来运行一个容器,通过图标来可视化这样做到底意味着什么
$ docker run -itd -p 8000:8000 python:3.7-slim python3 -m http.server --bind 127.0.0.1
现在我们遇到了第二个问题:服务监听的是容器network namespace中的127.0.0.1,而port-forwarding把流量全部转发到了容器的外部interface的IP:172.17.0.2
所以还会遇到 connection reset 或者 connection refused
解决方案:监听所有interfaces
port-forwarding只能转发到一个地址,但是你可以修改服务监听的interface,可以通过服务监听0.0.0.0,这样讲就可以监听所有的interfaces了,问题就得到了解决。
$ docker run -p 8000:8000 -itd python:3.7-slim python3 -m http.server --bind 0.0.0.0
1bd03d90310f02b5c8ca1d95f4dbadce543c6ca4a5741f5bbc6286e6a2b72850
$ curl 127.0.0.1:8000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href=".dockerenv">.dockerenv</a></li>
<li><a href="bin/">bin/</a></li>
<li><a href="boot/">boot/</a></li>
<li><a href="dev/">dev/</a></li>
<li><a href="etc/">etc/</a></li>
<li><a href="home/">home/</a></li>
<li><a href="lib/">lib/</a></li>
<li><a href="lib64/">lib64/</a></li>
<li><a href="media/">media/</a></li>
<li><a href="mnt/">mnt/</a></li>
<li><a href="opt/">opt/</a></li>
<li><a href="proc/">proc/</a></li>
<li><a href="root/">root/</a></li>
<li><a href="run/">run/</a></li>
<li><a href="sbin/">sbin/</a></li>
<li><a href="srv/">srv/</a></li>
<li><a href="sys/">sys/</a></li>
<li><a href="tmp/">tmp/</a></li>
<li><a href="usr/">usr/</a></li>
<li><a href="var/">var/</a></li>
</ul>
<hr>
</body>
</html>
注意:--bind 0.0.0.0 是一个http.server的参数;并不是Docker的参数。
此时网络图如下
结束语
本文翻译自 pythonspeed.com/articles/do… ,这篇文章让我收获还是挺大的,知道了interface的作用,知道了127.0.0.1和0.0.0.0的区别,还简单的了解到了Docker实现的原理。不过也挺尴尬的,都毕业一年半了,才知道这些东西!!!