在學習和使用Kubernetes過程中,你一定多次看到或者使用到了Service這個Kubernetes裡重要的服務對象。而Kubernetes之是以需要Service,一方面是因為Pod的IP不是固定的,另一方面則是因為一組Pod執行個體之間總會有負載均衡的需求。
一個最典型的Service定義,如下所示:
apiVersion: v1
kind: Service metadata: name: hostnames spec: selector: app: hostnames ports: - name: default protocol: TCP port: 80 targetPort: 9376
這個Service的例子,相信你應該不會陌生。其中,這裡使用了selector字段來聲明這個 Service 隻代理攜帶了app=hostnames 标簽的Pod。并且,這個Service的80 端口,代理的是Pod 的9376 端口。
然後,我們的應用的 Deployment,如下所示:
apiVersion: apps/v1 kind: Deployment metadata: name: hostnames spec: selector: matchLabels: app: hostnames replicas: 3 template: metadata: labels: app: hostnames spec: containers: - name: hostnames image: k8s.gcr.io/serve_hostname ports: - containerPort: 9376 protocol: TCP
這個應用的作用,就是每次通路 9376 端口時,傳回它自己的 hostname。
而被 selector 選中的 Pod,就稱為 Service 的 Endpoints,你可以使用 kubectl get ep 指令看到它們,如下所示:
$ kubectl get endpoints hostnames NAME ENDPOINTS hostnames 10.244.0.5:9376,10.244.0.6:9376,10.244.0.7:9376
需要注意的是,隻有處于 Running 狀态,且 readinessProbe 檢查通過的 Pod,才會出現在 Service 的 Endpoints 清單裡。并且,當某一個 Pod 出現問題時,Kubernetes 會自動把它從 Service 裡摘除掉。
而此時,通過該 Service 的 VIP 位址 10.0.1.175,你就可以通路到它所代理的 Pod 了:
$ kubectl get svc hostnames NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE hostnames ClusterIP 10.0.1.175 <none> 80/TCP 5s $ curl 10.0.1.175:80 hostnames-0uton $ curl 10.0.1.175:80 hostnames-yp2kp $ curl 10.0.1.175:80 hostnames-bvc05
這個 VIP 位址是 Kubernetes 自動為 Service 配置設定的。而像上面這樣,通過三次連續不斷地通路 Service 的 VIP 位址和代理端口 80,它就為我們依次傳回了三個 Pod 的 hostname。這也正印證了 Service 提供的是 Round Robin 方式的負載均衡。對于這種方式,我們稱為:ClusterIP 模式的 Service。
那Kubernetes 裡的 Service 究竟是如何工作的呢?
實際上,Service 是由 kube-proxy 元件,加上 iptables 來共同實作的。
舉個例子,對于我們前面建立的名叫 hostnames 的 Service 來說,一旦它被送出給 Kubernetes,那麼 kube-proxy 就可以通過 Service 的 Informer 感覺到這樣一個 Service 對象的添加。而作為對這個事件的響應,它就會在主控端上建立這樣一條 iptables 規則(你可以通過 iptables-save 看到它),如下所示:
-A KUBE-SERVICES -d 10.0.1.175/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3
可以看到,這條 iptables 規則的含義是:凡是目的位址是 10.0.1.175、目的端口
是 80 的 IP 包,都應該跳轉到另外一條名叫KUBE-SVC-NWV5X2332I4OT4T3
的 iptables 鍊進行處理。
而我們前面已經看到,10.0.1.175 正是這個 Service 的 VIP。是以這一條規則,就為這個 Service 設定了一個固定的入口位址。并且,由于 10.0.1.175 隻是一條 iptables 規則上的配置,并沒有真正的網絡裝置,是以你 ping 這個位址,是不會有任何響應的。
那麼,我們即将跳轉到的 KUBE-SVC-NWV5X2332I4OT4T3 規則,又有什麼作用呢?
實際上,它是一組規則的集合,如下所示:
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-WNBA2IHDGP2BOBGZ -A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-X3P2623AGDH6CDF3 -A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-57KPRZ3JQVENLNBR
可以看到,這一組規則,實際上是一組随機模式(–mode random)的 iptables
鍊。
而随機轉發的目的地,分别是 KUBE-SEP-WNBA2IHDGP2BOBGZ、
KUBE-SEP-X3P2623AGDH6CDF3 和 KUBE-SEP-57KPRZ3JQVENLNBR。
而這三條鍊指向的最終目的地,其實就是這個 Service 代理的三個 Pod。是以這一組規則,就是 Service 實作負載均衡的位置。
需要注意的是,iptables 規則的比對是從上到下逐條進行的,是以為了保證上述三條規則每條被選中的機率都相同,我們應該将它們的 probability 字段的值分别設定為 1/3(0.333…)、1/2 和 1。
這麼設定的原理很簡單:第一條規則被選中的機率就是 1/3;而如果第一條規則沒有被選中,那麼這時候就隻剩下兩條規則了,是以第二條規則的 probability 就必須設定為 1/2;類似地,最後一條就必須設定為 1。
可以想一下,如果把這三條規則的 probability 字段的值都設定成 1/3,最終每條規則被選中的機率會變成多少。
通過檢視上述三條鍊的明細,我們就很容易了解 Service 進行轉發的具體原理了,如下所示:
-A KUBE-SEP-57KPRZ3JQVENLNBR -s 10.244.3.6/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000 -A KUBE-SEP-57KPRZ3JQVENLNBR -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.3.6:9376 -A KUBE-SEP-WNBA2IHDGP2BOBGZ -s 10.244.1.7/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000 -A KUBE-SEP-WNBA2IHDGP2BOBGZ -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.1.7:9376 -A KUBE-SEP-X3P2623AGDH6CDF3 -s 10.244.2.3/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000 -A KUBE-SEP-X3P2623AGDH6CDF3 -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.2.3:9376
可以看到,這三條鍊,其實是三條 DNAT 規則。但在 DNAT 規則之前,iptables
對流入的 IP 包還設定了一個“标志”(–set-xmark)。
而 DNAT 規則的作用,就是在 PREROUTING 檢查點之前,也就是在路由之前,将流入 IP 包的目的位址和端口,改成–to-destination 所指定的新的目的位址和端口。可以看到,這個目的位址和端口,正是被代理 Pod 的 IP 位址和端口。
這樣,通路 Service VIP 的 IP 包經過上述 iptables 處理之後,就已經變成了通路具體某一個後端 Pod 的 IP 包了。不難了解,這些 Endpoints 對應的 iptables 規則,正是 kube-proxy 通過監聽 Pod 的變化事件,在主控端上生成并維護的。
以上,就是 Service 最基本的工作原理。
此外,你可能已經聽說過,Kubernetes 的 kube-proxy 還支援一種叫作 IPVS 的模式。這又是怎麼一回事兒呢?
其實,通過上面的講解,你可以看到,kube-proxy 通過 iptables 處理 Service 的過程,其實需要在主控端上設定相當多的 iptables 規則。而且,kube-proxy 還需要在控制循環裡不斷地重新整理這些規則來確定它們始終是正确的。
不難想到,當主控端上有大量 Pod 的時候,成百上千條 iptables 規則不斷地被重新整理,會大量占用該主控端的 CPU 資源,甚至會讓主控端“卡”在這個過程中。是以說,一直以來,基于 iptables 的 Service 實作,都是制約 Kubernetes 項目承載更多量級的 Pod 的主要障礙。
而 IPVS 模式的 Service,就是解決這個問題的一個行之有效的方法。
IPVS 模式的工作原理,其實跟 iptables 模式類似。當我們建立了前面的 Service 之後,kube-proxy 首先會在主控端上建立一個虛拟網卡(叫作:kube-ipvs0),并為它配置設定 Service VIP 作為 IP 位址,如下所示:
# ip addr ... 73:kube-ipvs0:<BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN qlen 1000 link/ether 1a:ce:f5:5f:c1:4d brd ff:ff:ff:ff:ff:ff inet 10.0.1.175/32 scope global kube-ipvs0 valid_lft forever preferred_lft forever
而接下來,kube-proxy 就會通過 Linux 的 IPVS 子產品,為這個 IP 位址設定三個 IPVS 虛拟主機,并設定這三個虛拟主機之間使用輪詢模式 (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.102.128.4:80 rr -> 10.244.3.6:9376 Masq 1 0 0 -> 10.244.1.7:9376 Masq 1 0 0 -> 10.244.2.3:9376 Masq 1 0 0
可以看到,這三個 IPVS 虛拟主機的 IP 位址和端口,對應的正是三個被代理的 Pod。
這時候,任何發往 10.102.128.4:80 的請求,就都會被 IPVS 子產品轉發到某一個後端 Pod 上了。
而相比于 iptables,IPVS 在核心中的實作其實也是基于 Netfilter 的 NAT 模式,是以在轉發這一層上,理論上 IPVS 并沒有顯著的性能提升。但是,IPVS 并不需要在主控端上為每個 Pod 設定 iptables 規則,而是把對這些“規則”的處理放到了核心态,進而極大地降低了維護這些規則的代價。
不過需要注意的是,IPVS 子產品隻負責上述的負載均衡和代理功能。而一個完整的 Service 流程正常工作所需要的包過濾、SNAT 等操作,還是要靠 iptables 來實作。隻不過,這些輔助性的 iptables 規則數量有限,也不會随着 Pod 數量的增加而增加。
是以,在大規模叢集裡,非常建議為 kube-proxy 設定–proxy-mode=ipvs 來開啟這個功能。它為 Kubernetes 叢集規模帶來的提升,還是非常巨大的。
在 Kubernetes 中,Service 和 Pod 都會被配置設定對應的 DNS A 記錄(從域名解析 IP 的記錄)。
對于 ClusterIP 模式的 Service 來說(比如我們上面的例子),它的 A 記錄的格式是:..svc.cluster.local。當你通路這條 A 記錄的時候,它解析到的就是該 Service 的 VIP 位址。
而對于指定了 clusterIP=None 的 Headless Service 來說,它的 A 記錄的格式也是:..svc.cluster.local。但是,當你通路這條 A 記錄的時候,它傳回的是所有被代理的 Pod 的 IP 位址的集合。當然,如果你的用戶端沒辦法解析這個集合的話,它可能會隻會拿到第一個 Pod 的 IP 位址。
此外,對于 ClusterIP 模式的 Service 來說,它代理的 Pod 被自動配置設定的 A 記錄的格式是:..pod.cluster.local。這條記錄指向 Pod 的 IP 位址。
而對 Headless Service 來說,它代理的 Pod 被自動配置設定的 A 記錄的格式是:...svc.cluster.local。這條記錄也指向 Pod 的 IP 位址。
但如果你為 Pod 指定了 Headless Service,并且 Pod 本身聲明了 hostname 和 subdomain 字段,那麼這時候 Pod 的 A 記錄就會變成:<pod 的 hostname>...svc.cluster.local,比如:
apiVersion: v1 kind: Service metadata: name: default-subdomain spec: selector: name: busybox clusterIP: None ports: - name: foo port: 1234 targetPort: 1234 --- apiVersion: v1 kind: Pod metadata: name: busybox1 labels: name: busybox spec: hostname: busybox-1 subdomain: default-subdomain containers: - image: busybox command: - sleep - "3600" name: busybox
上面的Service和Pod被建立之後,通過 busybox-1.default-subdomain.default.svc.cluster.local 解析到這個 Pod 的 IP 位址了。
需要注意的是,在 Kubernetes 裡,/etc/hosts 檔案是單獨挂載的,這也是為什麼 kubelet 能夠對 hostname 進行修改并且 Pod 重建後依然有效的原因。這跟 Docker 的 Init 層是一個原理。
總結
在這裡,我們詳細講解了 Service 的工作原理。實際上Service 機制,以及 Kubernetes 裡的 DNS 插件,都是在幫助你解決同樣一個問題,就是如何找到我的某一個容器。
這個問題在平台級項目中,往往就被稱作服務發現,當我的一個服務(Pod)的 IP 位址是不固定的且沒辦法提前獲知時,我該如何通過一個固定的方式通路到這個 Pod 呢?
而我在這裡所說的、ClusterIP 模式的 Service 提供的,就是一個 Pod 的穩定的 IP 位址,即 VIP。并且,這裡 Pod 和 Service 的關系是可以通過 Label 确定的。而 Headless Service提供的,則是一個 Pod 的穩定的 DNS 名字,并且,這個名字是可以通過 Pod 名字和 Service 名字拼接出來的。
在實際的場景裡,我們則應該根據業務的具體需求進行合理選擇。
👇👇👇點選關注,更多内容持續更新...
