乐趣区

关于云计算:详解openshiftsdn

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 

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 的数据面原理

路由配置和跳转

咱们在一个 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 的流示意用意如下:

咱们一一解释一下每个 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…

本文由博客群发一文多发等经营工具平台 OpenWrite 公布

退出移动版