一、簡介
- 有狀態實例:新實例和舊實例需要有相同的名稱、網絡標識和狀態
- 無狀態實例:可隨時被替換
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)