原文連接:fuckcloudnative.io/posts/ipvs-…python
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 更新期間鏈接保持不斷開。docker
目前網絡上關於 kube-proxy
工做原理的文檔幾乎都是以 iptables
模式爲例,不多說起 IPVS
,本文就來破例解讀 kube-proxy IPVS 模式的工做原理。爲了理解地更加完全,本文不會使用 Docker 和 Kubernetes,而是使用更加底層的工具來演示。後端
咱們都知道,Kubernetes 會爲每一個 Pod 建立一個單獨的網絡命名空間 (Network Namespace) ,本文將會經過手動建立網絡命名空間並啓動 HTTP 服務來模擬 Kubernetes 中的 Pod。api
本文的目標是經過模擬如下的 Service
來探究 kube-proxy 的 IPVS
和 ipset
的工做原理:bash
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 服務。爲了更好地理解本文的內容,推薦提早閱讀如下的文章:markdown
注意:本文全部步驟皆是在 Ubuntu 20.04 中測試的,其餘 Linux 發行版請自行測試。網絡
首先須要開啓 Linux 的路由轉發功能:app
$ sysctl --write net.ipv4.ip_forward=1
複製代碼
接下來的命令主要作了這麼幾件事:負載均衡
bridge_home
netns_dustin
和 netns_leah
bridge_home
netns_dustin
網絡命名空間中的 veth 設備分配一個 IP 地址爲 10.0.0.11
netns_leah
網絡命名空間中的 veth 設備分配一個 IP 地址爲 10.0.021
bridge_home
接口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
建立一個虛擬服務 (Virtual Service) 來模擬 Kubernetes 中的 Service :
$ ipvsadm \
--add-service \
--tcp-service 10.100.100.100:8080 \
--scheduler rr
複製代碼
--tcp-service
來指定 TCP 協議,由於咱們須要模擬的 Service 就是 TCP 協議。目前 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 點是不同的。
內核協議棧中預約義的回調函數有以下五個:
iptables 實現了全部的 Hook 點,而 IPVS 只實現了 LOCAL_IN
、LOCAL_OUT
、FORWARD
這三個 Hook 點。既然沒有實現 PRE_ROUTING
,就不會在進入 LOCAL_IN 以前進行地址轉換,那麼數據包通過路由判斷後,會進入 LOCAL_IN Hook 點,IPVS 回調函數若是發現目標 IP 地址不屬於該節點,就會將數據包丟棄。
若是將目標 IP 分配給了虛擬網絡接口,內核在處理數據包時,會發現該目標 IP 地址屬於該節點,因而能夠繼續處理數據包。
固然,咱們不須要將 IP 地址分配給任何已經被使用的網絡接口,咱們的目標是模擬 Kubernetes 的行爲。Kubernetes 在這裏建立了一個 dummy 接口,它和 loopback 接口相似,可是你能夠建立任意多的 dummy 接口。它提供路由數據包的功能,但實際上又不進行轉發。dummy 接口主要有兩個用途:
看來 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 網橋:
無論是 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
這個內核參數 (置爲 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
複製代碼
終於成功了!
雖然咱們能夠從網絡命名空間 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
複製代碼
在文章開頭準備實驗環境的章節,執行了這麼一條命令:
$ 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 性能問題。若是你對這篇文章有任何疑問,歡迎和我進行交流。