天天看點

Kubernetes服務發現之Service詳解

<code>Kubernetes Pod</code> 是有生命周期的,它們可以被建立,也可以被銷毀,然後一旦被銷毀生命就永遠結束。通過<code>ReplicationController</code> 能夠動态地建立和銷毀Pod(列如,需要進行擴縮容,或者執行滾動更新);每個Pod都會擷取它自己的IP位址,即使這些IP位址不總是穩定可依賴的。這會導緻一個問題;在Kubernetes叢集中,如果一組Pod(稱為backend)為其他Pod(稱為frontend)提供服務,那麼哪些frontend該如何發現,并連接配接到這組Pod中的那些backend呢?

關于Service

Kubernetes <code>Service</code>定義了這樣一種抽象:一個Pod的邏輯分組,一種可以通路它們不同的政策--通常稱為微服務。這一組Pod能夠被Service通路到,通常是通過<code>Label Sekector</code>實作的;

舉個例子,考慮一個圖檔處理backend,它運作了3個副本。這些副本是可交換的 -- frontend不需要關心它們調用了哪個backend副本。然後組成這一組backend程式的Pod實際上可能會發生變化,frontend用戶端不應該也沒必要知道,而且也不需要跟蹤這一組backend的狀态。Service定義的抽象能夠解耦這種關聯。

對Kubernetes叢集中的應用,Kubernetes提供了簡單的Endpoints API,隻要service中的一組Pod發生變更,應用程式就會被更新。對非Kubernetes叢集中的應用,Kubernetes提供了基本VIP的網橋的方式通路Service,再由Service重定向到backend Pod。

一個Service在Kubernetes中是一個REST對象,和Pod類似。像所有的REST對象一樣,Service定義可以基于POST方式,請求apiserver建立新的執行個體。例如,假定有一組Pod,它們對外暴漏了9376端口,同時還被打上“app=MyApp”标簽。

上述配置将建立一個名稱為“my-service”的Service對象,它會将請求代理到使用TCP端口9376,并且具有标簽“app=MyApp”的pod上。這個Service将被指派一個IP位址(通常為“Cluster IP”)它會被服務的代理使用。該Service的selector将會持續評估,處理結果将被POST到一個名稱為"my-service"的Endpoints對象上。

需要注意的是,Servie能夠将一個接收端口映射到任意的targetPort。預設情況下,targetPort将被設定為與port字段相同的值。可能更有趣的是,targetPort可以是一個字元串,引用了backend Pod的一個端口的名稱;但是,實際指派給該端口名稱的端口号,在每個backend Pod中可能并不相同。對于部署和設計Service,這種方式會提供更大的靈活性。例如,可以在backend軟體下一個版本中,修改Pod暴露的端口,并不會中斷用戶端的調用。

Kubernetes service能夠支援TCP和UDP協定,預設TCP協定

Service抽象了該如何通路Kubernetes Pod,但也能抽象其他類型的backend,例如:

希望在生産環境中使用外部的資料庫叢集,但測試環境使用自己的資料庫。

希望服務指向另一個Namespace中或其他叢集中的服務。

正在将工作負載轉移到Kubernetes叢集,和運作在Kubernetes叢集之外的backend。

在任何這些場景中,都能夠定義沒有selector 的 Service :

由于這個Service沒有selector,就不會建立相關的Endpoints對象。可以手動将Service映射到指定的Endpoints:

注意:Endpoint IP位址不能loopback(127.0.0.0/8),link-local(169.254.0.0/16),或者link-local多點傳播(224.0.0.0/24)。

通路沒有selector的Service,與selector的Service的原理相同。請求被路由到使用者定義的Endpoint(該示例中為1.2.3.4:9376)

ExternalName service是Service的特别,它沒有selector,也沒有定義任何的端口和Endpoint。相反地,對于運作在叢集外部的服務,它通過傳回該外部服務的别名這種方式來提供服務。

當查詢主機 <code>my-service.prod.svc.CLUSTER</code>時,叢集的DNS服務将傳回一個值為<code>my.database.example.com</code>的CNAME記錄,通路這個服務的功能方式與其他的相同,唯一不同的是重定向發生的DNS層,而且不會進行代理或轉發。如果後續決定要将資料庫遷移到Kubetnetes中,可以啟動對應的Pod,增加合适的Selector或Endpoint,修改service的typt。

在Kubernetes叢集中,每個Node運作一個kube-proxy程序。kube-proxy負責為Service實作了一種VIP(虛拟IP)的形式,而不是ExternalName的形式。在Kubernetes v1.0版本,代理完全在userspace。在Kubernetes v1.1版本,新增了iptables代理,但并不是預設的運作模式。從Kubernetes v1.2起,預設就是iptables代理。

在Kubernetes v1.0版本,Service是“4層”(TCP/UDP over IP)概念。在Kubernetes v1.1版本,新增了 Ingress API(beta版),用來表示“7層”(HTTP)服務。

這種模式,kube-proxy會監視Kubernetes master對service對象和Endpoints對象的添加和移除。對每個Service,它會在本地Node上打開一個端口(随機選擇)。任何連接配接到“代理端口”的請求,都會被代理到Service的backend Pods中的某一個上面(如 Endpoints 所報告的一樣)。使用哪個backend Pod,是基于Service的SessionAffinity來确定的。最後,它安裝iptables規則,捕獲到達該Service的clusterIP(是虛拟IP)和Port的請求,并重定向到代理端口,代理端口再代理請求到backend Pod。

網絡傳回的結果是,任何到達Service的IP:Port的請求,都會被代理到一個合适的backend,不需要用戶端知道關于Kubernetes,service或pod的任何資訊。

預設的政策是,通過round-robin算法來選擇backend Pod。實作基于用戶端IP的會話親和性,可以通過設定<code>service.spec.sessionAffinity</code>的值為“ClientIP”(預設值為“None”);

這種模式,kube-proxy會監視Kubernetes master對象和Endpoinnts對象的添加和移除。對每個Service,它會安裝iptables規則,進而捕獲到達該Service的clusterIP(虛拟IP)和端口的請求,進而将請求重定向到Service的一組backend中某個上面。對于每個Endpoints對象,它也會安裝iptables規則,這個規則會選擇一個backend Pod。

預設的政策是,随機選擇一個backend。實作基于用戶端IP的會話親和性,可以将<code>service.spec.sessionAffinity</code>的值設定為“ClientIP”(預設值為“None”)

和userspace代理類似,網絡傳回的結果是,任何到達Service的IP:Port的請求,都會被代理到一個合适的backend,不需要用戶端知道關于Kubernetes,service或Pod的任何資訊。這應該比userspace代理更快,更可靠。然而,不想userspace代理,如果始出選擇的Pod沒有響應,iptables代理不能自動地重試另一個Pod,是以它需要依賴readiness probes;

https://jimmysong.io/kubernetes-handbook/images/services-iptables-overview.jpg

這種模式,kube-proxy會監視Kubernetes service對象和Endpoints,調用netlink接口以相應地建立ipvs規則并定期與Kubernetes service對象和Endpoints對象同步ipvs規則,以確定ipvs狀态與期望一緻。通路服務時,流量将被重定向到其中一個後端Pod。

與iptables類似,ipvs基于netfilter的hook功能,但使用哈希表作為底層資料結構并在核心空間中工作。這意味着ipvs可以更快地重定向流量,并且在同步代理規則時具有更好的性能。此外,ipvs為負載均衡算法提供了更多的選項,例如:

rr:輪詢排程

lc:最小連接配接數

dh:目标哈希

sh:源哈希

sed:最短期望延遲

nq: 不排隊排程

注意: ipvs模式假定在運作kube-proxy之前在節點上都已經安裝了IPVS核心子產品。當kube-proxy以ipvs代理模式啟動時,kube-proxy将驗證節點上是否安裝了IPVS子產品,如果未安裝,則kube-proxy将回退到iptables代理模式。

很多Service需要暴露多個端口。對于這種情況,Kubernetes 支援在Service對象中定義多個端口。當使用多個端口時,必須給出所有端口的名稱,這樣Endpoint就不會産生歧義,例如:

在Service建立的請求中,可以通過設定spec.cluster IP字段來指定自己的叢集IP位址。比如,希望替換一個已經存在的DNS條目,或者遺留系統已經配置了一個固定IP且很難重新配置。使用者選擇的IP位址必須合法,并且這個IP位址在service-cluster-ip-range CIDR 範圍内,這對 API Server 來說是通過一個辨別來指定的。 如果 IP 位址不合法,API Server 會傳回 HTTP 狀态碼 422,表示值不合法。

一個不時出現的問題是,為什麼我們都使用VIP的方式,而不使用标準的round-robin DNS,有如下幾個原因:

長久以來,DNS 庫都沒能認真對待 DNS TTL、緩存域名查詢結果

很多應用隻查詢一次 DNS 并緩存了結果

就算應用和庫能夠正确查詢解析,每個用戶端反複重解析造成的負載也是非常難以管理的

我們盡力阻止使用者做那些對他們沒有好處的事情,如果很多人都來問這個問題,我們可能會選擇實作它。

Kubernetes 支援2種基本的服務發現模式 —— 環境變量和 DNS。

當Pod運作在NOde上,kubelet會為每個活躍的Service添加一組環境變量。它同時支援Docker links相容變量,簡單的{SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT 變量,這裡 Service 的名稱需大寫,橫線被轉換成下劃線。

舉個例子,一個名稱為 "redis-master" 的 Service 暴露了 TCP 端口 6379,同時給它配置設定了 Cluster IP 位址 10.0.0.11,這個 Service 生成了如下環境變量:

這意味着需要有順序的要求 —— Pod 想要通路的任何 Service 必須在 Pod 自己之前被建立,否則這些環境變量就不會被指派。DNS 并沒有這個限制。

一個可選(盡管強烈推薦)叢集插件 是 DNS 伺服器。 DNS 伺服器監視着建立新 Service 的 Kubernetes API,進而為每一個 Service 建立一組 DNS 記錄。 如果整個叢集的 DNS 一直被啟用,那麼所有的 Pod 應該能夠自動對 Service 進行名稱解析。

例如,有一個名稱為 "my-service" 的 Service,它在 Kubernetes 叢集中名為 "my-ns" 的 Namespace 中,為 "my-service.my-ns" 建立了一條 DNS 記錄。 在名稱為 "my-ns" 的 Namespace 中的 Pod 應該能夠簡單地通過名稱查詢找到 "my-service"。 在另一個 Namespace 中的 Pod 必須限定名稱為 "my-service.my-ns"。 這些名稱查詢的結果是 Cluster IP。

Kubernetes 也支援對端口名稱的 DNS SRV(Service)記錄。 如果名稱為 "my-service.my-ns" 的 Service 有一個名為 "http" 的 TCP 端口,可以對 "_http._tcp.my-service.my-ns" 執行 DNS SRV 查詢,得到 "http" 的端口号。

Kubernetes DNS 伺服器是唯一的一種能夠通路 ExternalName 類型的 Service 的方式。 更多資訊可以檢視 DNS Pod 和 Service。

對一些應用(如 Frontend)的某些部分,可能希望通過外部(Kubernetes 叢集外部)IP 位址暴露 Service。

Kubernetes ServiceTypes 允許指定一個需要的類型的 Service,預設是 ClusterIP 類型。

Type 的取值以及行為如下:

ClusterIP:通過叢集的内部 IP 暴露服務,選擇該值,服務隻能夠在叢集内部可以通路,這也是預設的 ServiceType。

NodePort:通過每個 Node 上的 IP 和靜态端口(NodePort)暴露服務。NodePort 服務會路由到 ClusterIP 服務,這個 ClusterIP 服務會自動建立。通過請求 :,可以從叢集的外部通路一個 NodePort 服務。

LoadBalancer:使用雲提供商的負載均衡器,可以向外部暴露服務。外部的負載均衡器可以路由到 NodePort 服務和 ClusterIP 服務。

ExternalName:通過傳回 CNAME 和它的值,可以将服務映射到 externalName 字段的内容(例如, foo.bar.example.com)。 沒有任何類型代理被建立,這隻有 Kubernetes 1.7 或更高版本的 kube-dns 才支援。

如果設定 type 的值為 "NodePort",Kubernetes master 将從給定的配置範圍内(預設:30000-32767)配置設定端口,每個 Node 将從該端口(每個 Node 上的同一端口)代理到 Service。該端口将通過 Service 的 spec.ports[*].nodePort 字段被指定。

如果需要指定的端口号,可以配置 nodePort 的值,系統将配置設定這個端口,否則調用 API 将會失敗(比如,需要關心端口沖突的可能性)。

這可以讓開發人員自由地安裝他們自己的負載均衡器,并配置 Kubernetes 不能完全支援的環境參數,或者直接暴露一個或多個 Node 的 IP 位址。

需要注意的是,Service 将能夠通過 :spec.ports[].nodePort 和 spec.clusterIp:spec.ports[].port 而對外可見。

使用支援外部負載均衡器的雲提供商的服務,設定 type 的值為 "LoadBalancer",将為 Service 提供負載均衡器。 負載均衡器是異步建立的,關于被提供的負載均衡器的資訊将會通過 Service 的 status.loadBalancer 字段被釋出出去。

來自外部負載均衡器的流量将直接打到 backend Pod 上,不過實際它們是如何工作的,這要依賴于雲提供商。 在這些情況下,将根據使用者設定的 loadBalancerIP 來建立負載均衡器。 某些雲提供商允許設定 loadBalancerIP。如果沒有設定 loadBalancerIP,将會給負載均衡器指派一個臨時 IP。 如果設定了 loadBalancerIP,但雲提供商并不支援這種特性,那麼設定的 loadBalancerIP 值将會被忽略掉。