详解openshift-sdn

fzu_huang

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。也就是说,我们修改了他,就可以给集群扩充网段。

这里我们看到在结构体中还有两个字段:hostsubnetlengthnetwork,值分别与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/2210.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网桥上的某个端口设备发出 比如vxlan0
  • move: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的流表示意图如下:

image

我们逐个解释一下每个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,进入table80
  • table70: 访问本机其他pod IP时,检查pod所属的namespace的租户id,配置包的目的vnid,并配置目的出口为目的pod的veth,进入table80
  • table80: 根据REG进行vnid的校验,REG0=REG1或REG0=0或REG1=0时,校验通过
  • table90: 记录了集群里每个node的网段对应的nodeIP,在该表里设置要封装的内容:

    • 源IP对应的vnid要设置到封装包的字段中
    • 目的地址的node的IP要设置为封装包的目的地址
  • table120: 收到组播时做的逻辑判断
  • table110: 发出组播时做的逻辑判断
  • table100: 访问外部IP时做的判断,通常只会单纯的设置走tun0
  • table110: 访问外部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...

https://docs.openshift.com/co...

https://docs.openshift.com/co...

阅读 976

kubernetes_docker集群管理
我学到的关于k8s和docker的种种。 仅作学习笔记用,不会发布在别的地方
272 声望
105 粉丝
0 条评论
你知道吗?

272 声望
105 粉丝
文章目录
宣传栏