kubernets的网络,从设计上来讲是“扁平、直接”的,即要求:
- 所有容器可以不使用NAT技术就可以与其他容器通信
- 所有节点(物理机 虚拟机 容器)都可以不使用NAT同容器通信
- 容器看到的IP地址和别的机器看到的IP是一致的
docker的网络方案
docker的网络支持如下四种:
- none
- host,与宿主机共享,占用宿主机资源
- container,使用某容器的namespace,例如k8s的同一pod内的各个容器
- bridge,挂到网桥docker0上,走iptables做NAT
实际上还有一种方法:先以none的方式run起来容器,然后使用pipework为容器增加一个veth网卡,该veth的另一端挂到新建的网桥br0上;再将宿主机的物理网卡挂到该网桥br0上:
[容器内eth0]--veth--br0--en0-->
需要注意这种方法与bridge的不同。docker0的网段是172.17.0.1/16,当bridge模式时,容器的ip地址均为此网段下的,报文是走NAT出去的。但pipework自定义网络时,br0、[eth0]均与宿主机的en0同一网段,报文走网桥转发出去。
docker的网络能否满足需求呢?
bridge模式下,不同物理机的容器ip是完全的平行空间,可能相同,不能满足k8s扁平的要求;pipework方式能够满足k8s的要求,但是需要为每个容器都指定ip地址,比较啰嗦。
k8s的flannel模式
k8s本身并不提供网络方案,而是交给flannel,ovs等add-on来处理。这里只对flannel做说明。
flannel模式原理
flannel是一种over-lay网络。简单来说,over-lay即报文在进入实际物理网络之前,会经过一层UDP封装,作为payload到达对端;对端拿到UDP报文后解包,得到真实的用户报文后,再转到真实的接收方。
以绿色线的一个具体报文来说:
1、Pod内的一个容器使用pod的网络namespace,发送报文;该网络namespace上的网卡类型为veth,其pair网卡为宿主机网络namespace空间上的veth网卡veth0。veth是一种类似管道的网络设备,总是成对出现,报文从一端的veth网卡发送后,另一端的veth网卡会收到该报文。通常容器、虚拟机,会创建一对veth网卡,并将其中一端加到自己的namespace中。因此,宿主机的veth0网卡会收到容器发出的报文。
2、veth0拿到后,由于目的地址10.1.20.x与veth不在同一网段,因此会将报文交给网桥来转发。官方图示为docker0,但出于网络地址规划的原因,实际在k8s上会新建一个cni0网桥,cni0网桥负责本node容器的ip分配(24位掩码)。 这里有一个问题:各个node都有自己的cni0网桥,怎么保证地址不会分配重复呢?这里就是靠flannel了,flannel会根据全局统一的etcd来为每个node分配全集群唯一的网段,避免地址分配冲突。 cnio拿到报文后,查询本机路由,匹配的是16位掩码的flannel.1,因此将报文丢给flannel.1。
[root@note2 ~]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.181.254 0.0.0.0 UG 100 0 0 eno16780032
10.1.0.0 0.0.0.0 255.255.0.0 U 0 0 0 flannel.1
10.1.15.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.181.0 0.0.0.0 255.255.255.0 U 100 0 0 eno16780032
3、flannel.1是个什么类型的网卡呢?图示的下一跳是flanneld,一个用户态进程(当然,这个进程已经包装在docker容器里了),这是怎么实现的呢? 这里需要重提一下linux上用户态和内核态通信的手段。一般来说,有以下几种:
- netlink socket
- syscall,例如调用用户态的read/write接口
- IOCTL
- procfs,例如读取/proc目录下的ip统计计数
还有一种手段,使用TUN/TAP接口。
tun/tap驱动程序实现了虚拟网卡的功能,tun表示虚拟的是点对点设备,tap表示虚拟的是以太网设备,这两种设备针对网络包实施不同的封装。利用tun/tap驱动,可以将tcp/ip协议栈处理好的网络分包传给任何一个使用tun/tap驱动的进程,由进程重新处理后再发到物理链路中。开源项目openvpn( http://openvpn.sourceforge.net)和Vtun( http://vtun.sourceforge.net)都是利用tun/tap驱动实现的隧道封装
具体到k8s上,flanneld包装在flannel-git容器中,该容器与宿主机是同一网络namespace;flanneld启动时会创建flannel.1网卡,用来接收所有发送到10.1.0.0/16网络的报文;上面第2步报文转给flannel.1后,内核会将报文上送给flanneld。
[root@node1 ~]# docker ps
92197740eeef quay.io/coreos/flannel-git:v0.6.1-62-g6d631ba-amd64 "/opt/bin/flanneld --" 22 hours ago Up 22 hours k8s_kube-flannel.135690a3_kube-flannel-ds-ze30q_kube-system_ce4936c7-dd2c-11e6-9af1-000c29906342_1faf7ca4
4、flanneld维护了一份全局的node网络信息,根据报文目的地址查询得到该地址对应的node信息后,将报文封装到udp中(新报文的目的地址为对应node的地址),再将封装后的udp报文查询路由后经过物理网络(eno16780032)发送给目的node。
5、对端node收到报文后,走普通的查询路由为本机后上送用户态流程,封装报文交给flanneld。之后,报文解包、根据新包目的地址查路由,交给目的pod的容器。
[root@node1 ~]# netstat -anup|more
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 0.0.0.0:8472 0.0.0.0:*
flannel模式的优缺点
最大的缺点是,所有报文都需要走flanneld这个用户态进程进行一次封装后,才能出去。当网络流量较大时,flanneld将会成为瓶颈;相对来说,open vswitch可能稳定性、可靠性会更好一些。 但flannel也有ovs所不具备的优点:flannel能够通过etcd感知k8s的service变动,动态维护自己的路由表(第4步)。
部署及验证
1、部署 flannel部署比较简单。在master上kubeadm init完成后,执行下面的命令。该yaml定义了flannel容器以及相关的配置容器。有一些材料没有使用容器部署,相对来说复杂一点。
kubectl create -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
之后查看kube-flannel的状态,Running则成功。此时k8s集群只有master一台机器,再登陆到2个node上,执行kubeadm join --token={token_id} master_ip
将node加入到k8s集群中。最终在master上查看结果如下。
[root@localhost k8s]# kubectl get nodes
NAME STATUS AGE
localhost.localdomain Ready 1d
node1 Ready 23h
note2 Ready 23h
[root@localhost k8s]#
[root@localhost k8s]# kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
dummy-2088944543-vafe7 1/1 Running 0 1d
etcd-localhost.localdomain 1/1 Running 1 1d
kube-apiserver-localhost.localdomain 1/1 Running 1 1d
kube-controller-manager-localhost.localdomain 1/1 Running 0 1d
kube-discovery-982812725-5j9ri 1/1 Running 0 1d
kube-dns-2247936740-gqifl 3/3 Running 0 1d
kube-flannel-ds-kfcpe 2/2 Running 0 1d
kube-flannel-ds-klmfz 2/2 Running 7 23h
kube-flannel-ds-ze30q 2/2 Running 4 23h
kube-proxy-amd64-2yx0g 1/1 Running 0 1d
kube-proxy-amd64-hcj9t 1/1 Running 0 23h
kube-proxy-amd64-vhevz 1/1 Running 0 23h
kube-scheduler-localhost.localdomain 1/1 Running 1 1d
在node上,可以看到flannel.1等网卡信息。
2、验证
将下面的RC保存为alpine.yaml。
apiVersion: v1
kind: ReplicationController
metadata:
name: alpine
labels:
name: alpine
spec:
replicas: 2
selector:
name: alpine
template:
metadata:
labels:
name: alpine
spec:
containers:
- image: mritd/alpine:3.4
imagePullPolicy: Always
name: alpine
command:
- "bash"
- "-c"
- "while true;do echo test;done"
ports:
- containerPort: 8080
name: alpine
并创建namespace、apply。
kubectl create namespace alpine
kubectl apply -n alpine -f alpine.yml
等待一段时间后,在master上查看apply情况(-o wide可以看到更多信息,即IP/node):
[root@localhost k8s]# kubectl get pods -n alpine -o wide
NAME READY STATUS RESTARTS AGE IP NODE
alpine-4zmey 1/1 Running 0 23h 10.244.1.2 note2
alpine-55zej 1/1 Running 0 23h 10.244.2.2 node1
2个pod分别跑在2个node上(前面yml定义的replicas为2)。
登陆到其中一个node上,进入对应的容器docker exec -it {docker_id} bash
,ping对端的ip地址看是否通。如果不通,可能你使用的也是CentOS操作系统,它的iptables默认会丢弃所有报文并回复icmp-host-prohibited,所以需要将flanneld监听的8472端口/udp协议的报文加到INPUT链上,各个物理node上都要执行。
当然,我觉得更好的做法是flannel自己来加这条规则。
[root@node1 ~]# iptables -I INPUT -p udp -m udp --dport 8472 -j ACCEPT
[root@node1 ~]#
[root@node1 ~]# iptables -L -n|more
Chain INPUT (policy ACCEPT)
target prot opt source destination
ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:8472
...
REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
在物理网卡上抓了个包,可以看到该UDP包的payload中黑色横线标记的2个ip地址。
以上。