深刻理解StatefulSet,用Kubernetes編排有狀態應用

前言

做爲一個後端工程師,由於負責的大部分項目都是Web服務這類的「無狀態應用」,在平時工做中接觸到的最經常使用的Kubernetes控制器是Deployment,可是Deployment只適合於編排「無狀態應用」,它會假設一個應用的全部 Pod是徹底同樣的,互相之間也沒有順序依賴,也無所謂運行在哪臺宿主機上。正由於每一個Pod都同樣,在須要的時候能夠水平擴/縮,增長和刪除Podhtml

可是並非全部應用都是無狀態的,尤爲是每一個實例之間有主從關係的應用和數據存儲類應用,針對這類應用使用Deployment控制器沒法實現正確調度,因此Kubernetes裏採用了另一個控制器StatefulSet負責調度有狀態應用的Pod,保持應用的當前狀態始終等於應用定義的所需狀態。node

什麼是StatefulSet

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編程

HeadlessService

在文章學練結合,快速掌握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 裏用過的例子來分析,使用的ServiceDeployment的定義以下:

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
複製代碼

由於ServiceClusterIp,直接被DNS解析了,那怎麼才能讓DNS經過Service解析Pod的IP呢?因此就有了Headless Service

建立Headless Service跟建立普通Service時惟一的不一樣就是在YAML定義裏指定spec:clusterIP: None,也就是不須要ClusterIPService

下面我建立一個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之間就能夠相互訪問。

劃重點:

  1. 這個分配給Pod的DNS域名就是Pod的固定惟一網絡標識,即便發生重建和調度DNS域名也不會改變。
  2. 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域名也不會改變。

保持Pod的編排順序

經過上面名字叫stat-go-appStatefulSet控制器建立Pod的過程咱們能發現,StatefulSet它所管理的全部 Pod ,名稱的命名規則是:StatefulSet名-序號。序號都是從 0 開始累加,與 StatefulSet 的每一個 Pod 實例一一對應,毫不重複

因此上面咱們經過kubectl get pod 命令看到了兩個名字分別爲stat-go-app-0stat-go-app-1的Pod實例。

更重要的是,這些Pod的建立,也是嚴格按照名稱的編號順序進行的。好比,在stat-go-app-0進入到 Running 狀態、而且細分狀態(Conditions)成爲 Ready 以前,stat-go-app-1會一直處於 Pending 等待狀態。

StatefulSet會一直記錄着這個拓撲狀態,即便發生調諧,從新調度Pod也是嚴格遵照這個順序,編號在前面的Pod建立完成而且進入Ready運行狀態後,下一個編號的Pod纔會開始建立。

保持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的集羣存儲資源了。集羣持久數據卷資源的配置和使用是經過PVPVC完成的,咱們先來了解一下這兩個概念。

PV和PVC

持久卷(PersistentVolume,PV)是集羣中的一塊存儲,能夠由管理員事先供應,或者 使用存儲類(Storage Class)來動態供應。 持久卷是集羣資源,就像節點也是集羣資源同樣,它們擁有獨立於任何使用PVPod的生命週期

做爲一個應用開發者,我可能對分佈式存儲項目(好比 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
複製代碼

能夠看到,在這個PodVolumes 定義中,咱們只須要聲明它的類型是 persistentVolumeClaim,而後指定 PVC 的名字,徹底不用關心持久卷自己的定義。

PVC建立出來後須要和PV完成綁定才能使用,不過對於使用者來講咱們能夠先不用關心這個細節。

能夠用編程領域的接口實現的關係來理解PVCPV的關係。

StatefulSet的PVC模板

關於StatefulSet、Pod、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來講,它建立出來的PodPVC的名稱以下:

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-0PVC因爲PVC的生命週期是獨立於使用它的Pod的,這樣新Pod就接管了之前舊Pod留下的數據

總結

StatefulSet就像是一種特殊的Deployment,它使用Kubernetes裏的兩個標準功能:Headless ServicePVC,實現了對的拓撲狀態和存儲狀態的維護。

StatefulSet經過Headless Service , 爲它管控的每一個Pod建立了一個固定保持不變的DNS域名,來做爲Pod在集羣內的網絡標識。加上爲Pod進行編號並嚴格按照編號順序進行Pod調度,這些機制保證了StatefulSet對維護應用拓撲狀態的支持。

而藉由StatefulSet定義文件中的volumeClaimTemplates聲明Pod使用的PVC,它建立出來的PVC會以名稱編號這些約定與它建立出來的Pod進行綁定,藉由PVC獨立於Pod的生命週期和二者之間的綁定機制的幫助,StatefulSet完成了應用存儲狀態的維護。

今天的文章就到這裏,後面會繼續分享學習Kuberntes的文章,力爭打造一個適合工程師的Kubernetes學習教程,喜歡的能夠在微信上關注公衆號「網管叨bi叨」,每週都會推送技術進階文章。

相關文章
相關標籤/搜索