kube-proxy工作原理

摘要:本文對kube-proxy做了一些總結說明,對其內部的實現原理進行了研究,並對userspace和iptables兩種mode的缺點進行的描述,都通過例子說明了iptable的工作。在下一篇博文中,我將對k8s v1.5中kube-proxy的源碼進行分析,有興趣的同學可以關注。

kube-proxy & service必要說明

說到kube-proxy,就不得不提到k8s中service,下面對它們兩做簡單說明:

  • kube-proxy其實就是管理service的訪問入口,包括集羣內Pod到Service的訪問和集羣外訪問service。
  • kube-proxy管理sevice的Endpoints,該service對外暴露一個Virtual IP,也成爲Cluster IP, 集羣內通過訪問這個Cluster IP:Port就能訪問到集羣內對應的serivce下的Pod。
  • service是通過Selector選擇的一組Pods的服務抽象,其實就是一個微服務,提供了服務的LB和反向代理的能力,而kube-proxy的主要作用就是負責service的實現。
  • service另外一個重要作用是,一個服務後端的Pods可能會隨着生存滅亡而發生IP的改變,service的出現,給服務提供了一個固定的IP,而無視後端Endpoint的變化。

服務發現

k8s提供了兩種方式進行服務發現:

  • 環境變量: 當你創建一個Pod的時候,kubelet會在該Pod中注入集羣內所有Service的相關環境變量。需要注意的是,要想一個Pod中注入某個Service的環境變量,則必須Service要先比該Pod創建。這一點,幾乎使得這種方式進行服務發現不可用。

    比如,一個ServiceName爲redis-master的Service,對應的ClusterIP:Port爲10.0.0.11:6379,則其對應的環境變量爲:

    REDIS_MASTER_SERVICE_HOST=10.0.0.11
    REDIS_MASTER_SERVICE_PORT=6379
    REDIS_MASTER_PORT=tcp://10.0.0.11:6379
    REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
    REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
    REDIS_MASTER_PORT_6379_TCP_PORT=6379
    REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11
  • DNS:這也是k8s官方強烈推薦的方式。可以通過cluster add-on的方式輕鬆的創建KubeDNS來對集羣內的Service進行服務發現。更多關於KubeDNS的內容,請查看我的博文:Kubernetes DNS Service技術研究 ,在此不再贅述。

發佈(暴露)服務

k8s原生的,一個Service的ServiceType決定了其發佈服務的方式。

  • ClusterIP:這是k8s默認的ServiceType。通過集羣內的ClusterIP在內部發布服務。
  • NodePort:這種方式是常用的,用來對集羣外暴露Service,你可以通過訪問集羣內的每個NodeIP:NodePort的方式,訪問到對應Service後端的Endpoint。
  • LoadBalancer: 這也是用來對集羣外暴露服務的,不同的是這需要Cloud Provider的支持,比如AWS等。
  • ExternalName:這個也是在集羣內發佈服務用的,需要藉助KubeDNS(version >= 1.7)的支持,就是用KubeDNS將該service和ExternalName做一個Map,KubeDNS返回一個CNAME記錄。

kube-proxy內部原理

kube-proxy當前實現了兩種proxyMode:userspace和iptables。其中userspace mode是v1.0及之前版本的默認模式,從v1.1版本中開始增加了iptables mode,在v1.2版本中正式替代userspace模式成爲默認模式。

userspace mode

userspace是在用戶空間,通過kube-proxy來實現service的代理服務。廢話不多說,其原理如下如圖所示:
輸入圖片說明

可見,這種mode最大的問題是,service的請求會先從用戶空間進入內核iptables,然後再回到用戶空間,由kube-proxy完成後端Endpoints的選擇和代理工作,這樣流量從用戶空間進出內核帶來的性能損耗是不可接受的。這也是k8s v1.0及之前版本中對kube-proxy質疑最大的一點,因此社區就開始研究iptables mode。

Example

$ kubectl get service
NAME             LABELS                                    SELECTOR              IP(S)            PORT(S)
kubernetes       component=apiserver,provider=kubernetes   <none>                10.254.0.1       443/TCP
ssh-service1     name=ssh,role=service                     ssh-service=true      10.254.132.107   2222/TCP

$ kubectl describe service ssh-service1 
Name:           ssh-service1
Namespace:      default
Labels:         name=ssh,role=service
Selector:       ssh-service=true
Type:           LoadBalancer
IP:         10.254.132.107
Port:           <unnamed>   2222/TCP
NodePort:       <unnamed>   30239/TCP
Endpoints:      <none>
Session Affinity:   None
No events.

NodePort的工作原理與ClusterIP大致相同,發送到某個NodeIP:NodePort的請求,通過iptables重定向到kube-proxy對應的端口(Node上的隨機端口)上,然後由kube-proxy再將請求發送到其中的一個Pod:TargetPort。

這裏,假如Node的ip爲10.0.0.5,則對應的iptables如下:

$ sudo iptables -S -t nat
...
-A KUBE-NODEPORT-CONTAINER -p tcp -m comment --comment "default/ssh-service1:" -m tcp --dport 30239 -j REDIRECT --to-ports 36463
-A KUBE-NODEPORT-HOST -p tcp -m comment --comment "default/ssh-service1:" -m tcp --dport 30239 -j DNAT --to-destination 10.0.0.5:36463
-A KUBE-PORTALS-CONTAINER -d 10.254.132.107/32 -p tcp -m comment --comment "default/ssh-service1:" -m tcp --dport 2222 -j REDIRECT --to-ports 36463
-A KUBE-PORTALS-HOST -d 10.254.132.107/32 -p tcp -m comment --comment "default/ssh-service1:" -m tcp --dport 2222 -j DNAT --to-destination 10.0.0.5:36463

可見:訪問10.0.0.5:30239端口會被轉發到node上的36463端口(隨機監聽端口)。而且在訪問clusterIP 10.254.132.107的2222端口時,也會把請求轉發到本地的36463端口。 36463端口實際被kube-proxy所監聽,將流量進行導向到後端的pod上。

iptables mode

另一種mode是iptables,它完全利用內核iptables來實現service的代理和LB。是v1.2及之後版本默認模式,其原理圖如下所示:
輸入圖片說明

iptables mode因爲使用iptable NAT來完成轉發,也存在不可忽視的性能損耗。另外,如果集羣中存在上萬的Service/Endpoint,那麼Node上的iptables rules將會非常龐大,性能還會再打折扣。

這也導致,目前大部分企業用k8s上生產時,都不會直接用kube-proxy作爲服務代理,而是通過自己開發或者通過Ingress Controller來集成HAProxy, Nginx來代替kube-proxy。

Example

iptables的方式則是利用了linux的iptables的nat轉發進行實現。

apiVersion: v1
kind: Service
metadata:
  labels:
    name: mysql
    role: service
  name: mysql-service
spec:
  ports:
    - port: 3306
      targetPort: 3306
      nodePort: 30964
  type: NodePort
  selector:
    mysql-service: "true"

mysql-service對應的nodePort暴露出來的端口爲30964,對應的cluster IP(10.254.162.44)的端口爲3306,進一步對應於後端的pod的端口爲3306。

mysql-service後端代理了兩個pod,ip分別是192.168.125.129和192.168.125.131。先來看一下iptables。

$iptables -S -t nat
...
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-SVC-67RL4FN6JRUPOJYM
-A KUBE-SEP-ID6YWIT3F6WNZ47P -s 192.168.125.129/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ID6YWIT3F6WNZ47P -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.129:3306
-A KUBE-SEP-IN2YML2VIFH5RO2T -s 192.168.125.131/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-IN2YML2VIFH5RO2T -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.131:3306
-A KUBE-SERVICES -d 10.254.162.44/32 -p tcp -m comment --comment "default/mysql-service: cluster IP" -m tcp --dport 3306 -j KUBE-SVC-67RL4FN6JRUPOJYM
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ID6YWIT3F6WNZ47P
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -j KUBE-SEP-IN2YML2VIFH5RO2T

首先如果是通過node的30964端口訪問,則會進入到以下鏈:

-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/mysql-service:" -m tcp --dport 30964 -j KUBE-SVC-67RL4FN6JRUPOJYM

然後進一步跳轉到KUBE-SVC-67RL4FN6JRUPOJYM的鏈:

-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-ID6YWIT3F6WNZ47P
-A KUBE-SVC-67RL4FN6JRUPOJYM -m comment --comment "default/mysql-service:" -j KUBE-SEP-IN2YML2VIFH5RO2T

這裏利用了iptables的–probability的特性,使連接有50%的概率進入到KUBE-SEP-ID6YWIT3F6WNZ47P鏈,50%的概率進入到KUBE-SEP-IN2YML2VIFH5RO2T鏈。

KUBE-SEP-ID6YWIT3F6WNZ47P的鏈的具體作用就是將請求通過DNAT發送到192.168.125.129的3306端口。

-A KUBE-SEP-ID6YWIT3F6WNZ47P -s 192.168.125.129/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-ID6YWIT3F6WNZ47P -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.129:3306

同理KUBE-SEP-IN2YML2VIFH5RO2T的作用是通過DNAT發送到192.168.125.131的3306端口。

-A KUBE-SEP-IN2YML2VIFH5RO2T -s 192.168.125.131/32 -m comment --comment "default/mysql-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-IN2YML2VIFH5RO2T -p tcp -m comment --comment "default/mysql-service:" -m tcp -j DNAT --to-destination 192.168.125.131:3306

分析完nodePort的工作方式,接下里說一下clusterIP的訪問方式。 對於直接訪問cluster IP(10.254.162.44)的3306端口會直接跳轉到KUBE-SVC-67RL4FN6JRUPOJYM。

-A KUBE-SERVICES -d 10.254.162.44/32 -p tcp -m comment --comment "default/mysql-service: cluster IP" -m tcp --dport 3306 -j KUBE-SVC-67RL4FN6JRUPOJYM

接下來的跳轉方式同NodePort方式。

參考:
- https://kubernetes.io/docs/user-guide/services/
- https://xuxinkun.github.io/2016/07/22/kubernetes-proxy/

發佈了62 篇原創文章 · 獲贊 12 · 訪問量 126萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章