在本系列的第一篇文章中,研究了kubernetes如何結合使用虛擬網絡設備和路由規則,以容許在一個羣集節點上運行的Pod與在另外一個羣集節點上運行的Pod通訊,只要發送者知道接收者的Pod網絡便可。 IP地址。若是您還不熟悉Pod的交流方式,那麼在繼續以前值得一讀。集羣中的Pod網絡很簡單,但僅憑其不足以建立持久性系統。那是由於kubernetes中的Pod是短暫的。您能夠將Pod IP地址用做終結點,但不能保證該地址在下次從新建立Pod時不會更改,這可能因爲多種緣由而發生。html
您可能已經意識到這是一個老問題,而且它有一個標準的解決方案:經過反向代理/負載均衡器運行流量。客戶端鏈接到代理,代理負責維護將請求轉發到的健康服務器列表。這對代理服務器提出了一些要求:代理服務器自己必須是耐用的而且可以抗故障;它必須具備能夠轉發到的服務器列表;而且它必須具備某種方式來了解特定服務器是否運行正常並可以響應請求。 kubernetes設計師以一種優雅的方式解決了這個問題,該方式創建在平臺的基本功能之上,能夠知足全部這三個需求,而且從一種稱爲服務的資源類型開始。python
在第一篇文章中,我展現了一個假設的集羣,其中包含兩個服務器Pod,並描述了它們如何跨節點通訊。在這裏,我想以該示例爲基礎來描述kubernetes服務如何在一組服務器Pod之間實現負載平衡,從而容許客戶端Pod獨立且持久地運行。要建立服務器容器,咱們可使用以下部署:數據庫
kind: Deployment apiVersion: extensions/v1beta1 metadata: name: service-test spec: replicas: 2 selector: matchLabels: app: service\_test\_pod template: metadata: labels: app: service\_test\_pod spec: containers: - name: simple-http image: python:2.7 imagePullPolicy: IfNotPresent command: \["/bin/bash"\] args: \["-c", "echo \\"<p>Hello from $(hostname)</p>\\" > index.html; python -m SimpleHTTPServer 8080"\] ports: - name: http containerPort: 8080
部署建立了兩個很是簡單的http服務器pod,它們在端口8080上以其運行的Pod的主機名進行響應。使用kubectl apply建立此部署後,咱們能夠看到Pod在集羣中運行,而且咱們還能夠查詢到查看他們的Pod網絡地址是什麼:json
**$ kubectl apply -f test-deployment.yaml deployment "service-test" created $ kubectl get pods service-test-6ffd9ddbbf-kf4j2 1/1 Running 0 15s service-test-6ffd9ddbbf-qs2j6 1/1 Running 0 15s $ kubectl get pods --selector=app=service_test_pod -o jsonpath='{.items[*].status.podIP}' 10.0.1.2 10.0.2.2
咱們能夠經過建立一個簡單的客戶端容器來發出請求,而後查看輸出來證實容器網絡正在運行。segmentfault
apiVersion: v1 kind: Pod metadata: name: service-test-client1 spec: restartPolicy: Never containers: - name: test-client1 image: alpine command: ["/bin/sh"] args: ["-c", "echo 'GET / HTTP/1.1\r\n\r\n' | nc 10.0.2.2 8080"]
建立此容器後,命令將運行至完成,容器將進入「已完成」狀態,而後可使用 kubectl logs
檢索輸出:後端
**$ kubectl logs service-test-client1 HTTP/1.0 200 OK <!-- blah --> <p>Hello from service-test-6ffd9ddbbf-kf4j2</p>
此示例中沒有任何內容顯示客戶端Pod在哪一個節點上建立,可是因爲Pod網絡,不管客戶端Pod在羣集中的哪一個位置運行,它均可以到達服務器Pod並得到響應。可是,若是服務器Pod死掉並從新啓動,或從新安排到其餘節點,則其IP幾乎能夠確定會發生變化,而且客戶端會中斷。咱們經過建立服務來避免這種狀況。api
kind: Service apiVersion: v1 metadata: name: service-test spec: selector: app: service_test_pod ports: - port: 80 targetPort: http
服務是一種Kubernetes資源,可致使將代理配置爲將請求轉發到一組Pod。將接收流量的Pod集合由選擇器肯定,該選擇器與建立Pod時分配給Pod的標籤匹配。建立服務後,咱們能夠看到已爲其分配了IP地址,並將在端口80上接受請求。bash
$ kubectl get service service-test NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE service-test 10.3.241.152 <none> 80/TCP 11s
能夠將請求直接發送到服務IP,可是最好使用解析爲IP地址的主機名。幸運的是,kubernetes提供了一個內部羣集DNS,用於解析服務名稱,而且在客戶端pod稍做更改的狀況下,咱們可使用它:服務器
apiVersion: v1 kind: Pod metadata: name: service-test-client2 spec: restartPolicy: Never containers: - name: test-client2 image: alpine command: ["/bin/sh"] args: ["-c", "echo 'GET / HTTP/1.1\r\n\r\n' | nc service-test 8080"]
此Pod運行完成後,輸出顯示服務已將請求轉發到服務器Pod之一。網絡
$ kubectl logs service-test-client2 HTTP/1.0 200 OK <!-- blah --> <p>Hello from service-test-6ffd9ddbbf-kf4j2</p>
您能夠繼續運行客戶端Pod,而且會看到兩個服務器Pod的響應,每一個服務器窗格大約收到50%的請求。若是您的目標是瞭解它是如何工做的,那麼最好的出發點是爲咱們的服務分配的IP地址。
分配測試服務的IP表示網絡上的地址,您可能已經注意到,該網絡與Pod所在的網絡不一樣。
thing IP network ----- -- ------- pod1 10.0.1.2 10.0.0.0/14 pod2 10.0.2.2 10.0.0.0/14 service 10.3.241.152 10.3.240.0/20
它也與節點所在的專用網絡不一樣,下面將更加清楚。在第一篇文章中,我注意到Pod網絡地址範圍未經過kubectl
公開,所以您須要使用提供程序特定的命令來檢索此集羣屬性。服務網絡地址範圍也是如此。若是您在Google Container Engine中運行,則能夠執行如下操做:
$ gcloud container clusters describe test | grep servicesIpv4Cidr servicesIpv4Cidr: 10.3.240.0/20
由該地址空間指定的網絡稱爲「服務網絡」。每一個「 ClusterIP」類型的服務都將在該網絡上分配一個IP地址。還有其餘類型的服務,在下一篇有關Ingress的文章中,我將討論其中的幾個。可是ClusterIP是默認的,這意味着「將爲該服務分配一個IP地址,該IP地址能夠從羣集中的任何Pod訪問。」您能夠經過運行帶有服務名稱的kubectl describe services
命令來查看服務的類型。
$ kubectl describe services service-test Name: service-test Namespace: default Labels: <none> Selector: app=service\_test\_pod Type: ClusterIP IP: 10.3.241.152 Port: http 80/TCP Endpoints: 10.0.1.2:8080,10.0.2.2:8080 Session Affinity: None Events: <none>
像Pod網絡同樣,服務網絡是虛擬的,可是它在某些有趣的方面不一樣於Pod網絡。考慮Pod網絡地址範圍10.0.0.0/14。若是您要查看組成集羣中節點的主機,列出網橋和接口,則將看到在該網絡上配置了地址的實際設備。這些是每一個Pod的虛擬以太網接口,以及將它們彼此鏈接以及與外界鏈接的橋樑。
如今查看服務網絡10.3.240.0/20。您能夠經過ifconfig令本身高興,而且在該網絡上找不到配置有地址的任何設備。您能夠在鏈接全部節點的網關上檢查路由規則,而找不到該網絡的任何路由。服務網絡不存在,至少不做爲鏈接接口存在。可是正如咱們在上面看到的那樣,當咱們以某種方式向該網絡上的IP發出請求時,該請求又將其發送給了在Pod網絡上運行的服務器Pod。那是怎麼發生的?讓咱們跟隨一個包看看。
想象一下,咱們上面運行的命令在測試集羣中建立了如下Pod:
在IP網絡一般配置有路由,這樣,當接口因爲本地不存在具備指定地址的設備而沒法將數據包傳遞到其目的地時,會將其轉發到其上游網關。所以,在此示例中,看到數據包的第一個接口是客戶端Pod內的虛擬以太網接口。該接口位於Pod網絡10.0.0.0/14上,而且不知道地址爲10.3.241.152的任何設備,所以它將數據包轉發到其網關即網橋cbr0。網橋很笨,只是來回傳遞流量,咱們有兩個節點,鏈接它們的網關(也具備Pod網絡的路由規則)和三個Pod:節點1上的客戶端Pod,節點1上的服務器Pod和節點2上的另外一個服務器Pod。客戶端使用DNS名稱service-test向服務發出http請求。羣集DNS系統將該名稱解析爲服務羣集IP 10.3.241.152,客戶端Pod最終建立了一個http請求,該請求致使一些數據包使用該IP在目標字段中發送。
IP網絡一般配置有路由,這樣,當接口因爲本地不存在具備指定地址的設備而沒法將數據包傳遞到其目的地時,會將其轉發到其上游網關。所以,在此示例中,看到數據包的第一個接口是客戶端Pod內的虛擬以太網接口。該接口位於Pod網絡10.0.0.0/14上,而且不知道地址爲10.3.241.152的任何設備,所以它將數據包轉發到其網關即網橋cbr0。網橋很是笨拙,只是來回傳遞流量,所以網橋將數據包發送到主機/節點以太網接口。
本例中的主機/節點以太網接口位於網絡10.100.0.0/24上,它也不知道地址爲10.3.241.152的任何設備,所以一般再次發生的狀況是,數據包將被轉發到該接口的網關,圖中所示的頂級路由器。相反,實際發生的是該數據包在飛行中被卡住並重定向到實時服務器Pod之一。
三年前,當我第一次開始使用kubernetes時,上圖中發生的事情彷佛很是神奇。個人客戶以某種方式可以鏈接到沒有任何接口的地址,而且這些數據包在羣集中的正確位置彈出。後來我瞭解到,這個謎題的答案是一款名爲kube-proxy的軟件。
就像kubernetes中的全部內容同樣,服務只是一種資源,是中央數據庫中的一條記錄,它描述瞭如何配置一些軟件來執行某些操做。實際上,一項服務會影響集羣中多個組件的配置和行爲,可是在這裏很重要的一項服務(使上述魔術得以實現的一項服務)是kube-proxy。大家中的許多人都會基於名稱對該組件的做用有一個大體的瞭解,可是關於kube-proxy的某些事情使其與典型的反向代理(如haproxy或linkerd)大不相同。
代理的通常行爲是經過兩個打開的鏈接在客戶端和服務器之間傳遞流量。客戶端將入站鏈接到服務端口,代理將出站鏈接到服務器。因爲全部此類代理都在用戶空間中運行,所以這意味着數據包在每次經過代理的過程當中都會被封送到用戶空間並返回內核空間。最初,kube-proxy只是做爲這樣的用戶空間代理實現的,但有所不一樣。代理須要一個接口,既能夠偵聽客戶端鏈接,又能夠用於鏈接到後端服務器。節點上惟一可用的接口是:a)主機的以太網接口;或b)Pod網絡上的虛擬以太網接口。
爲何不在這些網絡之一上使用地址?我沒有任何相關知識,可是我想我很早就知道在項目中這樣作會複雜化那些網絡的路由規則,這些規則旨在知足Pod和節點的需求,這兩個都是短暫的集羣中的實體。服務顯然須要它們本身的,穩定的,無衝突的網絡地址空間,而虛擬IP系統最有意義。可是,正如咱們指出的,該網絡上沒有實際的設備。您能夠在路由規則,防火牆過濾器等中使用假裝的網絡,但實際上沒法在端口上偵聽或經過不存在的接口打開鏈接。
Kubernetes使用Linux內核的一個名爲netfilter的功能和一個名爲iptables的用戶空間接口來解決這個問題。這篇已經很長的帖子沒有足夠的空間來介紹它的確切工做方式。若是您想了解更多信息,netfilter pages是一個很好的起點。netfilter是基於規則的數據包處理引擎。它在內核空間中運行,並在生命週期的各個點查看每一個數據包。它根據規則對數據包進行匹配,並在找到匹配的規則時採起指定的操做。它能夠採起的許多措施之一是將數據包重定向到另外一個目的地。沒錯,netfilter是內核空間代理。下面說明了當kube-proxy做爲用戶空間代理運行時netfilter扮演的角色。
在這種模式下,kube-proxy在本地主機接口上打開一個端口(在上面的示例中爲10400),以偵聽對test-service的請求,插入netfilter規則以將發往服務IP的數據包從新路由到其本身的端口,並將這些數據包轉發。對端口8080上的Pod的請求。這就是對10.3.241.152:80的請求如何神奇地變爲對10.0.2.2:8080的請求。鑑於netfilter的功能,使全部服務都能正常工做所須要的一切就是讓kube-proxy打開一個端口併爲該服務插入正確的netfilter規則,以響應來自主api服務器的通知更改。
這個故事還有一點曲折。上面我提到,因爲封送數據包,用戶空間代理很昂貴。在kubernetes 1.2中,kube-proxy得到了在iptables模式下運行的能力。在這種模式下,kube-proxy一般再也不充當集羣間鏈接的代理,而是委託netfilter進行檢測綁定到服務IP的數據包並將它們重定向到Pod的工做,全部這些操做都發生在內核空間中。在這種模式下,kube-proxy的工做或多或少地侷限於保持netfilter規則同步。
最後,讓咱們將上述全部內容與文章開頭提到的關於可靠代理的要求進行比較。服務代理系統是否耐用?默認狀況下,kube-proxy做爲systemd單元運行,所以若是失敗,它將從新啓動。在Google Container Engine中,它做爲由守護程序控制的容器運行。這將是未來的默認設置,多是1.9版。做爲用戶空間代理,kube-proxy仍然表明鏈接失敗的單點。當以iptables模式運行時,從嘗試嘗試鏈接的本地Pod的角度來看,該系統具備很高的持久性,由於若是節點啓動,則netfilter也是如此。
服務代理是否知道能夠處理請求的健康服務器容器?如上所述,kube-proxy偵聽主api服務器以瞭解集羣中的更改,其中包括對服務和端點的更改。當它收到更新時,它使用iptables使netfilter規則保持同步。建立新服務並填充其端點時,kube-proxy獲取通知並建立必要的規則。一樣,刪除服務時,它也會刪除規則。針對端點的運行情況檢查由kubelet(在每一個節點上運行的另外一個組件)執行,當發現不正常的端點時,kubelet經過api服務器通知kube-proxy,並編輯netfilter規則以刪除此端點,直到它再次恢復健康爲止。
全部這些加在一塊兒構成了一個高度可用的集羣範圍的設施,用於在Pod之間代理請求,同時容許Pod自己隨着集羣需求的變化而來來每每。可是,該系統並不是沒有缺點。最基本的是,它僅適用於針對羣集內部發出的請求(即從一個Pod到另外一個Pod的請求)進行描述。另外一個緣由是netfilter規則工做方式的結果:對於來自羣集外部的請求,規則會混淆原始IP。這引發了一些辯論,解決方案正在積極考慮中。當咱們在本系列的最後一篇文章中討論Ingress時,我將更仔細地研究這兩個問題。