原文連結:https://fuckcloudnative.io/posts/ipvs-how-kubernetes-services-direct-traffic-to-pods/
Kubernetes
中的 Service
就是一組同 label 型別 Pod
的服務抽象,為服務提供了負載均衡和反向代理能力,在叢集中表示一個微服務的概念。kube-proxy
元件則是 Service 的具體實現,瞭解了 kube-proxy 的工作原理,才能洞悉服務之間的通訊流程,再遇到網路不通時也不會一臉懵逼。
kube-proxy 有三種模式:userspace
、iptables
和 IPVS
,其中 userspace
模式不太常用。iptables
模式最主要的問題是在服務多的時候產生太多的 iptables 規則,非增量式更新會引入一定的時延,大規模情況下有明顯的效能問題。為解決 iptables
模式的效能問題,v1.11 新增了 IPVS
模式(v1.8 開始支援測試版,並在 v1.11 GA),採用增量式更新,並可以保證 service 更新期間連線保持不斷開。
目前網路上關於 kube-proxy
工作原理的文件幾乎都是以 iptables
模式為例,很少提及 IPVS
,本文就來破例解讀 kube-proxy IPVS 模式的工作原理。為了理解地更加徹底,本文不會使用 Docker 和 Kubernetes,而是使用更加底層的工具來演示。
我們都知道,Kubernetes 會為每個 Pod 建立一個單獨的網路名稱空間 (Network Namespace) ,本文將會通過手動建立網路名稱空間並啟動 HTTP 服務來模擬 Kubernetes 中的 Pod。
本文的目標是通過模擬以下的 Service
來探究 kube-proxy 的 IPVS
和 ipset
的工作原理:
apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
clusterIP: 10.100.100.100
selector:
component: app
ports:
- protocol: TCP
port: 8080
targetPort: 8080
跟著我的步驟,最後你就可以通過命令 curl 10.100.100.100:8080
來訪問某個網路名稱空間的 HTTP 服務。為了更好地理解本文的內容,推薦提前閱讀以下的文章:
- How do Kubernetes and Docker create IP Addresses?!
- iptables: How Docker Publishes Ports
- iptables: How Kubernetes Services Direct Traffic to Pods
注意:本文所有步驟皆是在 Ubuntu 20.04 中測試的,其他 Linux 發行版請自行測試。
準備實驗環境
首先需要開啟 Linux 的路由轉發功能:
$ sysctl --write net.ipv4.ip_forward=1
接下來的命令主要做了這麼幾件事:
- 建立一個虛擬網橋
bridge_home
- 建立兩個網路名稱空間
netns_dustin
和netns_leah
- 為每個網路名稱空間配置 DNS
- 建立兩個 veth pair 並連線到
bridge_home
- 給
netns_dustin
網路名稱空間中的 veth 裝置分配一個 IP 地址為10.0.0.11
- 給
netns_leah
網路名稱空間中的 veth 裝置分配一個 IP 地址為10.0.021
- 為每個網路名稱空間設定預設路由
- 新增 iptables 規則,允許流量進出
bridge_home
介面 - 新增 iptables 規則,針對
10.0.0.0/24
網段進行流量偽裝
$ ip link add dev bridge_home type bridge
$ ip address add 10.0.0.1/24 dev bridge_home
$ ip netns add netns_dustin
$ mkdir -p /etc/netns/netns_dustin
echo "nameserver 114.114.114.114" | tee -a /etc/netns/netns_dustin/resolv.conf
$ ip netns exec netns_dustin ip link set dev lo up
$ ip link add dev veth_dustin type veth peer name veth_ns_dustin
$ ip link set dev veth_dustin master bridge_home
$ ip link set dev veth_dustin up
$ ip link set dev veth_ns_dustin netns netns_dustin
$ ip netns exec netns_dustin ip link set dev veth_ns_dustin up
$ ip netns exec netns_dustin ip address add 10.0.0.11/24 dev veth_ns_dustin
$ ip netns add netns_leah
$ mkdir -p /etc/netns/netns_leah
echo "nameserver 114.114.114.114" | tee -a /etc/netns/netns_leah/resolv.conf
$ ip netns exec netns_leah ip link set dev lo up
$ ip link add dev veth_leah type veth peer name veth_ns_leah
$ ip link set dev veth_leah master bridge_home
$ ip link set dev veth_leah up
$ ip link set dev veth_ns_leah netns netns_leah
$ ip netns exec netns_leah ip link set dev veth_ns_leah up
$ ip netns exec netns_leah ip address add 10.0.0.21/24 dev veth_ns_leah
$ ip link set bridge_home up
$ ip netns exec netns_dustin ip route add default via 10.0.0.1
$ ip netns exec netns_leah ip route add default via 10.0.0.1
$ iptables --table filter --append FORWARD --in-interface bridge_home --jump ACCEPT
$ iptables --table filter --append FORWARD --out-interface bridge_home --jump ACCEPT
$ iptables --table nat --append POSTROUTING --source 10.0.0.0/24 --jump MASQUERADE
在網路名稱空間 netns_dustin
中啟動 HTTP 服務:
$ ip netns exec netns_dustin python3 -m http.server 8080
開啟另一個終端視窗,在網路名稱空間 netns_leah
中啟動 HTTP 服務:
$ ip netns exec netns_leah python3 -m http.server 8080
測試各個網路名稱空間之間是否能正常通訊:
$ curl 10.0.0.11:8080
$ curl 10.0.0.21:8080
$ ip netns exec netns_dustin curl 10.0.0.21:8080
$ ip netns exec netns_leah curl 10.0.0.11:8080
整個實驗環境的網路拓撲結構如圖:
安裝必要工具
為了便於除錯 IPVS 和 ipset,需要安裝兩個 CLI 工具:
$ apt install ipset ipvsadm --yes
本文使用的 ipset 和 ipvsadm 版本分別為
7.5-1~exp1
和1:1.31-1
。
通過 IPVS 來模擬 Service
下面我們使用 IPVS
建立一個虛擬服務 (Virtual Service) 來模擬 Kubernetes 中的 Service :
$ ipvsadm \
--add-service \
--tcp-service 10.100.100.100:8080 \
--scheduler rr
- 這裡使用引數
--tcp-service
來指定 TCP 協議,因為我們需要模擬的 Service 就是 TCP 協議。 - IPVS 相比 iptables 的優勢之一就是可以輕鬆選擇排程演算法,這裡選擇使用輪詢排程演算法。
目前 kube-proxy 只允許為所有 Service 指定同一個排程演算法,未來將會支援為每一個 Service 選擇不同的排程演算法,詳情可參考文章 IPVS-Based In-Cluster Load Balancing Deep Dive。
建立了虛擬服務之後,還得給它指定一個後端的 Real Server
,也就是後端的真實服務,即網路名稱空間 netns_dustin
中的 HTTP 服務:
$ ipvsadm \
--add-server \
--tcp-service 10.100.100.100:8080 \
--real-server 10.0.0.11:8080 \
--masquerading
該命令會將訪問 10.100.100.100:8080
的 TCP 請求轉發到 10.0.0.11:8080
。這裡的 --masquerading
引數和 iptables 中的 MASQUERADE
類似,如果不指定,IPVS 就會嘗試使用路由表來轉發流量,這樣肯定是無法正常工作的。
譯者注:由於 IPVS 未實現
POST_ROUTING
Hook 點,所以它需要 iptables 配合完成 IP 偽裝等功能。
測試是否正常工作:
$ curl 10.100.100.100:8080
實驗成功,請求被成功轉發到了後端的 HTTP 服務!
在網路名稱空間中訪問虛擬服務
上面只是在 Host 的網路名稱空間中進行測試,現在我們進入網路名稱空間 netns_leah
中進行測試:
$ ip netns exec netns_leah curl 10.100.100.100:8080
哦豁,訪問失敗!
要想順利通過測試,只需將 10.100.100.100
這個 IP 分配給一個虛擬網路介面。至於為什麼要這麼做,目前我還不清楚,我猜測可能是因為網橋 bridge_home
不會呼叫 IPVS,而將虛擬服務的 IP 地址分配給一個網路介面則可以繞過這個問題。
譯者注
Netfilter 是一個基於使用者自定義的 Hook 實現多種網路操作的 Linux 核心框架。Netfilter 支援多種網路操作,比如包過濾、網路地址轉換、埠轉換等,以此實現包轉發或禁止包轉發至敏感網路。
針對 Linux 核心 2.6 及以上版本,Netfilter 框架實現了 5 個攔截和處理資料的系統呼叫介面,它允許核心模組註冊核心網路協議棧的回撥功能,這些功能呼叫的具體規則通常由 Netfilter 外掛定義,常用的外掛包括 iptables、IPVS 等,不同外掛實現的 Hook 點(攔截點)可能不同。另外,不同外掛註冊進核心時需要設定不同的優先順序,例如預設配置下,當某個 Hook 點同時存在 iptables 和 IPVS 規則時,iptables 會被優先處理。
Netfilter 提供了 5 個 Hook 點,系統核心協議棧在處理資料包時,每到達一個 Hook 點,都會呼叫核心模組中定義的處理函式。呼叫哪個處理函式取決於資料包的轉發方向,進站流量和出站流量觸發的 Hook 點是不一樣的。
核心協議棧中預定義的回撥函式有如下五個:
- NF_IP_PRE_ROUTING: 接收的資料包進入協議棧後立即觸發此回撥函式,該動作發生在對資料包進行路由判斷(將包發往哪裡)之前。
- NF_IP_LOCAL_IN: 接收的資料包經過路由判斷後,如果目標地址在本機上,則將觸發此回撥函式。
- NF_IP_FORWARD: 接收的資料包經過路由判斷後,如果目標地址在其他機器上,則將觸發此回撥函式。
- NF_IP_LOCAL_OUT: 本機產生的準備傳送的資料包,在進入協議棧後立即觸發此回撥函式。
- NF_IP_POST_ROUTING: 本機產生的準備傳送的資料包或者經由本機轉發的資料包,在經過路由判斷之後,將觸發此回撥函式。
iptables 實現了所有的 Hook 點,而 IPVS 只實現了 LOCAL_IN
、LOCAL_OUT
、FORWARD
這三個 Hook 點。既然沒有實現 PRE_ROUTING
,就不會在進入 LOCAL_IN 之前進行地址轉換,那麼資料包經過路由判斷後,會進入 LOCAL_IN Hook 點,IPVS 回撥函式如果發現目標 IP 地址不屬於該節點,就會將資料包丟棄。
如果將目標 IP 分配給了虛擬網路介面,核心在處理資料包時,會發現該目標 IP 地址屬於該節點,於是可以繼續處理資料包。
dummy 介面
當然,我們不需要將 IP 地址分配給任何已經被使用的網路介面,我們的目標是模擬 Kubernetes 的行為。Kubernetes 在這裡建立了一個 dummy 介面,它和 loopback 介面類似,但是你可以建立任意多的 dummy 介面。它提供路由資料包的功能,但實際上又不進行轉發。dummy 介面主要有兩個用途:
- 用於主機內的程式通訊
- 由於 dummy 介面總是 up(除非顯式將管理狀態設定為 down),在擁有多個物理介面的網路上,可以將 service 地址設定為 loopback 介面或 dummy 介面的地址,這樣 service 地址不會因為物理介面的狀態而受影響。
看來 dummy 介面完美符合實驗需求,那就建立一個 dummy 介面吧:
$ ip link add dev dustin-ipvs0 type dummy
將虛擬 IP 分配給 dummy 介面 dustin-ipvs0
:
$ ip addr add 10.100.100.100/32 dev dustin-ipvs0
到了這一步,仍然訪問不了 HTTP 服務,還需要另外一個黑科技:bridge-nf-call-iptables
。在解釋 bridge-nf-call-iptables
之前,我們先來回顧下容器網路通訊的基礎知識。
基於網橋的容器網路
Kubernetes 叢集網路有很多種實現,有很大一部分都用到了 Linux 網橋:
- 每個 Pod 的網路卡都是 veth 裝置,veth pair 的另一端連上宿主機上的網橋。
- 由於網橋是虛擬的二層裝置,同節點的 Pod 之間通訊直接走二層轉發,跨節點通訊才會經過宿主機 eth0。
Service 同節點通訊問題
不管是 iptables 還是 ipvs 轉發模式,Kubernetes 中訪問 Service 都會進行 DNAT,將原本訪問 ClusterIP:Port
的資料包 DNAT 成 Service 的某個 Endpoint (PodIP:Port)
,然後核心將連線資訊插入 conntrack
表以記錄連線,目的端回包的時候核心從 conntrack
表匹配連線並反向 NAT,這樣原路返回形成一個完整的連線鏈路:
但是 Linux 網橋是一個虛擬的二層轉發裝置,而 iptables conntrack 是在三層上,所以如果直接訪問同一網橋內的地址,就會直接走二層轉發,不經過 conntrack:
-
Pod 訪問 Service,目的 IP 是 Cluster IP,不是網橋內的地址,走三層轉發,會被 DNAT 成 PodIP:Port。
-
如果 DNAT 後是轉發到了同節點上的 Pod,目的 Pod 回包時發現目的 IP 在同一網橋上,就直接走二層轉發了,沒有呼叫 conntrack,導致回包時沒有原路返回 (見下圖)。
由於沒有原路返回,客戶端與服務端的通訊就不在一個 “頻道” 上,不認為處在同一個連線,也就無法正常通訊。
開啟 bridge-nf-call-iptables
啟用 bridge-nf-call-iptables
這個核心引數 (置為 1),表示 bridge 裝置在二層轉發時也去呼叫 iptables 配置的三層規則 (包含 conntrack),所以開啟這個引數就能夠解決上述 Service 同節點通訊問題。
所以這裡需要啟用 bridge-nf-call-iptables
:
$ modprobe br_netfilter
$ sysctl --write net.bridge.bridge-nf-call-iptables=1
現在再來測試一下連通性:
$ ip netns exec netns_leah curl 10.100.100.100:8080
終於成功了!
開啟 Hairpin(髮夾彎)模式
雖然我們可以從網路名稱空間 netns_leah
中通過虛擬服務成功訪問另一個網路名稱空間 netns_dustin
中的 HTTP 服務,但還沒有測試過從 HTTP 服務所在的網路名稱空間 netns_dustin
中直接通過虛擬服務訪問自己,話不多說,直接測一把:
$ ip netns exec netns_dustin curl 10.100.100.100:8080
啊哈?竟然失敗了,這又是哪裡的問題呢?不要慌,開啟 hairpin
模式就好了。那麼什麼是 hairpin
模式呢? 這是一個網路虛擬化技術中常提到的概念,也即交換機埠的VEPA模式。這種技術藉助物理交換機解決了虛擬機器間流量轉發問題。很顯然,這種情況下,源和目標都在一個方向,所以就是從哪裡進從哪裡出的模式。
怎麼配置呢?非常簡單,只需一條命令:
$ brctl hairpin bridge_home veth_dustin on
再次進行測試:
$ ip netns exec netns_dustin curl 10.100.100.100:8080
還是失敗了。。。
然後我花了一個下午的時間,終於搞清楚了啟用混雜模式後為什麼還是不能解決這個問題,因為混雜模式和下面的選項要一起啟用才能對 IPVS 生效:
$ sysctl --write net.ipv4.vs.conntrack=1
最後再測試一次:
$ ip netns exec netns_dustin curl 10.100.100.100:8080
這次終於成功了,但我還是不太明白為什麼啟用 conntrack 能解決這個問題,有知道的大神歡迎留言告訴我!
譯者注:IPVS 及其負載均衡演算法只針對首個資料包,後繼的包必須被
conntrack
表優先反轉,如果沒有conntrack
,IPVS 對於回來的包是沒有任何辦法的。可以通過conntrack -L
檢視。
開啟混雜模式
如果想讓所有的網路名稱空間都能通過虛擬服務訪問自己,就需要在連線到網橋的所有 veth 介面上開啟 hairpin
模式,這也太麻煩了吧。有一個辦法可以不用配置每個 veth 介面,那就是開啟網橋的混雜模式。
什麼是混雜模式呢?普通模式下網路卡只接收發給本機的包(包括廣播包)傳遞給上層程式,其它的包一律丟棄。混雜模式就是接收所有經過網路卡的資料包,包括不是發給本機的包,即不驗證MAC地址。
如果一個網橋開啟了混雜模式,就等同於將所有連線到網橋上的埠(本文指的是 veth 介面)都啟用了 hairpin
模式。可以通過以下命令來啟用 bridge_home
的混雜模式:
$ ip link set bridge_home promisc on
現在即使你把 veth 介面的 hairpin
模式關閉:
$ brctl hairpin bridge_home veth_dustin off
仍然可以通過連通性測試:
$ ip netns exec netns_dustin curl 10.100.100.100:8080
優化 MASQUERADE
在文章開頭準備實驗環境的章節,執行了這麼一條命令:
$ iptables \
--table nat \
--append POSTROUTING \
--source 10.0.0.0/24 \
--jump MASQUERADE
這條 iptables 規則會對所有來自 10.0.0.0/24
的流量進行偽裝。然而 Kubernetes 並不是這麼做的,它為了提高效能,只對來自某些具體的 IP 的流量進行偽裝。
為了更加完美地模擬 Kubernetes,我們繼續改造規則,先把之前的規則刪除:
$ iptables \
--table nat \
--delete POSTROUTING \
--source 10.0.0.0/24 \
--jump MASQUERADE
然後新增針對具體 IP 的規則:
$ iptables \
--table nat \
--append POSTROUTING \
--source 10.0.0.11/32 \
--jump MASQUERADE
果然,上面的所有測試都能通過。先別急著高興,又有新問題了,現在只有兩個網路名稱空間,如果有很多個怎麼辦,每個網路名稱空間都建立這樣一條 iptables 規則?我用 IPVS 是為了啥?就是為了防止有大量的 iptables 規則拖垮效能啊,現在豈不是又繞回去了。
不慌,繼續從 Kubernetes 身上學習,使用 ipset
來解決這個問題。先把之前的 iptables 規則刪除:
$ iptables \
--table nat \
--delete POSTROUTING \
--source 10.0.0.11/32 \
--jump MASQUERADE
然後使用 ipset
建立一個集合 (set) :
$ ipset create DUSTIN-LOOP-BACK hash:ip,port,ip
這條命令建立了一個名為 DUSTIN-LOOP-BACK
的集合,它是一個 hashmap
,裡面儲存了目標 IP、目標埠和源 IP。
接著向集合中新增條目:
$ ipset add DUSTIN-LOOP-BACK 10.0.0.11,tcp:8080,10.0.0.11
現在不管有多少網路名稱空間,都只需要新增一條 iptables 規則:
$ iptables \
--table nat \
--append POSTROUTING \
--match set \
--match-set DUSTIN-LOOP-BACK dst,dst,src \
--jump MASQUERADE
網路連通性測試也沒有問題:
$ curl 10.100.100.100:8080
$ ip netns exec netns_leah curl 10.100.100.100:8080
$ ip netns exec netns_dustin curl 10.100.100.100:8080
新增虛擬服務的後端
最後,我們把網路名稱空間 netns_leah
中的 HTTP 服務也新增到虛擬服務的後端:
$ ipvsadm \
--add-server \
--tcp-service 10.100.100.100:8080 \
--real-server 10.0.0.21:8080 \
--masquerading
再向 ipset 的集合 DUSTIN-LOOP-BACK
中新增一個條目:
$ ipset add DUSTIN-LOOP-BACK 10.0.0.21,tcp:8080,10.0.0.21
終極測試來了,試著多執行幾次以下的測試命令:
$ curl 10.100.100.100:8080
你會發現輪詢演算法起作用了:
總結
相信通過本文的實驗和講解,大家應該理解了 kube-proxy IPVS 模式的工作原理。在實驗過程中,我們還用到了 ipset,它有助於解決在大規模叢集中出現的 kube-proxy 效能問題。如果你對這篇文章有任何疑問,歡迎和我進行交流。