kubernetes StatefulSet控制器

劉新元發表於2022-03-09

想學習更多相關知識請看博主的個人部落格地址:https://blog.huli.com/

https://blog.huli.com/

在Kubernetes系統中,Pod的管理物件RC、Deployment、DaemonSet 和Job都面向無狀態的服務。但現實中有很多服務是有狀態的,特別是 一些複雜的中介軟體叢集,例如MySQL叢集、MongoDB叢集、Akka集 群、ZooKeeper叢集等,這些應用叢集有4個共同點。
(1)每個節點都有固定的身份ID,通過這個ID,叢集中的成員可 以相互發現並通訊。
(2)叢集的規模是比較固定的,叢集規模不能隨意變動。
(3)叢集中的每個節點都是有狀態的,通常會持久化資料到永久 儲存中。
(4)如果磁碟損壞,則叢集裡的某個節點無法正常執行,叢集功 能受損。

如果通過RC或Deployment控制Pod副本數量來實現上述有狀態的集 群,就會發現第1點是無法滿足的,因為Pod的名稱是隨機產生的,Pod 的IP地址也是在執行期才確定且可能有變動的,我們事先無法為每個 Pod都確定唯一不變的ID。另外,為了能夠在其他節點上恢復某個失敗 的節點,這種叢集中的Pod需要掛接某種共享儲存,為了解決這個問 題,Kubernetes從1.4版本開始引入了PetSet這個新的資源物件,並且在 1.5版本時更名為StatefulSet,StatefulSet從本質上來說,可以看作 Deployment/RC的一個特殊變種,它有如下特性。

  • StatefulSet裡的每個Pod都有穩定、唯一的網路標識,可以用來 發現叢集內的其他成員。假設StatefulSet的名稱為kafka,那麼第1個Pod 叫kafka-0,第2個叫kafka-1,以此類推。
  • StatefulSet控制的Pod副本的啟停順序是受控的,操作第n個Pod 時,前n-1個Pod已經是執行且準備好的狀態。
  • StatefulSet裡的Pod採用穩定的持久化儲存卷,通過PV或PVC來 實現,刪除Pod時預設不會刪除與StatefulSet相關的儲存卷(為了保證數 據的安全)。
  • StatefulSet除了要與PV卷捆綁使用以儲存Pod的狀態資料,還要與 Headless Service配合使用,即在每個StatefulSet定義中都要宣告它屬於 哪個Headless Service。Headless Service與普通Service的關鍵區別在於, 它沒有Cluster IP,如果解析Headless Service的DNS域名,則返回的是該 Service對應的全部Pod的Endpoint列表。StatefulSet在Headless Service的 基礎上又為StatefulSet控制的每個Pod例項都建立了一個DNS域名,這個 域名的格式為:
	$(podname).$(headless service name)

比如一個3節點的Kafka的StatefulSet叢集對應的Headless Service的名 稱為kafka,StatefulSet的名稱為kafka,則StatefulSet裡的3個Pod的DNS 名稱分別為kafka-0.kafka、kafka-1.kafka、kafka-3.kafka,這些DNS名稱 可以直接在叢集的配置檔案中固定下來

1 簡單示例

以一個簡單的 nginx 服務 web.yaml為例:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
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-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi
(base) [root@liuxinyuan-master yaml]$ kubectl create -f web.yaml
service "nginx" created
statefulset "web" created

# 檢視建立的 headless service 和 statefulset
(base) [root@liuxinyuan-master yaml]$kubectl get service nginx
NAME      CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx     None         <none>        80/TCP    1m
(base) [root@liuxinyuan-master yaml]$ kubectl get statefulset web
NAME      DESIRED   CURRENT   AGE
web       2         2         2m

# 根據 volumeClaimTemplates 自動建立 PVC(在 GCE 中會自動建立 kubernetes.io/gce-pd 型別的 volume)
(base) [root@liuxinyuan-master yaml]$kubectl get pvc
NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-d064a004-d8d4-11e6-b521-42010a800002   1Gi        RWO           16s
www-web-1   Bound     pvc-d06a3946-d8d4-11e6-b521-42010a800002   1Gi        RWO           16s

# 檢視建立的 Pod,他們都是有序的
(base) [root@liuxinyuan-master yaml]$ kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          5m
web-1     1/1       Running   0          4m

# 使用 nslookup 檢視這些 Pod 的 DNS
(base) [root@liuxinyuan-master yaml]$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh
/ # nslookup web-0.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.2.10
/ # nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.3.12
/ # nslookup web-0.nginx.default.svc.cluster.local
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx.default.svc.cluster.local
Address 1: 10.244.2.10

還可以進行其他的操作

# 擴容
(base) [root@liuxinyuan-master yaml]$ kubectl scale statefulset web --replicas=5

# 縮容
(base) [root@liuxinyuan-master yaml]$ kubectl patch statefulset web -p '{"spec":{"replicas":3}}'

# 映象更新(目前還不支援直接更新 image,需要 patch 來間接實現)
(base) [root@liuxinyuan-master yaml]$ kubectl patch statefulset web --type='json' -p='[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"gcr.io/google_containers/nginx-slim:0.7"}]'

# 刪除 StatefulSet 和 Headless Service
(base) [root@liuxinyuan-master yaml]$ kubectl delete statefulset web
(base) [root@liuxinyuan-master yaml]$ kubectl delete service nginx

# StatefulSet 刪除後 PVC 還會保留著,資料不再使用的話也需要刪除
(base) [root@liuxinyuan-master yaml]$ kubectl delete pvc www-web-0 www-web-1

2 更新 StatefulSet

v1.7 + 支援 StatefulSet 的自動更新,通過 spec.updateStrategy 設定更新策略。目前支援兩種策略

  • OnDelete:當 .spec.template 更新時,並不立即刪除舊的 Pod,而是等待使用者手動刪除這些舊 Pod 後自動建立新 Pod。這是預設的更新策略,相容 v1.6 版本的行為
  • RollingUpdate:當 .spec.template 更新時,自動刪除舊的 Pod 並建立新 Pod 替換。在更新時,這些 Pod 是按逆序的方式進行,依次刪除、建立並等待 Pod 變成 Ready 狀態才進行下一個 Pod 的更新。

2.1 Partitions

RollingUpdate 還支援 Partitions,通過 .spec.updateStrategy.rollingUpdate.partition 來設定。當 partition 設定後,只有序號大於或等於 partition 的 Pod 會在 .spec.template 更新的時候滾動更新,而其餘的 Pod 則保持不變(即便是刪除後也是用以前的版本重新建立)。

# 設定 partition 為 3
(base) [root@liuxinyuan-master yaml]$ kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":3}}}}'
statefulset "web" patched

# 更新 StatefulSet
(base) [root@liuxinyuan-master yaml]$ kubectl patch statefulset web --type='json' -p='[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"gcr.io/google_containers/nginx-slim:0.7"}]'
statefulset "web" patched

# 驗證更新
(base) [root@liuxinyuan-master yaml]$ kubectl delete po web-2
pod "web-2" deleted
(base) [root@liuxinyuan-master yaml]$ kubectl get po -lapp=nginx -w
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          4m
web-1     1/1       Running             0          4m
web-2     0/1       ContainerCreating   0          11s
web-2     1/1       Running             0          18s

3 Pod 管理策略

v1.7 + 可以通過 .spec.podManagementPolicy 設定 Pod 管理策略,支援兩種方式

  • OrderedReady:預設的策略,按照 Pod 的次序依次建立每個 Pod 並等待 Ready 之後才建立後面的 Pod
  • Parallel:並行建立或刪除 Pod(不等待前面的 Pod Ready 就開始建立所有的 Pod)

3.1 Parallel 示例

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  podManagementPolicy: "Parallel"
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi

可以看到,所有 Pod 是並行建立的

(base) [root@liuxinyuan-master yaml]$ kubectl create -f webp.yaml
service "nginx" created
statefulset "web" created

(base) [root@liuxinyuan-master yaml]$ kubectl get po -lapp=nginx -w
NAME      READY     STATUS              RESTARTS  AGE
web-0     0/1       Pending             0         0s
web-0     0/1       Pending             0         0s
web-1     0/1       Pending             0         0s
web-1     0/1       Pending             0         0s
web-0     0/1       ContainerCreating   0         0s
web-1     0/1       ContainerCreating   0         0s
web-0     1/1       Running             0         10s
web-1     1/1       Running             0         10s

4 部署zookeeper

另外一個更能說明 StatefulSet 強大功能的示例為 zookeeper.yaml。

---
apiVersion: v1
kind: Service
metadata:
  name: zk-headless
  labels:
    app: zk-headless
spec:
  ports:
  - port: 2888
    name: server
  - port: 3888
    name: leader-election
  clusterIP: None
  selector:
    app: zk
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: zk-config
data:
  ensemble: "zk-0;zk-1;zk-2"
  jvm.heap: "2G"
  tick: "2000"
  init: "10"
  sync: "5"
  client.cnxns: "60"
  snap.retain: "3"
  purge.interval: "1"
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zk-budget
spec:
  selector:
    matchLabels:
      app: zk
  minAvailable: 2
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: zk
spec:
  serviceName: zk-headless
  replicas: 3
  template:
    metadata:
      labels:
        app: zk
      annotations:
        pod.alpha.kubernetes.io/initialized: "true"
        scheduler.alpha.kubernetes.io/affinity: >
            {
              "podAntiAffinity": {
                "requiredDuringSchedulingRequiredDuringExecution": [{
                  "labelSelector": {
                    "matchExpressions": [{
                      "key": "app",
                      "operator": "In",
                      "values": ["zk-headless"]
                    }]
                  },
                  "topologyKey": "kubernetes.io/hostname"
                }]
              }
            }
    spec:
      containers:
      - name: k8szk
        imagePullPolicy: Always
        image: gcr.io/google_samples/k8szk:v1
        resources:
          requests:
            memory: "4Gi"
            cpu: "1"
        ports:
        - containerPort: 2181
          name: client
        - containerPort: 2888
          name: server
        - containerPort: 3888
          name: leader-election
        env:
        - name : ZK_ENSEMBLE
          valueFrom:
            configMapKeyRef:
              name: zk-config
              key: ensemble
        - name : ZK_HEAP_SIZE
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: jvm.heap
        - name : ZK_TICK_TIME
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: tick
        - name : ZK_INIT_LIMIT
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: init
        - name : ZK_SYNC_LIMIT
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: tick
        - name : ZK_MAX_CLIENT_CNXNS
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: client.cnxns
        - name: ZK_SNAP_RETAIN_COUNT
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: snap.retain
        - name: ZK_PURGE_INTERVAL
          valueFrom:
            configMapKeyRef:
                name: zk-config
                key: purge.interval
        - name: ZK_CLIENT_PORT
          value: "2181"
        - name: ZK_SERVER_PORT
          value: "2888"
        - name: ZK_ELECTION_PORT
          value: "3888"
        command:
        - sh
        - -c
        - zkGenConfig.sh && zkServer.sh start-foreground
        readinessProbe:
          exec:
            command:
            - "zkOk.sh"
          initialDelaySeconds: 15
          timeoutSeconds: 5
        livenessProbe:
          exec:
            command:
            - "zkOk.sh"
          initialDelaySeconds: 15
          timeoutSeconds: 5
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/zookeeper
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
  volumeClaimTemplates:
  - metadata:
      name: datadir
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 20Gi
(base) [root@liuxinyuan-master yaml]$ kubectl create -f zookeeper.yaml

詳細的使用說明見 zookeeper stateful application

StatefulSet 注意事項

  1. 推薦在 Kubernetes v1.9 或以後的版本中使用
  2. 所有 Pod 的 Volume 必須使用 PersistentVolume 或者是管理員事先建立好
  3. 為了保證資料安全,刪除 StatefulSet 時不會刪除 Volume
  4. StatefulSet 需要一個 Headless Service 來定義 DNS domain,需要在 StatefulSet 之前建立好

相關文章