原文链接:
问题场景:Egg 服务跑在127.0.0.1:3002
,通过 Docker-compose 进行端口映射8080:3002
,发现在访问8080 端口的时候一直报 connection refused 错误。
解决方案:将 127.0.0.1:3002
改成 0.0.0.0:3002
即可,具体可参照下述文章。
Connection refused? Docker 的网络
通常本地运行服务的时候,我们都会监听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 的问题(注:个人的 egg 服务监听 **127.0.0.1:3002**
,通过 Docker 进行端口映射,则直接报 connection refused)。
$ 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 可以使得服务可以正常的访问
1. 不使用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(注:即 Sever 监听 127.0.0.1),然后本地(注:Browser)直接访问这个服务,其可视化的图如下:
2. 网络的 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)。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。
3. 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
4. 解决方案:监听所有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的参数(注:就是说当有个 node 服务,如果监听的是**127.0.0.1**
,应修改成监听**0.0.0.0**
)。
5. 结束语
本文翻译自 pythonspeed.com/articles/do… ,这篇文章让我收获还是挺大的,知道了 interface 的作用,知道了 127.0.0.1 和 0.0.0.0 的区别,还简单的了解到了 Docker 实现的原理。不过也挺尴尬的,都毕业一年半了,才知道这些东西!!!
注:
127.0.0.1 是一个环回地址,并不表示“本机”。0.0.0.0 才是真正表示“本网络中的本机”。
在实际应用中,一般我们在服务端绑定端口的时候可以选择绑定到 0.0.0.0,这样我的服务访问方就可以通过我的多个ip地址访问我的服务。
比如我有一台服务器,一个外放地址A,一个内网地址B,如果我绑定的端口指定了0.0.0.0,那么通过内网地址或外网地址都可以访问我的应用。但是如果我只绑定了内网地址,那么通过外网地址就不能访问。 所以如果绑定0.0.0.0,也有一定安全隐患,对于只需要内网访问的服务,可以只绑定内网地址。
参考链接