原文链接

问题场景:Egg 服务跑在127.0.0.1:3002,通过 Docker-compose 进行端口映射8080:3002,发现在访问8080 端口的时候一直报 connection refused 错误。
解决方案:将 127.0.0.1:3002 改成 0.0.0.0:3002 即可,具体可参照下述文章。
企业微信20210820021959.png

Connection refused? Docker 的网络

通常本地运行服务的时候,我们都会监听127.0.0.1,比如下面这样(需要安装python3)

  1. $ python3 -m http.server --bind 127.0.0.1
  2. 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)。

  1. $ docker run -p 8000:8000 -itd python:3.7-slim python3 -m http.server --bind 127.0.0.1
  2. c2e94f44dc86dc48b9b4d03cc547c265134d070c55c9c144e5d0adcfd0da85a9
  3. $ curl 127.0.0.1:8000
  4. curl: (56) Recv failure: 连接被对方重设

这个是为什么呢?为了知道如何解决这个为题,我们需要最小程度的了解 Docker 网络是如何运行的。这边文章将会覆盖如下三方面:

  • 网络的 namespaces,以及Docker是如何使用它们的
  • docker run -p 5000:5000 到底做了什么,以及为什么上述的例子不能正常访问
  • 如何修复 image 可以使得服务可以正常的访问

1. 不使用Docker时候的网络

我们从第一个场景出发:直接在操作系统内运行一个服务,然后直接访问这个服务。操作系统有多个网络的interfaces。例如我的电脑就有如下的网络接口:

  1. $ ifconfig
  2. docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
  3. inet 172.17.0.1
  4. ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
  5. inet 192.168.0.101
  6. lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
  7. 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)直接访问这个服务,其可视化的图如下:
image.png

2. 网络的 namespaces

你可以注意到上图中是一个**Default network namespace**,这个是什么?

Docker 是一个运行容器的系统: 一种让进程间互相隔离的方式。这个特性是基于一系列 Linux 内核特性构建起来的,其中一个就是 network namespaces —— 一种使得不同的进程拥有不同的网络设备、IPs、防护墙规则等

默认情况下,每个由 Docker 运行的容器都有自己的 network namespace 和自己的IP:

  1. $ docker run --rm -it busybox
  2. / # ifconfig
  3. eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
  4. inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
  5. lo Link encap:Local Loopback
  6. inet addr:127.0.0.1 Mask:255.0.0.0

所以这个容器有两个 interface(eth0 和 lo)。eth0 和 lo 每个都有自己的 IP 地址。但是由于这个是另外一个network namespace,所以上面的 Default network namespace 是不同的。

为了让上面的表述更加清晰,我们在容器中运行 http.server:

  1. docker run -itd python:3.7-slim python3 -m http.server --bind 127.0.0.1

其网络结构如下图:
image.png
现在我们知道连接为什么会被拒绝了:服务监听的是容器 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 端口。

让我们来运行一个容器,通过图标来可视化这样做到底意味着什么

  1. $ docker run -itd -p 8000:8000 python:3.7-slim python3 -m http.server --bind 127.0.0.1

image.png

现在我们遇到了第二个问题:服务监听的是容器 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了,问题就得到了解决。

  1. $ docker run -p 8000:8000 -itd python:3.7-slim python3 -m http.server --bind 0.0.0.0
  2. 1bd03d90310f02b5c8ca1d95f4dbadce543c6ca4a5741f5bbc6286e6a2b72850
  3. $ curl 127.0.0.1:8000
  4. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
  5. <html>
  6. <head>
  7. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  8. <title>Directory listing for /</title>
  9. </head>
  10. <body>
  11. <h1>Directory listing for /</h1>
  12. <hr>
  13. <ul>
  14. <li><a href=".dockerenv">.dockerenv</a></li>
  15. <li><a href="bin/">bin/</a></li>
  16. <li><a href="boot/">boot/</a></li>
  17. <li><a href="dev/">dev/</a></li>
  18. <li><a href="etc/">etc/</a></li>
  19. <li><a href="home/">home/</a></li>
  20. <li><a href="lib/">lib/</a></li>
  21. <li><a href="lib64/">lib64/</a></li>
  22. <li><a href="media/">media/</a></li>
  23. <li><a href="mnt/">mnt/</a></li>
  24. <li><a href="opt/">opt/</a></li>
  25. <li><a href="proc/">proc/</a></li>
  26. <li><a href="root/">root/</a></li>
  27. <li><a href="run/">run/</a></li>
  28. <li><a href="sbin/">sbin/</a></li>
  29. <li><a href="srv/">srv/</a></li>
  30. <li><a href="sys/">sys/</a></li>
  31. <li><a href="tmp/">tmp/</a></li>
  32. <li><a href="usr/">usr/</a></li>
  33. <li><a href="var/">var/</a></li>
  34. </ul>
  35. <hr>
  36. </body>
  37. </html>

注意--bind 0.0.0.0 是一个 http.server 的参数,并不是 Docker的参数(注:就是说当有个 node 服务,如果监听的是**127.0.0.1**,应修改成监听**0.0.0.0**)。

此时网络图如下 image.png

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,也有一定安全隐患,对于只需要内网访问的服务,可以只绑定内网地址。

参考链接