利用pipework与OVS构建跨主机容器网络

时间:2022-05-06
本文章向大家介绍利用pipework与OVS构建跨主机容器网络,主要内容包括一、前言、二、构建网络前的准备、2. 安装必须的工具:、三、网络方案一、2. 实验:、3. 测试:、4. 测试总结:、四、网络方案二、2. 实验:、3. 测试:、4. 测试总结:、五、析疑、六、总结、参考资料、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

一、前言

本文提供了一种可行的、灵活的方案用于构建跨主机的Docker容器网络。鉴于目前的各类网络方案均依赖其他大型项目(比如CoreOS,Kubernetes)。本文的主旨就是抛开其他不必要的依赖,构建一个在大多数平台上都能运行的网络方案。

pipework,是由Docker的工程师Jérôme Petazzoni开发的一个Docker网络配置工具,由200多行shell代码写成,适合在多种不同的网络环境下配置容器网络,支持多种功能。github获取地址:https://github.com/jpetazzo/pipework

二、构建网络前的准备

1. 环境及软件版本信息:

Ubuntu 14.04 LTS x64 OpenvSwitch 2.3.0 Docker 1.4.1 Pipework Bridge-utils Nsenter

2. 安装必须的工具:

OpenvSwitch与Docker:(本文不再多论述它们的安装,请参考文末的参考资料。)

Pipework:

git clone https://github.com/jpetazzo/pipework
cp ~/pipework/pipework /usr/local/bin/

bridge-utils:

apt-get install bridge-utils

Nsenter:

curl https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz 
| tar -zxf-
cd util-linux-2.24
./configure --without-ncurses
make nsenter
cp nsenter /usr/local/bin

配合Nsenter进入容器操作的脚本injec.py:

    import os
    print 'please choose a contianer'
	a=os.popen("docker ps")
	print a.read()
	coid = raw_input('please input the container name,id,or sth to inject:')
	result=os.popen("docker inspect --format '{{.State.Pid}}' %s" % coid)
	print result.read()
	value = raw_input("please input the value:")
	os.system('nsenter --target %s --mount --uts --ipc --net --pid' % value)

三、网络方案一

1. 目标拓扑:

如上图所示,共有4个节点,每个节点有internal address(用于服务器管理)、external address(用于外部访问),每个节点安装、启动一个OVS示例并创建两个网桥br0与br1(ip地址不是必须的)。节点上的OVS通过overlay连接,注意在构建环路之前先启用STP生成树协议。这样一来,同一广播域的容器节点都能在整个网络中通讯。

2. 实验:

在每个节点上创建所需的网桥:

brctl addbr br0
ip link set dev br0 up
brctl addbr br1
ip link set dev br1 up
 
ovs-vsctl add-br ovs0
ovs-vsctl set bridge ovs0 stp_enable=true
ovs-vsctl add-port ovs0 br0
ovs-vsctl add-port ovs0 br1

注意:在相应的节点上创建隧道

创建host1至host2的GRE隧道(可以选择VXLAN)(在host1上执行)

ovs-vsctl add-port ovs0 gre0 -- set interface gre0 type=gre options:remote_ip=10.20.10.71

创建host2至host1的GRE隧道:(在host2上执行)

ovs-vsctl add-port ovs0 gre0 -- set interface gre0 type=gre options:remote_ip=10.20.10.70

创建host2至host3的GRE隧道:

ovs-vsctl add-port ovs0 gre1 -- set interface gre1 type=gre options:remote_ip=10.20.10.72

创建host3至host2的GRE隧道:

ovs-vsctl add-port ovs0 gre1 -- set interface gre1 type=gre options:remote_ip=10.20.10.71

创建host3至host4的GRE隧道:

ovs-vsctl add-port ovs0 gre2 -- set interface gre2 type=gre options:remote_ip=10.20.10.73

创建host4至host3的GRE隧道:

ovs-vsctl add-port ovs0 gre2 -- set interface gre2 type=gre options:remote_ip=10.20.10.72

创建host4至host1的GRE隧道:

ovs-vsctl add-port ovs0 gre3 -- set interface gre3 type=gre options:remote_ip=10.20.10.70

创建host1至host4的GRE隧道:

ovs-vsctl add-port ovs0 gre3 -- set interface gre3 type=gre options:remote_ip=10.20.10.73

3. 测试:

在host1上创建一个容器(使用ubuntu镜像):

docker run -itd --name=test1 ubuntu
pipework br0 -i eth1 test1 192.168.2.11/24

在host2上创建一个不同广播域的容器:

docker run -itd --name=test2 ubuntu
pipework br1 -i eth1 test2 192.168.3.11/24

在host3上创建与test1同广播域的容器:

docker run -itd --name=test3 ubuntu
pipework br0 -i eth1 test3 192.168.2.12/24

在host3上创建另一个与test1同广播域的容器:

docker run -itd --name=test4 ubuntu
pipework br0 -i eth1 test4 192.168.2.13/24

使用injec.py进入容器测试:

root@workgroup0:~# python injec.py 
please choose a contianer
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
5057572a8f79        ubuntu:latest       "/bin/bash"         About an hour ago   Up About an hour                        test1               
 
please input the container name,id,or sth to inject:test1
2741
 
please input the value:2741
root@5057572a8f79:/# 

用ping操作验证相应的容器检测网络连通性:

使用inject.py进入test1后:

root@c0729d64348a:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host 
valid_lft forever preferred_lft forever
11: eth0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:2/64 scope link 
valid_lft forever preferred_lft forever
13: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 2e:04:ed:71:54:1f brd ff:ff:ff:ff:ff:ff
inet 192.168.2.11/24 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::2c04:edff:fe71:541f/64 scope link 
valid_lft forever preferred_lft forever

可以看到我们指定的设备eth1已经存在,而且拥有相应的IP地址。

测试连通性:

root@c0729d64348a:/# ping -c 4 192.168.3.11   
PING 192.168.3.11 (192.168.3.11) 56(84) bytes of data.
From 192.168.3.1 icmp_seq=1 Destination Host Unreachable
From 192.168.3.1 icmp_seq=2 Destination Host Unreachable
From 192.168.3.1 icmp_seq=3 Destination Host Unreachable
From 192.168.3.1 icmp_seq=4 Destination Host Unreachable
 
--- 192.168.3.11 ping statistics ---
4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3017ms
pipe 3

可以看到,ping不同广播域的test2是无法连通的。

ping其他的容器:

root@c0729d64348a:/# ping -c 4 192.168.2.12
PING 192.168.2.12 (192.168.2.12) 56(84) bytes of data.
64 bytes from 192.168.2.12: icmp_seq=1 ttl=64 time=0.905 ms
64 bytes from 192.168.2.12: icmp_seq=2 ttl=64 time=0.113 ms
64 bytes from 192.168.2.12: icmp_seq=3 ttl=64 time=0.125 ms
64 bytes from 192.168.2.12: icmp_seq=4 ttl=64 time=0.101 ms
 
--- 192.168.2.12 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3001ms
rtt min/avg/max/mdev = 0.101/0.311/0.905/0.343 ms
 
root@c0729d64348a:/# ping -c 4 192.168.2.13
PING 192.168.2.13 (192.168.2.13) 56(84) bytes of data.
64 bytes from 192.168.2.13: icmp_seq=1 ttl=64 time=5.15 ms
64 bytes from 192.168.2.13: icmp_seq=2 ttl=64 time=0.571 ms
64 bytes from 192.168.2.13: icmp_seq=3 ttl=64 time=0.584 ms
64 bytes from 192.168.2.13: icmp_seq=4 ttl=64 time=0.725 ms
 
--- 192.168.2.13 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3002ms
rtt min/avg/max/mdev = 0.571/1.758/5.155/1.962 ms 

虽然有的容器并不在本地,但通过这个网络还是可以访问到他们。

4. 测试总结:

在这个网络中,通过设置GRE隧道,建立同一广播域的容器,不管在哪台主机上,都可以互相访问。无论是local to remote还是local to local都可以访问。

四、网络方案二

1. 目标拓扑:

在这个网络图中,省略了linux bridge,直接把容器挂在ovs0上,这样可以支持vlan。至于ovs0之间的连接,我还是坚持使用了overlay的技术。如果要扩展为一个类似方案一的环状拓扑,请开启ovs的STP并手动建立GRE或VXLAN连接。

2. 实验:

在每台主机上创建ovs0网桥并配置好GRE隧道(host1和host2的IP地址参考方案一):

ovs-vsctl add-br ovs0

创建host1至host2的GRE隧道(可以选择VXLAN):(在host1上执行)

ovs-vsctl add-port ovs0 gre0 -- set interface gre0 type=gre options:remote_ip=10.20.10.71

创建host2至host1的GRE隧道:(在host2上执行)

ovs-vsctl add-port ovs0 gre0 -- set interface gre0 type=gre options:remote_ip=10.20.10.70

修改Pipework源码:

vi /usr/local/bin/pipework

找到这一行:

ovs-vsctl add-port $IFNAME $LOCAL_IFNAME ${VLAN:+"tag=$VLAN"}

换行添加一行代码(echo $LOCAL_IFNAME),结果如下:

ovs-vsctl add-port $IFNAME $LOCAL_IFNAME ${VLAN:+"tag=$VLAN"}
echo $LOCAL_IFNAME

3. 测试:

在host1上启动数个容器,设置vlan id:

docker run -itd --name=test1 ubuntu
pipework ovs0 -i eth1 test1 192.168.2.11/24 @100
 
docker run -itd --name=test2 ubuntu
pipework ovs0 -i eht1 test2 192.168.3.11/24 @100
 
docker run -itd --name=test3 ubuntu
pipework ovs0 -i eth1 test3 192.168.2.12/24 @100

在host2上启动数个容器,设置vlan id:

docker run -itd --name=test1 ubuntu
pipework ovs0 -i eth1 test1 192.168.2.13/24 @100
 
docker run -itd --name=test2 ubuntu
pipework ovs0 -i eth1 test2 192.168.2.14/24 @200

同样使用injec.py进入容器进行测试:

进入test1:

root@54376d35f10e:/# ping -c 4 192.168.2.12
PING 192.168.2.12 (192.168.2.12) 56(84) bytes of data.
bytes from 192.168.2.12: icmp_seq=1 ttl=64 time=3.14 ms
64 bytes from 192.168.2.12: icmp_seq=2 ttl=64 time=0.073 ms
64 bytes from 192.168.2.12: icmp_seq=3 ttl=64 time=0.073 ms
64 bytes from 192.168.2.12: icmp_seq=4 ttl=64 time=0.084 ms
 
--- 192.168.2.12 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3002ms
rtt min/avg/max/mdev = 0.073/0.844/3.147/1.329 ms
 
root@54376d35f10e:/# ping -c 4 192.168.2.13
PING 192.168.2.13 (192.168.2.13) 56(84) bytes of data.
64 bytes from 192.168.2.13: icmp_seq=1 ttl=64 time=3.81 ms
64 bytes from 192.168.2.13: icmp_seq=2 ttl=64 time=0.389 ms
64 bytes from 192.168.2.13: icmp_seq=3 ttl=64 time=0.451 ms
64 bytes from 192.168.2.13: icmp_seq=4 ttl=64 time=0.561 ms
 
--- 192.168.2.13 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 2999ms
rtt min/avg/max/mdev = 0.389/1.303/3.812/1.449 ms
 
root@54376d35f10e:/# ping -c 4 192.168.3.11
PING 192.168.3.11 (192.168.3.11) 56(84) bytes of data.
From 192.168.3.1 icmp_seq=1 Destination Host Unreachable
From 192.168.3.1 icmp_seq=2 Destination Host Unreachable
From 192.168.3.1 icmp_seq=3 Destination Host Unreachable
From 192.168.3.1 icmp_seq=4 Destination Host Unreachable
 
--- 192.168.3.11 ping statistics ---
4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3015ms
pipe 3
 
root@54376d35f10e:/# ping -c 4 192.168.2.14
PING 192.168.2.14 (192.168.2.14) 56(84) bytes of data.
From 192.168.2.11 icmp_seq=1 Destination Host Unreachable
From 192.168.2.11 icmp_seq=2 Destination Host Unreachable
From 192.168.2.11 icmp_seq=3 Destination Host Unreachable
From 192.168.2.11 icmp_seq=4 Destination Host Unreachable
 
--- 192.168.2.14 ping statistics ---
4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3016ms
pipe 3

可以看到,只能ping通同一个vlan内同广播域的容器。

4. 测试总结:

这个方案提供了VLAN支持,修改pipework的原因是为了反馈容器加入ovs网桥的veth peer一头的信息。因为在该方案中,删除容器后,veth peer还有一头是记录在ovs中的,可以通过ovs-vsctl查看。但是它并没有提示该残留的veth属于哪个容器,因此通过pipework的反馈,用户可以用程序记录该值,方便以后的删除操作。

五、析疑

1. 关于Nsenter:Nsenter是util-linux包含的一个工具,安装完之后,可以这样访问:

PID=$(docker inspect --format '{{.State.Pid}}' CONT_NAME/ID)
nsenter --target $PID --mount --uts --ipc --net --pid

原理就是获取指定容器的pid,然后再使用nsenter访问容器。CONT_NAME/ID可以填容器的名字或ID。这实现了docker attach的功能,当然,你也可以单纯使用docker attach进入容器。

2. 关于Pipework: Q:如何在挂载容器到某个网桥时指定IP地址? A:

pipework ovs0 CONTAINER_NAME 192.168.3.11/24,

不可以漏了掩码。

Q:如果要把容器挂载到多个网桥中,提示RTNETLINK answers: File exists,怎么解决? A:你可以显式地指定挂载到网桥的设备名称。通过参数-i指定:

pipework ovs0 -i eth1 test1 192.168.2.11/24

这里指定了一个设备eth1。

Q:在方案2中,怎样指定vlan id? A:在掩码后加上@VLAN_ID

pipework ovs0 -i eth1 test1 192.168.2.11/24 @100

Q:有详细的使用文档吗? A:有,见github:http://https://github.com/jpetazzo/pipework 或者参考这篇资料:http://http://www.infoq.com/cn/articles/docker-network-and-pipework-open-source-explanation-practice/

3. 关于方案1: 在方案1中,我使用了多个linux bridge。但实际上,多个bridge其实不是必须的,一个可能的优化是只建立一个linux bridge(无IP设置),挂载到ovs0上,可以把不同广播域的容器都挂载到网桥上,基本上不会发生冲突,只有同一个广播域的容器可以通讯。这样就可以不创建那么多网桥了。 另外,如果你要构建环状拓扑,一定要开启ovs的STP,否则会出现广播风暴问题。

4. 关于方案2: 在方案2中,容器直接挂载在ovs0上,因为docker会创建一个veth peer,它的一头放在容器中,另一头放在bridge中。pipework把veth peer的一头通过ovs-vsctl add-port添加至ovs0。如果你删除容器,veth peer的一头其实还会留在ovs0上,但你不清楚哪个才是属于你刚删除的容器的veth peer,因此你应该修改pipework,令它返回添加到ovs0上的那个veth peer,方便后续的管理工作。

5. 关于两种方案的共同问题: 这两个方案有一个共同的缺点:我还没找出能用的L3 router方案,在未来我会使用openflow重新做一个方案,克服这一缺陷。

如果你构建环状拓扑,环路无法通信,因为STP的原因,跨主机的网络会短暂中断。

方案中,我没有配置交换机的chunk口,而是直接用overlay技术。因为GRE和VXLAN的IP头增加,用户可能需要修改docker的设置,设置一个合适的MTU值,否则网络性能会很差。

两个方案中,我都没有修改本来的docker0网桥。这是因为考虑到外部访问的问题。常规的docker应用,在docker run时已经指定好要执行的命令,设置端口映射之类的操作。如果启动时设置--net=none,外部将无法通过端口访问容器提供的服务。pipework支持修改路由,在掩码后加@GATEWAY,可以使外部流量从新的设备经过,不再经过默认的eth0(即挂载在docker0上的设备),但明显这不是我们想要的结果。所以,在这个方案中,用户只需要以传统的方式启动容器,在需要时使用pipework挂载到不同的网桥上即可。对于服务发现这类需求,用户可以自行通过etcd这类工具实现。

pipework设置的内容将会在容器重启后丢失,但用户可以在容器启动后重新使用pipework设置。

六、总结

希望大家能从这些方案中找到灵感,寻找一个合适自己的网络方案。本人也正在自己的个人项目中应用这些方案。

参考资料

http://www.infoq.com/cn/articles/docker-network-and-pipework-open-source-explanation-practice/

https://github.com/jpetazzo/pipework

http://my.oschina.net/hochikong/blog/379354

http://blog.163.com/digoal%40126/blog/static/163877040201411602548445/

https://www.sdnlab.com/5889.html

ovs安装:https://www.sdnlab.com/3166.html