openshift-sdn的由来和现状
openshift-sdn是红帽推出的一款容器集群网络方案。一直集成于openshift平台中。 但红帽将项目代码进行了开源。
实际上,我们通过一些修改,完全可以将openshift-sdn作为一款通用的容器集群的网络方案。
openshift-sdn官方建议使用network-operator工具进行网络部署,实际上在该项目中我们甚至可以扒出一套基本完整的部署模板。基于这套模板我们可以直接部署openshift-sdn。
为了加深大家的理解,本文我们会详细地介绍整个方案的功能、使用和原理。我们相信,如果你完全理解了本文的内容,你也能在集群的openshift-sdn网络出现故障时,能游刃有余地进行排障。
openshift-sdn的功能
openshift-sdn依赖于了openvswitch技术,也就是虚拟交换机,在k8s集群的每个节点上都要求部署好openvswitch并启动服务:
systemctl status openvswitch-switch.service
注:社区较新版本已经通过sidecar集成了openvswitch,不再需要宿主机上部署该服务
openshift-sdn通过构建和维护一套流表,以及一些路由和iptables策略,就实现了基本的容器网络需求:
- 集群中跨节点的pod通信
- pod到service的通信
- pod到外部网络的通信
除此之外,还提供了丰富的扩展能力:
- 提供multi-tenant模式,支持namespace维度的租户隔离
- 提供networkpolicy模式,支持k8s networkpolicy
- 支持在上述两种模式下,在pod间使用多播流量
可以说openshift-sdn的功能已经趋于完备。
openshift-sdn的组成
openshift-sdn包括了管控面和数据面。
- ctrl。 管控面,是一套deployment,用于自动化地给每个节点分配网段,并记录到crd中
- node。 数据面,是一套daemonset,用于根据crd变化,构建节点网络数据面。包括路由、网卡、流表、iptables规则。
openshift-sdn的用法
基础用法
没有任何特殊的操作,规划好集群里pod、service的网段、 并部署好openshift-sdn组件后,我们就可以部署pod了
租户隔离
在使用mulit-tenant模式时,集群中每个namespace都会被创建出一个同名的netnamespace
,这是openshift-sdn设计的crd,我们看看里头记录了啥:
kubectl get netnamespaces kube-system -o yaml
apiVersion: network.openshift.io/v1
kind: NetNamespace
metadata:
creationTimestamp: 2020-07-08T09:47:15Z
generation: 1
name: kube-system
resourceVersion: "33838361"
selfLink: /apis/network.openshift.io/v1/netnamespaces/kube-system
uid: 017460a8-c100-11ea-b605-fa163e6fe7d6
netid: 4731218
netname: kube-system
整体看下来,唯一有意义的字段就是netid了,这个整型表示了全局唯一的id,当不同的netnamespace,彼此之间的netid不同时,他们对应的namespace下的pod,就彼此不通。
当某个netnamespace的netid为0,表示这个netnamespace下的pod可以与任何namespace下的pod互通。
通过这种逻辑,我们可以基于namespace来设计租户,实现租户隔离
集群的扩展
如果集群的pod IP不够用了怎么办?这是众多开源的容器网络方案的共同问题。openshift-sdn提供了一个灵活的扩展机制。
刚才提到集群部署时要先规划好集群pod的CIDR和service的CIDR,当部署好openshift-sdn后,我们可以看到:
# kubectl get clusternetwork default -o yaml
apiVersion: network.openshift.io/v1
clusterNetworks:
- CIDR: 10.178.40.0/21
hostSubnetLength: 10
hostsubnetlength: 10
kind: ClusterNetwork
metadata:
creationTimestamp: 2020-07-09T03:04:22Z
generation: 1
name: default
resourceVersion: "36395511"
selfLink: /apis/network.openshift.io/v1/clusternetworks/default
uid: e3b4a921-c190-11ea-b605-fa163e6fe7d6
network: 10.178.40.0/21
pluginName: redhat/openshift-ovs-multitenant
serviceNetwork: 10.178.32.0/21
vxlanPort: 4789
openshift-sdn设计的一个CRD,名为ClusterNetwork,这个CRD的对象记录了集群里使用的网络网段,当集群里有多个这种ClusterNetwork对象时,openshift-sdn只会取名为default
的那个对象。
关注里面的内容,我们发现clusterNetworks
是一个数组,他的每个成员都可以定义一个CIDR和hostsubnetlength。也就是说,我们修改了他,就可以给集群扩充网段。
这里我们看到在结构体中还有两个字段:hostsubnetlength
和network
,值分别与clusterNetworks
数组的唯一一个成员的字段相对应。这是openshift-sdn
的历史遗留问题,早先版本不支持配置clusterNetworks
数组,后面添加后,这两个字段只有当数组长度为1时,会进行一次校验。
我们将default这个ClusterNetwork
的内容改成:
# kubectl get clusternetwork default -o yaml
apiVersion: network.openshift.io/v1
clusterNetworks:
- CIDR: 10.178.40.0/21
hostSubnetLength: 10
- CIDR: 10.132.0.0/14
hostSubnetLength: 9
hostsubnetlength: 10
kind: ClusterNetwork
metadata:
creationTimestamp: 2020-07-09T03:04:22Z
generation: 2
name: default
resourceVersion: "36395511"
selfLink: /apis/network.openshift.io/v1/clusternetworks/default
uid: e3b4a921-c190-11ea-b605-fa163e6fe7d6
network: 10.178.40.0/21
pluginName: redhat/openshift-ovs-multitenant
serviceNetwork: 10.178.32.0/21
vxlanPort: 4789
但这仅仅修改了控制面,数据面的修改还没有做,节点上此时根本不知道有这个新增的网段。
关于数据面的改动,官方的做法是:将每个node进行驱逐:kubectl drain $nodename
, 然后重启node, 重启后节点上ovs流表会清空、ovs-node 组件会重启,并重新配置流表和路由、iptables规则。
这样对数据面的影响未免太大了!以后我IP不够用了, 还要把集群里每个node重启一次,相当于所有在用的业务容器都要至少重建一次!有没有优雅一点的方案呢?
优雅扩展
我们对openshift-sdn进行了深入的研究和社区追踪,并聚焦于如何优雅地、不影响业务容器地、完成网段的扩展。
我们实践发现,老节点上node组件重启后,就会重新同步最新的clusternetwork信息,将新的网段配置到节点的路由表,和ovs流表中, 但是,已有的容器还是无法访问新加入的网段。
进行详细的排查,我们发现老的容器里,访问新网段会走的路由是:
default via 10.178.40.1 dev eth0
正常来说,访问集群pod cidr的路由是:
10.178.40.0/21 dev eth0 scope link
于是我们写了个工具,在老节点上运维了一把,往已有的容器中加入到达新网段的路由。如:
10.132.0.0/14 dev eth0
测试了一下网络终于通了~
在反复的实践后,我们使用该方案对用户的业务集群进行了网段扩容。
但是我们不禁产生了疑问,为啥访问新的网段,不可以走网关呢?我们意识到:为了更好地支持,有必要进行更深入的了解。openshift-sdn的官方文档对此没有特别细致的解释,因此我们决定重新梳理一遍了一通源码和流表,好好地整理清楚,openshift-sdn,到底是怎么做的?
openshift-sdn的设计
CRD
openshift-sdn给集群增加了一些CRD,包括
- clusternetworks.network.openshift.io 记录集群里的pod的CIDR
- egressnetworkpolicies.network.openshift.io 记录集群里的出站规则
- hostsubnets.network.openshift.io 记录集群里某个node上的CIDR
- netnamespaces.network.openshift.io 记录集群里的网络租户空间
组件
openshift-sdn的组件包含了中心化的控制器,去中心化的agent和CNI插件,agent会直接影响节点上的数据面,他们各自负责的主要内容包括:
controller
- 负责配置集群级别的pod cidr,对应openshift-sdn的CRD:clusterNetwork
- 给新加入的node分配子段,对应openshift-sdn的CRD:hostSubnet
- 观察k8s集群中namespace、networkpolicy等对象的变更,同步地更新openshift-sdn的CRD:netnamespaces、egressnetworkpolicies(专门针对出站的networkpolicy)
agent
- 每次启动时获取集群clusterNetwork,与本地流表做对比,当发现有出入,就会重新配置本地的集群网络流表、节点上的路由、以及iptables规则
- 观察集群中openshift-sdn的CRD:hostSubnet的变化,配置到达其他node的流表
- 观察集群中openshift-sdn的CRD:netnamespaces、egressnetworkpolicies的变化,配置相应的租户隔离和出站限制的流表
- 生成节点上的CNI二进制文件,并提供IP分配功能
- 针对本节点的每个pod,配置对应的流表
CNI
- 负责被kubelet调用,以进行容器网络的配置和解除
- 会向agent申请和释放IP
- 会配置容器内部的IP和路由
openshift-sdn的数据面原理
我个人认为,一言蔽之,openshift-sdn就是构建了一套pod-to-pod的大二层网络。所有pod的ip都属于一个虚拟的L2中,他们彼此可以互相通过arp请求确认对方物理地址,并进行正常的网络发包。不管是arp包还是普通的ip包,都会被ovs流处理并进行必要的封装。
路由配置和跳转
我们在一个k8s集群中部署了openshift-sdn网络,通过对路由、流表、iptables的分析,可以勾画出网络的架构。
首先看容器里的内容。当我们使用openshift-sdn时,需要先提供整个集群规划的pod IP CIDR,以及每个node上可以从CIDR里分配多少IP作为子段。我们这里规划10.178.40.0/21
为集群的pod cidr, 每个节点上可以分配2^10个IP ,这样集群里只能支持两个节点。两个节点的IP段分别为:10.178.40.0/22
和10.178.44.0/22
随意创建一个pod,进入容器中检查IP和路由:
# docker exec -it bfdf04f24e01 bash
root@hytest-5db48599dc-95gfh:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
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
3: eth0@if95: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1350 qdisc noqueue state UP group default
link/ether 0a:58:0a:b2:28:0f brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.178.40.15/22 brd 10.178.43.255 scope global eth0
valid_lft forever preferred_lft forever
root@hytest-5db48599dc-95gfh:/# ip r
default via 10.178.40.1 dev eth0
10.178.40.0/22 dev eth0 proto kernel scope link src 10.178.40.15
10.178.40.0/21 dev eth0
224.0.0.0/4 dev eth0
可以看到IP:10.178.40.15/22
是处于网段10.178.40.0/22
中的。路由表的含义,从底向上为:
- 第四条路由:224.0.0.0/4为组播段,这是一条组播路由
- 第三条路由:表示IP所在的二层广播域。也就是整个node分到的CIDR,也就是说,一个node上所有的pod彼此是二层互联的。
- 第二条路由:集群级别的pod CIDR的路由,结合第三条规则,我们可以确认,当pod访问集群里任何一个podIP时,都会直接从eth0发出
- 第一条路由:默认路由,这里设置了一个网关地址10.178.40.1, pod访问其他目的地址时,需要经由网关转发。
到此为止,我们知道了容器里的配置,要想了解更多,就要接着看宿主机配置(为了可读性我们不展示一些无关的网卡和路由):
# ip a
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1400 qdisc pfifo_fast state UP group default qlen 1000
link/ether fa:16:3e:6f:e7:d6 brd ff:ff:ff:ff:ff:ff
inet 10.173.32.63/21 brd 10.173.39.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe6f:e7d6/64 scope link
valid_lft forever preferred_lft forever
85: vxlan_sys_4789: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 65485 qdisc noqueue master ovs-system state UNKNOWN group default qlen 1000
link/ether ae:22:fc:f9:77:92 brd ff:ff:ff:ff:ff:ff
inet6 fe80::ac22:fcff:fef9:7792/64 scope link
valid_lft forever preferred_lft forever
86: ovs-system: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 8a:95:6e:5c:65:cb brd ff:ff:ff:ff:ff:ff
87: br0: <BROADCAST,MULTICAST> mtu 1350 qdisc noop state DOWN group default qlen 1000
link/ether 0e:52:ed:b2:b2:49 brd ff:ff:ff:ff:ff:ff
88: tun0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1350 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether 06:60:ae:a8:f5:22 brd ff:ff:ff:ff:ff:ff
inet 10.178.40.1/22 brd 10.178.43.255 scope global tun0
valid_lft forever preferred_lft forever
inet6 fe80::460:aeff:fea8:f522/64 scope link
valid_lft forever preferred_lft forever
95: vethadbc25e1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1350 qdisc noqueue master ovs-system state UP group default
link/ether 06:48:6c:da:8f:4b brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::448:6cff:feda:8f4b/64 scope link
valid_lft forever preferred_lft forever
# ip r
default via 10.173.32.1 dev eth0
10.173.32.0/21 dev eth0 proto kernel scope link src 10.173.32.63
10.178.32.0/21 dev tun0
10.178.40.0/21 dev tun0 scope link
宿主机的IP是位于eth0上的10.173.32.63
,我们看到机器上还有一些特殊的网卡:
- ovs-system 所有ovs网桥在内核中有一个统一名字,即ovs-system,我们不需要太关注
- br0 ovs服务创建的一个以太网交换机,也就是一个ovs网桥
- vethadbc25e1 使用vethpair做容器网卡虚拟化,在宿主机上会出现一个网卡
- vxlan_sys_4789 ovs网桥上的一个端口(port),用来做vxlan封装
- tun0 tun0的IP是10.178.40.1,也就是容器里的默认网关。用来转发到node、service、外部网络的流量
通过执行以下命令可以看到:
# ovs-vsctl show
fde6a881-3b54-4c50-a86f-49dcddaa5a95
Bridge "br0"
fail_mode: secure
Port "vethadbc25e1"
Interface "vethadbc25e1"
Port "tun0"
Interface "tun0"
type: internal
Port "br0"
Interface "br0"
type: internal
Port "vxlan0"
Interface "vxlan0"
type: vxlan
options: {dst_port="4789", key=flow, remote_ip=flow}
ovs_version: "2.8.4"
tun0、vxlan0、各个veth,都是在ovs网桥上开的端口,当这些端口收到包时,会直接被内核态的datapath监听并进行流表的规则匹配,以确定包最终的处理方式。
veth是与容器内的eth0直连的,容器里的包通过这对vethpair发送到宿主机,并且直接被datapath接管。
宿主机上有一个vxlan0,专门用来封装/解封vxlan协议的包。在ovs流表中,会将需要封装的包发给vxlan0进行封装。
当pod访问其他节点的pod时,流表会将包引向vxlan0,IP地址封装为node的IP,封装好之后,可以直接通过宿主机的网络发到对端节点所在的node。
宿主机上有一个tun0,在宿主机的路由中,可以看到:
10.178.32.0/21 dev tun0
表示的是k8s集群里service 的网段,通过tun0发出10.178.40.0/21 dev tun0 scope link
表示的是,k8s里的集群pod CIDR,通过tun0发出。
所以当node访问集群里任何一个pod/service,都要走tun0, tun0 是openvswitch在虚拟交换机上开启的一个端口(port),从tun0流入的数据包(pod发给对端的包),会被内核态的datapath监听到,并去走内核态的、缓存好的流表规则。流表规则记录了一个数据包应该如何被正确地处理。
ovs-vswitchd 本质是一个守护进程,是 OvS 的核心部件。ovs-vswitchd 和 Datapath 一起实现 OvS 基于流表(Flow-based Switching)的数据交换。它通过 OpenFlow 协议可以与 OpenFlow 控制器通信,使用 ovsdb 协议与 ovsdb-server 数据库服务通信,使用 netlink 和 Datapath 内核模块通信。ovs-vswitchd 支持多个独立的 Datapath,ovs-vswitchd 需要加载 Datapath 内核模块才能正常运行。ovs-vswitchd 在启动时读取 ovsdb-server 中的配置信息,然后自动配置 Datapaths 和 OvS Switches 的 Flow Tables,所以用户不需要额外的通过执行 ovs-dpctl 指令工具去操作 Datapath。当 ovsdb 中的配置内容被修改,ovs-vswitched 也会自动更新其配置以保持数据同步。ovs-vswitchd 也可以从 OpenFlow 控制器获取流表项。
接下来我们就要看流表是如何配置的~
ovs流表规则
通过执行:ovs-ofctl dump-flows br0 -O openflow13 table=XX
命令我们可以看到ovs中某个表的流规则, table0是这个规则集合的入口。所以我们可以从table=0开始看起
# ovs-ofctl dump-flows br0 -O openflow13 table=0
cookie=0x0, duration=82110.449s, table=0, n_packets=0, n_bytes=0, priority=250,ip,in_port=tun0,nw_dst=224.0.0.0/4 actions=drop
cookie=0x0, duration=82110.450s, table=0, n_packets=2, n_bytes=84, priority=200,arp,in_port=vxlan0,arp_spa=10.178.40.0/21,arp_tpa=10.178.40.0/22 actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10
cookie=0x0, duration=82110.450s, table=0, n_packets=1, n_bytes=98, priority=200,ip,in_port=vxlan0,nw_src=10.178.40.0/21 actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10
cookie=0x0, duration=82110.450s, table=0, n_packets=0, n_bytes=0, priority=200,ip,in_port=vxlan0,nw_dst=10.178.40.0/21 actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10
cookie=0x0, duration=82110.450s, table=0, n_packets=2, n_bytes=84, priority=200,arp,in_port=tun0,arp_spa=10.178.40.1,arp_tpa=10.178.40.0/21 actions=goto_table:30
cookie=0x0, duration=82110.450s, table=0, n_packets=1, n_bytes=98, priority=200,ip,in_port=tun0 actions=goto_table:30
cookie=0x0, duration=82110.450s, table=0, n_packets=0, n_bytes=0, priority=150,in_port=vxlan0 actions=drop
cookie=0x0, duration=82110.450s, table=0, n_packets=37, n_bytes=2678, priority=150,in_port=tun0 actions=drop
cookie=0x0, duration=82110.450s, table=0, n_packets=4, n_bytes=168, priority=100,arp actions=goto_table:20
cookie=0x0, duration=82110.450s, table=0, n_packets=2, n_bytes=196, priority=100,ip actions=goto_table:20
cookie=0x0, duration=82110.450s, table=0, n_packets=0, n_bytes=0, priority=0 actions=drop
我们主要关注规则的后半段,从priority
开始到action
之前的一串,是匹配逻辑:
- priority 表示优先级,同一个表中,我们总是先看优先级更高的规则,不匹配再去找低的规则。同优先级的规则还有很多的过滤条件。
- ip/arp 表示数据包的协议类型,有:arp、ip、tcp、udp
- in_port表示从ovs网桥的哪个port收到的这个包
- nw_src/nw_dst 顾名思义,就是包的源IP和目的IP
之后的actions
,表示针对前面的规则得到的包,要进行如何处理,一般有:
drop
丢弃goto_table:**
转到某个表继续匹配规则set_field:10.173.32.62->tun_dst
表示封装包目的地址load:0x483152->NXM_NX_REG1[]
寄存器赋值操作,用来将某个租户的vnid保存到寄存器,后续做租户隔离的判断,这里将0x483152记录到REG1中,REG0表示源地址所属的vnid,REG1表示目的地址,REG2表示包要从哪个port发出(ovs上每个port都有id)output:***
表示从ovs网桥上的某个端口设备发出 比如vxlan0move:NXM_NX_REG0[]->NXM_NX_TUN_ID[0..31]
表示将REG0中的值拷贝到封装包的vnid字段中
例——容器访问service的处理流程
大致清楚流表里的主要语法后,我们可以结合一个机器上的ovs流表内容,分析一下从pod访问service的时候,整个处理链路:
- 容器访问service(比如clusterIP:10.178.32.32),通过容器内路由直接发出,宿主机上的veth由于是ovs网桥上的一个port,所以包直接到达内核datapath,也就是进入table0
- table0中选择了
cookie=0x0, duration=17956.652s, table=0, n_packets=20047, n_bytes=1412427, priority=100,ip actions=goto_table:20
进入table20 - table20中选择了
cookie=0x0, duration=17938.360s, table=20, n_packets=0, n_bytes=0, priority=100,ip,in_port=vethadbc25e1,nw_src=10.178.40.15 actions=load:0->NXM_NX_REG0[],goto_table:21
规则,进入table21,而且做了load操作,给REG0设置值为0,意思是这个数据包的源IP能适配任何租户 - table21中记录的是k8s networkpolicy生成的对应的策略,由于我们没有用,所以只能选择
cookie=0x0, duration=18155.706s, table=21, n_packets=3, n_bytes=182, priority=0 actions=goto_table:30
进入table30 - 在table30中选择了:
cookie=0x0, duration=12410.821s, table=30, n_packets=0, n_bytes=0, priority=100,ip,nw_dst=10.178.32.0/21 actions=goto_table:60
- 在table60中选择了:
cookie=0x0, duration=12438.404s, table=60, n_packets=0, n_bytes=0, priority=100,udp,nw_dst=10.178.32.32,tp_dst=53 actions=load:0x483152->NXM_NX_REG1[],load:0x2->NXM_NX_REG2[],goto_table:80
。 注意这里我们在action中做了load操作,告知将目的地址的vnid设置为4731218,这个值是ovs通过service所属的namespace的信息得到的,是multi-tenant的特性;并设置了REG2,表示:如果包要发出,就要从id为2的port发出 - 在table80中,我们继续判断,如果REG0的值为0,或REG1的值为0,或REG0的值等于REG1的值,就表示这个包可以发出,于是从REG2对应的port发出。这里REG2的值为2,我们在机器上执行
ovs-vsctl list interface
, 可以看到ofport
值为2的设备是tun0.也就是说包是从tun0发出。 - 包开始走宿主机的路由和iptbales规则,经过k8s的service负载均衡,做了一次DNAT,此时变成了pod访问pod的包。根据路由查找,发现还是要发给tun0,另外,openshift-sdn还会做一次masquerade,通过
-A OPENSHIFT-MASQUERADE -s 10.178.40.0/21 -m comment --comment "masquerade pod-to-service and pod-to-external traffic" -j MASQUERADE
这条iptables规则实现,这样源IP就不再是pod而是node的IP【openshift-sdn支持开启ct支持,开启ct支持后,就不需要做这个额外的masq了,但开启该功能要求ovs达到2.6的版本】 - 再次进入到流表。还是走table0
- 这次我们适配了
cookie=0x0, duration=19046.682s, table=0, n_packets=21270, n_bytes=10574507, priority=200,ip,in_port=tun0 actions=goto_table:30
直接进入table30 - 假设包被iptablesDNAT为另一个节点上的pod(10.178.44.22),那么table30中应该走
cookie=0x0, duration=13508.548s, table=30, n_packets=1, n_bytes=98, priority=100,ip,nw_dst=10.178.40.0/21 actions=goto_table:90
- table90中找到了到另一个节点的cidr的流表规则:
cookie=0xb4e80ae4, duration=13531.936s, table=90, n_packets=1, n_bytes=98, priority=100,ip,nw_dst=10.178.44.0/22 actions=move:NXM_NX_REG0[]->NXM_NX_TUN_ID[0..31],set_field:10.173.32.62->tun_dst,output:vxlan0
,意味着要从vxlan0这个port发出,并且我们记录了tun_dst为10.173.32.62, 还将此时的REG0,也就是源IP的vnid记录到包中,作为封装包中的内容。 - vxlan0这个port做了一个封装,将包封装了源IP和目的IP,目的IP为另一个节点的IP地址(tun_dst:10.173.32.62)。封装好后从vxlan0发出
- 走机器上的路由,通过机器所在的网络发送到对端。
- 在对端节点上,内核判断到包有一个vxlan的协议头,交给对端节点的vxlan0解封,由于vxlan0也是ovs网桥上的一个port,所以解封后送入datapath进行流表解析
- 这里有两条规则都适配这个包,两个规则优先级还一样,
cookie=0x0, duration=14361.893s, table=0, n_packets=1, n_bytes=98, priority=200,ip,in_port=vxlan0,nw_src=10.178.40.0/21 actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10
,cookie=0x0, duration=14361.893s, table=0, n_packets=0, n_bytes=0, priority=200,ip,in_port=vxlan0,nw_dst=10.178.40.0/21 actions=move:NXM_NX_TUN_ID[0..31]->NXM_NX_REG0[],goto_table:10
当遇到这种情况时,选择哪一条规则是我们无法确定的,也就是说可能随便选一条,但是此处两个规则都导向了table10。并且还将封包中的vnid取出,复制到REG0这个寄存器里 - table10里做了源地址的校验:
cookie=0xc694ebd2, duration=19282.596s, table=10, n_packets=3, n_bytes=182, priority=100,tun_src=10.173.32.63 actions=goto_table:30
.封装的包的源地址是不是合法的?如果不合法,那么就应该drop掉,如果没问题就进入table30 - table30中根据
cookie=0x0, duration=19341.929s, table=30, n_packets=21598, n_bytes=10737703, priority=200,ip,nw_dst=10.178.44.0/22 actions=goto_table:70
,匹配了目的IP,进入table70 - table70中,根据
cookie=0x0, duration=19409.718s, table=70, n_packets=21677, n_bytes=10775797, priority=100,ip,nw_dst=10.178.44.22 actions=load:0x483152->NXM_NX_REG1[],load:0x5->NXM_NX_REG2[],goto_table:80
进入table80,并且我们将目的端的vnid设置为了0x483152。 目的端出口的port的id为0x5 - table80中,还是一样,判断vnid彼此是否兼容,因为我们发包时就设置了REG0为0,所以即便REG1不为0且不等于REG0,也一样是放行的。所以从id为5的port出去。
- 对端node上,id为5的port,对应的就是pod的hostveth,因此这个包从veth发出, veth发出的包会直接被容器net namespace里的一端(eth0)收到,至此,访问service的包到达了后端某个pod。
归纳
整个openshift-sdn的流表示意图如下:
我们逐个解释一下每个table主要的负责内容:
table10
:由vxlan收包并处理时,会走table10表,10表会判断封包的源IP是否是其他节点的nodeIP,如果不是就丢弃table20
: 由veth收到的包会进入20表,也就是pod发出的包,会进入20表,20表中主要是做了源IP的vnid的设置table21
: table20处理完毕后会进入table21,在里面会处理k8s networkpolicy的逻辑,如果判断这个包的访问路径是通的,就会进入30表table30
:30表值主要的选路表,这里会判断协议是ip还是arp:- 判断arp包的来源或目的,请求本地pod IP的arp,到40,请求其他节点pod IP的arp到50
- 判断ip包的目的地址属于哪个段,属于本机段、集群段、service IP段,会分别走70、90、60表
table40
:将请求本地podIP的arp请求从对应的veth发出table50
: 对于请求集群里网段的IP的arp请求,封装后通过vxlan0发出table60
: 检查要访问的具体是哪个service,根据service所属的namespace的租户id,配置包的目的vnid,并配置目的出口为tun0,进入table80table70
: 访问本机其他pod IP时,检查pod所属的namespace的租户id,配置包的目的vnid,并配置目的出口为目的pod的veth,进入table80table80
: 根据REG进行vnid的校验,REG0=REG1或REG0=0或REG1=0时,校验通过table90
: 记录了集群里每个node的网段对应的nodeIP,在该表里设置要封装的内容:- 源IP对应的vnid要设置到封装包的字段中
- 目的地址的node的IP要设置为封装包的目的地址
table120
: 收到组播时做的逻辑判断table110
: 发出组播时做的逻辑判断table100
: 访问外部IP时做的判断,通常只会单纯的设置走tun0table110
: 访问外部IP时做的networkpolicy判断
基于上面的整理,我们可以知道,在使用openshift-sdn的时候,集群里各种网络访问的链路:
- 同节点的pod与pod访问:包从客户端pod的veth,到宿主机的ovs网桥,直接到达对端pod的veth
- 跨节点的pod与pod访问:包从客户端pod的veth,到宿主机的ovs网桥,走vxlan0端口封装后,经过宿主机的协议栈,从宿主机的物理网卡发出,到对端pod所在宿主机的物理网卡,被识别为vxlan,进入对端机器的ovs网桥,然后到对端pod的veth
- pod访问node:包从客户端pod的veth,到宿主机ovs网桥,因为node的物理网卡IP与pod的网络不在一个平面,所以直接走table100,然后从tun0口发出,经过宿主机的协议栈,进行路由转发,最后走宿主机所在的网络到达某个node的物理网卡
- pod访问其他外部网络(out-of-clusternetwork)也都是走tun0
- node访问本节点的pod:根据宿主机的路由,包从tun0发出,进入宿主机的ovs网桥,送达对端pod的veth
- node访问其他节点的pod:根据宿主机路由,从tun0发出,进入宿主机的ovs网桥,送达vxlan0进行封装,然后走宿主机的路由和网络,到对端pod所在宿主机的物理网卡,被识别为vxlan,进入对端机器的ovs网桥,然后到对端pod的veth
- pod访问service: 包从客户端pod的veth,到宿主机ovs网桥,从tun0发出,经过宿主机协议栈,受iptables规则做了DNAT和MASQUERADE,至此变成了node访问其他节点的pod
- service的后端回包给pod:因为上一步,pod访问service时,做了MASQUERADE,所以service后端会认为是某个node访问了自己,回包给客户端pod所在的node,node上收到后对照conntrack表,确认是之前连接的响应包,于是对包的源地址和目的地址做了修改(对应之前做的DNAT和MASQUERADE),变成了serviceIP访问客户端pod的包。根据node上的路由,走tun0,进入ovs网桥后,直接送到pod的veth
注意这里的第二点,pod到pod是不需要走tun0的,也就是说,集群里所有的cluster network对应的cidr,都被视为一个“二层”,不需要依赖网关的转发。上文中我们在扩展集群网段时,需要在老容器里加一条直连路由,原因就在这:
老容器发包到新容器时,走网关转发,包的目的MAC是老节点的tun0的mac,这个包直接被流表封装发出到对端,对端解封后送到对端容器,对端容器会发现包的目的MAC本地没有,因此肯定会丢弃。所以我们不能让这种pod-to-pod的访问链路走网关,而应该是通过直连路由。
流表检查工具
如果你觉得一条一条地看流表,特别麻烦,那么有一个很方便的实践方法,比如:
先通过ovs-vsctl list interface
命令查看到IP在ovs网桥上对应的网口的id。
ovs-vsctl list interface |less
_uuid : e6ca4571-ac3b-46d4-b155-c541affa5a96
admin_state : up
bfd : {}
bfd_status : {}
cfm_fault : []
cfm_fault_status : []
cfm_flap_count : []
cfm_health : []
cfm_mpid : []
cfm_remote_mpids : []
cfm_remote_opstate : []
duplex : full
error : []
external_ids : {ip="10.178.40.15", sandbox="6c0a268503b577936a34dd762cc6ca7a3e3f323d1b0a56820b2ef053160266ff"}
ifindex : 95
ingress_policing_burst: 0
ingress_policing_rate: 0
lacp_current : []
link_resets : 0
link_speed : 10000000000
link_state : up
lldp : {}
mac : []
mac_in_use : "06:48:6c:da:8f:4b"
mtu : 1350
mtu_request : []
name : "vethadbc25e1"
ofport : 12
ofport_request : []
options : {}
other_config : {}
statistics : {collisions=0, rx_bytes=182, rx_crc_err=0, rx_dropped=0, rx_errors=0, rx_frame_err=0, rx_over_err=0, rx_packets=3, tx_bytes=2930, tx_dropped=0, tx_errors=0, tx_packets=41}
status : {driver_name=veth, driver_version="1.0", firmware_version=""}
type : ""
...
如上,我们看到10.178.40.15
这个IP所在的端口,ofport
字段是12。 接着,执行:
ovs-appctl ofproto/trace br0 'ip,in_port=12,nw_src=10.178.40.15,nw_dst=10.173.32.62'
在这条命令中,我们模拟往某个port(id为12)塞一个包,源IP是10.178.40.15,目的IP是10.173.32.62。
输出是:
Flow: ip,in_port=12,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,nw_src=10.178.40.15,nw_dst=10.173.32.62,nw_proto=0,nw_tos=0,nw_ecn=0,nw_ttl=0
bridge("br0")
-------------
0. ip, priority 100
goto_table:20
20. ip,in_port=12,nw_src=10.178.40.15, priority 100
load:0->NXM_NX_REG0[]
goto_table:21
21. priority 0
goto_table:30
30. ip, priority 0
goto_table:100
100. priority 0
goto_table:101
101. priority 0
output:2
Final flow: unchanged
Megaflow: recirc_id=0,eth,ip,in_port=12,nw_src=10.178.40.15,nw_dst=10.173.32.62,nw_frag=no
Datapath actions: 3
会把整个链路走的所有的表,以及最后从哪个口发出,做的封装(此例中不做封装,Final flow=unchanged)全部显示出来。
结语
本文我们由浅入深地介绍了openshift-sdn这个网络方案,了解了他的架构和用法,并深入地探索了它的实现。 ovs流表的阅读和跟踪是一个比较吃力的活,但当我们啃下来之后,会发现openshift-sdn的流表设计还是比较简洁易懂的,希望读完本文的你能有所收获~
引用
https://blog.csdn.net/Jmilk/j...
https://www.cnblogs.com/sammy...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。