Docker网络模式
在讨论Kubernetes网络之前,让我们先来看一下Docker网络。Docker采用插件化的网络模式,默认提供bridge、host、none、overlay、maclan和Network plugins这几种网络模式,运行容器时可以通过–network参数设置具体使用那一种模式。
- bridge:这是Docker默认的网络驱动,此模式会为每一个容器分配Network Namespace和设置IP等,并将容器连接到一个虚拟网桥上。如果未指定网络驱动,这默认使用此驱动。
- host:此网络驱动直接使用宿主机的网络。
- none:此驱动不构造网络环境。采用了none 网络驱动,那么就只能使用loopback网络设备,容器只能使用127.0.0.1的本机网络。
- overlay:此网络驱动可以使多个Docker daemons连接在一起,并能够使用swarm服务之间进行通讯。也可以使用overlay网络进行swarm服务和容器之间、容器之间进行通讯,
- macvlan:此网络允许为容器指定一个MAC地址,允许容器作为网络中的物理设备,这样Docker - daemon就可以通过MAC地址进行访问的路由。对于希望直接连接网络网络的遗留应用,这种网络驱动有时可能是最好的选择。
- Network plugins:可以安装和使用第三方的网络插件。可以在Docker Store或第三方供应商处获取这些插件:flannel等。
在默认情况,Docker使用bridge网络模式,bridge网络驱动的示意图如下,我们先以bridge模式对Docker的网络进行说明。
1.1 bridge网络的构建过程如下:
1)安装Docker时,创建一个名为docke0的虚拟网桥,虚拟网桥使用“10.0.0.0 -10.255.255.255 “、”172.16.0.0-172.31.255.255″和“192.168.0.0——192.168.255.255”这三个私有网络的地址范围。
通过 ifconfig 命令可以查看docker0网桥的信息:
通过 docker network inspect bridge 可以查看网桥的子网网络范围和网关:
2)运行容器时,在宿主机上创建虚拟网卡veth pair设备,veth pair设备是成对出现的,从而组成一个数据通道,数据从一个设备进入,就会从另一个设备出来。将veth pair设备的一端放在新创建的容器中,命名为eth0;另一端放在宿主机的docker0中,以veth为前缀的名字命名。通过 brctl show 命令查看放在docker0中的veth pair设备
子网掩码和网关
设
- A:10.1.1.10 /24
- B:10.1.1.20 /24
- C:50.1.1.80 /24AB在同一局域网,C位于外网。
三个表: - ARP表:主机维护,存放IP地址和MAC地址对应关系。
- MAC地址表:交换机维护,存放MAC地址和交换机端口对应关系。
- 路由表:路由器维护,存放IP地址和路由器端口对应关系。
首先AB通信,例如A要给B发送一个数据包,目前A知道B的IP地址,根据掩码规则判定B和自己在同一个局域网,同一个广播域。接下来A通过广播方式获取B的MAC地址,添加到自己的ARP表中。然后把要发送的包封装,然后发送给交换机,交换机收到数据包后解封装得到B的MAC地址,根据MAC地址表转发到B所连接的交换机端口,完成发送。
如果A要和C通信,发送一个包给C的话,也只知道C的IP地址,然后A根据掩码规则发现C和自己不是同一个局域网的,广播不到C,所以A只能把数据包发给网关,由网关发出去给到C。A同样通过广播方式获取网关的MAC地址,然后把C的IP地址和网关的MAC地址封装到数据包后发给交换机,交换机解封装后对比MAC地址表,发现是发给网关的包,就转发到网关即路由器所在的交换机端口。路由器收到包之后再解封装,得到C的IP地址,然后根据自己的路由表转发到相应的端口。完成通信。
所以如果计算机上不设置子网掩码,从第一步就不能完成,下面就更不能继续了。如果同一个广播域里有机器设置不同的子网掩码,依然能够通信,只不过有的内网包需要到网关绕一圈。外网包的话只要网关设置对了就没问题。
Veth-Pair
一、什么是容器网络栈
所谓容器能看见的“网络栈”,被隔离在自己的Network Namespace当中
网卡(network interface)
回环设备(loopback device)
路由表(Routing Table)
iptables规则
当然 ,容器可以直接声明使用宿主机的网络栈:-net=host(不开启Network Namespace),这样可以为容器提供良好的网络性能,但是也引入了共享网络资源的问题,比如端口冲突。
大多数情况下,我们希望容器能使用自己的网络栈,拥有属于自己的IP和端口
二、容器如何和其他不同Network Namespace的容器交互 ?
将容器看做一台主机,两台主机通信直接的办法,就是用一根网线连接,如果是多台主机之间通信,就需要用网络将它们连接到一台交换机上
在linux中,能够起到虚拟交换机作用的设备是网桥(Bridge)
网桥是一个工作在数据链路层的设备,功能是根据MAC地址学习来将数据包转发到网桥不同端口上,为了实现上述上的,Docker项目默认在宿主机上创建了一个docker0的网桥,连接在上面的容器,可以通过它来通信
三、如何把容器连接到docker0网络上
需要一种叫Veth Pair的虚拟设备。当Veth Pair被创建出来后,总是以两张虚拟网卡(Veth Peer)的形式成对出现 ,而且从其中一个网卡出的数据包,可以直接出现在它对应的另一张网卡上,即使两张卡在不同的Network Namespace里
这就使Veth Pair常常被用作连接不同的Network Namspace的网线
四、示例演示
下面将在宿主机启动两个容器,分别是nginx-1和nginx-2演示,nginx-1容器访问nginx-2是如何通信的
- 运行一个nginx-1容器
docker run -d --name=nginx-1 nginx
2.查看nginx-1的网络设备
root@d5bfaab9222e:/# ifconfigeth0: flags=4163 mtu 1500 inet 172.18.0.2 netmask 255.255.0.0 broadcast 172.18.255.255 ether 02:42:ac:12:00:02 txqueuelen 0 (Ethernet) RX packets 3520 bytes 8701343 (8.2 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 3010 bytes 210777 (205.8 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0lo: flags=73 mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 loop txqueuelen 1000 (Local Loopback) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth0的网卡,是Veth Pari设备的其中一端
3.查看nginx-1的路由表,通过route命令查看
root@d5bfaab9222e:/# routeKernel IP routing tableDestination Gateway Genmask Flags Metric Ref Use Ifacedefault 172.18.0.1 0.0.0.0 UG 0 0 0 eth0172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
所有172.18.0.0/16网段的请求,都交给eth0处理,这是Veth Pair设备的一端,另一端在宿主机上
5.查看宿主机的网桥
通过brctl show查看
root@VM-0-8-ubuntu:/home/ubuntu# brctl showbridge name bridge id STP enabled interfacesdocker0 8000.0242c635ddb8 no veth4d1c130
可以看到,docker0上插入上veth4d1c130网卡
6.再运行一个nginx-2
docker run -d --name=nginx-2 nginx
7.再查看宿主机网桥
root@VM-0-8-ubuntu:/home/ubuntu# brctl showbridge name bridge id STP enabled interfacesdocker0 8000.0242c635ddb8 no veth4d1c130 vetha9356e9
可以看到,docker0插上了两张网卡veth4d1c130、 vetha9356e9
此时,当容器nginx-1(172.18.0.2)ping容器nginx-2(172.18.0.3)的时候,就会发现两个容器是可以连通的
五、nginx-1能访问nginx-2的原理是什么?
被限制在Network Namespace的容器进程,是通过Veth Pair设备+宿主机网桥的方式,实现跟其它容器的数据交换
1.nginx-1访问nginx-2时,IP地址会匹配到nginx-1容器的每二条路由规则
172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
凡是匹配这条规则 的IP包,应该经过本机的eth0网卡,通过二层网络发往目的主机
2.要通过二层网络到达nginx-2 ,需要有172.18.0.3的MAC地址,找到
nginx-1容器需要通过eth0网卡发送一个ARP广播,通过IP来查看对应的MAC地址
这个eth0网卡,是一个Veth Pair,它的一端在nginx-1容器的Network Namespace,另一端位于宿主机上(Host Namespace),并且插入在了宿主机的docker0网桥上
一旦虚假网卡(veth4d1c130)被插在网桥(docker0)上,调用网络的数据包会被转发给对应的网桥,所以ARC请求会被发给docker0
3.docker0转发数据到到相应的nginx-2
docker0会继续扮演二层交换机的角色,根据数据包的上的MAC地址(nginx-2的MAC地址),在它的CAM表里查对应的端口,会找到vetha9356e9,然后将数据包发往这个端口,这样数据包就到了nginx-2容器的Network Namespace里
nginx-2看到它自己的eth0网卡上出现了流入的数据包,然后对请求进行处理,再返回响应给nginx-1
可以打开iptables的TRACE功能查看数据包的传输过程,通过tail -f /var/log/syslog
iptables -t raw -A OUTPUT -p icmp -j TRACE# iptables -t raw -A PREROUTING -p icmp -j TRACE
原理图如下
3.1 直接相连
直接相连是最简单的方式,以下图,一对 veth-pair 直接将两个 namespace 链接在一块儿。
给 veth-pair 配置 IP,测试连通性:
# 建立 namespace
ip netns a ns1
ip netns a ns2
# 建立一对 veth-pair veth0 veth1
ip l a veth0 type veth peer name veth1
# 将 veth0 veth1 分别加入两个 ns
ip l s veth0 netns ns1
ip l s veth1 netns ns2
# 给两个 veth0 veth1 配上 IP 并启用
ip netns exec ns1 ip a a 10.1.1.2/24 dev veth0
ip netns exec ns1 ip l s veth0 up
ip netns exec ns2 ip a a 10.1.1.3/24 dev veth1
ip netns exec ns2 ip l s veth1 up
# 从 veth0 ping veth1
[root@localhost ~]# ip netns exec ns1 ping 10.1.1.3
PING 10.1.1.3 (10.1.1.3) 56(84) bytes of data.
64 bytes from 10.1.1.3: icmp_seq=1 ttl=64 time=0.073 ms
64 bytes from 10.1.1.3: icmp_seq=2 ttl=64 time=0.068 ms
--- 10.1.1.3 ping statistics ---
15 packets transmitted, 15 received, 0% packet loss, time 14000ms
rtt min/avg/max/mdev = 0.068/0.084/0.201/0.032 ms
3.2 经过 Bridge 相连
Linux Bridge 至关于一台交换机,能够中转两个 namespace 的流量,咱们看看 veth-pair 在其中扮演什么角色。
以下图,两对 veth-pair 分别将两个 namespace 连到 Bridge 上。
一样给 veth-pair 配置 IP,测试其连通性:
# 首先建立 bridge br0
ip l a br0 type bridge
ip l s br0 up
# 而后建立两对 veth-pair
ip l a veth0 type veth peer name br-veth0
ip l a veth1 type veth peer name br-veth1
# 分别将两对 veth-pair 加入两个 ns 和 br0
ip l s veth0 netns ns1
ip l s br-veth0 master br0
ip l s br-veth0 up
ip l s veth1 netns ns2
ip l s br-veth1 master br0
ip l s br-veth1 up
# 给两个 ns 中的 veth 配置 IP 并启用
ip netns exec ns1 ip a a 10.1.1.2/24 dev veth0
ip netns exec ns1 ip l s veth0 up
ip netns exec ns2 ip a a 10.1.1.3/24 dev veth1
ip netns exec ns2 ip l s veth1 up
# veth0 ping veth1
[root@localhost ~]# ip netns exec ns1 ping 10.1.1.3
PING 10.1.1.3 (10.1.1.3) 56(84) bytes of data.
64 bytes from 10.1.1.3: icmp_seq=1 ttl=64 time=0.060 ms
64 bytes from 10.1.1.3: icmp_seq=2 ttl=64 time=0.105 ms
--- 10.1.1.3 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.060/0.082/0.105/0.024 ms
3.3 经过 OVS 相连
OVS 是第三方开源的 Bridge,功能比 Linux Bridge 要更强大,对于一样的实验,咱们用 OVS 来看看是什么效果。
以下图所示:
一样测试两个 namespace 之间的连通性:
# 用 ovs 提供的命令建立一个 ovs bridge
ovs-vsctl add-br ovs-br
# 建立两对 veth-pair
ip l a veth0 type veth peer name ovs-veth0
ip l a veth1 type veth peer name ovs-veth1
# 将 veth-pair 两端分别加入到 ns 和 ovs bridge 中
ip l s veth0 netns ns1
ovs-vsctl add-port ovs-br ovs-veth0
ip l s ovs-veth0 up
ip l s veth1 netns ns2
ovs-vsctl add-port ovs-br ovs-veth1
ip l s ovs-veth1 up
# 给 ns 中的 veth 配置 IP 并启用
ip netns exec ns1 ip a a 10.1.1.2/24 dev veth0
ip netns exec ns1 ip l s veth0 up
ip netns exec ns2 ip a a 10.1.1.3/24 dev veth1
ip netns exec ns2 ip l s veth1 up
# veth0 ping veth1
[root@localhost ~]# ip netns exec ns1 ping 10.1.1.3
PING 10.1.1.3 (10.1.1.3) 56(84) bytes of data.
64 bytes from 10.1.1.3: icmp_seq=1 ttl=64 time=0.311 ms
64 bytes from 10.1.1.3: icmp_seq=2 ttl=64 time=0.087 ms
^C
--- 10.1.1.3 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.087/0.199/0.311/0.112 ms
端口映射
为什么要端口映射?
在启动容器时,如果不配置宿主机器与虚拟机的端口映射,外部程序是无法访问虚拟机的,因为没有端口。
端口映射的指令是什么?
docker指令: docker run -p ip:hostPort:containerPort redis
使用-p参数 会分配宿主机的端口映射到虚拟机。
- IP表示主机的IP地址。
- hostPort表示宿主机的端口。
- containerPort表示虚拟机的端口。
支持的格式有三种:
- ip:hostPort:containerPort:映射指定地址的指定端口到虚拟机的指定端口(不常用)
如:127.0.0.1:3306:3306,映射本机的3306端口到虚拟机的3306端口。 - ip::containerPort:映射指定地址的任意端口到虚拟机的指定端口。(不常用)
如:127.0.0.1::3306,映射本机的3306端口到虚拟机的3306端口。
- hostPort:containerPort:映射本机的指定端口到虚拟机的指定端口。(常用)
如:3306:3306,映射本机的3306端口到虚拟机的3306端口。
5.1 自动映射端口
-P使用时需要指定--expose选项,指定需要对外提供服务的端口
$ sudo docker run -t -P --expose 22 --name server ubuntu:14.04
使用 docker run -P自动绑定所有对外提供服务的容器端口,映射的端口将会从没有使用的端口池中 (49000..49900) 自动选择 ,
你可以通过docker ps、 docker inspect <container_id> 或者 docker port <container_id> <port> 确定具体的绑定信息。
5.2 绑定端口到指定接口
基本语法
$ sudo docker run -p [([<host_interface>:[host_port]])|(<host_port>):]<container_port>[/udp] <image> <cmd>
默认不指定绑定 ip 则监听所有网络接口。
绑定 TCP 端口
- Bind TCP port 8080 of the container to TCP port 80 on 127.0.0.1 of the host machine.
$ sudo docker run -p 127.0.0.1:80:8080 <image> <cmd>
- Bind TCP port 8080 of the container to a dynamically allocated TCP port on 127.0.0.1 of the host machine.
$ sudo docker run -p 127.0.0.1::8080 <image> <cmd>
- Bind TCP port 8080 of the container to TCP port 80 on all available interfaces of the host machine.
$ sudo docker run -p 80:8080 <image> <cmd>
Bind TCP port 8080 of the container to a dynamically allocated TCP port on all available interfaces
$ sudo docker run -p 8080 <image> <cmd>
绑定 UDP 端口
- Bind UDP port 5353 of the container to UDP port 53 on 127.0.0.1 of the host machine.
$ sudo docker run -p 127.0.0.1:53:5353/udp <image> <cmd>
1、单IP多容器映射规划方案
此种环境适用只有单个IP环境下,如云主机等。
1.1 端口映射规划表格:
规划不同的端口段,映射到容器从而对外提供服务。
docker run -h="redis-test" --name redis-test -d -p 51000:22 -p 51001:3306 -p 51003:6379 -p 51004:6381 -p 51005:80 -p 51006:8000 -p 51007:8888 debian02 /etc/rc.local
docker run -h="salt_zabbix_manager02" --name salt_zabbix_manager02 -d -p 52000:22 -p 52001:3306 -p 52003:6379 -p 52004:6381 -p 52005:80 -p 52006:8000 -p 52007:8888 debian02 /etc/rc.local
1.3 上述启动参数解释:
-h 是指启动后容器中的主机名。
--name 是宿主机上容器的名称,以后启动停止容器不必用容器ID,用名称即可,如docker stop redis-test。
-d 以后台形式运行。
-p 指定映射端口,如果需要映射UDP端口,则格式是 -p3000:3000/udp。
debian02 是基础镜像名称。
/etc/rc.local 是容器的启动命令,把多个启动脚本放/etc/rc.local中,方便多个程序随容器开机自启动。
2、多IP多容器映射规划方案
此规划比较适用于内网测试研发环境,所有对外访问IP都需要配置在宿主机上,如以第二IP eth0:1,eth0:2这种形式配置,然后每个IP和容器的端口映射配置就可以一致了。
2.1 端口与IP映射规划表格:
2.2 对应容器启动命令:
docker run -h="iframe-test" --name iframe-test -d -p 10.18.103.2:22:22 -p 10.18.103.2:3306:3306 -p 10.18.103.2:6379:6379 -p 10.18.103.2:6381:6381 -p 10.18.103.2:80:80 -p 10.18.103.2:8000:8000 -p 10.18.103.2:8888:8888 -p 10.18.103.2:443:443 debian-iframe-test /etc/rc.local
docker run -h="web-test" --name web-test -d -p 10.18.103.3:22:22 -p 10.18.103.3:3306:3306 -p 10.18.103.3:6379:6379 -p 10.18.103.3:6381:6381 -p 10.18.103.3:80:80 -p 10.18.103.3:8000:8000 -p 10.18.103.3:8888:8888 -p 10.18.103.3:443:443 debian-iframe-test /etc/rc.local
3、端口映射动态扩容方案
在工作当中,一般增加新的服务时,就需新增一个端口映射,由于无法动态调整,通常都需要commit到新的镜像,然后在基于新的镜像来起容器,确实是一件很麻烦的事。
但映射的本质,是通过iptables来完成的。所以我们可以动态的用iptables增加端口映射即可,如下:
3.1 用iptables查看容器映射情况:
root@qssec-iframe:~# iptables -t nat -nvL
Chain DOCKER (2 references)
pktsbytes target prot opt in out source destination
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8000 to:172.17.0.3:8000
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 to:172.17.0.3:443
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3306 to:172.17.0.3:3306
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:6379 to:172.17.0.3:6379
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:6381 to:172.17.0.3:6381
3470 190K DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.3:80
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8888 to:172.17.0.3:8888
41 2336 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:50000 to:172.17.0.3:22
从这里可以找到docker容器里使用的IP,然后在用iptables增加映射即可。
3.2 举例新增zabbix端口的映射:
3.2.1 单IP单容器端口扩容:
iptables -t nat -A PREROUTING -p tcp -m tcp --dport 10050 -j DNAT --to-destination 172.17.0.3:10050
iptables -t nat -A PREROUTING -p tcp -m tcp --dport 10051 -j DNAT --to-destination 172.17.0.3:10051
3.2.2 单IP多容器端口扩容:
iptables -t nat -A PREROUTING -p tcp -m tcp --dport 50010 -j DNAT --to-destination 172.17.0.3:10050
iptables -t nat -A PREROUTING -p tcp -m tcp --dport 50011 -j DNAT --to-destination 172.17.0.3:10051
另一个容器则可以规划为60010,60011,这样在zabbix监控的时候,就需要指定客户容器的端口连接了。
3.2.3 多IP多容器端口扩容:
iptables -t nat -A PREROUTING -d 10.18.103.2 -p tcp -m tcp --dport 10050 -j DNAT --to-destination 172.17.0.3:10050
iptables -t nat -A PREROUTING -d 10.18.103.2 -p tcp -m tcp --dport 10051 -j DNAT --to-destination 172.17.0.3:10051
#iptables -t nat -A PREROUTING -d 10.18.103.3 -p tcp -m tcp --dport 10050 -j DNAT --to-destination 172.17.0.4:10050
#iptables -t nat -A PREROUTING -d 10.18.103.3 -p tcp -m tcp --dport 10051 -j DNAT --to-destination 172.17.0.4:10051
这样zabbix连接10.18.103.2,3的正常zabbix端口就可以了。
Kubernetes网络模式
二、Kubernetes网络模式
Kubernetes与Docker网络有些不同。Kubernetes网络需要解决下面的4个问题:
集群内:
- 容器与容器之间的通信
- Pod和Pod之间的通信
- Pod和服务之间的通信
集群外: - 外部应用与服务之间的通信
因此,Kubernetes假设Pod之间能够进行通讯,这些Pod可能部署在不同的宿主机上。每一个Pod都拥有自己的IP地址,因此能够将Pod看作为物理主机或者虚拟机,从而能实现端口设置、命名、服务发现、负载均衡、应用配置和迁移。为了满足上述需求,则需要通过集群网络来实现。
下文主要分析容器与容器之间,以及Pod和Pod之间的通信;
2.1 同一个Pod中容器之间的通信
这种场景对于Kubernetes来说没有任何问题,根据Kubernetes的架构设计。Kubernetes创建Pod时,首先会创建一个pause容器,为Pod指派一个唯一的IP地址。然后,以pause的网络命名空间为基础,创建同一个Pod内的其它容器(–net=container:xxx)。因此,同一个Pod内的所有容器就会共享同一个网络命名空间,在同一个Pod之间的容器可以直接使用localhost进行通信。
2.2 不同Pod中容器之间的通信
对于此场景,情况现对比较复杂一些,这就需要解决Pod间的通信问题。在Kubernetes通过flannel、calic等网络插件解决Pod间的通信问题。
Flannel是CoreOS开源的CNI网络插件,下图flannel官网提供的一个数据包经过封包、传输以及拆包的示意图:
从这个图片里面里面可以看出两台机器的docker0分别处于不同的段:10.1.20.1/24 和 10.1.15.1/24 ,如果从Web App Frontend1 pod(10.1.15.2)去连接另一台主机上的Backend Service2 pod(10.1.20.3),网络包从宿主机192.168.0.100发往192.168.0.200,内层容器的数据包被封装到宿主机的UDP里面,并且在外层包装了宿主机的IP和mac地址。这就是一个经典的overlay网络,因为容器的IP是一个内部IP,无法从跨宿主机通信,所以容器的网络互通,需要承载到宿主机的网络之上。
flannel的支持多种网络模式,常用用都是vxlan、UDP、hostgw、ipip以及gce和阿里云等。
vxlan和UDP的区别是vxlan是内核封包,而UDP是flanneld用户态程序封包,所以UDP的方式性能会稍差;
hostgw模式是一种主机网关模式,容器到另外一个主机上容器的网关设置成所在主机的网卡地址,这个和calico非常相似,只不过calico是通过BGP声明,而hostgw是通过中心的etcd分发,所以hostgw是直连模式,不需要通过overlay封包和拆包,性能比较高,但hostgw模式最大的缺点是必须是在一个二层网络中,毕竟下一跳的路由需要在邻居表中,否则无法通行。
Pod和服务之间,以及外部应用与服务之间的通信请参考《Kubernetes-核心资源之Service》和《Kubernetes-核心资源之Ingress》。
flannel安装部署和在Kubernetes中运行的整体过程
flannel运行的基本流程:
1)设置网段(地址空间):flannel利用Kubernetes API或者etcd用于存储整个集群的网络配置,其中最主要的内容为设置集群的网络地址空间。例如,设定整个集群内所有容器的IP都取自网段“10.1.0.0/16”。
2)flannel服务:flannel在每个主机中运行flanneld作为agent,它会为所在主机从集群的网络地址空间中,获取一个小的网段subnet,本主机内所有容器的IP地址都将从中分配。
然后,flanneld再将本主机获取的subnet以及用于主机间通信的Public IP,同样通过kubernetes API或者etcd存储起来。
3)跨主机通信:最后,flannel利用各种backend mechanism,例如udp,vxlan等等,跨主机转发容器间的网络流量,完成容器间的跨主机通信。
1、下载安装
flannel和etcd一样,直接从官方下载二进制执行文件就可以用了。当然,你也可以自己编译。
下载地址:https://github.com/coreos/flannel/releases/
解压后主要有flanneld、mk-docker-opts.sh这两个文件,其中flanneld为主要的执行文件,sh脚本用于生成Docker启动参数。
2、 etcd注册网段
由于flannel需要依赖etcd来保证集群IP分配不冲突的问题,所以首先要在etcd中设置 flannel节点所使用的IP段。
所有Node上的flanneld都依赖etcd cluster来做集中配置服务,etcd保证了所有node上flanned所看到的配置是一致的。
etcdctl --endpoints="http://node1.etcd.tulingapi.com:2379,http://node2.etcd.tulingapi.com:2379,http://node2.etcd.tulingapi.com:2379" set /k8s/network/config '{ "Network": "10.0.0.0/16", "Backend": {"Type": "vxlan"}}'
{ "Network": "10.0.0.0/16", "Backend": {"Type": "vxlan"}}
写入的 Pod 网段 ${CLUSTER_CIDR} 必须是 /16 段地址,必须与 kube-controller-manager 的 --cluster-cidr 参数值一致;
flannel默认的backend type是udp,如果想要使用vxlan作为backend,可以加上backend参数: {"Type": "vxlan"}}
flannel backend为vxlan比起预设的udp性能相对好一些。
通过如下的命令能够查询网络配置信息:
$ etcdctl --endpoints "http://node1.etcd.tulingapi.com:2379" ls /k8s/network/config
/coreos.com/network/subnets/10.0.2.0-24
获取子网列表
$ etcdctl ls /k8s/network/subnets
/k8s/network/subnets/10.0.86.0-24
/k8s/network/subnets/10.0.35.0-24
/k8s/network/subnets/10.0.24.0-24
3、启动flannel
flanneld 运行时需要 root 权限;命令行方式运行:
ln -s /mnt/app/flannel-v0.11.0/flanneld /usr/bin/flanneld
$ flanneld --etcd-endpoints="http://node1.etcd.tulingapi.com:2379" --ip-masq=true #命令行
$ cd /mnt/logs/flannel && nohup flanneld --etcd-endpoints="http://node1.etcd.tulingapi.com:2379" -etcd-prefix=/k8s/network --ip-masq=true &
命令行后台
也可以创建一个flannel systemd服务,方便以后管理。
启动参数:
--logtostderr=false
--log_dir= /mnt/logs/flannel
--etcd-endpoints="http://node1.etcd.tulingapi.com:2379"
-etcd-prefix=/k8s/network #etcd路径前缀
--iface=192.168.10.50 #要绑定的网卡的IP地址,请根据实际情况修改。
启动时如果出现以下错误:
Couldn't fetch network config: 100: Key not found (/coreos.com)
通过-etcd-prefix指定/k8s/network。
flannel启动过程解析:
flannel服务需要先于Docker启动。flannel服务启动时主要做了以下几步的工作:
1)启动参数设置网卡及对外IP选择
2)从etcd中获取network的配置信息。
3)划分子网subnet,并在etcd中进行注册。
4)将子网信息记录到/run/flannel/subnet.env中。
5)在Node节点上,会创建一个名为flannel.1的虚拟网卡。
可以看到每个node上/run/flannel/subnet.env 子网掩码不一样。
启动参数设置网卡及对外IP选择
flanneld的启动参数中通过”–iface”或者”–iface-regex”进行指定。其中”–iface”的内容可以是完整的网卡名或IP地址,而”–iface-regex”则是用正则表达式表示的网卡名或IP地址,并且两个参数都能指定多个实例。flannel将以如下的优先级顺序来选取:
如果”–iface”和”—-iface-regex”都未指定时,则直接选取默认路由所使用的输出网卡
如果”–iface”参数不为空,则依次遍历其中的各个实例,直到找到和该网卡名或IP匹配的实例为止
如果”–iface-regex”参数不为空,操作方式和2)相同,唯一不同的是使用正则表达式去匹配
最后,对于集群间交互的Public IP,我们同样可以通过启动参数”–public-ip”进行指定。否则,将使用–iface获取网卡的IP作为Public IP。
验证flannel网络:
在node1节点上看etcd中的内容:
$ etcdctl --endpoints "http://node1.etcd.tulingapi.com:2379" ls /k8s/network/subnets
/k8s/network/subnets/10.0.24.0-24
[root@k8s-master flannel]# cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.0.0.0/16
FLANNEL_SUBNET=10.0.24.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
通过文件/run/flannel/subnet.env设定docker的网络。我们发现这里的MTU并不是以太网规定的1500,这是因为外层的vxlan封包还要占据50 Byte。
查看flannel1.1的网络情况,注意查看docker0和flannel是不是属于同一网段
可以看到flannel1.1网卡的地址和etcd中存储的地址一样,这样flannel网络配置完成。
4、创建docker网桥
容器配置名为docker0的网桥,实际是通过修改Docker的启动参数–bip来实现的。通过这种方式,为每个节点的Docker0网桥设置在整个集群范围内唯一的网段,从保证创建出来的Pod的IP地址是唯一。
在etcd1节点上看etcd中的内容:
etcdctl --endpoints "http://node1.etcd.tulingapi.com:2379" ls /k8s/network/subnets
/k8s/network/subnets/10.0.24.0-24
在各个节点安装好以后最后要更改Docker的启动参数,使其能够使用flannel进行IP分配,以及网络通讯。
flannel运行后会生成一个环境变量文件,包含了当前主机要使用flannel通讯的相关参数。
1)查看flannel分配的网络参数:
cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.0.0.0/16
FLANNEL_SUBNET=10.0.24.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
2)创建Docker运行参数
使用flannel提供的脚本mk-docker-opts.sh将subnet.env转写成Docker启动参数,创建好的启动参数位于/run/docker_opts.env文件中。
/mnt/app/flannel/mk-docker-opts.sh -d /run/docker_opts.env -c
$ cat /run/docker_opts.env
DOCKER_OPTS=" --bip=10.0.24.1/24 --ip-masq=false --mtu=1450"
- 修改Docker启动参数
修改docker的启动参数,并使其启动后使用由flannel生成的配置参数,修改如下:
编辑 systemd service 配置文件
$ vim /lib/systemd/system/docker.service
(1)、指定这些启动参数所在的文件位置:(这个配置是新增的,同样放在Service标签下)
EnvironmentFile=/run/docker_opts.env
(2)、在启动时增加flannel提供的启动参数:
ExecStart=/usr/bin/dockerd $DOCKER_OPTS
ubuntu修改如下:
然后重新加载systemd配置,并重启Docker即可
systemctl daemon-reload
systemctl restart docker
此时可以看到docker0的网卡ip地址已经处于flannel网卡网段之内。
到此节点etcd1的flannel安装配置完成了,其它两节点按以上方法配置完成就行了。
测试flannel
5、修改路由表
flannel会对路由表进行修改,从而能够实现容器跨主机的通信。
四、backend原理解析
集群范围内的网络地址空间为10.1.0.0/16:
Machine A获取的subnet为10.1.15.0/24,且其中的两个容器IP分别为10.1.15.2/24和10.1.15.3/24,两者都在10.1.15.0/24这一子网范围内。Machine B同理。
Machine A中的容器要与Machine B中的容器进行通信,封包是如何进行转发的?
从上文可知,每个主机的flanneld会将自己与所获取subnet的关联信息存入etcd中,例如,subnet 10.1.15.0/24所在主机可通过IP 192.168.0.100访问,subnet 10.1.16.0/24可通过IP 192.168.0.200访问。反之,每台主机上的flanneld通过监听etcd,也能够知道其他的subnet与哪些主机相关联。如上图,Machine A上的flanneld通过监听etcd已经知道subnet 10.1.16.0/24所在的主机可以通过Public 192.168.0.200访问,而且熟悉docker桥接模式的同学肯定知道,目的地址为10.1.20.3/24的封包一旦到达Machine B,就能通过veth0网桥转发到相应的pod,从而达到跨宿主机通信的目的。
因此,flanneld只要想办法将封包从Machine A转发到Machine B就OK了,其中backend就是用于完成这一任务。不过,达到这个目的的方法是多种多样的,所以我们也就有了很多种backend. 即网络模式:
flannel的支持多种网络模式,常用用都是vxlan、UDP、hostgw、ipip以及gce和阿里云等。即我们启动Backend参数:
etcdctl --endpoints="http://node1.etcd.tulingapi.com:2379,http://node2.etcd.tulingapi.com:2379,http://node2.etcd.tulingapi.com:2379" set /k8s/network/config '{ "Network": "10.0.0.0/16", "Backend": {"Type": "vxlan"}}'
我们将对hostgw,udp和vxlan三种backend进行解析。
1、 hostgw
hostgw是最简单的backend,它的原理非常简单,直接添加路由,将目的主机当做网关,直接路由原始封包。
因为Machine A和Machine B处于同一个子网内,它们原本就能直接互相访问。因此最简单的方法是:在Machine A中的容器要访问Machine B的容器时,我们可以将Machine B看成是网关,当有封包的目的地址在subnet 10.1.16.0/24范围内时,就将其直接转发至B即可。
图中那条红色标记的路由就能完成:我们从etcd中监听到一个EventAdded事件subnet为10.1.15.0/24被分配给主机Machine A Public IP 192.168.0.100,hostgw要做的工作就是在本主机上添加一条目的地址为10.1.15.0/24,网关地址为192.168.0.100,输出设备为上文中选择的集群间交互的网卡即可。对于EventRemoved事件,只需删除对应的路由。
2、 udp
我们知道当backend为hostgw时,主机之间传输的就是原始的容器网络封包,封包中的源IP地址和目的IP地址都为容器所有。这种方法有一定的限制,就是要求所有的主机都在一个子网内,即二层可达,否则就无法将目的主机当做网关,直接路由。
而udp类型backend的基本思想是:既然主机之间是可以相互通信的(并不要求主机在一个子网中),那么我们为什么不能将容器的网络封包作为负载数据在集群的主机之间进行传输呢?这就是所谓的overlay。具体过程如图所示:
当容器10.1.15.2/24要和容器10.1.20.3/24通信时:
1)因为该封包的目的地不在本主机subnet内,因此封包会首先通过网桥转发到主机中。最终在主机上经过路由匹配,进入如图的网卡flannel0。需要注意的是flannel0是一个tun设备,它是一种工作在三层的虚拟网络设备,而flanneld是一个proxy,它会监听flannel0并转发流量。当封包进入flannel0时,flanneld就可以从flannel0中将封包读出,由于flannel0是三层设备,所以读出的封包仅仅包含IP层的报头及其负载。最后flanneld会将获取的封包作为负载数据,通过udp socket发往目的主机。同时,在目的主机的flanneld会监听Public IP所在的设备,从中读取udp封包的负载,并将其放入flannel0设备内。由此,容器网络封包到达目的主机,之后就可以通过网桥转发到目的容器了。
最后和hostgw不同的是,udp backend并不会将从etcd中监听到的事件里所包含的lease信息作为路由写入主机中。每当收到一个EventAdded事件,flanneld都会将其中的subnet和Public IP保存在一个数组中,用于转发封包时进行查询,找到目的主机的Public IP作为udp封包的目的地址。
3、 vxlan
首先,我们对vxlan的基本原理进行简单的叙述。从下图所示的封包结构来看,vxlan和上文提到的udp backend的封包结构是非常类似的,不同之处是多了一个vxlan header,以及原始报文中多了个二层的报头。
下面让我们来看看,当有一个EventAdded到来时,flanneld如何进行配置,以及封包是如何在flannel网络中流动的。
如上图所示,当主机B加入flannel网络时,和其他所有backend一样,它会将自己的subnet 10.1.16.0/24和Public IP 192.168.0.101写入etcd中,和其他backend不一样的是,它还会将vtep设备flannel.1的mac地址也写入etcd中。
之后,主机A会得到EventAdded事件,并从中获取上文中B添加至etcd的各种信息。这个时候,它会在本机上添加三条信息:
路由信息:所有通往目的地址10.1.16.0/24的封包都通过vtep设备flannel.1设备发出,发往的网关地址为10.1.16.0,即主机B中的flannel.1设备。
fdb信息:MAC地址为MAC B的封包,都将通过vxlan首先发往目的地址192.168.0.101,即主机B
3)arp信息:网关地址10.1.16.0的地址为MAC B
现在有一个容器网络封包要从A发往容器B,和其他backend中的场景一样,封包首先通过网桥转发到主机A中。此时通过,查找路由表,该封包应当通过设备flannel.1发往网关10.1.16.0。通过进一步查找arp表,我们知道目的地址10.1.16.0的mac地址为MAC B。到现在为止,vxlan负载部分的数据已经封装完成。由于flannel.1是vtep设备,会对通过它发出的数据进行vxlan封装(这一步是由内核完成的,相当于udp backend中的proxy),那么该vxlan封包外层的目的地址IP地址该如何获取呢?事实上,对于目的mac地址为MAC B的封包,通过查询fdb,我们就能知道目的主机的IP地址为192.168.0.101。
最后,封包到达主机B的eth0,通过内核的vxlan模块解包,容器数据封包将到达vxlan设备flannel.1,封包的目的以太网地址和flannel.1的以太网地址相等,三层封包最终将进入主机B并通过路由转发达到目的容器。
事实上,flannel只使用了vxlan的部分功能,由于VNI被固定为1,本质上工作方式和udp backend是类似的,区别无非是将udp的proxy换成了内核中的vxlan处理模块。而原始负载由三层扩展到了二层,但是这对三层网络方案flannel是没有意义的,这么做也仅仅只是为了适配vxlan的模型。vxlan详细的原理参见文后的参考文献,其中的分析更为具体,也更易理解。
4、数据传递过程
在源容器宿主机中的数据传递过程:
1)源容器向目标容器发送数据,数据首先发送给docker0网桥
在源容器内容查看路由信息:
$ kubectl exec -it -p {Podid} -c {ContainerId} -- ip route
2)docker0网桥接受到数据后,将其转交给flannel.1虚拟网卡处理
docker0收到数据包后,docker0的内核栈处理程序会读取这个数据包的目标地址,根据目标地址将数据包发送给下一个路由节点:
查看源容器所在Node的路由信息:
$ ip route
3)flannel.1接受到数据后,对数据进行封装,并发给宿主机的eth0
flannel.1收到数据后,flannelid会将数据包封装成二层以太包。
Ethernet Header的信息:
From:{源容器flannel.1虚拟网卡的MAC地址}
To:{目录容器flannel.1虚拟网卡的MAC地址}
4)对在flannel路由节点封装后的数据,进行再封装后,转发给目标容器Node的eth0
由于目前的数据包只是vxlan tunnel上的数据包,因此还不能在物理网络上进行传输。因此,需要将上述数据包再次进行封装,才能源容器节点传输到目标容器节点,这项工作在由linux内核来完成。
Ethernet Header的信息:
From:{源容器Node节点网卡的MAC地址}
To:{目录容器Node节点网卡的MAC地址}
IP Header的信息:
From:{源容器Node节点网卡的IP地址}
To:{目录容器Node节点网卡的IP地址}
通过此次封装,就可以通过物理网络发送数据包。
在目标容器宿主机中的数据传递过程:
5)目标容器宿主机的eth0接收到数据后,对数据包进行拆封,并转发给flannel.1虚拟网卡;
6)flannel.1 虚拟网卡接受到数据,将数据发送给docker0网桥;
7)最后,数据到达目标容器,完成容器之间的数据通信。
五、Kubernetes Cluster中的几个“网络”
node network:承载kubernetes集群中各个“物理”Node(master和minion)通信的网络;
service network:由kubernetes集群中的Services所组成的“网络”;
flannel network: 即Pod网络,集群中承载各个Pod相互通信的网络。
1、node network (Node IP)
自不必多说,node间通过你的本地局域网(无论是物理的还是虚拟的)通信。
2、service network (clusterI):比较特殊,每个新创建的service会被分配一个service IP 即spec.clusterIP,在集群中,这个IP的分配范围并不“真实”,更像一个“占位符”并且只有入口流量,所谓的“network”也是“名不符实”的,后续我们会详尽说明。
Service network(看cluster-ip一列):
# kubectl get services
3、flannel network (Pod IP):是我们要理解的重点,cluster中各个Pod要实现相互通信,必须走这个网络,无论是在同一node上的Pod还是跨node的Pod。
Flannel network(看IP那列):
# kubectl get pod -o wide