Docker容器间通信方法

33,549 阅读9分钟

几天前,为了解决日常在本地进行日常工作和开发测试之间的矛盾,利用docker在Windows系统中搭建了基于Linux的测试环境:借助Docker,在win10下编码,一键在Linux下测试。在这边文章里主要介绍了如何在本地通过docker构建与生产环境基本一致的环境并一键运行、测试我们的代码。Docker官方建议每个容器中只运行一个服务[1],但是我们的项目可能是由多个服务组成,在服务中可能会需要mysql、redis等中间件的支持,所以通常我们将一个项目的不同服务划分到不同容器中。这种做法虽然具有低耦合、高隔离性等优点,但是也增加了服务之间通信的复杂度。

Docker容器间的通信方式根据媒介可以分为:volume共享通信网络通信等;根据通信范围也可以分为:同主机通信跨主机通信等。而本文主要针对容器间的网络通信方法进行讨论。

1. Docker的网络驱动模型

Docker的网络驱动模型分类:

  • bridge:Docker中默认的网络驱动模型,在启动容器时如果不指定则默认为此驱动类型;
  • host:打破Docker容器与宿主机之间的网络隔离,直接使用宿主机的网络环境,该模型仅适用于Docker17.6及以上版本;
  • overlay:可以连接多个docker守护进程或者满足集群服务之间的通信;适用于不同宿主机上的docker容器之间的通信;
  • macvlan:可以为docker容器分配MAC地址,使其像真实的物理机一样运行;
  • none:即禁用了网络驱动,需要自己手动自定义网络驱动配置;
  • plugins:使用第三方网络驱动插件;

以上是Docker支持的几种网络驱动模型,它们都具有独特的特点和应用范围,为了更加详细地了解Docker的网络运行原理,下面挑选几种较为重要的网络模型进行研究。

2. bridge

2.1 docker的默认网桥

bridge原理示意图

如上图所示为Docker中bridge驱动模式的示意图,其中蓝色的模块表示主机上的网卡。当Docker启动时会自动在主机上创建一个虚拟网桥docker0,使用默认网络模式创建docker容器时会自动创建一对儿veth pair接口,一端连接在docker容器中(如图容器中的eth0),一端连接在虚拟网桥docker0上(如图veth)。这种veth pair是一种虚拟网络设备,主要用于不同namespace中(意味着网络隔离)的网络通信,它总是成对存在的。在这里可以把它想象成一对儿靠虚拟网线连接起来的两个虚拟网卡,一端连接着docker容器,一端连接着虚拟网桥docker0

通过这种方式,不同docker容器之间可以通过ip地址互相通信,也可以通过虚拟网桥访问主机上的网络eth0(添加iptables规则,将docker容器对目标地址发出的访问通过地址伪装的方式修改为主机对目标地址进行访问)。

如果想要外界网络访问docker容器时,需要在docker容器启动时加上参数'-p [主机端口]:[容器端口]'进行端口映射,原理也是通过修改iptables规则将访问[主机端口]的数据转发到docker容器的[容器端口]中,但是这种做法也存在着占用主机有限的端口资源的缺点。

在主机上通过命令docker network ls可以查看docker中存在的网络:

docker network ls
# 输出结果:
NETWORK ID          NAME                DRIVER              SCOPE
e79b7548b225        bridge              bridge              local
666d5f1f459d        host                host                local
d0d785cf4794        none                null                local

然后通过命令docker network inspect bridge查看bridge网络的详细配置:

docker network inspect bridge
# 输出结果:
[
    {
        "Name": "bridge",
        "Id": "e79b7548b225c3c80d0f70d0de0b5911ed70a7f39ac20f75a8ae71c5cef05b3a",
        "Created": "2019-05-17T13:01:27.6581642Z",
        "Scope": "local",
        "Driver": "bridge",         # bridge驱动模式
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",          # 子网,docker容器的IP范围
                    "Gateway": "172.17.0.1"             # 网关,即docker0
                }
            ]
        },
        ...
        省略
        ...
        # 两个docker容器(一个mysql容器,一个Web服务容器)的网络配置
        "Containers": {
            # mysql容器
            "d6f33e9bbd60e10d02dd2eebea424a7fc129d9646c96742ec3fe467833017679": {
                "Name": "mysqld5.7",
                "EndpointID": "32e900f33367e3570c416c43a5618bd7a742cf94f36799e92895951ed1784736",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",         # 容器的IP
                "IPv6Address": ""
            },
            # 基于tornado的Web服务容器
            "dd292d535d16bbfe5382e29486756f4dddfea8e9b10af769db61618d739c5c4e": {
                "Name": "test_demo",
                "EndpointID": "eaf8e294f7b54aa50c6e6b30ac91f63b1a0ccbc5b56d6fbdcfeacd0471b15eb3",
                "MacAddress": "02:42:ac:11:00:03",
                "IPv4Address": "172.17.0.3/16",         # 容器的IP
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

由上述配置信息可以看出,docker网桥的网关的IP为"172.17.0.1",也即docker0,而docker的子网为"172.17.0.0/16",docker将会为容器在"172.17.0.0/16"中分配IP,如其中的mysql容器的IP为"172.17.0.2/16"、test_demo容器的IP为"172.17.0.3/16"。由于不同容器通过veth pair连接在虚拟网桥docker0上,所以容器之间可以通过IP互相通信,但是无法通过容器名进行通信:

# 在test_demo容器中访问mysqld5.7容器(通过IP)
ping 172.17.0.2 -c 3
# 输出结果:
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.147 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.185 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=64 time=0.209 ms

--- 172.17.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2057ms
rtt min/avg/max/mdev = 0.147/0.180/0.209/0.027 ms


# 在test_demo容器中访问mysqld5.7容器(通过容器名)
ping mysqld5.7
# 输出结果:
ping: mysqld5.7: Name or service not known

所以,默认的网桥bridge上的容器只能通过IP互连,无法通过DNS解析名称或别名。假如我们在container1中部署了Web服务,在container2中部署了mysql,container1中的Web服务往往需要连接container2的mysql,这是只能靠IP进行连接,但是docker也无法保证容器重启后的IP地址不变,所以更好的方式是通过别名进行互联,在网络中加入DNS服务器,将容器名与IP地址进行匹配,省去了手动修改Web服务中连接mysql的IP的过程。

为了实现不同容器通过容器名或别名的互连,docker提供了以下几种:

  1. 在启动docker容器时加入--link参数,但是目前已经被废弃,废弃的主要原因是需要在连接的两个容器上都创建--link选项,当互连的容器数量较多时,操作的复杂度会显著增加;

  2. 启动docker容器后进入容器并修改/etc/host配置文件,缺点是手动配置较为繁杂;

  3. 用户自定义bridge网桥,这是目前解决此类问题的主要方法;

2.2 用户自定义bridge

用户自定义bridge相对于使用默认bridge的主要优势:

  • 用户自定义bridge可以在容器化的应用程序提供更好的隔离效果和更好的互通性: 这段话看似有些矛盾,但是其中更好的隔离效果是针对外界网络,而更好的互通性则是指同一bridge下的不同容器之间。还是以之前的分别部署了Web服务和mysql服务的两个容器container1、container2为例,container1只需要对外界网络暴露Web服务的80端口,而负责后端的container2只需与container1互连,不需要对外暴露,有效地保护了后端容器的安全性,提高了容器对外的隔离效果。而同属于用户自定义bridge的容器container1、container2之间自动将所有端口暴露,方便容器间进行无障碍的通信,而不会遭受到外界的意外访问。
  • 用户自定义bridge在容器之间提供了自动DNS解析: 这也是本文讨论的重点,不同于默认bridge只能通过IP互连的限制,用户自定义的bridge自动提供了容器间的DNS解析功能,容器间可以通过容器名或别名进行通信。
2.2.1 如何使用用户自定义bridge
  1. 创建用户自定义bridge:

    docker network create my-net        # 创建了一个名为"my-net"的网络
    
  2. 将Web服务容器和mysql服务容器加入到"my-net"中,并观察变化:

    docker network connect my-net test_demo         # 将Web服务加入my-net网络中
    docker network connect my-net mysqld5.7         # 将mysql服务加入my-net网络中
    
    # 查看my-net的网络配置
    docker network inspect my-net
    # 输出结果(省略部分内容):
    [
        {
            "Name": "my-net",
            "Scope": "local",
            "Driver": "bridge",
            "EnableIPv6": false,
            "IPAM": {
                "Config": [
                    {
                        "Subnet": "172.18.0.0/16",      # my-net的子网
                        "Gateway": "172.18.0.1"         # my-net的网关
                    }
                ]
            },
            "Containers": {
                "d6f33e9bbd60e10d02dd2eebea424a7fc129d9646c96742ec3fe467833017679": {
                    "Name": "mysqld5.7",
                    "EndpointID": "7d0e8d70bb523cceb4d2d8d4e3f8231fc68332c70f7f9b4e5d4abccce2b31a65",
                    "MacAddress": "02:42:ac:12:00:03",
                    "IPv4Address": "172.18.0.3/16",         # mysql服务容器的IP,与之前不同
                    "IPv6Address": ""
                },
                "dd292d535d16bbfe5382e29486756f4dddfea8e9b10af769db61618d739c5c4e": {
                    "Name": "test_demo",
                    "EndpointID": "f7802f1af81e258f77e227609dfdcdf66c49f19776381eb8b0dca6d9e794ccad",
                    "MacAddress": "02:42:ac:12:00:02",
                    "IPv4Address": "172.18.0.2/16",         # Web服务容器的IP,与之前不同
                    "IPv6Address": ""
                }
            },
        }
    ]
    

    如上所示,用户自定义网络my-net的子网为"172.18.0.0/16",所以两docker容器在my-net网络中的IP分别为:"172.18.0.3/16"、"172.18.0.2/16",与之前的"172.17.0.2/16"、"172.17.0.3/16"不同。

  3. 通过容器名或别名互连: 进入到Web服务器container1中连接container2:

    ping mysqld5.7 -c 3
    # 输出结果:
    PING mysqld5.7 (172.18.0.3) 56(84) bytes of data.
    64 bytes from mysqld5.7.my-net (172.18.0.3): icmp_seq=1 ttl=64 time=0.066 ms
    64 bytes from mysqld5.7.my-net (172.18.0.3): icmp_seq=2 ttl=64 time=0.145 ms
    64 bytes from mysqld5.7.my-net (172.18.0.3): icmp_seq=3 ttl=64 time=0.143 ms
    
    --- mysqld5.7 ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 2048ms
    rtt min/avg/max/mdev = 0.066/0.118/0.145/0.036 ms
    

    如上所示,我们已经可以通过容器名"mysqld5.7"连接mysql服务器了。

  4. 断开网络: 由于我们的容器仍然连接着默认bridgedocker0,而现在我们已经不需要它,所以应该将容器与docker0的连接断开,执行以下操作:

    # 断开容器与docker0的连接
    docker network disconnect bridge test_demo
    docker network disconnect bridge mysqld5.7
    

3. overlay

关键字:跨主机通信、集群

【待续。。】


  1. Run multiple services in a container. docs.docker.com/config/cont… ↩︎