Docker容器跨主机通信之:直接路由方式

Docker容器跨主机通信之:直接路由方式

一、Docker网络基本原理

直观上看,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)与外界相通,并可以收发数据包;此外,如果不同子网之间要进行通信,需要额外的路由机制。

 

Docker中的网络接口默认都是虚拟的接口。虚拟接口的最大优势就是转发效率极高。这是因为Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将被直接复制到接收接口的接收缓存中,而无需通过外部物理网络设备进行交换。对于本地系统和容器内系统来看,虚拟接口跟一个正常的以太网卡相比并无区别,只是它速度要快得多。

 

Docker容器网络就很好地利用了Linux虚拟网络技术,在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做veth pair)。

Docker容器跨主机通信之:直接路由方式_第1张图片

一般情况下,Docker创建一个容器的时候,会具体执行如下操作:

1.创建一对虚拟接口,分别放到本地主机和新容器的命名空间中;

2.本地主机一端的虚拟接口连接到默认的docker0网桥或指定网桥上,并具有一个以veth开头的唯一名字,如veth1234;

3.容器一端的虚拟接口将放到新创建的容器中,并修改名字作为eth0。这个接口只在容器的命名空间可见;

4.从网桥可用地址段中获取一个空闲地址分配给容器的eth0(例如172.17.0.2/16),并配置默认路由网关为docker0网卡的内部接口docker0的IP地址(例如172.17.42.1/16)。

完成这些之后,容器就可以使用它所能看到的eth0虚拟网卡来连接其他容器和访问外部网络。用户也可以通过docker network命令来手动管理网络。

 

二、Docker网络默认模式

按docker官方的说法,docker容器的网络有五种模式:

 View Code

 

bridge模式

bridge模式是docker默认的,也是开发者最常使用的网络模式。在这种模式下,docker为容器创建独立的网络栈,保证容器内的进程使用独立的网络环境,
实现容器之间、容器与宿主机之间的网络栈隔离。同时,通过宿主机上的docker0网桥,容器可以与宿主机乃至外界进行网络通信。
其网络模型可以参考下图:

 Docker容器跨主机通信之:直接路由方式_第2张图片

 

从上面的网络模型可以看出,容器从原理上是可以与宿主机乃至外界的其他机器通信的。同一宿主机上,容器之间都是连接掉docker0这个网桥上的,它可以作为虚拟交换机使容器可以相互通信。
然而,由于宿主机的IP地址与容器veth pair的 IP地址均不在同一个网段,故仅仅依靠veth pair和namespace的技术,还不足以使宿主机以外的网络主动发现容器的存在。为了使外界可以方位容器中的进程,docker采用了端口绑定的方式,也就是通过iptables的NAT,将宿主机上的端口
端口流量转发到容器内的端口上。

 View Code

 

三、方案介绍

概述

就目前Docker自身默认的网络来说,单台主机上的不同Docker容器可以借助docker0网桥直接通信,这没毛病,而不同主机上的Docker容器之间只能通过在主机上用映射端口的方法来进行通信,有时这种方式会很不方便,甚至达不到我们的要求,因此位于不同物理机上的Docker容器之间直接使用本身的IP地址进行通信很有必要。再者说,如果将Docker容器起在不同的物理主机上,我们不可避免的会遭遇到Docker容器的跨主机通信问题。本文就来尝试一下。

情景构造

如下图所示,我们有两个物理主机1和主机2,我们在各自宿主机上启动一个centos容器,启动成功之后,两个容器分别运行在两个宿主机之上,默认的IP地址分配如图所示,这也是Docker自身默认的网络。

Docker容器跨主机通信之:直接路由方式_第3张图片

 

 

此时两台主机上的Docker容器如何直接通过IP地址进行通信?

一种直接想到的方案便是通过分别在各自主机中 添加路由 来实现两个centos容器之间的直接通信。我们来试试吧

 

方案原理分析

由于使用容器的IP进行路由,就需要避免不同主机上的容器使用了相同的IP,为此我们应该为不同的主机分配不同的子网来保证。于是我们构造一下两个容器之间通信的路由方案,如下图所示。

 Docker容器跨主机通信之:直接路由方式_第4张图片

 

 

各项配置如下:

  • 主机1的IP地址为:192.168.91.128
  • 主机2的IP地址为:192.168.91.129
  • 为主机1上的Docker容器分配的子网:10.0.128.0/24
  • 为主机2上的Docker容器分配的子网:10.0.129.0/24

这样配置之后,两个主机上的Docker容器就肯定不会使用相同的IP地址从而避免了IP冲突。

我们接下来 定义两条路由规则 即可:

  • 所有目的地址为10.0.128.0/24的包都被转发到主机1上
  • 所有目的地址为10.0.129.0/24的包都被转发到主机2上

综上所述,数据包在两个容器间的传递过程如下:

  • 从container1 发往 container2 的数据包,首先发往container1的“网关”docker0,然后通过查找主机1的路由得知需要将数据包发给主机2,数据包到达主机2后再转发给主机2的docker0,最后由其将数据包转到container2中;反向原理相同,不再赘述。

我们心里方案想的是这样,接下来实践一下看看是否可行。

 

四、实际试验

环境介绍

操作系统 服务器地址 Dockerd地址
ubuntu-16.04.5-server-amd64 192.168.91.128 10.0.128.2
ubuntu-16.04.5-server-amd64 192.168.91.129 10.0.129.2

 

 

 

 

请确保已经安装好了docker,这里是使用以下命令安装的

apt-get install -y docker.io

 

docker的版本为  17.03.2-ce

 

比如主机1,我需要运行一个docker镜像,要求它的网段必须是 10.0.128.0/24 ,怎么设置呢?

有3个办法:

1. 修改docker0的网段

2. 创建一个docker网桥

 

这里为了快速实现,选用第一种方案。直接修改 /etc/default/docker 文件,添加 DOCKER_OPTS 参数即可

修改docker0

分别对主机1和主机2上的docker0进行配置

主机1

编辑主机1上的 /etc/default/docker 文件,最后一行添加

DOCKER_OPTS="--bip 10.0.128.1/24"

 

特别注意,DOCKER_OPTS参数后面必须有引号。--bip后面的ip就是docker0的ip地址,一般从第一个ip开始!

不要修改 /etc/docker/daemon.json 文件添加bip,我测试了一些,重启docker会报错!

 

主机2

编辑主机1上的 /etc/default/docker 文件,最后一行添加

DOCKER_OPTS="--bip 10.0.129.1/24"

 

重启docker服务

主机1和主机2上均执行如下命令重启docker服务以使修改后的docker0网段生效

systemctl restart docker

 

查看主机1上docker0的ip地址

复制代码

root@ubuntu:~# ifconfig docker0
docker0   Link encap:Ethernet  HWaddr 02:42:8a:46:e2:eb
          inet addr:10.0.128.1  Bcast:0.0.0.0  Mask:255.255.255.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

复制代码

 

查看主机2上docker0的ip地址

复制代码

root@ubuntu:~# ifconfig docker0
docker0   Link encap:Ethernet  HWaddr 02:42:22:b1:25:66
          inet addr:10.0.129.1  Bcast:0.0.0.0  Mask:255.255.255.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

复制代码

 

发现默认的网段已经改变了!

 

添加路由规则

主机1

查看路由表

复制代码

root@ubuntu:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.91.2    0.0.0.0         UG    0      0        0 ens32
10.0.128.0      0.0.0.0         255.255.255.0   U     0      0        0 docker0192.168.91.0    0.0.0.0         255.255.255.0   U     0      0        0 ens32

复制代码

 

默认只有自己本身的路由,如果需要访问 10.0.129.0/24 网段,需要添加路由

 

主机1上添加路由规则如下:

route add -net 10.0.129.0/24 gw 192.168.91.129

 

gw 表示下一跳地址,这里的地址就是主机2的ip地址

 

再次查看路由,发现已经添加上了

复制代码

root@ubuntu:~# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.91.2    0.0.0.0         UG    0      0        0 ens32
10.0.128.0      0.0.0.0         255.255.255.0   U     0      0        0 docker0
10.0.129.0      192.168.91.129  255.255.255.0   UG    0      0        0 ens32
192.168.91.0    0.0.0.0         255.255.255.0   U     0      0        0 ens32

复制代码

 

主机2

主机2上添加路由规则如下:

route add -net 10.0.128.0/24 gw 192.168.91.128

 

在主机1上,ping主机2的docker0地址

复制代码

root@ubuntu:~# ping 10.0.129.1 -c 1
PING 10.0.129.1 (10.0.129.1) 56(84) bytes of data.
64 bytes from 10.0.129.1: icmp_seq=1 ttl=64 time=1.35 ms

--- 10.0.129.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.355/1.355/1.355/0.000 ms

复制代码

 

在主机2上,ping主机1的docker0地址

复制代码

root@ubuntu:~# ping 10.0.128.1 -c 1
PING 10.0.128.1 (10.0.128.1) 56(84) bytes of data.
64 bytes from 10.0.128.1: icmp_seq=1 ttl=64 time=1.73 ms

--- 10.0.128.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.732/1.732/1.732/0.000 ms

复制代码

 

ok,既然docker0都通了,那么起一个docker容器,会不会也是通的的呢?测试一下吧

在主机1上面启动一个容器,这里选用apline镜像,它只有4.5M,并且自带ping命令!

先查看ip地址

复制代码

root@ubuntu:~# docker run -it alpine
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:0A:00:80:02
          inet addr:10.0.128.2  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::42:aff:fe00:8002/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:14 errors:0 dropped:0 overruns:0 frame:0
          TX packets:7 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1156 (1.1 KiB)  TX bytes:578 (578.0 B)

复制代码

 

它的ip地址为 10.0.128.2 ,由于docker0占用了第一个ip地址。所以容器启动时,ip地址从第二个开始分配!

 

在主机2上面启动一个容器

复制代码

root@ubuntu:~# docker run -it alpine
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:0A:00:81:02
          inet addr:10.0.129.2  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::42:aff:fe00:8102/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:12 errors:0 dropped:0 overruns:0 frame:0
          TX packets:6 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1016 (1016.0 B)  TX bytes:508 (508.0 B)

复制代码

 

在主机1上的容器中 ping 主机2中的容器

先来ping 主机2的docker0,再ping 主机2中的容器

复制代码

/ # ping 10.0.129.1 -c 1
PING 10.0.129.1 (10.0.129.1): 56 data bytes
64 bytes from 10.0.129.1: seq=0 ttl=63 time=0.419 ms

--- 10.0.129.1 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.419/0.419/0.419 ms
/ # ping 10.0.129.2 -c 1
PING 10.0.129.2 (10.0.129.2): 56 data bytes

--- 10.0.129.2 ping statistics ---
1 packets transmitted, 0 packets received, 100% packet loss

复制代码

 

从结果中,可以发现。docker0是通的,但是主机2中的容器是不通的,为什么呢?

Docker Bridge创建创建过程

Docker容器跨主机通信之:直接路由方式_第5张图片

1)首先宿主机上创建一对虚拟网卡veth pair设备,veth设备总是成对出现的,形成一个通信通道,数据传输就是基于这个链路的,veth设备常用来连接两个网络设备

2)Docker将veth pair设备的一端放在容器中,并命名为eth0,然后将另一端加入docker0网桥中,可以通过brctl show命令查看

3)从docker0字网卡中分配一个IP到给容器使用,并设置docker0的IP地址为容器默认网关

4)此时容器IP与宿主机是可以通信的,宿主机也可以访问容器中的ip地址,在bridge模式下,连接同一网桥的容器之间可以相互通信,同时容器可以访问外网,但是其他物理机不能访问docker容器IP,需要通过NAT将容器的IP的port映射为宿主机的IP和port;

 

在主机1上面,再开一个窗口,使用ifconfig查看

 View Code

 

会发现有一个 veth077daec 的网卡设备。咦,这是个啥?

当运行docker容器后,再次执行ifconfig命令可以看到会多出个网卡驱动veth开头的名字,所以补充下veth。

veth

Linux container 中用到一个叫做veth的东西,这是一种新的设备,专门为 container 所建。veth 从名字上来看是 Virtual ETHernet 的缩写,它的作用很简单,就是要把从一个 network namespace 发出的数据包转发到另一个 namespace。veth 设备是成对的,一个是 container 之中,另一个在 container 之外,即在真实机器上能看到的。
VETH设备总是成对出现,一端请求发送的数据总是从另一端以请求接受的形式出现。创建并配置正确后,向其一端输入数据,VETH会改变数据的方向并将其送入内核网络子系统,完成数据的注入,而在另一端则能读到此数据。(Namespace,其中往veth设备上任意一端上RX到的数据,都会在另一端上以TX的方式发送出去)veth工作在L2数据链路层,veth-pair设备在转发数据包过程中并不串改数据包内容。
Docker容器跨主机通信之:直接路由方式_第6张图片

成数据的注入,而在另一端则能读到此数据。(Namespace,其中往veth设备上任意一端上RX到的数据,都会在另一端上以TX的方式发送出去)veth工作在L2数据链路层,veth-pair设备在转发数据包过程中并不串改数据包内容。
显然,仅有veth-pair设备,容器是无法访问网络的。因为容器发出的数据包,实质上直接进入了veth1设备的协议栈里。如果容器需要访问网络,需要使用bridge等技术,将veth1接收到的数据包通过某种方式转发出去 。
veth参考链接:http://blog.csdn.net/sld880311/article/details/77650937
 

因此,如果要多台主机之间的docker通信,需要使用NAT转换。那么接下来,就是设置iptables规则了!

配置iptables规则

主机1

在主机1上查看默认的nat 规则

复制代码

root@ubuntu:~# iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere            !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  10.0.128.0/24        anywhere

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere

复制代码

 

这些nat规则,都是docker帮你做的。

增加一条规则

iptables -t nat -I PREROUTING -s 10.0.128.0/24 -d 10.0.129.0/24 -j DNAT --to 10.0.128.1

 

PREROUTING:可以在这里定义进行目的NAT的规则,因为路由器进行路由时只检查数据包的目的ip地址,所以为了使数据包得以正确路由,我们必须在路由之前就进行目的NAT;

 

上面那一条路由规则是啥意思呢?就是当源地址为10.0.128.0/24网段 访问 10.0.129.0/24 时,在路由之前,将ip转换为10.0.128.1。

注意:一定要加-d参数。如果不加,虽然docker之间可以互通,但是不能访问网站,比如百度,qq之类的!

为什么呢?访问10.0.129.0/24 时,通过docker0网卡出去的。但是访问百度,还是通过docker0,就出不去了!

真正连接外网的是ens32网卡,必须通过它才行!因此必须要指定-d参数!

 

这个时候,直接ping 主机2上的docker地址

复制代码

/ # ping 10.0.129.2 -c 3
PING 10.0.129.2 (10.0.129.2): 56 data bytes
64 bytes from 10.0.129.2: seq=0 ttl=64 time=0.073 ms
64 bytes from 10.0.129.2: seq=1 ttl=64 time=0.143 ms
64 bytes from 10.0.129.2: seq=2 ttl=64 time=0.149 ms

--- 10.0.129.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.073/0.121/0.149 ms

复制代码

 

是可以通讯的!

注意:iptables必须在 PREROUTING 上面做,而不是常规的 POSTROUTING。我测试在POSTROUTING做规则,始终无法通讯!

 

主机2

主机2上添加如下规则:

iptables -t nat -I PREROUTING -s 10.0.129.0/24 -d 10.0.128.0/24 -j DNAT --to 10.0.129.1

 

测试ping 主机1上的容器地址

复制代码

/ # ping 10.0.128.2 -c 3
PING 10.0.128.2 (10.0.128.2): 56 data bytes
64 bytes from 10.0.128.2: seq=0 ttl=64 time=0.092 ms
64 bytes from 10.0.128.2: seq=1 ttl=64 time=0.140 ms
64 bytes from 10.0.128.2: seq=2 ttl=64 time=0.141 ms

--- 10.0.128.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.092/0.124/0.141 ms

复制代码

 

也是可以通讯的!

注意:如果发现还是不通,重启一下docker服务,应该就可以了!

 

五、3台主机测试

环境介绍

操作系统 服务器地址 Dockerd地址
ubuntu-16.04.5-server-amd64 192.168.91.128 10.0.128.2
ubuntu-16.04.5-server-amd64 192.168.91.129 10.0.129.2
ubuntu-16.04.5-server-amd64 192.168.91.131 10.0.131.2

 

 

 

 

 

拓扑图

Docker容器跨主机通信之:直接路由方式_第7张图片

一键脚本

上面已经实现了2台docker之间的通信,如果是3台呢?怎么搞?还是一样的。

只不过每台主机都要增加2条路由规则以及2条iptables规则。

做路由规则时,容器搞混淆,为了避免这种问题,做一个一键脚本就可以了!

 

环境要求

1. 每台主机已经安装好了docker,并且已经启动

2. 请确保每台主机的 /etc/default/docker 没有被更改过。还是默认的172.17.0.2/16网段

如果是虚拟机,直接还原快照即可!

 

docker_dr.sh

复制代码

#!/bin/bash

# 主机ip后缀清单
hosts="128 129 131"

# 循环主机
for i in `echo $hosts`;do
    # 写入临时文件
    cat >/tmp/dockerc<> /etc/default/docker"
    # 重启docker服务
    ssh 192.168.91.$i "systemctl restart docker"
    # 清空nat规则
    # ssh 192.168.91.$i "sudo iptables -t nat -F"

    # 再次循环
    for j in `echo $hosts`;do
        # 排除自身
        if [ "$j" != "$i" ];then
            # 添加路由
            ssh 192.168.91.$i "route add -net 10.0.$j.0/24 gw 192.168.91.$j"
            # 添加nat规则
            ssh 192.168.91.$i "iptables -t nat -I PREROUTING -s 10.0.$i.0/24 -d 10.0.$j.0/24 -j DNAT --to 10.0.$i.1"
        fi
    done
    # 重启docker服务,写入默认的nat规则
    ssh 192.168.91.$i "systemctl restart docker"
done

复制代码

 

ssh免密登录

在主机1执行以下命令

生成秘钥,并写入到authorized_keys

ssh-keygen -t rsa -P "" -f ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

 

复制公钥,执行以下3个命令

ssh-copy-id 192.168.91.128
ssh-copy-id 192.168.91.129
ssh-copy-id 192.168.91.131

 

正式执行 docker_dr.sh

bash docker_dr.sh

 执行之后,是没有啥输出的

 

3台主机都启动alpine镜像

docker run -it alpine

 

在主机1上面,测试访问另外2个docker地址,并测试上网,效果如下:

复制代码

/ # ping 10.0.129.2 -c 2
PING 10.0.129.2 (10.0.129.2): 56 data bytes
64 bytes from 10.0.129.2: seq=0 ttl=64 time=0.078 ms
64 bytes from 10.0.129.2: seq=1 ttl=64 time=0.107 ms

--- 10.0.129.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.078/0.092/0.107 ms
/ # ping 10.0.131.2 -c 2
PING 10.0.131.2 (10.0.131.2): 56 data bytes
64 bytes from 10.0.131.2: seq=0 ttl=64 time=0.073 ms
64 bytes from 10.0.131.2: seq=1 ttl=64 time=0.093 ms

--- 10.0.131.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.073/0.083/0.093 ms
/ # ping www.baidu.com -c 2
PING www.baidu.com (61.135.169.121): 56 data bytes
64 bytes from 61.135.169.121: seq=0 ttl=127 time=25.826 ms
64 bytes from 61.135.169.121: seq=1 ttl=127 time=26.172 ms

--- www.baidu.com ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 25.826/25.999/26.172 ms
/ #

复制代码

 

 测试成功,大功告成!!!

你可能感兴趣的:(Docker)