Kubernetes 网络管理
在 Kubernetes 中,并不是用 docker0 来作为网桥,而是通过一个 CNI 接口来替代 docker0,它在宿主机上的默认名字叫 cni0。
以 Flannel 的 XVLAN 模式为例,其在 Kubernetes 中的工作流程不变,只是其中的 docker0 网桥替换为 cni0 网桥了,其流程如下:
注意:
CNI 网桥只负责 Kubernetes 创建的 Pod,如果你单独用 docker run 启动一个容器,其网桥依然是 docker0。
Kubernetes 中设计一个与 docker0 相同功能的 CNI 网桥,其主要原因有以下两点:
- Kubernetes 没有 Docker 的网络模型,它并不希望也不具备配置 docker0 的能力;
- 与 Kubernetes 配置 Infra 容器的 Network Namespace 密切相关;
CNI 的设计思想即为:Kubernetes 在启动 Infra 容器之后,就可以直接调用 CNI 网络插件,为这个 Infra 容器的 Network Namespace 配置符合预期的网络栈。
# CNI
CNI 中有两个重要的概念:
- 容器(Container):是拥有独立 Linux 网络命名空间的环境,例如使用 Docker 或 rkt 创建的容器。关键之处是容器需要拥有自己的 Linux 网络命名空间,这是加入网络的必要条件。
- 网络(Network):表示可以互连的一组实体,这些实体拥有各自独立、唯一的 IP 地址,可以是容器、物理机或者其他网络设备(比如路由器)等。
对容器网络的设置和操作都通过插件(Plugin)进行具体实现,CNI 插件包括两种类型:CNIPlugin 和 IPAM(IP Address Management)Plugin。CNI Plugin 负责为容器配置网络资源,IPAM Plugin 负责对容器的 IP 地址进行分配和管理。IPAM Plugin 作为 CNI Plugin 的一部分,与 CNI Plugin 一起工作。
# Flannel
Kubernetes 中解决网络跨主机通信的一个经典插件就是 Flannel。Flannel 实质上只是一个框架,真正为我们提供网络功能的是后端的 Flannel 实现,目前 Flannel 后端实现的方式有三种:
- UDP
- VXLAN
- HOST-GW
# 一、UDP
UDP 是最早的实现方式,但是由于其性能原因,现已经被废弃,但是 UDP 模式是最直接,也最容易理解的跨主机实现方式。
假如有两台 Node,如下:
- Node01 上有容器 nginx01,其 IP 为 172.20.1.107,其 docker0 的地址为 172.20.1.1/24;
- Node02 上有容器 nginx02,其 IP 为 172.20.2.133,其 docker0 的地址为 172.20.2.1/24;
那么现在 nginx01 要访问 nginx02,其流程应该是怎么样的呢?
- 首先从 nginx01 发送 IP 包,源 IP 是 172.20.1.107,目的 IP 是 172.20.2.133。
- 由于目的 IP 并不在 Node01 上的 docker0 网桥里,所以会将包通过默认路由转发到 docker0 网桥所在的宿主机上;
- 它会通过本地的路由规则,转发到下一个目的 IP,我们可以通过 ip route 查看本地的路由信息,通过路由信息可以看到它被转发到一个 flannel0 的设备中;
- flannel0 设备会把这个 IP 包交给创建这个设备的应用程序,也就是 Flannel 进程(从内核状态向用户状态切换);
- Flannel 进程收到 IP 包后,将这个包封装在 UDP 中,就根据其目的地址将其转发给 Node02(通过每个宿主机上监听的 8285 端口),这时候的源地址是 Node01 的地址,目的地址是 Node02 的地址;
- Node02 收到包后,就会直接将其转发给 flannel0 设备,然后进行解包,匹配本地路由规则转发给 docker0 网桥,然后 docker0 网桥就扮演二层交换机的功能,将包转发到最终的目的地;
其流程图如下:
注:
1、flannel0 是一个 TUN 设备,它的作用是在操作系统和应用程序之间传递 IP 包;
2、Flannel 是根据子网(Subnet)来查看 IP 地址对应的容器是运行在那个 Node 上;
3、这些子网和 Node 的对应关系,是保存在 Etcd 中(仅限 UDP 模式);
4、UDP 模式其实是一个三层的 Overlay 网络;它首先对发出的 IP 包进行 UDP 封装,然后接收端对包进行解封拿到原始 IP,进而把这个包转发给目标容器。这就好比 Flannel 在不同的宿主机上的两容器之间打通了一条隧道,使得这个两个 IP 可以通信,而无需关心容器和宿主机的分布情况;
UDP 之所以被废弃是主要是由于其仅在发包的过程中就在用户态和内核态进行来回的数据交换,这样的性能代价是很高的。如下:
# 二、VXLAN
VXLAN:Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种虚拟化网络技术,它可以完全在内核态实现上述的封装和解封装过程,减少用户态到内核态的切换次数,把核心的处理逻辑都放到内核态,其通过与前面相似的隧道技术,构建出覆盖网络或者叠加网络(Overlay Network)。
其设计思想为在现有的三层网络下,叠加一层虚拟的并由内核 VXLAN 维护的二层网络,使得连接在这个二层网络上的主机可以像在局域网一样通信。
为了能够在二层网络中打通隧道,VXLAN 会在宿主机上设置一个特殊的网络设备作为隧道的两端,这个隧道就叫 VTEP(Virtual Tunnel End Point 虚拟隧道端点)。而 VTEP 的作用跟上面的 flanneld 进程非常相似,只不过它进行封装和解封的对象是二层的数据帧,而且这个工作的执行流程全部在内核中完成。
其流程如下:
我们可以看到每台 Node 上都由一个 flannel.1 的网卡,它就是 VXLAN 所需要的 VTEP 设备,它既有 IP 地址,也由 MAC 地址。
现在我们 nginx01 要访问 nginx02,其流程如下:
- nginx01 发送请求包会被转发到 docker0;
- 然后会通过路由转发到本机的 flannel.1;
- flannel.1 收到包后通过 ARP 记录找到目的 MAC 地址,并将其加原始包上,封装成二层数据帧(将源 MAC 地址和目的 MAC 地址封装在它们对应的 IP 头外部);
- Linux 内核把这个数据帧封装成普通的可传输的数据帧,通过宿主机的 eth0 进行传输(也就是在原有的数据帧上面加一个 VXLAN 头 VNI,它是识别某个数据帧是不是归自己处理的的重要标识,而在 flannel 中,VNI 的默认值就是 1,这是由于宿主机上的 VTEP 设备名称叫 flannel.1,这里的 1 就是 VNI 的值);
- 然后 Linux 内核会把这个数据帧封装到 UDP 包里发出去;
- Node02 收到包后发现 VNI 为 1,Linux 内核会对其进行解包,拿到里面的数据帧,然后根据 VNI 的值把它交给 Node02 上的 flannel.1 设备,然后继续进行接下来的处理;
在这种场景下,flannel.1 设备实际扮演的是一个网桥的角色,在二层网络进行 UDP 包的转发,在 Linux 内核中,网桥设备进行转发的依据是一个叫做 FDB(Foewarding Database)的转发数据库,它的内容可以通过 bridge fdb 命令可以查看。
# 三、HOST-GW
前面的两种模式都是二层网络的解决方案,对于三层网络,Flannel 提供 host-gw 解决方案。
以下是 host-gw 示意图:
如上所示,如果我 nginx01 要访问 nginx02,则起流程如下:
- 转发请求包会被转发到 cni0;
- 到达本机后会匹配本机的路由,如上的路由信息,然后发现要到 172.20.2.0/24 的请求要经过 eth0 出去,并且吓一跳地址为 172.16.1.130;
- 到达 Node2 过后,通过路由规则到 node02 的 cni0,再转发到 nginx02;
其工作流程比较简单,主要是会在节点上生成许多路由规则。
host-gw 的工作原理就是将 Flannel 的所有子网的下一跳设置成该子网对应的宿主机的 IP 地址,也就是说 Host 会充当这条容器通信路径的网关,当然,Flannel 子网和主机的信息会保存在 Etcd 中,flanneld 进程只需要 WATCH 这个数据的变化,然后实时更新路由表。
在这种模式下,就免除了额外的封包解包的性能损耗,在这种模式下,性能损耗大约在 10%左右,而 XVLAN 隧道的机制,性能损耗大约在 20%~30%。
从上面可以知道,host-gw 的工作核心为 IP 包在封装成帧发送出去的时候会在使用路由表中写下一跳来设置目的的 MAC 地址,这样它就会经过二层转发到达目的宿主机。这就要求集群宿主机必须是二层连通的。
要修改 flannel 模式就修改如下配置:
net-conf.json: |
{
"Network": "172.20.0.0/16",
"Backend": {
"Type": "host-gw"
}
}
net-conf.json: |
{
"Network": "172.20.0.0/16",
"Backend": {
"Type": "vxlan",
"Directrouting": true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Calico
前面介绍 Flannel 的三层网络解决方案是 host-gw,其实还有一个非常出名的单独的三层网络解决方案 Calico,它的工作模式和 host-gw 基本一致,也是会在每台宿主机上添加如下格式的路由规则:
<目的容器IP地址段> via <网关的IP地址> dev eth0
其中网关的 IP 地址就是目的容器所在的宿主机的 IP 地址。而且这个三层网络得以正常工作的核心就是为每个容器的 IP 地址找到它所对应的下一跳的网关地址。
不同于 Flannel,Calico 是通过 BGP(Border Gateway Protocol 边界网关协议)来自动的在集群中分发路由信息。BGP 是 Linux 原生就支持的专门用在大规模数据中心维护不同的"自治系统"之间的路由信息、无中心得路由协议。而所谓得"自治系统"就是一个组织管辖下得所有 IP 网络和路由器的全体。如下图所示 AS1 和 AS2 就是独立的"自治系统",这两个"自治系统"要进行通信,就必须要用路由器把它们连接起来。
如上,比如 AS1 的 10.10.0.2 要访问 172.17.0.2,它发出的 IP 包就会先到达 AS1 的 route1,然后根据里面的规则找到从 C 口出去到达 router2,然后经过 route2 的路由表到达目的地 172.17.0.2。
像这样负责把"自治系统"连接在一起的路由器,我们称之为"边界 网关",它跟普通的路由器的不同之处在于它的路由表里拥有其他自治系统里的主机路由信息
。
而 BGP 就可以认为是在每个边界网关上运行的一个小程序,其作用将各自的路由表信息通过 TCP 传输给其他的边界网关,而其他边界网关上的小程序就会对接收到的数据进行处理分析,将需要的路由表添加到自己的路由表里。所以,所谓的 BGP,就是大规模网络中实现节点路由信息共享的一种协议。
明白了 BGP 的大体工作原理后,其 Calico 的工作架构就非常容易理解,它主要由 Felix,BGP Client,BGP Router Reflector 组成:
- Felix:Calico Agent,每个节点都需要运行,主要负责配置路由,处理 ACL,报告状态等;
- BGP Client:负责将 Felix 配置的路由信息分发到其他节点
- BGP Router Reflector:大规模集群需要使用到,作为 BGP Client 的中心连接点,避免每个节点互联
如上就是 Calico 的工作原理(Calico 不会在宿主机上创建任何网桥设备,这与 Flannel 是不同的)。
其工作流程如下:
- 首先 CNI 会创建一对 Veth Pair 设备,其中一端放在宿主机上;
- 由于 Calico 没有使用 CNI 的网桥模式,所以还需要在宿主机上为每个容器的 Veth Pair 添加一条路由规则,用于接收传入的 IP 包,例如在 Container4 上会添加" 10.233.2.3 dev cali5863f3 scope link",即发往 10.233.2.3 的 IP 包应该进入 cali5863f3 设备;
- 然后的流程在路由表中找到下一跳的信息,然后找到 MAC 地址,通过 eth0 传到下一跳,再进行解包转发等,和 Flannel host-gw 的工作流程一样;
从上面可以看到 Calico 将集群中的所有节点都当作是边界路由器来处理,它们之间组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则,这些节点我们称之为 BGP Peer。
# Node-to-Node Mesh
Calico 的默认模式是 Node-to-Node Mesh,这种模式下,所有主机上的 BGP Client 都需要于其他主机进行通信交换路由规则信息,但是随着节点 N 的增加,其连接数也是 N*2 倍增加,从而给集群的网络带来很大的眼里,所以使用该模式,一般建议集群节点在 100 个之内。
# Route Reflector
如果节点数超过了 100,那么就可以使用 Route Reflector 模式,它会指定一个或者几个专门的节点,来负责跟所有的 BGP 建立连接从而学习全局的路由规则,而其他的节点只需要跟这个专门的节点进行路由信息的交换,就可以获取整个集群的路由信息了。而这些专门的节点就是 Route Reflector 节点,它扮演中间代理的角色。
# IP-IP
如果不同子网的宿主机要进行通信,则需要打开 IPIP 模式了,如下:
在 Calico 的 IPIP 模式下,Felix 进程就会在 Node 上添加如下路由规则,如 Node1:
10.233.2.0/24 via 192.168.2.2 tunl0
其中的 tunl0 就是一个 IP Tunnel 设备。IP 包进入 IP 隧道后就会被 Linux 内核中的 IPIP 驱动接管,IPIP 驱动会将这个 IP 包直接封装在一个宿主机网络的 IP 包中,经过封装后的 IP 包的目的地址就是原 IP 包的下一跳地址,即目的容器所在的宿主机的 IP 地址,然后将 IP 包直接转发到目的宿主机,然后会使用 IPIP 驱动进行解包,然后根据本地的路由信息转发到对应的容器中去。
参考文档:
- 极客时间《深入剖析 Kubernetes》之解读 kubernetes 三层网络方案
# NweworkPolicy
在 Kubernetes 中,网络隔离功能是通过叫 NetworkPolicy 的 API 对象来描述的。
如下一个完整的 NetworkPolicy 定义:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
解释:
- podSelector:定义这个 NetworkPolicy 的限制范围,上面就是定义 namespace 中标签为 role: db 的 Pod,如果为空,则标识这个 namespace 下的所有 Pod,如果一旦被 podSelector 选中,则这个 Pod 就会进入拒绝所有的状态,即这个 Pod 既不允许被外界访问,也不能对外界发起访问。
- policyTypes:定义 NetworkPolicy 的类型,ingress 表示流入请求,egress 表示流出请求。
- ingress:定义流入的规则
- egress:定义流出的规则
其中 ingress 字段中的 from 和 ports,定义允许流入的白名单和端口,这里面的白名单有三种限制方式:
- ipBlock:限制 IP,上面定义的即为允许 172.17.0.0/16 但不是 172.17.1.0/24 的网段请求;
- namespaceSelector:限制 namespace,上面定义即为允许 default namespace 下标签为 project: myproject 的的 Pod 请求;
- podSelector:限制 Pod,上面定义即为允许标签为 role: frontend 的 Pod 请求;
而 egress 字段中的 to 和 ports,则指定允许流出的白名单和端口,这里的限制方式和 ingress 类似。
注意:下面这两种白名单的定义方式是不一样的。
(1)、第一种
...
ingress:
- from:
- namespaceSelector:
matchLabels:
user: alice
- podSelector:
matchLabels:
role: client
...
2
3
4
5
6
7
8
9
10
11
(2)、第二种
...
ingress:
- from:
- namespaceSelector:
matchLabels:
user: alice
podSelector:
matchLabels:
role: client
...
2
3
4
5
6
7
8
9
10
11
这两种看起来类似,但是其表示的意义是不一样的,对于第一种表示的是一种 OR(或)的关系,对于这种情况只要其中一种规则满足要求都可以通过,而对于第二种则表示 AND(与)的关系,必须两种同时满足才会通过。
Kubernetes 对 Pod 的网络隔离其实是靠宿主机上生成 NetworkPolicy 对应的 iptables 规则来实现的。
比如定义好了上面的 NetworkPolicy,那么就会生成类似下面的 iptables 规则:
iptables -A KUBE-NWPLCY-CHAIN -s $srcIP -d $dstIP -p $protocol -m $protocol --dport $port -j ACCEPT
其中:
- srcIP:原 IP
- dstIP:目的 IP
- protocol:协议
- port:端口
这些参数都是从我们定义的 NetworkPolicy 中取出来,然后还将对所有对被隔离 Pod 的访问请求都转发到 KUBE-NWPLCY-CHAIN 上去匹配,如果匹配不通过则拒绝。
第一组 KUBE-NWPLCY-CHAIN 规则如下:
iptables -A FORWARD -d $podIP -m physdev --physdev-is-bridged -j KUBE-POD-SPECIFIC-FW-CHAIN
iptables -A FORWARD -d $podIP -j KUBE-POD-SPECIFIC-FW-CHAIN
2
其中:
- 第一条 FORWARD 链的作用是通过本机网桥设备发往 podIP 的 IP 包;
- 第二条就是拦截跨主机通信,定义规则都到 KUBE-POD-SPECIFIC-FW-CHAIN 规则上
第二组 KUBE-POD-SPECIFIC-FW-CHAIN 规则如下:
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j KUBE-NWPLCY-CHAIN
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j REJECT --reject-with icmp-port-unreachable
2
其中:
- 第一条是把数据包发到 KUBE-NWPLCY-CHAIN 去匹配;
- 第二条就是把不满足 NetworkPolicy 中定义的请求都拒绝掉,从而实现对容器的隔离;
默认的 Policies:
(1)、默认拒绝所有 Ingress:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
2
3
4
5
6
7
8
(2)、默认允许所有 ingress:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all
spec:
podSelector: {}
ingress:
- {}
policyTypes:
- Ingress
2
3
4
5
6
7
8
9
10
(3)、默认拒绝所有 Egress:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Egress
2
3
4
5
6
7
8
(4)、默认允许所有 Egress:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all
spec:
podSelector: {}
egress:
- {}
policyTypes:
- Egress
2
3
4
5
6
7
8
9
10
(5)、拒绝所有 Ingress 和 Egress:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
2
3
4
5
6
7
8
9
例子:
1、创建两个 namespace
# kubectl create ns dev
# kubectl create ns sit
2
2、给 dev 下的所有 pod 配置 Ingress 权限,不允许所有人访问
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: networkpolicy-ingress-demo
spec:
podSelector: {}
policyTypes:
- Ingress
2
3
4
5
6
7
8
然后使其作用于 dev
# kubectl apply -f ingress-demo.yaml -n dev
我们在 dev 里创建一个 Pod,在外部访问查看情况
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: myapp
image: nginx:1.7.9
imagePullPolicy: IfNotPresent
command:
- "/bin/sh"
- "-c"
args:
- "nginx && sleep 3600"
2
3
4
5
6
7
8
9
10
11
12
13
14
启动 Pod
# kubectl apply -f pod-demo.yaml -n dev