计算机系统应用教程网站

网站首页 > 技术文章 正文

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

btikc 2024-12-02 15:33:55 技术文章 22 ℃ 0 评论

一、Docker网络基本原理

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

Docker中的网络接口默认都是虚拟的接口。虚拟接口的最大优势就是转发效率极高

这是因为Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发

即发送接口的发送缓存中的数据包将被直接复制到接收接口的接收缓存中,而无需通过外部物理网络设备进行交换

对于本地系统和容器内系统来看,虚拟接口跟一个正常的以太网卡相比并无区别,只是它速度要快得多

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

一般情况下,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容器的网络有五种模式:

1)bridge模式,--net=bridge(默认) 这是dokcer网络的默认设置,为容器创建独立的网络命名空间,容器具有独立的网卡等所有单独的网络栈,是最常用的使用方式。 在docker run启动容器的时候,如果不加--net参数,就默认采用这种网络模式。安装完docker,系统会自动添加一个供docker使用的网桥docker0,我们创建一个新的容器时, 容器通过DHCP获取一个与docker0同网段的IP地址,并默认连接到docker0网桥,以此实现容器与宿主机的网络互通。

2)host模式,--net=host

这个模式下创建出来的容器,直接使用容器宿主机的网络命名空间。 将不拥有自己独立的Network Namespace,即没有独立的网络环境。它使用宿主机的ip和端口

3)none模式,--net=none 为容器创建独立网络命名空间,但不为它做任何网络配置,容器中只有lo,用户可以在此基础上,对容器网络做任意定制。 这个模式下,dokcer不为容器进行任何网络配置。需要我们自己为容器添加网卡,配置IP。 因此,若想使用pipework配置docker容器的ip地址,必须要在none模式下才可以

4)其他容器模式(即container模式),--net=container:NAME_or_ID 与host模式类似,只是容器将与指定的容器共享网络命名空间。 这个模式就是指定一个已有的容器,共享该容器的IP和端口。除了网络方面两个容器共享,其他的如文件系统,进程等还是隔离开的。 5)用户自定义:docker 1.9版本以后新增的特性,允许容器使用第三方的网络实现或者创建单独的bridge网络,提供网络隔离能力。

bridge模式

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

从上面的网络模型可以看出,容器从原理上是可以与宿主机乃至外界的其他机器通信的。同一宿主机上,容器之间都是连接掉docker0这个网桥上的,它可以作为虚拟交换机使容器可以相互通信

然而,由于宿主机的IP地址与容器veth pair的 IP地址均不在同一个网段,故仅仅依靠veth pair和namespace的技术,还不足以使宿主机以外的网络主动发现容器的存在。为了使外界可以发现容器中的进程,docker采用了端口绑定的方式,也就是通过iptables的NAT,将宿主机上的端口流量转发到容器内的端口上


举一个简单的例子,使用下面的命令创建容器,并将宿主机的3306端口绑定到容器的3306端口: docker run -tid --name db -p 3306:3306 MySQL

在宿主机上,可以通过iptables -t nat -L -n,查到一条DNAT规则: DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:3306 to:172.17.0.5:3306

上面的172.17.0.5即为bridge模式下,创建的容器IP。 很明显,bridge模式的容器与外界通信时,必定会占用宿主机上的端口,从而与宿主机竞争端口资源,对宿主机端口的管理会是一个比较大的问题。同时,由于容器与外界通信是基于三层上iptables NAT,性能和效率上的损耗是可以预见的。


三、方案介绍

概述

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

情景构造

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

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

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

方案原理分析

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

各项配置如下:

主机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地址

查看主机2上docker0的ip地址


添加路由规则

主机1

查看路由表

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

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

route add -net 10.0.129.0/24 gw 192.168.91.129

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

主机2

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

route add -net 10.0.128.0/24 gw 192.168.91.128

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

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

Docker Bridge创建创建过程

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查看

会发现有一个 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设备在转发数据包过程中并不串改数据包内容

显然,仅有veth-pair设备,容器是无法访问网络的。因为容器发出的数据包,实质上直接进入了veth1设备的协议栈里。如果容器需要访问网络,需要使用bridge等技术,将veth1接收到的数据包通过某种方式转发出去 。 veth参考链接:http://blog.csdn.net/sld880311/article/details/77650937

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

配置iptables规则

主机1

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

iptables -t nat -L

这些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

iptables -t nat -I PREROUTING -s 172.34.0.0/16 -d 172.35.0.0/16 -j DNAT --to 172.34.0.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地址

一键脚本

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

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

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

环境要求

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

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

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

#!/bin/bash
# 主机ip后缀清单
hosts="128 129 131"
# 循环主机
for i in `echo $hosts`;do
    # 写入临时文件
    cat >/tmp/dockerc<<EOF
    DOCKER_OPTS=\"--bip 10.0.$i.1/24\"
EOF
    # 远程执行命令,更改docker0网段
    ssh 192.168.91.$i "echo $(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_rsacat ~/.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

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表