Docker(四)Docker Container Network(容器互联、docker-compose.yml连接外部网络配置)

文章主要解决两个问题:

  1. 单独的容器A,连接到另一个容器B
  2. 在docker-compose.yml中如何配置连接到共用的容器(比如Nginx代理容器)

1. 场景1: 应用内的服务容器互联

当我们通过docker-compose编排了一组服务构成了一个应用,服务内部的网络可以简单的通过服务的网络配置+顶级(networks)网络配置连接(默认的网桥模式)

以下模拟了一个单独的应用服务(容器镜像均采用nginx),采用了三层:简单的三层:代理层、服务层、数据层,其中:

  • 代理层和服务层互联
  • 服务层处于中间网络状态,和代理层以及数据层互联
  • 数据层仅和服务层互联
// 简单的三层:代理层、服务层、数据层
version: "3.7"
services:
  proxy:
    image: "nginx:alpine"
    networks:
      - net-frontend
    extra_hosts:
      - "tkstorm_web:127.0.0.1"
      - "ioio.cool:127.0.0.1"
  websrv-a:
    image: "nginx:alpine"
    networks:
      - net-frontend
      - net-backend
  websrv-b:
    image: "nginx:alpine"
    networks:
      - net-frontend
      - net-backend
  db:
    image: "nginx:alpine"
    networks:
      - net-backend
  mq:
    image: "nginx:alpine"
    networks:
      - net-backend
networks:
    net-frontend:
    net-backend:

1.1. websrv-a的路由表(172.18.0.0/16、172.19.0.0/16、)

通过查看websrv-a服务的路由发现,其连接到了两个网络(net-frontendnet-backend

$ docker-compose exec websrv-a /bin/sh
/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.19.0.1      0.0.0.0         UG    0      0        0 eth0
172.18.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth1
172.19.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

2. 场景2: 容器直连某个网络

通过路由表查看到common-proxywebsrv-a在不同的网络段,故不能相互网络通信,若两个容器想相互通信,进行相关公共调试,有两种方式:

  1. common-proxy容器,加入到websrv-a所在桥接网络(net-frontend或者net-backend)均可
  2. websrv-a容器,加入到common-proxy所在桥接网络(net-proxy

2.1. common-proxy服务配置

version: "3.7"
services:
    common-proxy:
        container_name: "common-proxy"
        image: "nginx:alpine"
        networks:
            - net-proxy
networks:
    net-proxy:

2.2. common-proxy的路由表(172.20.0.0/16)

/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.20.0.1      0.0.0.0         UG    0      0        0 eth0
172.20.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

2.3. 查看网络情况

$ docker network ls
NETWORK ID          NAME                        DRIVER              SCOPE
83162e33b5ad        bridge                      bridge              local
abe01a786883        cproxy_net-proxy            bridge              local
220b80e495d7        host                        host                local
dd8cff4a042c        network_test_net-backend    bridge              local
206ffbb7bf2f        network_test_net-frontend   bridge              local
02f2210c0a6e        none                        null                local

2.4. 通过docker网络命令将容器连接到指定网络

通过上面的网络查看,我们知道common-proxy所在网络为cproxy_net-proxy,故可以通过以下命令,将websrv-a容器连入

// 将容器`network_test_websrv-a_1`加入`cproxy_net-proxy`网络,注意网络名称以及容器名称
$ docker network connect cproxy_net-proxy network_test_websrv-a_1

// 在查看websrv-a所在容器的路由,发现多了一个路由路径,websrv-a容器也创建了一个虚拟网络接口(eth2)
$ docker-compose exec websrv-a route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.20.0.1      0.0.0.0         UG    0      0        0 eth2
172.18.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth1
172.19.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0
172.20.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth2

3. 场景3: 通过compose将服务加入某个外部已存在网络

虽然我们可以通过手动方式加入容器到指定的网络,但每次这样操作很不方便。(每次都需要单独指定容器名称,若我们的websrv-a服务的容器很多,不可能一个一个的去连)。

故可以考虑在docker-compose.yml中设定容器连接外部命名网络(external: truename: cproxy_net-proxy),如下:

version: "3.7"
services:
  websrv-a:
    image: "nginx:alpine"
    networks:
      - net-proxy
      - net-backend
  websrv-b:
    image: "nginx:alpine"
    networks:
      - net-proxy
      - net-backend
  db:
    image: "nginx:alpine"
    networks:
      - net-backend
  mq:
    image: "nginx:alpine"
    networks:
      - net-backend
networks:
    net-backend:
    net-proxy:
        external: true
        name: cproxy_net-proxy
  • 顶级网络这块,net-proxy,我们设置了:
    • external: true,告知dockerd我们需要使用外部网络(卷、秘钥等也有类似操作),无需自动帮我创建net-proxy
    • name: cproxy_net-proxy,告知dockerd我们使用common-proxy的网络,因此websrv服务可以与common-proxy容器互联
  • websrv-awebsrv-b我们移除了proxy服务,以及net-frontend,均配置了net-proxy,使common-proxywebsrv服务可以互通

4. 最后,网络命名优化

上面我们在使用过程中,如果不指定网络的命名,则docker默认会在网络命名前加上目录_前缀,比如cproxy_,为此,我们可以通过指定命名来优化:

4.1. common-proxy代理应用

version: "3.7"
services:
    common-proxy:
        container_name: "common-proxy"
        image: "nginx:alpine"
        networks:
            net-proxy:
                aliases:
                    - pshare_net
networks:
    net-proxy:
        name: "my-proxy-net"

4.2. websrv系列应用

version: "3.7"
services:
  websrv-a:
    image: "nginx:alpine"
    container_name: "websrv-a"
    networks:
      - net-proxy
      - net-backend
  websrv-b:
    image: "nginx:alpine"
    container_name: "websrv-b"
    networks:
      - net-proxy
      - net-backend
  db:
    container_name: "web-db"
    image: "nginx:alpine"
    networks:
      - net-backend
  mq:
    container_name: "web-mq"
    image: "nginx:alpine"
    networks:
      - net-backend
networks:
    net-backend:
    net-proxy:
        external: true
        name: my-proxy-net

4.3. 容器运行情况

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
80ccb82bb645        nginx:alpine        "nginx -g 'daemon of…"   About a minute ago   Up About a minute   80/tcp              websrv-b
661146aaa3d8        nginx:alpine        "nginx -g 'daemon of…"   About a minute ago   Up About a minute   80/tcp              web-db
84def129e814        nginx:alpine        "nginx -g 'daemon of…"   About a minute ago   Up About a minute   80/tcp              websrv-a
09ada62ac946        nginx:alpine        "nginx -g 'daemon of…"   About a minute ago   Up About a minute   80/tcp              web-mq
09fee51bceaa        nginx:alpine        "nginx -g 'daemon of…"   16 minutes ago       Up 16 minutes       80/tcp              common-proxy

4.4. 连通性调试

/ # ping websrv-a -c 2
PING websrv-a (172.23.0.3): 56 data bytes
64 bytes from 172.23.0.3: seq=0 ttl=64 time=0.078 ms
64 bytes from 172.23.0.3: seq=1 ttl=64 time=0.087 ms

--- websrv-a ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.078/0.082/0.087 ms
/ # ping websrv-b -c 2
PING websrv-b (172.23.0.4): 56 data bytes
64 bytes from 172.23.0.4: seq=0 ttl=64 time=0.072 ms
64 bytes from 172.23.0.4: seq=1 ttl=64 time=0.124 ms

--- websrv-b ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.072/0.098/0.124 ms

Tips:如果由多个容器,通过dns解析的容器可能是多组容器中的一个(非指定确定的IP容器)

4.5. 清理工作

  • 之前的调试网络删除(注意删除之前先核查)
$ docker network prune
WARNING! This will remove all networks not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Networks:
network_test_net-frontend
cproxy_net-proxy

5. 补充 - 基于alpine镜像模拟网络连通性测试

如果不使用nginx的镜像,可以使用alpine的镜像(更小),适合网络模拟,加入entrypoint,使其进程不退出(Tips:注意该方式仅开发调试用)

此处,通过tail -f /dev/null进程未退出,模拟了一个服务的常驻,然后基于debug-srv进行调试

version: "3.7"
services:
    debug-srv:
        container_name: "debug-srv"
        image: "alpine:latest"
        entrypoint: ["tail", "-f", "/dev/null"]
        networks:
            debug-net:
networks:
    debug-net:
        name: "debug-net"

6. 其他方面网络连通性问题

6.1. 无法在Mac宿主机器,访问容器服务

可以将容器内的服务端口映射到Mac主机,如此可以在Mac宿主机器上面访问容器内的服务(常用)

// 端口映射
docker run --publish 8000:80 --name webserver nginx
docker run -d -P --name webserver nginx

6.2. 无法在容器内直接访问Mac上的服务

我们可能在Mac上面装了Mysql或者MQ其他类似的服务,我们希望在容器内直接连接MAC上面运行的服务;

默认情况,Docker For Mac,在容器内无法直接与MAC主机通讯,我们可以采用:

  • 基于host网络(和Mac网络一致,Mac上面的端口均可以在容器内可用,包括不想要的服务)
  • 基于特定的DNS解析:host.docker.internalgateway.docker.internal,更早一些的Docker版本可能有不同的DNS域名,可自行Google
  • 偏门:给lo0添加别名、代理通道等方式(推荐上面两种)
// 方式1:基于host网络
$ docker run -it --rm --network=host alpine /bin/sh
/ # netstat -ntlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:35135           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      -
tcp        0      0 :::8086                 :::*                    LISTEN      -
tcp        0      0 :::3000                 :::*                    LISTEN      -
tcp        0      0 :::443                  :::*                    LISTEN      -
tcp        0      0 :::32768                :::*                    LISTEN      -
tcp        0      0 :::58671                :::*                    LISTEN      -
tcp        0      0 :::111                  :::*                    LISTEN      -
tcp        0      0 :::80                   :::*                    LISTEN      -
tcp        0      0 :::2003                 :::*                    LISTEN      -
tcp        0      0 :::8083                 :::*                    LISTEN      -

// 方式2:基于DNS解析
/ # curl -I host.docker.internal
HTTP/1.1 200 OK
Server: nginx/1.14.1
Date: Mon, 05 Aug 2019 10:11:41 GMT
Content-Type: text/html; charset=utf-8
Connection: keep-alive

6.3. 无法在容器内的ping通外部Host

  1. 启动一个新的干净的alpine镜像,检测是否仍然ping不同(可能之前我们自己有改动了一些route或者dns解析配置等)
  2. 如果不行,尝试重启Docker服务可以解决(我之前就出现过类型的情况,在容器内无法ping通外部域名)

7. 小结

我们可以通过单个docker network connect直接简单的将两个容器连接在一起,让容器互通,但这种方式在容器过多情况下不方便;为此,在docker-compose.yml中我们可以通过指定外部已存在网络来告知服务连接。

由于docker的网络命名方式(默认情况下与目录前缀相关),我们可以通过指定命名网络依旧命名容器来方便调试。

通过这种应用容器编排和网络互连,可以做到统一网关和域名的入口配置(比如在common-proxy上面的Nginx做统一的网关操作),而不用重复的在多个应用中不断的导出映射不同端口或申总通过创建VM以IP区分方式来解决类似共用80、443端口复用问题。

最后,还针对不同容器的网络连通问题做了简要梳理,希望可以节省大家一部分时间!

8. 参考

  1. https://docs.docker.com/compose/compose-file/#networks
  2. https://docs.docker.com/network/bridge/