編者按:雲原生是網易杭州研究院(網易杭研)奉行的核心技術方向之一,開源容器平臺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
參考文檔
作者簡介
李嵐清,網易杭州研究院雲計算技術中心容器編排團隊資深系統開發工程師,具有多年Kubernetes開發、運維經驗,主導實現了容器網絡管理、容器混部等生產級核心系統研發,推動網易集團內部電商、音樂、傳媒、教育等多個業務的容器化。