做爲一個後端工程師,由於負責的大部分項目都是Web
服務這類的「無狀態應用」,在平時工做中接觸到的最經常使用的Kubernetes
控制器是Deployment
,可是Deployment
只適合於編排「無狀態應用」,它會假設一個應用的全部 Pod
是徹底同樣的,互相之間也沒有順序依賴,也無所謂運行在哪臺宿主機上。正由於每一個Pod
都同樣,在須要的時候能夠水平擴/縮,增長和刪除Pod
。html
可是並非全部應用都是無狀態的,尤爲是每一個實例之間有主從關係的應用和數據存儲類應用,針對這類應用使用Deployment
控制器沒法實現正確調度,因此Kubernetes
裏採用了另一個控制器StatefulSet
負責調度有狀態應用的Pod
,保持應用的當前狀態始終等於應用定義的所需狀態。node
和Deployment
同樣StatefulSet
也是一種能夠幫助你部署和擴展Kubernetes
Pod
的控制器,使用Deployment
時多數時候你不會在乎Pod
的調度方式。但當你須要關心Pod
的部署順序、對應的持久化存儲或者要求Pod
擁有固定的網絡標識(即便重啓或者從新調度後也不會變)時,StatefulSet
控制器會幫助你,完成調度目標。mysql
每一個由StatefulSet
建立出來的Pod
都擁有一個序號(從0開始)和一個固定的網絡標識。你還能夠在YAML
定義中添加VolumeClaimTemplate來聲明Pod存儲使用的PVC
。當StatefulSet
部署Pod
時,會從編號0到最終編號逐一部署每一個Pod
,只有前一個Pod
部署完成並處於運行狀態後,下一個Pod
纔會開始部署。nginx
StatefulSet
,是在Deployment
的基礎上擴展出來的控制器,在1.9版本以後才加入Kubernetes
控制器家族,它把有狀態應用須要保持的狀態抽象分爲了兩種狀況:web
拓撲狀態。這種狀況意味着,應用的多個實例之間不是徹底對等的關係。這些應用實例,必須按照某些順序啓動,好比應用的主節點 A 要先於從節點 B 啓動。而若是你把 A 和 B 兩個 Pod 刪除掉,它們再次被建立出來時也必須嚴格按照這個順序才行。而且,新建立出來的 Pod,必須和原來 Pod 的網絡標識同樣,這樣原先的訪問者才能使用一樣的方法,訪問到這個新 Pod。sql
存儲狀態。這種狀況意味着,應用的多個實例分別綁定了不一樣的存儲數據。對於這些應用實例來講,Pod A 第一次讀取到的數據,和Pod A 被從新建立後再次讀取到的數據,應該是同一份 。這種狀況最典型的例子,就是一個數據庫應用的多個存儲實例。shell
因此,StatefulSet 的核心功能,就是經過某種方式記錄這些狀態,而後在 Pod 被從新建立時,可以爲新 Pod 恢復這些狀態。數據庫
想要維護應用的拓撲狀態,必須保證能用固定的網絡標識訪問到固定的Pod
實例,Kubernetes
是經過Headless Service
給每一個Endpoint(Pod)
添加固定網絡標識的,因此接下來咱們花些時間瞭解下Headless Service
。編程
在文章學練結合,快速掌握Kubernetes Service 寫過後端
Service
是在邏輯抽象層上定義了一組Pod
,爲他們提供一個統一的固定IP和訪問這組Pod
的負載均衡策略。對於
ClusterIP
模式的Service
來講,它的 A 記錄的格式是:serviceName.namespace.svc.cluster.local,當你訪問這條 A 記錄的時候,它解析到的就是該 Service 的 VIP 地址。
對於指定了 clusterIP=None 的 Headless Service來講,它的A記錄的格式跟上面同樣,可是訪問記錄後返回的是Pod的IP地址集合。Pod 也會被分配對應的 DNS A 記錄,格式爲:podName.serviceName.namesapce.svc.cluster.local
普通的Service
都有ClusterIP
,它其實就是一個虛擬IP
,會把請求轉發到該Service
所代理的某一個Pod
上。
仍是拿文章學練結合,快速掌握Kubernetes Service 裏用過的例子來分析,使用的Service
和Deployment
的定義以下:
apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
type: NodePort #建立NodePort類型Service時會先建立一個ClusterIp類型的Service
selector:
app: go-app
ports:
- name: http
protocol: TCP
nodePort: 30080
port: 80
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-go-app
spec:
replicas: 2
selector:
matchLabels:
app: go-app
template:
metadata:
labels:
app: go-app
spec:
containers:
- name: go-app-container
image: kevinyan001/kube-go-app
ports:
- containerPort: 3000
複製代碼
在Kubernetes裏建立好上述資源後,能夠進入其中一個Pod
查看Service的A記錄
➜ kubectl exec -it my-go-app-69d6844c5c-gkb6z -- /bin/sh
/app # nslookup app-service.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: app-service.default.svc.cluster.local
Address: 10.108.26.155
複製代碼
若是想讓DNS經過剛纔的Service
名直接解析出Pod
名對應的IP是不能夠的:
/app # nslookup my-go-app-69d6844c5c-gkb6z.app-service.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
** server can't find my-go-app-69d6844c5c-gkb6z.app-service.default.svc.cluster.local: NXDOMAIN
複製代碼
由於Service
有ClusterIp
,直接被DNS解析了,那怎麼才能讓DNS經過Service
解析Pod
的IP呢?因此就有了Headless Service
。
建立
Headless Service
跟建立普通Service
時惟一的不一樣就是在YAML
定義裏指定spec:clusterIP: None
,也就是不須要ClusterIP
的Service
。
下面我建立一個Headless Service
代理上面例子中的那兩個應用Pod
實例,它的YAML
定義以下
# headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: app-headless-svc
spec:
clusterIP: None # <-- Don't forget!!
selector:
app: go-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
複製代碼
建立Service的命令
➜ kubectl apply -f headless-service.yaml service/app-headless-svc created
Headless Service
建立完後,咱們再來看一下這個Service
在DNS裏對應的A記錄
仍是在剛纔進入的那個Pod裏,記住Service的DNS記錄的格式是 serviceName.namespace.svc.cluster.local
/app # nslookup app-headless-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: app-headless-svc.default.svc.cluster.local
Address: 10.1.0.38
Name: app-headless-svc.default.svc.cluster.local
Address: 10.1.0.39
複製代碼
DNS查詢會返回HeadlessService
代理的兩個Endpoint (Pod)
對應的IP,這樣客戶端就能經過Headless Service
拿到每一個EndPoint
的 IP,若是有須要能夠本身在客戶端作些負載均衡策略。Headless Service
還有一個重要用處(也是使用StatefulSet
時須要Headless Service
的真正緣由),它會爲代理的每個StatefulSet
建立出來的Endpoint
也就是Pod
添加DNS域名解析,這樣Pod
之間就能夠相互訪問。
劃重點:
- 這個分配給
Pod
的DNS域名就是Pod
的固定惟一網絡標識,即便發生重建和調度DNS域名也不會改變。- Deployment建立的
Pod
的名字是隨機的,因此HeadlessService
不會爲Deployment
建立出來的Pod單獨添加域名解析。
咱們把上面的例子稍做修改,新增一個StatefulSet
對象用它建立Pod
來驗證一下。
apiVersion: v1
kind: Service
metadata:
name: app-headless-svc
spec:
clusterIP: None # <-- Don't forget!!
selector:
app: stat-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
---
apiVersion: apps/v1
kind: StatefulSet # <-- claim stateful set
metadata:
name: stat-go-app
spec:
serviceName: app-headless-svc # <-- Set headless service name
replicas: 2
selector:
matchLabels:
app: stat-app
template:
metadata:
labels:
app: stat-app
spec:
containers:
- name: go-app-container
image: kevinyan001/kube-go-app
resources:
limits:
memory: "64Mi"
cpu: "50m"
ports:
- containerPort: 3000
複製代碼
這個YAML
文件,和咱們在前面用到的Deployment
的惟一區別,就是多了一個spec.serviceName
字段。
StatefulSet
給它所管理的全部 Pod
名字,進行了編號,編號規則是:StatefulSet名-序號。這些編號都是從 0 開始累加,與 StatefulSet 的每一個 Pod 實例一一對應,毫不重複。
➜ kubectl get pod
NAME READY STATUS RESTARTS AGE
stat-go-app-0 1/1 Running 0 9s
stat-go-app-1 0/1 ContainerCreating 0 1s
複製代碼
咱們能夠進入stat-go-app-0
這個Pod
查看一下這兩個Pod
的DNS記錄
提示: Headless Service給Pod添加的DNS的格式爲podName.serviceName.namesapce.svc.cluster.local
/app # nslookup stat-go-app-0.app-headless-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: stat-go-app-0.app-headless-svc.default.svc.cluster.local
Address: 10.1.0.46
/app # nslookup stat-go-app-1.app-headless-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: stat-go-app-1.app-headless-svc.default.svc.cluster.local
Address: 10.1.0.47
複製代碼
因而乎這樣就保證了Pod
之間可以相互通訊,若是要用StatefulSet
編排一個有主從關係的應用,就能夠經過DNS域名訪問的方式保證相互之間的通訊,即便出現Pod
從新調度它在內部的DNS域名也不會改變。
經過上面名字叫stat-go-app
的StatefulSet
控制器建立Pod
的過程咱們能發現,StatefulSet
它所管理的全部 Pod
,名稱的命名規則是:StatefulSet名-序號。序號都是從 0 開始累加,與 StatefulSet 的每一個 Pod 實例一一對應,毫不重複。
因此上面咱們經過kubectl get pod
命令看到了兩個名字分別爲stat-go-app-0
和stat-go-app-1
的Pod實例。
更重要的是,這些Pod
的建立,也是嚴格按照名稱的編號順序進行的。好比,在stat-go-app-0
進入到 Running 狀態、而且細分狀態(Conditions)成爲 Ready 以前,stat-go-app-1
會一直處於 Pending 等待狀態。
StatefulSet
會一直記錄着這個拓撲狀態,即便發生調諧,從新調度Pod
也是嚴格遵照這個順序,編號在前面的Pod
建立完成而且進入Ready運行狀態後,下一個編號的Pod
纔會開始建立。
理解了Headless Service
真正的用途後,關於Kubernetes
內部如何讓Pod
固定惟一網絡標識這個問題的答案就是:Headless Service
爲代理的每個StatefulSet
建立出來的Pod
添加DNS域名解析。因此在用StatefulSet
編排實例之間有主從關係這樣的有狀態應用時,Pod
相互之間就能以podName.serviceName.namesapce.svc.cluster.local 這個域名格式進行通訊,這樣就不用在擔憂Pod
被從新調度到其餘的節點上後IP的變化。
前面的文章Kubernetes Pod入門指南在介紹Pod
使用的數據卷的時候,我曾提到過,要在一個Pod
裏聲明 Volume
,只須要在Pod
定義里加上spec.volumes
字段便可。而後,你就能夠在這個字段裏定義一個具體類型的Volume
了,好比:hostPath
類型。
......
spec:
volumes:
- name: app-volume
hostPath:
# 在宿主機上的目錄位置
path: /data
containers:
- image: mysql:5.7
name: mysql
ports:
- containerPort: 3306
volumeMounts:
- mountPath: /usr/local/mysql/data
name: app-volume
......
複製代碼
可是這種聲明使用數據卷的方式,對於每一個Pod
實例都綁定了存儲數據的數據存儲類應用是不適用的。因爲hostPath
類型的Volume是基於宿主機目錄的,若是一旦Pod
發生從新調度,去了其餘節點,就沒有辦法在新節點上把Pod
的存儲數據恢復回來了。
既然在Pod
宿主機上的數據卷不適用,那麼只能讓Pod
去使用Kubernetes
的集羣存儲資源了。集羣持久數據卷資源的配置和使用是經過PV
和PVC
完成的,咱們先來了解一下這兩個概念。
持久卷(PersistentVolume,PV)是集羣中的一塊存儲,能夠由管理員事先供應,或者 使用存儲類(Storage Class)來動態供應。 持久卷是集羣資源,就像節點也是集羣資源同樣,它們擁有獨立於任何使用PV
的Pod
的生命週期。
做爲一個應用開發者,我可能對分佈式存儲項目(好比 Ceph、GFS、HDFS 等)一竅不通,天然不會編寫它們對應的 Volume 定義文件,這不只超越了開發者的知識儲備,還會有暴露公司基礎設施敏感信息(祕鑰、管理員密碼等信息)的風險。因此Kubernetes
後來又引入了持久卷申領(PersistentVolumeClaim,PVC)。
PVC
表達的是Pod
對存儲的請求。概念上與Pod
相似。 Pod
會耗用節點資源,而PVC
申領會耗用PV
資源。有了PVC
後,在須要使用持久卷的Pod
的定義裏只須要聲明使用這個PVC
便可,這爲使用者隱去了不少關於存儲的信息,舉個例子來講就是,我能夠在徹底不知道遠程存儲的空間名、服務器地址、AccessKey之類的信息時直接把遠程存儲掛載到Pod
的容器裏。好比像下面這樣:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: pv-pod
spec:
containers:
- name: pv-container
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: pv-storage
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim
複製代碼
能夠看到,在這個Pod
的Volumes
定義中,咱們只須要聲明它的類型是 persistentVolumeClaim
,而後指定 PVC
的名字,徹底不用關心持久卷自己的定義。
PVC
建立出來後須要和PV
完成綁定才能使用,不過對於使用者來講咱們能夠先不用關心這個細節。
能夠用編程領域的接口和實現的關係來理解
PVC
和PV
的關係。
關於StatefulSet、Pod、PVC和PV之間的關係能夠用下面這張圖表示
在StatefulSet
的定義裏咱們能夠額外添加了一個spec.volumeClaimTemplates
字段。它跟 Pod
模板(spec.template
字段)的做用相似。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.9.1
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
複製代碼
Note: StatefulSet和Deployment裏都有Pod模板,他是控制器建立Pod實例的依據,關於這部分知識能夠查看之前關於Deployment的文章詳細瞭解。
也就是說,凡是被這個StatefulSet
管理的Pod
,都會聲明一個對應的PVC
;而這個PVC
的定義,就來自於volumeClaimTemplates
這個模板字段。更重要的是,這個 PVC
的名字,會被分配一個與這個Pod
徹底一致的編號。
StatefulSet
建立的這些PVC
,都以**"PVC名-StatefulSet名-序號"**這個格式命名的。
對於上面這個StatefulSet
來講,它建立出來的Pod
和PVC
的名稱以下:
Pod: web-0, web-1
PVC: www-web-0, www-web-1
複製代碼
假如發生從新調度web-0
這個Pod
被從新建立調度到了其餘節點,在這個新的Pod
對象的定義裏,因爲volumeClaimTemplates
的存在,它聲明使用的PVC
的名字,仍是叫做:www-web-0。因此,在這個新的web-0
被建立出來以後,Kubernetes
會爲它查找綁定名叫www-web-0
的PVC
。因爲PVC
的生命週期是獨立於使用它的Pod
的,這樣新Pod
就接管了之前舊Pod
留下的數據。
StatefulSet
就像是一種特殊的Deployment
,它使用Kubernetes
裏的兩個標準功能:Headless Service
和 PVC
,實現了對的拓撲狀態和存儲狀態的維護。
StatefulSet
經過Headless Service
, 爲它管控的每一個Pod
建立了一個固定保持不變的DNS域名,來做爲Pod
在集羣內的網絡標識。加上爲Pod
進行編號並嚴格按照編號順序進行Pod
調度,這些機制保證了StatefulSet
對維護應用拓撲狀態的支持。
而藉由StatefulSet
定義文件中的volumeClaimTemplates
聲明Pod
使用的PVC
,它建立出來的PVC
會以名稱編號這些約定與它建立出來的Pod
進行綁定,藉由PVC
獨立於Pod
的生命週期和二者之間的綁定機制的幫助,StatefulSet
完成了應用存儲狀態的維護。
今天的文章就到這裏,後面會繼續分享學習Kuberntes的文章,力爭打造一個適合工程師的Kubernetes學習教程,喜歡的能夠在微信上關注公衆號「網管叨bi叨」,每週都會推送技術進階文章。