理論來源:https://blog.doorta.com/?p=57
回顧
在k8s集羣中運行最基礎的單元是Pod,其它資源都是圍繞着Pod應用而實現,如Service用於給Pod提供一個固定端點,並且爲動態Pod變動提供一個服務發現機構功能,運行在k8s之上服務指的就是Service,服務註冊、服務發現都是藉助於Service在k8s中運行的DNS服務來實現;
而DNS也是隻一個附件,其本身也以Pod狀態運行於k8s之上,其主要爲Pod提供基礎服務邏輯,我們稱之爲CoreDNS,Service註冊在CoreDNS之上,從而爲其它客戶端完成服務發現(通過DNS解析服務名稱時);
CoreDNS圍繞着基礎服務構建,其扮演着服務發現、服務註冊功能,Pod爲了提供更好的動態、自愈功能也包括了自動伸縮等功能則需要藉助於Pod控制器,而Pod控制則歸類爲一組控制器,它們分別也有多種不同的實現方式,分別應用於不同的邏輯,
比如Deployment,它真正構建在ReplicaSet之上,完成真正意義上所謂無狀態應用的各種運維管理工作,比如部署、變更、編輯等功能,
如果期望在每一個節點運行一個系統級應用,或者期望在集羣的部分節點上運行一個Pod副本,則需要DaemonSet來實現;
statefulset
有狀態應用副本集,有狀態應用如redis,mysql,etcd,zk
對於有狀態應用來說,應該使用的是StatefulSet控制器,引用程序一般有四種類型,它們分別是有狀態有存儲、無狀態無存儲、有狀態無存儲、有存儲無狀態,絕大多數的服務都是有狀態有存儲或無狀態無存儲的;
比如nginx,它就是無狀態無存儲,而對於用狀態有存儲,比如mysql來說,如果它必須用到存儲,我們就必須給每一個實例起一個獨有的標識,重建的Pod是隨機的,在sts下就不能隨機了,將來當集羣中的某一個mysql掛掉時,控制器重建的mysql也會屬於這個掛掉的mysql,並且同時會將原先掛掉的MYSQL的數據加載進來,進行一對一標識,不像ds一樣爲每一個重建的Pod名稱後面跟一個隨機字符串;
對於絕大部分有狀態服務來說都是有存儲的,如果Pod數據掛載放在本地,那麼就會隨着這個Pod生命週期的結束而結束,數據也就丟失了,爲了避免這種情況,我們應該給有狀態應用提供共享存儲的能力,因此每一個實例都得有自己的專用存儲,彼此之間是不能重疊的,而且每一個實例的名字應該也是固定的,所有實例應該各自使用各自的專有存儲,
假設滿足瞭如上條件,也無法滿足於其它問題,比如存儲集羣,對於集羣來講,集羣維護所謂的變更,假設這個變更是系統的擴縮容,對於mysql主從複製集羣來說,隨便加入的節點通常應該只能是一個從節點,這個從也應該位於其它從節點之後,而且不能與其它的節點同名,如果在一主多從的節點中,某一個節點掛了,那麼主與從的替換方式也是不同的,如果是從,我們加入一個節點將把配置爲主節點的從就行,但如果是主,需要將某一個從節點提升爲主節點,同時還得確保其它的從節點能夠正常連接主節點進行復制纔可以,還需要檢驗數據均衡,並且還得確保主節點獲取此前主節點的數據是正常的;
在比如Redis集羣的擴縮容問題,我們所有數據集都是分散在多個節點上的,那麼進行縮容就可能會產生各種各樣的問題,所以操作就會很複雜,而這個問題就算能解決,redis的縮容與mysql主從縮容邏輯也是不一樣的,對於有狀態應用來講,它所需的運維邏輯或操作步驟都不盡相同,所以縮容不是那麼簡單就能縮的,擴容也不是想擴就能擴的,如果是主從加個節點很簡單,但如果是集羣,它本來就是數據切分,或者是分片存儲的,那麼此時擴縮容就會很困難了;
因此,這些功能對其進行擴縮容時都需要考慮在內,而各種各樣的或存儲、或消息隊列等一類的服務,基本上沒有通用法則,因此沒有任何一個機制能夠通用語有狀態的應用,邏輯極其複雜,發佈變更處理擴縮容處理,基本上沒有一個統一的辦法來解決問題,所以我們的Kubernetes之上的StatefulSet能實現的是最多能幫你解決確保每一個實例的名字是固定的,可以爲每一個實例分配一個固定存儲,至於擴縮容,還是沒有解決,Kubernetes也解決不了;
所以如果想使用Kubernetes的StatefulSet,你得在很大程序上,自己對特定應用程序的集羣編寫一大堆的代碼來實現這個功能,也就是說,自己去寫一個清單,去定義StatefulSet的Pod模版,以實現擴縮容,比如加一個節點或者減一個節點,你得把自己的成熟的運維操作邏輯或者過程,封裝成程序,寫在配置清單當中,以便於擴容、縮容不會出現故障纔行;
但是,還是有很多人有可能會在使用StatefulSet來運行一個有狀態的應用,但是StatefulSet又無法完全做到有狀態的管理,所以有很多人,紛紛把自己寫的專有的StatefulSet的配置清單,比如MySQL做了一個項目,開源到GITHUB,供人下載,但是這個配置清單也是極爲複雜的,任何一個環節出現問題,都有可能會遇到致命災難,所以早期Kubernetes在應用的時候,任然只會把無狀態應用部署到Kubernetes上,把有狀態應用依然留到Kubernetes之外,無狀態應用對Kubernetes之外的有狀態訪問就通過所謂外部服務引入到集羣內部的方式訪問,但是這終究不是解決方案;
不同的分佈式系統它們的管理、邏輯和運維操作都是不盡相同的,因此沒有辦法有一種控制器把每一種應用都操作起來,即便有了statefulset在定義不同的分佈式系統時使用也是即其麻煩的,statefulset儘管在一定程序上能實現有狀態應用的管理橋,但仍需要自行把我們對某一個應用的運維管理過程寫成腳本寫成腳本注入到statefulset的應用文件中才能使用
Operator
所以Kubernetes就提出瞭解決方案,後來由一家叫做CoreOS的公司提出了一個解決方法,它提供了一個接口,能夠讓用戶自行的去開發一段代碼,這個代碼可以使用任何編程語言編寫,比如Python、Java,C代碼封裝,而對於應用程序的解決方案需要成熟的運維人員手動操作所有運維步驟,只這些只針對一種特定的應用;
比如Redis主從複製集羣,那麼它就需要對redis在必要時可以初始化集羣、在有可能必要的情況下進行動態擴展、動態縮容、以及銷燬,就是將這些操作功能,用一個非常成熟的方式將運維人員所需要的操作,用代碼封裝起來,並將這個封裝成一個應用程序;
而這個用戶定義的這個redis-controller就不叫controller,CoreOS爲了區別Kubernetes之上原來那個簡單的Controller給它取名叫做Operator,它需要適用於每一種不同的有狀態應用 ,開發了一個需要專門用到所有運維技能的封裝,而這個應用程序,只需要在互聯網上開源出來,人人都可以下載來,部署爲Pod控制器,就像ingress一樣,所以以後對這個集羣的管理就可以委託給Operator就行了;
而這個Operator也需要運行爲一個Pod,它也需要一個控制器來管理,所以我們可以使用Deployment控制它即可,它本身是無狀態的,可以被替換的,所以使用Deployment來控制着這個Pod,這個Pod裏面控制是另外一個Controller,控制着另外一組集羣,每一組集羣叫做一個實例,有了這樣的項目,那我們就能夠安全無虞的,將有狀態服務跑在Kubernetes之上了;
而云原生是2018年的關鍵詞之一,很多有項目的官方都開始自己去開發這麼一個Operator,比如redis官方就有redis的Operator,可以藉助這個Operator把它部署在Kubernetes之上用來管理redis,MySQL官方也就是Oracle,也專門開發了一款基於MySQL管理的Operator,zookeeper官方也有自己的Operator,所以在將來我們在Kubernetes之上去部署有狀態應用,使用的不是StatefulSet而是Operator;
目前來講這些Operator越來越多,Operator其實就是對StatefulSet進行的一些功能擴展,對我們用戶來講不應該使用StatefulSet而是Operator,但是Operator內部封裝的還是StatefulSet,所以還是需要了解StatefulSet的運行機制;
自從出現了Operator以後,我們就需要開發Operator才能夠更好的在Kubernetes之上去部署有狀態應用,而不是在StatefulSet上去開發它的配置清單了,如何能夠讓用戶更快的去開發Operator,那麼CoreOS這個組織就在對應的Kubernetes之上額外引入了一個開發接口,就是,Operator的SDK,用戶可以藉助於SDK開發出來Operator控制器,這也就意味着,第三方程序員再去開發雲原生應用不是直接針對於Kubernetes雲原生API,而是針對這個SDK,CoreOS是屬於RedHat旗下的產品,RedHat是IBM旗下的產品;
=所以對於Operator,只不過把StatefulSet的代碼,結合某一個特有應用程序的特有運行邏輯,做了二次封裝而已,Operator SDK只是讓封裝寫起來更容易的一個開發工具箱和API;
目前主流工具的Operator列表:https://github.com/operator-framework/awesome-operators
StatefulSets要求
特點
- 每一個節點穩定且需要有唯一的網絡標識符
- 穩定且持久的存儲設備;
- 要求有序、平滑的部署和擴展; 如redis主從負載集羣 (先主後從)
- 有序、平滑的終止和刪除; 如8個從節點 R1-R8開, 那就R8-R1關
- 有序的、自動的滾動更新;
穩定意味着 Pod 調度或重調度的整個過程是有持久性的。如果應用程序不需要任何穩定的標識符或有序的部署、刪除或伸縮,則應該使用由一組無狀態的副本控制器提供的工作負載來部署應用程序,比如 Deployment 或者 ReplicaSet 可能更適用於您的無狀態應用部署需要
限制
- 給定 Pod 的存儲必須由 PersistentVolume 驅動 基於所請求的
storage class
來提供,或者由管理員預先提供 - 刪除或者收縮 StatefulSet 並不會刪除它關聯的存儲卷。這樣做是爲了保證數據安全,它通常比自動清除 StatefulSet 所有相關的資源更有價值
- StatefulSet 當前需要 headless 服務 來負責 Pod 的網絡標識。您需要負責創建此服務。
- 當刪除 StatefulSets 時,StatefulSet 不提供任何終止 Pod 的保證。爲了實現 StatefulSet 中的 Pod 可以有序和優雅的終止,可以在刪除之前將 StatefulSet 縮放爲 0。
- 在默認 Pod 管理策略(
OrderedReady
) 時使用 滾動更新,可能進入需要 人工干預 才能修復的損壞狀態。
組件與特性
**三個組件:**headless service、Statefulset控制器、volumeClaimTemplate(存儲卷申請模板)
- 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域名,這個域名格式爲: pod_name.service_name.ns_name.svc.cluster.local
如 kafka-0.kafka.default.svc.cluster.local
比如一個3節點的Kafka的StatefulSet集羣,對應的Headless Service名字爲kafka, StatefulSet的名字爲kafka,則StatefulSet裏面的3個Pod的DNS名稱分別爲 kafka-0.kafka、kafka-1.kafka、kafka-2.kafka,這些DNS名稱可以直接在集羣的配置文件中固定下來
Pod管理策略
一般來說,在創建StatefulSet時Pod是串行構建的,編號從小到大依次構建,縮容也是如此,StatefulSet的Pod管理就有如下幾種策略,通過spec.podManagementPolicy定義
- OrderedReady Pod Management:ready之後才下一個
- Parallel Pod Management:可以同時創建
StatefulSets示例
初始化
- 搭建nfs
# 隨便找臺機器整個nfs做下測試
]# yum -y install nfs-utils
]# cat /etc/exports
/data 192.168.2.0/24(rw)
]# showmount -e
/data 192.168.2.0/24
-
創建pv
apiVersion: v1 kind: PersistentVolume metadata: name: sts-pv-1 # 每個名稱不同即可 labels: name: sts-pv release: qa spec: accessModes: - ReadWriteOnce # 單路訪問 capacity: storage: 1Gi nfs: server: 192.168.2.221 path: /data/nfs/v1 storageClassName: slow # 機械盤 slow volumeMode: Filesystem # 掛載卷的類型是文件系統 --- ... # 共創建5個
-
創建pvc
# 底層存儲 --> pv --> pvc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sts-pvc-1
labels:
name: sts-pvc
release: qa
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: slow # 類型
volumeMode: Filesystem
selector: # 標籤選擇器選擇
matchLabels:
name: sts-pv
release: qa
--- # 這裏也定義5個
創建sts
- 說明
Service 需要定義爲無頭服務
幾個必要的參數
sts.spec
replicas: 定義幾個副本
selector: 哪些pod副本可被管理
serviceName: 必須要關連到某一個無頭服務上, 必須在創建sts之前,只有基於這個無頭服務才能給每個pod分配一個唯一的持久的固定標識符
template: pod模板, 在此處關連某個存儲卷(pvc) 容器掛載
volumeClaimTemplates: pvc應該由這個來生成
- 創建 StatefulSet
]# cat sts_test.yaml
apiVersion: v1
kind: Service # 注意,必須在sts之前先創建 無頭service
metadata:
name: sts-server
labels:
app: sts-server
spec:
clusterIP: None
type: ClusterIP
ports:
- name: myapp
targetPort: 30080
port: 80
selector:
app: my-sts-nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: myapp
namespace: default
labels:
app: my-sts-nginx
annotations:
state/myapps: "v1"
spec:
replicas: 3
selector:
matchLabels:
app: my-sts-nginx
serviceName: sts-server
template:
metadata:
name: myapp
labels:
app: my-sts-nginx
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v1
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
volumeMounts: # 掛載點
- name: myapp-pv
mountPath: /usr/share/nginx/html
volumeClaimTemplates: # 讓sts自動創建pvc關連pv
- metadata:
name: myapp-pv
namespace: default
spec:
accessModes: ["ReadWriteOnce"] # 每次只能一個節點訪問pv
resources:
requests:
storage: 1Gi
- 應用之後查看狀態
]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
myapp-pv-myapp-0 Bound data-nfs5 1Gi RWO 3m51s
myapp-pv-myapp-1 Bound data-nfs2 1Gi RWO 3m35s
myapp-pv-myapp-2 Bound data-nfs1 1Gi RWO 3m33s
]# kubectl get pv
NAME CAPACITY 訪問模式 回收策略 STATUS CLAIM AGE
data-nfs1 1Gi RWO Retain Bound default/myapp-pv-myapp-2 4m6s
data-nfs2 1Gi RWO Retain Bound default/myapp-pv-myapp-1 4m6s
data-nfs3 1Gi RWO Retain Available 4m6s
data-nfs4 1Gi RWO Retain Available 4m5s
data-nfs5 1Gi RWO Retain Bound default/myapp-pv-myapp-0 4m5s
# 創建過程 穩定、唯一的網絡標識 往0後創建 pod_name_number
]# kubectl get pods -w 有序的從前往後一個一個創建
NAME READY STATUS RESTARTS AGE
myapp-0 1/1 Running 0 17s
myapp-1 0/1 ContainerCreating 0 1s
myapp-1 1/1 Running 0 2s
myapp-2 0/1 Pending 0 0s
myapp-2 0/1 Pending 0 0s
myapp-2 0/1 Pending 0 1s
myapp-2 0/1 ContainerCreating 0 1s
myapp-2 1/1 Running 0 3s
]# kubectl describe sts myapp
Name: myapp
Namespace: default
CreationTimestamp: Fri, 08 May 2020 15:14:07 +0800
Selector: app=my-sts-nginx
Labels: app=my-sts-nginx
Annotations: state/myapps: v1
Replicas: 4 desired | 4 total
Update Strategy: RollingUpdate
Partition: 0
Pods Status: 4 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=my-sts-nginx
Containers:
myapp:
Image: ikubernetes/myapp:v1
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts:
/usr/share/nginx/html from myapp-pv (rw)
Volumes: <none>
Volume Claims:
Name: myapp-pv
StorageClass:
Labels: <none>
Annotations: <none>
Capacity: 1Gi
Access Modes: [ReadWriteOnce]
Events: <none>
- 檢驗
]# kubectl exec -it myapp-0 -- /bin/sh
/ # wget -O - -q myapp-0/index.html
5 # 修改nfs中v5目錄下的目錄,訪問是即時生效的
- 擴容一個副本
]# kubectl scale --replicas=4 statefulset myapp
~]# kubectl get pod -w
NAME READY STATUS RESTARTS AGE
myapp-0 1/1 Running 0 29m
myapp-1 1/1 Running 0 29m
myapp-2 1/1 Running 0 29m
myapp-3 0/1 Pending 0 1s
myapp-3 0/1 ContainerCreating 0 1s
myapp-3 1/1 Running 0 3s
]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
myapp-pv-myapp-3 Bound data-nfs3 1Gi RWO 52s
]# kubectl get pv
NAME CAPACITY 訪問模式 回收策略 STATUS CLAIM AGE
data-nfs3 1Gi RWO Retain Bound default/myapp-pv-myapp-3 30m
sts更新策略
- 參數說明
~]# kubectl explain sts.spec.updateStrategy.rollingUpdate
updateStrategy: 自定義更新策略
rollingUpdate: 自定義更新策略
partition: 更新分區, 默認分區0, pod標識符
大於等於N的標籤更新,如5個pod, N爲5就只更新第5個,0就更新全部
type:
- 配置更新策略
# 與sts配置一樣
spec: # 添加更新策略
updateStrategy:
rollingUpdate:
partition: 3 # 大於3的,從第4個節點開始金絲雀方式更新
# 或者 直接用裁剪patch
~]# kubectl patch statefulsets myapp -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":3}}}}'
]# kubectl describe sts myapp
Name: myapp
Namespace: default
CreationTimestamp: Sat, 09 May 2020 08:42:23 +0800
Selector: app=my-sts-nginx
Labels: app=my-sts-nginx
Annotations: state/myapps: v1
Replicas: 5 desired | 5 total
Update Strategy: RollingUpdate
Partition: 3
- 滾動更新
]# kubectl set image sts/myapp myapp=ikubernetes/myapp:v2
statefulset.apps/myapp image updated
~]# kubectl get pods -w 更新策略 從大於第3個開始更新 ,從最後往前推
NAME READY STATUS RESTARTS AGE
myapp-0 1/1 Running 0 14m
myapp-1 1/1 Running 0 14m
myapp-2 1/1 Running 0 14m
myapp-3 1/1 Running 0 14m
myapp-4 1/1 Running 0 14m
myapp-4 1/1 Terminating 0 15m
myapp-4 0/1 Pending 0 0s
myapp-4 0/1 ContainerCreating 0 1s
myapp-4 1/1 Running 0 3s
myapp-3 0/1 Terminating 0 15m
myapp-3 0/1 Pending 0 0s
myapp-3 0/1 ContainerCreating 0 0s
myapp-3 1/1 Running 0 2s