Kubernetes之服務發現Service
一、service的概念
在Kubernetes中,Pod是有生命週期的,當Pod的生命週期結束之後,Pod會被重新分配IP。這樣就會導致一個問題:在Kubernetes叢集中,如果一組Pod(稱為backend)為其他Pod(稱為frontend)提供服務,那麼那些frontend該如何發現並連線到作為backend的Pod呢? Kubernetes中service是一組提供相同功能的Pods的抽象,併為他們提供一個同意的入口。藉助Service,應用可以方便的實現服務發現於負載均衡,並實現應用的零宕機升級。Service通過spec.selector來選取後端服務,一般配合ReplicationController或者Deployment來保證後端容器的正常執行。
- 只提供4層負載均衡能力,而沒有7層功能,但有時我們可能需要更多的匹配規則來轉發請求,這點上4層負載均衡是不支援的
- 使用NodePort型別的Service時,需要在叢集外部部署外部的負載均衡器
- 使用LoadBalancer型別的Service時,Kubernetes必須執行在特定的雲服務上
二、Service的型別
有以下四種類型:
-
ClusterIp:預設型別,自動分配一個僅Cluster內部可以訪問的虛擬IP
-
NodePort:在ClusterIP基礎上為Service在每臺機器上繫結一個埠,這樣就可以通過<NodeIP>: NodePort來訪問該服務
-
LoadBalancer:在NodePort的基礎上,藉助cloud provider建立一個外部負載均衡器,並將請求轉發到<NodeIP>: NodePort
-
ExternalName:把叢集外部的服務引入到叢集內部來,在叢集內部直接使用。沒有任何型別代理被建立,這隻有kubernetes 1.7或更高版本的kube-dns才支援
-
ClusterIP clusterIP主要在每個node節點使用iptables,將發向clusterIP對應埠的資料,轉發到kube-proxy中。然後kube-proxy自己內部實現有負載均衡的方法,並可以查詢到這個service下對應pod的地址和埠,進而把資料轉發給對應的pod的地址和埠
-
NodePort nodePort的原理在於在node上開了一個埠,將向該埠的流量匯入到kube-proxy,然後由kube-proxy進一步到給對應的pod
-
LoadBalancer loadBalancer和nodePort其實是同一種方式。區別在於loadBalancer比nodePort多了一步,就是可以呼叫cloud provider去建立LB來向節點導流。
1. ClusterIP
此模式會提供一個叢集內部的虛擬IP(與Pod不在同一網段),以供叢集內部的Pod之間通訊使用。 為了實現圖上的功能,主要需要以下幾個元件的協同工作:
- apiserver 使用者通過kubectl命令向apiserver傳送建立service的命令,apiserver接收到請求後將資料儲存到etcd中
- kube-proxy kubernetes的每個節點中都有一個叫做kube-porxy的程序,這個程序負責感知service,pod的變化,並將變化的資訊寫入本地的iptables規則中
- iptables 使用NAT等技術將virtualIP的流量轉至endpoint中
1.1. 釋出一個deployment,並指定replicas為3
建立myapp-deploy.yaml檔案
[[email protected] manifests]# vim myapp-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-deploy
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: myapp
release: stabel
template:
metadata:
labels:
app: myapp
release: stabel
env: test
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v2
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
儲存退出。 使用kubectl apply命令建立deployment
[[email protected] manifests]# kubectl apply -f myapp-deploy.yaml
deployment.apps/myapp-deploy created
檢視建立好的deployment和pod
[[email protected] manifests]# kubectl get deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
myapp-deploy 3 3 3 3 7s
[[email protected] manifests]# kubectl get pods
NAME READY STATUS RESTARTS AGE
myapp-deploy-6d9c8d8797-4k8hl 1/1 Running 0 10s
myapp-deploy-6d9c8d8797-bllxq 1/1 Running 0 10s
myapp-deploy-6d9c8d8797-bvhw2 1/1 Running 0 10s
這樣就建立好了副本數為3的deployment了。
1.2. 釋出一個service,並指定步驟1中的label
建立myapp-service.yaml檔案
[[email protected] manifests]# vim myapp-service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: default
spec:
type: ClusterIP
selector:
app: myapp
release: stabel
ports:
- name: http
port: 80
targetPort: 80
使用kubectl apply命令建立service
[[email protected] manifests]# kubectl apply -f myapp-service.yaml
service/myapp created
[[email protected] manifests]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 12d
myapp ClusterIP 10.100.68.105 <none> 80/TCP 4s
這樣就建立好ClusterIP型別的service了。此時myapp的Cluster-IP(也可以稱為VirtualIP)為10.100.68.105。 檢視myapp的endpoint資訊:
[[email protected] manifests]# kubectl get endpoints
NAME ENDPOINTS AGE
kubernetes 192.168.116.130:6443 12d
myapp 10.244.1.29:80,10.244.2.32:80,10.244.2.33:80 10m
然後檢視剛剛創建出來的pod的資訊:
[[email protected] manifests]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
myapp-deploy-6d9c8d8797-4k8hl 1/1 Running 0 33m 10.244.1.29 node1
myapp-deploy-6d9c8d8797-bllxq 1/1 Running 0 33m 10.244.2.33 node2
myapp-deploy-6d9c8d8797-bvhw2 1/1 Running 0 33m 10.244.2.32 node2
可以發現myapp對應的endpoint剛好是三個pod的地址
1.3. 檢視iptables,觀察其NAT表中的資訊(只截取了部分和這個servcie有關的資訊)
檢視iptables中NAT表的命令:iptables -t nat -nvL
[[email protected] manifests]# iptables -t nat -nvL
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
86 5488 KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
268 16156 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
....
....
Chain KUBE-SERVICES (2 references)
pkts bytes target prot opt in out source destination
...
...
0 0 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.100.68.105 /* default/myapp:http cluster IP */ tcp dpt:80
0 0 KUBE-SVC-JOCJVTCKLKOLHFVR tcp -- * * 0.0.0.0/0 10.100.68.105 /* default/myapp:http cluster IP */ tcp dpt:80
...
...
Chain KUBE-SVC-JOCJVTCKLKOLHFVR (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-AJTG34DMLXUWJC3E all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/myapp:http */ statistic mode random probability 0.33332999982
0 0 KUBE-SEP-S4C5QHO5KWOWFNCG all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/myapp:http */ statistic mode random probability 0.50000000000
0 0 KUBE-SEP-4H47DMTPBM73YXWU all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/myapp:http */
...
...
Chain KUBE-SEP-AJTG34DMLXUWJC3E (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 10.244.1.29 0.0.0.0/0 /* default/myapp:http */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/myapp:http */ tcp to:10.244.1.29:80
...
...
Chain KUBE-SEP-S4C5QHO5KWOWFNCG (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 10.244.2.32 0.0.0.0/0 /* default/myapp:http */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/myapp:http */ tcp to:10.244.2.32:80
...
...
Chain KUBE-SEP-4H47DMTPBM73YXWU (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 10.244.2.33 0.0.0.0/0 /* default/myapp:http */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/myapp:http */ tcp to:10.244.2.33:80
在PREROUTING鏈中會先匹配到KUBE-SERVICES這個Chain; 所有destination IP為10.100.68.105的包都轉到KUBE-SVC-JOCJVTCKLKOLHFVR這個Chain; 這裡能看到請求會平均執行KUBE-SEP-AJTG34DMLXUWJC3E,KUBE-SEP-S4C5QHO5KWOWFNCG,KUBE-SEP-4H47DMTPBM73YXWU這三個Chain; 最後我們看一下KUBE-SEP-AJTG34DMLXUWJC3E這條Chain:這條Chain使用了DNAT規則將流量轉發到10.244.1.29:80這個地址上,這樣從其他Pod發出來的流量通過本地的iptables規則將流量轉到myapp Service定義的Pod上。
2. NodePort
對於這種型別,Kubernetes將會在每個Node上開啟一個埠並且每個Node的埠都是一樣的,通過<NodeIP>:NodePort的方式Kubernetes叢集外部的程式可以訪問Service。 修改myapp-service.yaml,將Service的type改為NodePort型別。
[[email protected] manifests]# vim myapp-service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: default
spec:
type: **NodePort**
selector:
app: myapp
release: stabel
ports:
- name: http
port: 80
targetPort: 80
儲存退出。
[[email protected] manifests]# kubectl apply -f myapp-service.yaml
service/myapp configured
可以看到kubernetes已經隨機給我們分配了一個NodePort埠了。 觀察iptables中的變化,KUBE-NODEPORTS這個Chain中增加了如下內容
Chain KUBE-NODEPORTS (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/myapp:http */ tcp dpt:31693
0 0 KUBE-SVC-JOCJVTCKLKOLHFVR tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/myapp:http */ tcp dpt:31693
可以看到流量會轉到KUBE-SVC-JOCJVTCKLKOLHFVR這個Chain中處理一次,也就是ClusterIP中提到的通過負載均衡將流量平均分配到3個endpoint上。
3. LoadBalancer
這種型別Service需要藉助cloud provider能力建立LB來向節點導流。
4. ExternalName
這種型別的Service通過返回CNAME和它的值,可以將服務對映到externalName欄位的內容(例如:foo.bar.example. com)。 ExternalName Service是Service的特例,它沒有selector,也沒有定義任何的埠和Endpoint。相反的,對於執行在叢集外部的服務,它通過返回該外部服務的別名這種方式來提供服務。
kind: Service
apiVersion: v1
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com
當查詢主機my-service.prod.svc.cluster.local(SVC_NAME.NAMESPACE.svc.cluster.local)時,叢集的DNS服務將返回一個值my.database.example.com
的CNAME記錄。訪問這個服務的工作方式和其他的相同,唯一不同的是重定向發生在DNS層,而且不會進行代理或轉發。
三、Service的實現模式
在Kubernetes叢集中,每個Node執行一個kube-proxy程序。kube-proxy負責為Service實現一種VIP(虛擬IP)的形式,而不是ExternalName的形式。在Kubernetes v1.0版本中,代理完全在userspace。在Kubernetes v1.1版本中,新增了iptables代理,但並不是預設的執行模式。從Kubernetes v1.2起,預設就是iptables代理。在Kubernetes v1.8.0-beta.0中,添加了ipvs代理。在Kubernetes v1.0版本中,Service是“4層”(TCP/UDP over IP)概念。在Kubernetes v1.1版本中,新增了Ingress API(beta版),用來表示“7層”(HTTP)服務。 kube-proxy這個元件始終監視著apiserver中有關service的變動資訊,獲取任何一個與service資源相關的變動狀態,通過watch監視,一旦有servcie資源相關的變動和建立,kube-proxy都要轉換為當前節點上的能夠實現排程的規則(例如:iptables、ipvs)。
3.1. userspace代理模式
這種模式下,當Client Pod請求核心空間的service ip後,把請求轉到使用者空間kube-porxy監聽的埠,由kube-proxy處理後,再由kube-proxy將請求轉給核心空間的service ip,再由service ip根據請求轉給各節點中的service pod。 由此可見這個模式將流量反覆的從使用者空間進出核心空間,這樣帶來的效能損耗是非常大的。
3.2. iptables代理模式
這種模式下,Client Pod直接請求本地核心service ip,根據iptables規則直接將請求轉發到各pod上,因為使用iptable NAT來完成轉發,也存在不可忽視的效能損耗。另外,如果叢集中存在上萬的Service/Endpoint,那麼Node上的iptables rules將會非常龐大,效能還會再打折扣。
3.3. ipvs代理模式
這種模式下,Client Pod請求到達核心空間時,根據ipvs規則直接分發到各Pod上。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代理模式 如果某一個服務後端pod發生變化,標籤選擇器適應的pod又多一個,適應的資訊會立即反映到apiserver上,而kube-proxy一定可以watch到etcd中的資訊變化,而將它立即轉為ipvs或iptables規則中,這一切都是動態和實時的,刪除一個pod也是同樣的原理。如圖:
四、Headless Service
有時不需要或不想要負載均衡,以及單獨的Service IP。遇到這種情況,可以通過指定Cluster IP(spec.clusterIP)的值為“None”來建立Headless Service。 這個選項允許開發人員自由的尋找他們自己的方式,從而降低與Kubernetes系統的耦合性。應用仍然可以使用一種自注冊的模式和介面卡,對其他需要發現機制的系統能夠很容易的基於這個API來構建。 對這類Service並不會分配Cluster IP,kube-proxy不會處理它們,而且平臺也不會為它們進行負載均衡和路由。DNS如何實現自動配置,依賴於Service時候定義了selector。
(1)編寫headless service配置清單
[[email protected] mainfests]# cp myapp-svc.yaml myapp-svc-headless.yaml
[[email protected] mainfests]# vim myapp-svc-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-headless
namespace: default
spec:
selector:
app: myapp
release: canary
clusterIP: "None" #headless的clusterIP值為None
ports:
- port: 80
targetPort: 80
(2)建立headless service
[[email protected] mainfests]# kubectl apply -f myapp-svc-headless.yaml
service/myapp-headless created
[[email protected] mainfests]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 36d
myapp NodePort 10.101.245.119 <none> 80:30080/TCP 1h
myapp-headless ClusterIP None <none> 80/TCP 5s
redis ClusterIP 10.107.238.182 <none> 6379/TCP 2h
(3)使用coredns進行解析驗證
[[email protected] mainfests]# dig -t A myapp-headless.default.svc.cluster.local. @10.96.0.10
; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> -t A myapp-headless.default.svc.cluster.local. @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62028
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;myapp-headless.default.svc.cluster.local. IN A
;; ANSWER SECTION:
myapp-headless.default.svc.cluster.local. 5 IN A 10.244.1.18
myapp-headless.default.svc.cluster.local. 5 IN A 10.244.1.19
myapp-headless.default.svc.cluster.local. 5 IN A 10.244.2.15
myapp-headless.default.svc.cluster.local. 5 IN A 10.244.2.16
myapp-headless.default.svc.cluster.local. 5 IN A 10.244.2.17
;; Query time: 4 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Thu Sep 27 04:27:15 EDT 2018
;; MSG SIZE rcvd: 349
[[email protected] mainfests]# kubectl get svc -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 36d
[[email protected] mainfests]# kubectl get pods -o wide -l app=myapp
NAME READY STATUS RESTARTS AGE IP NODE
myapp-deploy-69b47bc96d-4hxxw 1/1 Running 0 1h 10.244.1.18 k8s-node01
myapp-deploy-69b47bc96d-95bc4 1/1 Running 0 1h 10.244.2.16 k8s-node02
myapp-deploy-69b47bc96d-hwbzt 1/1 Running 0 1h 10.244.1.19 k8s-node01
myapp-deploy-69b47bc96d-pjv74 1/1 Running 0 1h 10.244.2.15 k8s-node02
myapp-deploy-69b47bc96d-rf7bs 1/1 Running 0 1h 10.244.2.17 k8s-node02
(4)對比含有ClusterIP的service解析
[[email protected] mainfests]# dig -t A myapp.default.svc.cluster.local. @10.96.0.10
; <<>> DiG 9.9.4-RedHat-9.9.4-61.el7 <<>> -t A myapp.default.svc.cluster.local. @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50445
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;myapp.default.svc.cluster.local. IN A
;; ANSWER SECTION:
myapp.default.svc.cluster.local. 5 IN A 10.101.245.119
;; Query time: 1 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Thu Sep 27 04:31:16 EDT 2018
;; MSG SIZE rcvd: 107
[[email protected] mainfests]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 36d
myapp NodePort 10.101.245.119 <none> 80:30080/TCP 1h
myapp-headless ClusterIP None <none> 80/TCP 11m
redis ClusterIP 10.107.238.182 <none> 6379/TCP 2h
從以上的演示可以看到對比普通的service和headless service,headless service做dns解析是直接解析到pod的,而servcie是解析到ClusterIP的,那麼headless有什麼用呢?這將在statefulset中應用到,這裡暫時僅僅做了解什麼是headless service和建立方法。