一文讀懂 K8S Service 原理

概述

在 K8S 集羣中, K8S 會給每一個 Pod 分配一個 podIP,默認網絡模式下,這個 podIP 是隨機分配的虛擬 IP,且該 podIP 會由於 Pod 重啓而自動更新,那麼客戶端訪問 Pod 時,必然要更新訪問地址。另一方面如果一個服務由多個 Pod 實例負載均衡提供服務,那麼在客戶端側實現負載均衡訪問也不合理。

基於以上兩個問題,K8S 實現了 Service,Service 也是 K8S 中的一個資源對象。每一個 Kubernetes 的 Service 都是一組 Pod 的邏輯集合和訪問方式的抽象,由 Service 去代理訪問上游每個 Pod 實例。

在這篇文章中,會詳細分析 K8S Service 的使用和實現原理。

Service 創建流程

當有 Service 對象創建事件觸發時,Kube-controller-manager 中的 Endpoints controller 通過 Kube-apiserver Watch/List 到 Service 創建事件時,會根據 Service 的資源定義創建一個 Endpoints 對象,這個對象即定義了上游對應的 Pod 實例組的 IP、Port 。Endpoints 資源對象如下:

apiVersion: v1
kind: Endpoints
metadata:
  name: svc-a
  namespace: default
subsets:
# 上游 Pod 組 IP、Port
- addresses:
  - ip: 101.76.9.141
    targetRef:
      kind: Pod
      name: pod-a-55fcf5456c-86mw6
      namespace: default
      uid: dfcbaa48-912d-4ab6-b935-99e119553cca
  ports:
  - name: http
    port: 80
    protocol: TCP

Endpoints controller 創建完 Endpoint 對象時,會去創建 Service 對象,Service 根據 Label_selector 去匹配綁定 Endpoints 對象,Service 資源對象如下:

apiVersion: v1
kind: Service
metadata:
  name: svc-a
  namespace: default
spec:
  clusterIP: 10.233.56.44
  clusterIPs:
  - 10.233.56.44
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: pod-a
  sessionAffinity: None
  # Service 類型
  type: ClusterIP
status:
  loadBalancer: {}

當 Service、Endpoints 創建好之後,K8S 中的每個節點上的 Kube-proxy 組件會去 Watch/List 該對象,然後在節點上創建響應的 Iptables/IPVS 規則。那麼這些 Iptables/IPVS 規則就是將流量根據策略轉發至上游 Pod。

所以這裏涉及 Service、Endpoint、Pod 這三個對象,整體流程如下:

IPVS 原理

每個 Node 上都運行一個 Kube-proxy 組件,負責爲 Service 創建 Iptables 或者 IPVS 規則,實現一種 VIP 的代理形式。

Kube-proxy 有三種模式:userspaceIptables 和 IPVS,其中 userspace 模式不太常用。Iptables 模式最主要的問題是在服務多的時候產生太多的 Iptables 規則,非增量式更新會引入一定的時延,大規模情況下有明顯的性能問題。爲解決 Iptables 模式的性能問題,K8S v1.11 新增了 IPVS 模式(v1.8 開始支持測試版,並在 v1.11 GA),採用增量式更新,並可以保證 service 更新期間連接保持不斷開。K8S v1.14 版本默認使用 IPVS 模式,本篇文章只講解 IPVS 模式。

當使用 IPVS 做集羣內服務的負載均衡可以解決 Iptables 帶來的性能問題。它不會像 iptables 一樣隨着 K8S 節點的增加,其 Iptables 規則也隨之線性增長。IPVS 使用更高效的數據結構 (散列表),允許無限規模擴張。

IPVS 是 Linux 內核實現的四層負載均衡,是 LVS 負載均衡模塊的實現。IPVS 和 Iptables 一樣都是基於 Netfilter 實現。IPVS 支持 TCP、UDP、SCTP、IPv4、IPv6 等協議,也支持多種負載均衡策略,例如 rr、wrr、lc、wlc、sh、dh、lblc 等。

Iptables 向 Netfilter 5 個階段都註冊了對應的表,每個表都有具體的函數來處理數據包。LVS 主要通過向 Netfilter 的 3 個階段註冊鉤子函數來對數據包進行處理,如下圖:

所以說要使用 IPVS 實現 Service,數據包必須要經過 INPUT、FORWARD、POSTROUTING 任意一個鏈,這樣才能觸發三個 hook 函數。那麼 K8S Service 使用 IPVS 如何才能保證報文經過這三個鏈呢?在下面文章會解釋。

IPVS 支持三種負載均衡模式:Direct Routing ( 簡稱 DR )、Tunneling ( 也稱 ipip 模式)、NAT ( 也稱 Masq 模式)。由於 DR 和 Tunneling 模式都不支持端口映射,只有 NAT 模式支持端口映射,所以只有 NAT 模式支撐 Kubernetes Service 所有場景。下面主要講解 NAT 模式原理。

NAT 模式

因爲 K8S Pod 提供服務,肯定需要通過端口訪問,那麼通過 Service 訪問 Pod,也需要支持 Service 的端口轉發至上游 Pod 的端口。IPVS 只有 NAT 模式支持端口轉發映射,其實和 Iptables 原理一樣,包含 DNAT、SNAT。例如一個 IPVS 服務端口 3080 到 Pod 端口 8080 的 DNAT 映射樣例如下:

TCP  10.233.56.44:3080 rr
  -> 101.76.9.141:8080              Masq    1      0          0
  -> 101.76.9.142:8080              Masq    1      0          0

但是隻有 DNAT 會導致回包報文被丟棄,還需要一次 SNAT。

下面我們看看沒有做 SNAT 會出現什麼問題:

上圖 Client ip 地址爲 192.168.1.1,Server ip 地址爲 172.16.1.1,Client 無法直接訪問 Server,但是 Client 和 Gateway 在同一內網,Gateway 具有到達 Server 的路由。所以 Client 可以通過 Gateway 來訪問 Server。

Client 向 Server 發起一個訪問,其原始報文報文中源目 ip 地址爲 (192.168.1.1,192.168.1.254) ,那麼客戶端期待得到的回程報文源地址是 192.168.1.254 即 Gateway ip 地址。當報文經過 Gateway 的 netfilter 進行一次 DNAT 後,報文的目的地址被修改成了 172.16.1.1,即 Server 端地址。當報文送到 Server 後,Server 一看報文的源地址是 Client ip 地址後便直接把響應報文返回給 Client,即此時響應報文的源和目的地址對爲 (172.16.1.1, 192.168.1.1)。這與 Client 端期待的報文源目地址不匹配,Client 端收到後會直接丟棄該報文。

因此,當報文不直接送達後端服務,而是訪問中間設備 (Gateway) 時,都需要再網關處做一次 SNAT 把報文的源 IP 修改成 Gateway 地址。這樣 Server 響應報文會先回到 Gateway,然後 Gateway 會把回程報文目的地址改成 Client 地址,源地址改爲 Gateway 地址。

因此,IPVS 訪問 Service VIP 做了一次 DNAT 後,必須要要做一次 SNAT 才能讓報文順利返回。但是 linux 內核原生版本的 IPVS 只做 DNAT,不做 SNAT。所以在該模式下,依舊藉助 Iptables 來實現 SNAT。

有些定製版本的 IPVS,例如華爲和阿里自己維護的分支支持 fullNAT,既同時支持 SNAT 和 DNAT。
這裏就解釋了爲什麼使用 IPVS 模式,依然存在 Iptables 規則

那麼使用 Iptables 創建的 SNAT 規則如下:

# 該命令是查詢 POSTROUGING 鏈上的 nat 表規則
$ iptables -t nat -L POSTROUTING
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
KUBE-POSTROUTING  all  --  anywhere             anywhere             /* kubernetes postrouting rules */

# 該命令是查詢 KUBE-POSTROUGING 鏈上的 nat 表規則
$ iptables -t nat -L KUBE-POSTROUTING
Chain KUBE-POSTROUTING (1 references)
target     prot opt source               destination         
MARK       all  --  anywhere             anywhere             MARK xor 0x4000
MASQUERADE  all  --  anywhere             anywhere             /* kubernetes service traffic requiring SNAT */ random-fully

由上述規則可知,IPVS 報文經過節點 netfilter POSTROUTING 鏈時,會跳到 KUBE-POSTROUTING 鏈去處理。在 KUBE-POSTROUTING 鏈中,對每個報文都設置 Kubernetes 獨有的 MARK 標記 ( 0x4000/0x4000)。並且將數據包進行一次 SNAT,即 MASQUERADE (用節點 IP 替換包的源 IP)。

Service 原理

Service 常用支持三種類型,即 ClusterIP、NodePort、LoadBalancer,下面詳細講解每個類型的原理。

ClusterIP

我們在定義 Service 的時候可以指定一個自己需要的類型的 Service,如果不指定的話默認是 ClusterIP 類型。Kube-proxy 啓動時會在當前節點上創建一個 kube-ipvs0 網卡,每當創建一個 ClusterIP 類型的 Service,Kube-proxy 都會在 kube-ipvs0 上動態分配一個 IP 地址,這個地址就是該 Service 的虛擬 VIP。當然也可以在創建 Service 時手動配置具體的 IP。下面是集羣中某個節點的 kube-ipvs0 網卡信息。

$ ip addr show kube-ipvs0
33597871: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether 56:b2:39:2a:8b:f5 brd ff:ff:ff:ff:ff:ff
    inet 10.233.0.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.0.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.38.167/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.87.218/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.220.119/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.56.44/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.98.21/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

上面遺留一個問題,就是如何保證 Service 報文能經過 netfilter 的 INPUT、FORWARD、POSTROUTING 這三個鏈。其實節點上創建 kube-ipvs0 網卡並將 Service ip 綁定到該網卡,讓內核覺得需 IP 就是本機 IP,進而報文進入 INPUT 鏈。

而接下來,Kube-proxy 就會通過 Linux 的 IPVS 模塊,爲這個 IP 地址設置多個虛擬主機,並且這些虛擬主機之間使用輪詢模式 (rr) 來作爲負載均衡策略。我們可以通過 ipvsadm 命令查看,如下所示:

$ ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.233.56.44:80 rr
  -> 101.76.9.141:80              Masq    1      0          0
  -> 101.76.9.142:80              Masq    1      0          0

以上 IPVS 規則中,10.233.56.44 就是分配的虛擬 VIP,而 101.76.9.141、101.76.9.142 即是上游兩個 Pod ip 地址。

在集羣中不管是在 Pod 裏還是主機節點上,任何任何發往 10.233.56.44:80 的請求,都會被當前節點的 kube-ipvs0 網卡獲取然後通過 IPVS 模塊轉發至某一個虛擬主機。流量如下:

IPVS 提供如下負載均衡策略:
rr :輪詢調度

lc :最小連接數

dh :目標哈希

sh :源哈希

sed :最短期望延遲

nq : 不排隊調度

根據上面描述,使用 IPVS 模式後,主機上依然還會創建 SNAT Iptables 規則。只不過這條規則是共用的,不需要每個 Service 都創建。以上就是 ClusterIP 模式的原理。

NodePort

使用 ClusterIP 只能在集羣內部訪問,如果想在集羣外部訪問 K8S 內的資源,需要在物理主機層面開放端口訪問,那麼 NodePort 是一種方式。

NodePort 類型的 service 會在集羣內部署了 Kube-proxy 的節點打開一個指定的端口,之後所有的流量直接發送到這個端口,然後會被轉發到 Service 後端真實的服務進行訪問。

當開啓 NodePort Service 時,每個節點的 Kube-proxy 都會在該節點創建一個 lvs 規則:

$ ipvsadm -l
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn    
TCP  172.17.0.1:32257 rr
  -> 101.76.9.141:9153             Masq    1      0          0         
  -> 101.76.9.142:9153             Masq    1      0          0

這條 lvs 規則的意思就是訪問 172.17.0.1:32257 負載均衡到 101.76.9.141:9153、101.76.9.142:9153,其中 172.17.0.1 是主機節點 IP,101.76.9.141、101.76.9.142 是 Pod IP。流量如下:

同樣,NodePort 模式也會進行一次 SNAT,規則共用。

K8S 的 Service 的 NodePort 默認端口範圍是 30000-32767,也可以通過修改 Kube-apiserver—-service-node-port-range啓動參數來修改 NodePort 的範圍,例如—service-node-port-range=8000-9000

如果需要修改 NodePort 默認端口範圍,除了修改 Kube-apiserver—service-node-port-range參數外,也需要修改 Linux 主機的ip_local_port_range內核參數,默認範圍是:32768-60999,正好是 NodePort 默認範圍的下一個端口。
該內核參數作用是系統會在 32768 到 60999 之間爲本地的臨時端口分配端口號。這些端口用於系統發起的出站連接,並在出站連接結束時釋放。
所以ip_local_port_rangeKube-apiserver—service-node-port-range 的端口範圍不能衝突,否則會導致異常。

LoaBalancer

NodePort 的模式雖然可以實現集羣外訪問,但是,不能保證高可用。因爲如果使用 Node-1:80 去訪問,當 Node-1 宕機,那麼就需要手動切換節點。那麼 LoadBalancer 就是這個問題,使用外部 VIP 訪問,只要保證這個外部 VIP 正常即可。LoadBalancer 的工作需要搭配第三方的負載均衡器來完成,各大雲廠商都有自己的 LoadBalancer,開源的 MetalLB 也支持。

當創建一個類型爲 LoadBalancer 的 Service。新創建的 Service 的 EXTERNAL-IP 狀態是 pending,假如沒有負載均衡器的話,會一直處於 pending 狀態:

NAME       TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)                                    AGE
test       LoadBalancer   10.96.0.10   <pending>     53:32083/UDP,53:30321/TCP,9153:32257/TCP   30d

如果集羣中存在負載均衡器,那麼就會自動分配一個 VIP

NAME       TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)                                    AGE
test       LoadBalancer   10.96.0.10   172.17.0.3    53:32083/UDP,53:30321/TCP,9153:32257/TCP   30d

總結

K8S Service 三種模式,都存在不同的使用場景:

Kube-proxy 實現的是分佈式負載均衡器,而非集中式負載均衡器。就是每個節點的充當一個負載均衡器,每個節點上都會配置一摸一樣的規則,包括 IPVS/Iptables。在生產使用時,建議少用 NodePort 模式,因爲會導致某個節點的流量高於其他節點。建議集羣內使用 ClusterIP,集羣外使用 LoadBalancer。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/HgQrub757qhIBCYO45o4NQ