Kubernetes 實戰——有狀態應用(StatefulSet)

LB477發表於2021-06-19

一、簡介

  • 有狀態例項:新例項和舊例項需要有相同的名稱、網路標識和狀態
  • 無狀態例項:可隨時被替換

1. ReplicaSet 和有狀態 Pod

ReplicaSet 通過 Pod 模板建立多個 Pod 副本,這些副本除了名字和 IP 地址不同,沒有其他差異。若 Pod 模板指定了 PVC,則其建立的所有 Pod 共享相同的 PVC 和 PV

叢集應用可能要求例項具有唯一的網路標識。可針對每個例項建立一個獨立的 Service 來提供穩定的網路地址(因為服務 IP 固定)。但 Pod 無法獲取該 IP,不能在別的 Pod 裡通過 IP 自行註冊

2. 瞭解 StatefulSet

  • 每一個例項不可替代,都擁有穩定的名字(從零開始的順序索引)和狀態(獨立的資料卷)
  • 有狀態的 Pod 有時需要通過其主機名來定位。因為彼此狀態不同,通常希望操作的是指定的那個
    • 一個 StatefulSet 常要求建立一個用來記錄每個 Pod 網路標記的 headless Service。通過該 Service,每個 Pod 將擁有獨立的 DNS 記錄,這樣叢集中的 Pod 或客戶端可以通過主機名來定位
    • 如一個 default 名稱空間,名為 foo 的服務,它的一個 Pod 名為 a-0,就可以通過a-0.foo.default.svc.cluster.local來定位該 Pod
    • 也可以通過 DNS 服務查詢域名foo.default.svc.cluster.local對應的所有 SRV 記錄,獲取一個 StatefulSet 所有 Pod 的資訊
  • 當 StatefulSet 管理的 Pod 消失後,會重啟一個標識完全一致的 Pod 替換(不一定在同一個節點)
  • 擴容用下一個索引值,縮容先刪除最高索引值,擴/縮容都是逐步進行的K8s 保證兩個擁有相同標記和繫結相同 PVC 的有狀態 Pod 不會同時執行
    • 若有不健康例項,則不允許做縮容操作(避免一次刪除兩個)
    • 縮容只刪除 Pod,保留建立的持久卷宣告(PVC 被刪除後,與之繫結的 PV 也會被回收或刪除),需要手動刪除。再擴容會重新掛載上

3. 專屬儲存

  • 有狀態的 Pod 儲存必須是持久的,且與 Pod 解耦。即 StatefulSet 的 Pod 需要關聯到不同的持久卷宣告,且與獨立的持久卷對應
  • 持久卷可以預先建立,也可以由持久卷的動態供應機制實時建立

卷宣告模板

StatefulSet 可以有一個或多個卷宣告模板,會在建立 Pod 前建立持久卷宣告,並繫結到 Pod 例項上

二、使用 StatefulSet

1. 建立

① 容器準備

docker.io/luksa/kubia-pet

  • POST 請求將 body 中的資料儲存到 /var/data/kubia.txt
  • GET 請求返回主機名和儲存的資料

② 手動建立儲存卷

apiVersion: v1
kind: List
items:
- apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv-a  # 持久卷名稱 pv-a、pv-b、pv-c
  spec:
    capacity:
      storage: 1Mi  # 持久卷大小
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle  # 卷被宣告釋放後,空間被回收再利用
    nfs:  # 卷使用 nfs 持久磁碟。見 https://www.cnblogs.com/lb477/p/14713883.html
      server: 192.168.11.210
      path: "/nfs/pv-a"
...

③ 建立控制 Service

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  clusterIP: None  # StatefulSet 的控制 Service 必須是 headless 模式
  selector:
    app: kubia
  ports:
  - name: http
    port: 80

④ 建立 StatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kubia
spec:
  selector:
    matchLabels:
      app: kubia
  serviceName: kubia
  replicas: 2
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia-pet
        ports:
        - name: http
          containerPort: 8080
        volumeMounts:
        - name: data
          mountPath: /var/data  # Pod 中的容器會把 pvc 資料卷嵌入指定目錄
  volumeClaimTemplates:  # 建立持久卷宣告的模板,會為每個 Pod 建立並關聯一個 pvc
  - metadata:
      name: data
    spec:
      resources:
        requests:
          storage: 1Mi
      accessModes:
      - ReadWriteOnce

⑤ 檢視建立結果

$ kubectl get pod -w
NAME      READY   STATUS              RESTARTS   AGE
kubia-0   0/1     ContainerCreating   0          35s
kubia-0   1/1     Running             0          53s
kubia-1   0/1     Pending             0          0s
kubia-1   0/1     ContainerCreating   0          3s
kubia-1   1/1     Running             0          20s
$ kubectl get pv
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                  STORAGECLASS   REASON   AGE
pv-a   1Mi        RWO            Recycle          Bound       default/data-kubia-0                           18m
pv-b   1Mi        RWO            Recycle          Bound       default/data-kubia-1                           18m
pv-c   1Mi        RWO            Recycle          Available                                                  18m
$ kubectl get pvc
NAME           STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-kubia-0   Bound    pv-a     1Mi        RWO                           2m3s
data-kubia-1   Bound    pv-b     1Mi        RWO                           70s

2. 測試

  • 直連 Pod 來訪問:藉助另一個 Pod,在其內部執行 curl 命令或使用埠轉發
  • 通過 API 伺服器與 Pod 通訊:API 伺服器可通過代理直接連線到指定 Pod:可通過訪問<apiServerHost>:<port>/api/v1/namespaces/default/pods/kubia-0/proxy/<path>請求 Pod,但 API 伺服器有安全保障,需要在每次請求中新增授權令牌。因此可使用 kubectl 代理和 API 伺服器代理與 Pod 通訊:
$ kubectl proxy
Starting to serve on 127.0.0.1:8001
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
You've hit kubia-0
Data stored on this pod: No data posted yet

測試

# 1. 應用的狀態獨立
$ curl -X POST -d "Hello kubia-0" localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
Data stored on pod kubia-0
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
You've hit kubia-0
Data stored on this pod: Hello kubia-0
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-1/proxy/
You've hit kubia-1
Data stored on this pod: No data posted yet

# 2. 重新啟動一個完全相同的 Pod(新的 Pod 可能被排程到其他節點)
$ kubectl delete pod kubia-0
pod "kubia-0" deleted
$ kubectl get pod
NAME      READY   STATUS              RESTARTS   AGE
kubia-0   0/1     ContainerCreating   0          1s
kubia-1   1/1     Running             0          106m
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
You've hit kubia-0
Data stored on this pod: Hello kubia-0

暴露 StatefulSet 的 Pod

# 一個常規的 ClusterIP Service,只能在叢集內部訪問
apiVersion: v1
kind: Service
metadata:
  name: kubia-public
spec:
  selector:
    app: kubia
  ports:
  - port: 80
    targetPort: 8080
$ curl localhost:8001/api/v1/namespaces/default/services/kubia-public/proxy/
You've hit kubia-1 / 0

3. 發現夥伴節點

SRV 記錄指向提供指定服務的伺服器的主機名和埠號

獲取 StatefulSet 裡的所有 Pod 資訊

# 執行一個名為 srvlookup 的一次性 Pod,關聯控制檯並在終止後立即刪除
$ kubectl run -it srvlookup --image=tutum/dnsutils --rm --restart=Never -- dig SRV kubia.default.svc.cluster.local
;; ANSWER SECTION:
kubia.default.svc.cluster.local. 30 IN	SRV	0 50 80 kubia-0.kubia.default.svc.cluster.local.
kubia.default.svc.cluster.local. 30 IN	SRV	0 50 80 kubia-1.kubia.default.svc.cluster.local.
;; ADDITIONAL SECTION:
kubia-0.kubia.default.svc.cluster.local. 30 IN A 10.244.0.15
kubia-1.kubia.default.svc.cluster.local. 30 IN A 10.244.0.16
...
# 返回的 SRV 記錄順序隨機

讓節點返回所有叢集節點的資料

4. 處理節點失效

可通過關閉節點的 eth0 網路介面模擬節點的網路斷開

  • 當一個節點失效,執行在該節點上的 Kubelet 服務就無法與 K8s API 伺服器通訊,即無法彙報節點及其 Pod 的狀態
  • StatefulSet 在明確知道一個 Pod 不再執行之前,不會建立一個替換的 Pod
  • 一段時間後,該節點狀態變為 NotReady,Pod 狀態變為 Unknown
    • 若節點恢復,彙報狀態後 Pod 會被重新標記為 Running
    • 若 Pod 的 Unknown 狀態持續幾分鐘(可配置)後,主節點就會將 Pod 從節點驅逐(刪除 Pod 資源)
      • 若此時 describe Pod,可看到其狀態為 Terminating,即已經被標記為刪除。但由於節點不能通訊,該 Pod 仍會一直執行
    • 可強制刪除:kubectl delete pod kubia-0 --force --grace-period 0(除非確定節點不再執行,否則不要強制刪除有狀態的 Pod)

相關文章