【解構雲原生】初識Kubernetes Service
編者按:雲原生是網易杭州研究院(網易杭研)奉行的核心技術方向之一,開源容器平臺Kubernetes作為雲原生產業技術標準、雲原生生態基石,在設計上不可避免有其複雜性,Kubernetes系列文章基於網易杭研資深工程師總結,多角度多層次介紹Kubernetes的原理及運用,如何解決生產中的實際需求及規避風險,希望與讀者深入交流共同進步。
本文由作者授權釋出,未經許可,請勿轉載。
作者:李嵐清,網易杭州研究院雲端計算技術中心資深工程師
為什麼引入service
眾所周知,pod的生命週期是不穩定的,可能會朝生夕死,這也就意味著pod的ip是不固定的。
比如我們使用三副本的deployment部署了nginx服務,每個pod都會被分配一個ip,由於pod的生命週期不穩定,pod可能會被刪除重建,而重建的話pod的ip地址就會改變。也有一種場景,我們可能會對nginx deployment進行擴縮容,從3副本擴容為5副本或者縮容為2副本。當我們需要訪問上述的nginx服務時,客戶端對於nginx服務的ip地址就很難配置和管理。
因此,kubernetes社群就抽象出了service
這個資源物件或者說邏輯概念。
什麼是service
service是kubernetes中最核心的資源物件之一,kubernetes中的每個service其實就是我們經常提到的“微服務”。
service定義了一個服務的入口地址,它通過label selector 關聯後端的pod。service會被自動分配一個ClusterIP,service的生命週期是穩定的,它的ClusterIP也不會發生改變,使用者通過訪問service的ClusterIP來訪問後端的pod。所以,不管後端pod如何擴縮容、如何刪除重建,客戶端都不需要關心。
(1)建立一個三副本的nginx deployment:
nginx.yaml
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - image: nginx imagePullPolicy: Always name: nginx # kubectl create -f nginx.yaml deployment.extensions/nginx created # kubectl get pods -o wide nginx-5c7588df-5dmmp 1/1 Running 0 57s 10.120.49.230 pubt2-k8s-for-iaas4.dg.163.org <none> <none> nginx-5c7588df-gb2d8 1/1 Running 0 57s 10.120.49.152 pubt2-k8s-for-iaas4.dg.163.org <none> <none> nginx-5c7588df-gdngk 1/1 Running 0 57s 10.120.49.23 pubt2-k8s-for-iaas4.dg.163.org <none> <none>
(2)建立service,通過label selector關聯nginx pod:
svc.yaml
apiVersion: v1 kind: Service metadata: name: nginx spec: type: ClusterIP selector: app: nginx ports: - port: 80 protocol: TCP targetPort: 80 # kubectl create -f svc.yaml service/nginx created # kubectl get svc nginx -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR nginx ClusterIP 10.178.4.2 <none> 80/TCP 23s app=nginx
(3)在k8s節點上訪問service地址
# curl 10.178.4.2:80 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
實現原理
service中有幾個關鍵欄位:
spec.selector
: 通過該欄位關聯屬於該service的podspec.clusterIP
: k8s自動分配的虛擬ip地址spec.ports
: 定義了監聽埠和目的埠。使用者可以通過訪問clusterip:監聽埠
來訪問後端的pod
當用戶建立一個service時,kube-controller-manager會自動建立一個跟service同名的endpoints資源:
# kubectl get endpoints nginx
NAME ENDPOINTS AGE
nginx 10.120.49.152:80,10.120.49.23:80,10.120.49.230:80 12m
endpoints資源中,儲存了該service關聯的pod列表,這個列表是kube-controller-manager自動維護的,當發生pod的增刪時,這個列表會被自動重新整理。
比如,我們刪除了其中的一個pod:
# kubectl delete pods nginx-5c7588df-5dmmp pod "nginx-5c7588df-5dmmp" deleted # kubectl get pods nginx-5c7588df-ctcml 1/1 Running 0 6s nginx-5c7588df-gb2d8 1/1 Running 0 18m nginx-5c7588df-gdngk 1/1 Running 0 18m
可以看到kube-controller-manager立馬補充了一個新的pod。然後我們再看一下endpoints資源,後端pod列表也被自動更新了:
# kubectl get endpoints nginx NAME ENDPOINTS AGE nginx 10.120.49.152:80,10.120.49.23:80,10.120.49.73:80 16m
那麼,當用戶去訪問clusterip:port
時,流量是如何負載均衡到後端pod的呢?
k8s在每個node上運行了一個kube-proxy
元件,kube-proxy
會watch service和endpoints資源,通過配置iptables規則(現在也支援ipvs,不過不在本文章討論範圍之內)來實現service的負載均衡。
可以在任一個k8s node上看一下上述nginx service的iptables規則:
# iptables -t nat -L PREROUTING Chain PREROUTING (policy ACCEPT) target prot opt source destination KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */ # iptables -t nat -L KUBE-SERVICES Chain KUBE-SERVICES (2 references) target prot opt source destination KUBE-SVC-4N57TFCL4MD7ZTDA tcp -- anywhere 10.178.4.2 /* default/nginx: cluster IP */ tcp dpt:http # iptables -t nat -L KUBE-SVC-4N57TFCL4MD7ZTDA Chain KUBE-SVC-4N57TFCL4MD7ZTDA (1 references) target prot opt source destination KUBE-SEP-AHN4ALGUQHWJZNII all -- anywhere anywhere statistic mode random probability 0.33332999982 KUBE-SEP-BDD6UBFFJ4G2PJDO all -- anywhere anywhere statistic mode random probability 0.50000000000 KUBE-SEP-UR2OSKI3P5GEGC2Q all -- anywhere anywhere # iptables -t nat -L KUBE-SEP-AHN4ALGUQHWJZNII Chain KUBE-SEP-AHN4ALGUQHWJZNII (1 references) target prot opt source destination KUBE-MARK-MASQ all -- 10.120.49.152 anywhere DNAT tcp -- anywhere anywhere tcp to:10.120.49.152:80
當用戶訪問clusterip:port
時,iptables會通過iptables DNAT 均衡的負載均衡到後端pod。
service ClusterIP
service的ClusterIP是一個虛擬ip,它沒有附著在任何的網路裝置上,僅僅存在於iptables規則中,通過dnat實現訪問clusterIP時的負載均衡。
當用戶建立service時,k8s會自動從service網段中分配一個空閒ip設定到.spec.clusterIP
欄位。當然,k8s也支援使用者在建立svc時自己指定clusterIP。
service的網段是通過 kube-apiserver的命令列引數--service-cluster-ip-range
配置的,不允許變更。
service網段不能跟機房網路、docker網段、容器網段衝突,否則可能會導致網路不通。
service的clusterIP是k8s叢集內的虛擬ip,不同的k8s叢集可以使用相同的service網段,在k8s叢集外是訪問不通service的clusterIP的。
service域名
kubernetes是有自己的域名解析服務的。比如我們可以通過訪問域名nginx.default.svc.cluster.local
來訪問上述的nginx服務:
$ curl nginx.default.svc.cluster.local <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
域名格式為: ${ServiceName}.${Namespace}.svc.${ClusterDomain}
. 其中${ClusterDomain}的預設值是cluster.local
,可以通過kubelet的命令列引數----cluster-domain
進行配置。
headless service
當不需要service ip的時候,可以在建立service的時候指定spec.clusterIP: None
,這種service即是headless service。由於沒有分配service ip,kube-proxy也不會處理這種service。
DNS對這種service的解析:
- 當service裡定義selector的時候:Endpoints controller會建立相應的endpoints。DNS裡的A記錄會將svc地址解析為這些pods的地址
- 當service裡沒有定義selector:Endpoints controller不會建立endpoints。DNS會這樣處理:
- 首先CNAME到service裡定義的ExternalName
- 沒有定義ExternalName的話,會搜尋所有的和這個service共享name的Endpoints,然後將A記錄解析到這些Endpoints的地址
service的不同型別
service支援多種不同的型別,包括ClusterIP
、NodePort
、LoadBalancer
,通過欄位spec.type
進行配置。
ClusterIP service
預設型別。對於ClusterIP service, k8s會自動分配一個只在叢集內可達的虛擬的ClusterIP,在k8s叢集外無法訪問。
NodePort service
k8s除了會給NodePort service自動分配一個ClusterIP,還會自動分配一個nodeport埠。叢集外的客戶端可以訪問任一node的ip加nodeport,即可負載均衡到後端pod。
nodeport的埠範圍可以通過kube-apiserver的命令列引數--service-node-port-range
配置,預設值是30000-32767
,當前我們的配置是30000-34999
。
但是客戶端訪問哪個node ip也是需要考慮的一個問題,需要考慮高可用。而且NodePort會導致訪問後端服務時多了一跳,並且可能會做snat看不到源ip。
另外需要注意的是,service-node-port-range
不能夠跟幾個埠範圍衝突:
- Linux的
net.ipv4.ip_local_port_range
,可以配置為35000-60999
- ingress nginx中的四層負載均衡,埠必須小於30000
- 其他普通業務的埠也需要小於30000
LoadBalancer service
LoadBalancer service需要對接雲服務提供商的NLB服務。當用戶建立一個LoadBalancer型別的sevice時,cloud-controller-manager
會呼叫NLB的API自動建立LB例項,並且將service後端的pod掛到LB例項後端。
apiVersion: v1 kind: Service metadata: name: nginx spec: ports: - port: 80 protocol: TCP targetPort: 80 type: LoadBalancer $ kubectl get svc nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx LoadBalancer 10.178.8.216 10.194.73.147 80:32514/TCP 3s
service中會話保持
使用者可以通過配置spec.serviceAffinity=ClientIP
來實現基於客戶端ip的會話保持功能。 該欄位預設為None。
還可以通過適當設定 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds
來設定最大會話停留時間。 (預設值為 10800 秒,即 3 小時)
apiVersion: v1 kind: Service metadata: name: nginx spec: ports: - port: 80 protocol: TCP targetPort: 80 type: ClusterIP sessionAffinity: ClientIP
kubernetes
service
當我們部署好一個k8s叢集之後,發現系統自動幫忙在default
namespace下建立了一個name為kubernetes
的service:
# kubectl get svc kubernetes -o yaml apiVersion: v1 kind: Service metadata: labels: component: apiserver provider: kubernetes name: kubernetes namespace: default spec: clusterIP: 10.178.4.1 ports: - name: https port: 443 protocol: TCP targetPort: 6443 sessionAffinity: None type: ClusterIP status: loadBalancer: {}
可以看到kubernetes
svc的ip是--service-cluster-ip-range
的第一個ip,並且該service沒有設定spec.selector
。理論上來說,對於沒有設定selector的svc,kube-controller-manager不會自動建立同名的endpoints資源出來。
但是我們看到是有同名的endpoints存在的,並且多個apiserver的地址也被儲存在endpoints資源中:
# kubectl get ep kubernetes NAME ENDPOINTS AGE kubernetes 10.120.0.2:6443,10.120.0.3:6443 137d
具體是如何實現的,感興趣的可以看下原始碼k8s.io/kubernetes/pkg/master/reconcilers
Frequently Asked Questions
問題一 為什麼service clusterip無法ping通
因為service clusterip是一個k8s叢集內部的虛擬ip,沒有附著在任何網路裝置上,僅僅存在於iptables nat規則中,用來實現負載均衡。
問題二 為什麼service的網段不能跟docker網段、容器網段、機房網段衝突
假如service網段跟上述網段衝突,很容易導致容器或者在k8s node上訪問上述網段時發生網路不通的情況。
問題三 為什麼在k8s叢集外無法訪問service clusterip
service clusterip是k8s叢集內可達的虛擬ip,叢集外不可達。不同的k8s叢集可以使用相同的service網段。
或者說,叢集外的機器上沒有本k8s叢集的kube-proxy元件,沒有建立對應的iptables規則,因此叢集外訪問不通service clusterip。
問題四 能否擴容service網段
原則上這個網段是不允許更改的,但是假如因為前期規劃的問題分配的網段過小,實際可以通過比較hack的運維手段擴容service網段。
問題五 service是否支援七層的負載均衡
service僅支援四層的負載均衡,七層的負載均衡需要使用ingress
參考文件
- https://kubernetes.io/docs/concepts/services-networking/service/
作者簡介
李嵐清,網易杭州研究院雲端計算技術中心容器編排團隊資深系統開發工程師,具有多年Kubernetes開發、運維經驗,主導實現了容器網路管理、容器混部等生產級核心系統研發,推動網易集團內部電商、音樂、傳媒、教育等多個業務的容器