詳解openshift-sdn

openshift-sdn的由來和現狀

openshift-sdn是紅帽推出的一款容器集羣網絡方案。一直集成於openshift平臺中。 但紅帽將項目代碼進行了開源。html

實際上,咱們經過一些修改,徹底能夠將openshift-sdn做爲一款通用的容器集羣的網絡方案。java

openshift-sdn官方建議使用network-operator工具進行網絡部署,實際上在該項目中咱們甚至能夠扒出一套基本完整的部署模板。基於這套模板咱們能夠直接部署openshift-sdn。node

爲了加深你們的理解,本文咱們會詳細地介紹整個方案的功能、使用和原理。咱們相信,若是你徹底理解了本文的內容,你也能在集羣的openshift-sdn網絡出現故障時,能遊刃有餘地進行排障。git

openshift-sdn的功能

openshift-sdn依賴於了openvswitch技術,也就是虛擬交換機,在k8s集羣的每一個節點上都要求部署好openvswitch並啓動服務:github

systemctl status openvswitch-switch.service

openshift-sdn經過構建和維護一套流表,以及一些路由和iptables策略,就實現了基本的容器網絡需求:docker

  • 集羣中跨節點的pod通訊
  • pod到service的通訊
  • pod到外部網絡的通訊

除此以外,還提供了豐富的擴展能力:數據庫

  • 提供multi-tenant模式,支持namespace維度的租戶隔離
  • 提供networkpolicy模式,支持k8s networkpolicy
  • 支持在上述兩種模式下,在pod間使用多播流量

能夠說openshift-sdn的功能已經趨於完備。後端

openshift-sdn的組成

openshift-sdn包括了管控面和數據面。api

  • 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的流表示意圖以下:

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...

本文由博客羣發一文多發等運營工具平臺 OpenWrite 發佈
相關文章
相關標籤/搜索