為了恰當地展示Statefulset的行為,將會創建一個小的集群數據存儲。沒有太多功能,就像石器時代的一個數據存儲。
10.3.1 創建應用和容器鏡像
你將使用書中一直使用的kubia應用作為你的基礎來擴展它,達到它的每個pod實例都能用來存儲和接收一個數據項。 下面列舉了你的數據存儲的關鍵代碼。
代碼清單10.1 一個簡單的有狀態應用:kubia-pet-image/app.js
const http = require('http');
const os = require('os');
const fs = require('fs');
const dataFile = "/var/data/kubia.txt";
function fileExists(file) {
try {
fs.statSync(file);
return true;
} catch (e) {
return false;
}
}
var handler = function(request, response) {
if (request.method == 'POST') {
var file = fs.createWriteStream(dataFile);
file.on('open', function (fd) {
request.pipe(file); #存儲到一個數據文件中
console.log("New data has been received and stored.");
response.writeHead(200);
response.end("Data stored on pod " + os.hostname() + "\n");
});
} else {
var data = fileExists(dataFile) ? fs.readFileSync(dataFile, 'utf8') : "No data posted yet"; #返回主機名和數據文件名稱
response.writeHead(200);
response.write("You've hit " + os.hostname() + "\n");
response.end("Data stored on this pod: " + data + "\n");
}
};
var www = http.createServer(handler);
www.listen(8080);
當應用接收到一個POST請求時,它把請求中的body數據內容寫入 /var/data/kubia.txt
文件中。而在收到GET請求時,它返回主機名和存儲數據(文件中的內容)。是不是很簡單呢?這是你的應用的第一版本。它還不是一個集群應用,但它足夠讓你可以開始工作。在本章的后面,你會來擴展這個應用。
用來構建這個容器鏡像的Dockerfile文件與之前的一樣,如下面的代碼清單所示。
代碼清單10.2 有狀態應用的Dockerfile:kubia-pet-image/Dockerfile
FROM node:7
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]
現在來構建容器鏡像,或者使用筆者上傳的鏡像:docker.io/luksa/kubia-pet
。
10.3.2 通過Statefulset部署應用
為了部署你的應用,需要創建兩個(或三個)不同類型的對象:
- 存儲你數據文件的持久卷(當集群不支持持久卷的動態供應時,需要手動創建)
- Statefulset必需的一個控制Service
- Statefulset本身
對于每一個pod實例,Statefulset都會創建一個綁定到一個持久卷上的持久卷聲明。如果你的集群支持動態供應,就不需要手動創建持久卷(可跳過下一節)。如果不支持的話,可以按照下一節所述創建它們。
創建持久化存儲卷
因為你會調度Statefulset創建三個副本,所以這里需要三個持久卷。如果你計劃調度創建更多副本,那么需要創建更多持久卷。
如果你使用Minikube,請參考本書代碼附件中的 Chapter06/persistentvolumes-hostpath.yaml
來部署持久卷。
如果你在使用谷歌的Kubernetes引擎,需要首先創建實際的GCE持久磁盤:
$ gcloud compute disks create --size=1GiB --zone=europe-west1-b pv-a
$ gcloud compute disks create --size=1GiB --zone=europe-west1-b pv-b
$ gcloud compute disks create --size=1GiB --zone=europe-west1-b pv-c
注意 保證創建的持久磁盤和運行的節點在同一區域。
然后通過 persistent-volumes-hostpath.yaml
文件創建需要的持久卷,如下面的代碼清單所示。
代碼清單10.3 三個持久卷:persistent-volumes-hostpath.yaml
kind: List
apiVersion: v1
items:
- apiVersion: v1
kind: PersistentVolume #持久卷的描述
metadata:
name: pv-a #持久卷的名稱
spec:
capacity:
storage: 1Mi #持久卷的大小
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /tmp/pv-a
- apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-b
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle #當卷聲明釋放后,空間會被回收利用
hostPath:
path: /tmp/pv-b
- apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-c
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /tmp/pv-c
注意 在上一節通過在同一YAML文件中添加三個橫杠(---)來區分定義多個資源,這里使用另外一種方法,定義一個List對象,然后把各個資源作為List對象的各個項目。上述兩種方法的效果是一樣的。
通過上訴文件創建了pv-a、pv-b和pv-c三個持久卷。
創建控制Service
如我們之前所述,在部署一個Statefulset之前,需要創建一個用于在有狀態的pod之間提供網絡標識的headless Service。下面的代碼顯示了Service的詳細信息。
代碼清單10.4 在Statefulset中使用的 kubia-service-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None #Statefulset的控制Service必須是headless模式
selector:
app: kubia #標簽選擇器
ports:
- name: http
port: 80
上面指定了clusterIP為None,這就標記了它是一個headless Service。它使得你的pod之間可以彼此發現(后續會用到這個功能)。創建完這個Service之后,就可以繼續往下創建實際的Statefulset了。
創建Statefulset詳單
最后可以創建Statefulset了,下面的代碼清單顯示了其詳細信息。
代碼清單10.5 Statefulset詳單:kubia-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kubia
spec:
serviceName: kubia
replicas: 2
selector:
matchLabels:
app: kubia # has to match .spec.template.metadata.labels
template:
metadata:
labels:
app: kubia #定義標簽
spec:
containers:
- name: kubia
image: luksa/kubia-pet
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: data
mountPath: /var/data #pvc數據卷嵌入指定目錄
volumeClaimTemplates:
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce
這個Statefulset詳單與之前創建的ReplicaSet和Deployment的詳單沒太多區別,這里使用的新組件是volumeClaimTemplates列表。其中僅僅定義了一個名為data的卷聲明,會依據這個模板為每個pod都創建一個持久卷聲明。如之前在第6章中介紹的,pod通過在其詳單中包含一個PersistentVolumeClaim卷來關聯一個聲明。但在上面的pod模板中并沒有這樣的卷,這是因為在Statefulset創建指定pod時,會自動將PersistentVolumeClaim卷添加到pod詳述中,然后將這個卷關聯到一個聲明上。
創建Statefulset
現在就要創建Statefulset了:
$ kubectl create -f kubia-statefulset.yaml
現在列出你的pod:
$ kubectl get po
有沒有發現不同之處?是否記得一個ReplicaSet會同時創建所有的pod實例?你的Statefulset配置去創建兩個副本,但是它僅僅創建了單個pod。
不要擔心,這里沒有出錯。第二個pod會在第一個pod運行并且處于就緒狀態后創建。Statefulset這樣的行為是因為:狀態明確的集群應用對同時有兩個集群成員啟動引起的競爭情況是非常敏感的。所以依次啟動每個成員是比較安全可靠的。特定的有狀態應用集群在兩個或多個集群成員同時啟動時引起的競態條件是非常敏感的,所以在每個成員完全啟動后再啟動剩下的會更加安全。
再次列出pod并查看pod的創建過程:
$ kubectl get poNAME READY STATUS RESTARTS AGEkubia-0 1/1 Running 0 2m11skubia-1 1/1 Running 0 81s
可以看到,第一個啟動的pod狀態是running,第二個pod已經創建并在啟動過程中。
檢查生成的有狀態pod
現在讓我們看一下第一個pod的詳細參數,看一下Statefulset如何從pod模板和持久卷聲明模板來構建pod,如下面的代碼清單所示。
代碼清單10.6 Statefulset創建的有狀態pod
$ k get po kubia-0 -o yamlmetadata: creationTimestamp: "2021-07-16T01:18:16Z" generateName: kubia- labels: app: kubia controller-revision-hash: kubia-c94bcb69b statefulset.kubernetes.io/pod-name: kubia-0 name: kubia-0 namespace: custom........................spec: containers: - image: luksa/kubia-pet imagePullPolicy: Always name: kubia ports: - containerPort: 8080 name: http protocol: TCP resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /var/data #存儲掛載點 name: data - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: kube-api-access-fb4qg readOnly: true ....................... volumes: - name: data persistentVolumeClaim: #Statefulset創建的數據卷 claimName: data-kubia-0 - name: kube-api-access-fb4qg #數據卷相關聲明 projected: defaultMode: 420 sources:
通過持久卷聲明模板來創建持久卷聲明和pod中使用的與持久卷聲明相關的數據卷。
檢查生成的持久卷聲明
現在列出生成的持久卷聲明來確定它們被創建了:
$ kubectl get pvc
生成的持久卷聲明的名稱由在volumeClaimTemplate字段中定義的名稱和每個pod的名稱組成。可以檢查聲明的YAML文件來確認它們符合模板的定義。
10.3.3 使用你的pod
現在你的數據存儲集群的節點都已經運行,可以開始使用它們了。因為之前創建的Service處于headless模式,所以不能通過它來訪問你的pod。需要直接連接每個單獨的pod來訪問(或者創建一個普通的Service,但是這樣還是不允許你訪問指定的pod)。
前面已經介紹過如何直接訪問pod:借助另一個pod,然后在里面運行curl命令或者使用端口轉發。這次來介紹另外一種方法,通過API服務器作為代理。
通過API服務器與pod通信
API服務器的一個很有用的功能就是通過代理直接連接到指定的pod。如果想請求當前的kubia-0 pod,可以通過如下URL:
<apiServerHost>:<port>/api/v1/namespaces/default/pods/kubia-0/proxy/<path>
因為API服務器是有安全保障的,所以通過API服務器發送請求到pod是煩瑣的(需要額外在每次請求中添加授權令牌)。幸運的是,在第8章中已經學習了如何使用kubectl proxy來與API服務器通信,而不必使用麻煩的授權和SSL證書。再次運行代理如下:
$ kubectl proxy
現在,因為要通過kubectl代理來與API服務器通信,將使用 localhost:8001
來代替實際的API服務器主機地址和端口。你將發送一個如下所示的請求到kubia-0 pod:
$ curl localhost:8001/api/v1/namespaces/custom/pods/kubia-0/proxy/
返回的消息表明你的請求被正確收到,并在kubia-0 pod的應用中被正確處理。
注意 如果你收到一個空的回應,請確保在URL的最后沒有忘記輸入/符號(或者用curl的-L選項來允許重定向)
因為你正在使用代理的方式,通過API服務器與pod通信,每個請求都會經過兩個代理(第一個是kubectl代理,第二個是把請求代理到pod的API服務器)。詳細的描述如圖10.10所示。
image
圖10.10 通過kubectl代理和API服務器代理來與一個pod通信
上面介紹的是發送一個GET請求到pod,也可以通過API服務器發送POST請求。發送POST請求使用的代理URL與發送GET請求一致。
當你的應用收到一個POST請求時,它把請求的主體內容保存到本地一個文件中。發送一個POST請求到kubia-0 pod的示例:
$ curl -X POST -d "Hey there! This greeting was submitted to kubia-0." localhost:8001/api/v1/namespaces/custom/pods/kubia-0/proxy/
你發送的數據現在已經保存到pod中,那讓我們檢查一下當你再次發送一個GET請求時,它是否返回存儲的數據:
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
挺好的,到目前為止都工作正常。現在讓我們看看集群其他節點(kubia-1 pod
):
$ curl localhost:8001/api/v1/namespaces/custom/pods/kubia-1/proxy/
與期望的一致,每個節點擁有獨自的狀態。那這些狀態是否是持久的呢?讓我們進一步驗證。
刪除一個有狀態pod來檢查重新調度的pod是否關聯了相同的存儲
你將會刪除kubia-0 pod,等待它被重新調度,然后就可以檢查它是否會返回與之前一致的數據:
$ kubectl delete po kubia-0
如果你列出當前pod,可以看到該pod正在終止運行:
$ kubectl get po
當它一旦成功終止,Statefulset會重新創建一個具有相同名稱的新的pod:
$ kubectl get po
請記住,新的pod可能會被調度到集群中的任何一個節點,并不一定保持與舊的pod所在的節點一致。舊的pod的全部標記(名稱、主機名和存儲)實際上都會轉移到新的pod上。如果你在使用Minikube,你將看不到這些,因為它僅僅運行在單個節點上,但是對于多個節點的集群來說,可以看到新的pod會被調度到與之前pod不一樣的節點上。
圖10.11 一個有狀態pod會被重新調度到新的節點,但會保留它的名稱、主機名和存儲
現在新的pod已經運行了,那讓我們檢查一下它是否擁有與之前的pod一樣的標記。pod的名稱是一樣的,那它的主機名和持久化數據呢?可以通過訪問pod來確認:
$ curl localhost:8001/api/v1/namespaces/custom/pods/kubia-0/proxy/
從pod返回的信息表明它的主機名和持久化數據與之前pod是完全一致的,所以可以確認Statefulset會使用一個完全一致的pod來替換被刪除的pod。
擴縮容Statefulset
縮容一個Statefulset,然后在完成后再擴容它,與刪除一個pod后讓Statefulset立馬重新創建它的表現是沒有區別的。需要記住的是,縮容一個Statefulset只會刪除對應的pod,留下卸載后的持久卷聲明。可以嘗試縮容一個Statefulset,來進行確認。
需要明確的關鍵點是,縮容/擴容都是逐步進行的,與Statefulset最初被創建時會創建各自的pod一樣。當縮容超過一個實例的時候,會首先刪除擁有最高索引值的pod。只有當這個pod被完全終止后,才會開始刪除擁有次高索引值的pod。
通過一個普通的非headless的Service暴露Statefulset的pod
在閱讀這一章的最后一部分之前,需要為你的pod添加一個適當的非headless Service,這是因為客戶端通常不會直接連接pod,而是通過一個服務。
你應該知道了如何創建Service,如果不知道的話,請看下面的代碼清單。
代碼清單10.7 一個用來訪問有狀態pod的常規Service:kubia-servicepublic.yaml
apiVersion: v1kind: Servicemetadata: name: kubia-publicspec: selector: app: kubia ports: - port: 80 targetPort: 8080
因為它不是外部暴露的Service(它是一個常規的ClusterIP Service,不是一個NodePort或LoadBalancer-type Service),只能在你的集群內部訪問它。那是否需要一個pod來訪問它呢?答案是不需要。
通過API服務器訪問集群內部的服務
不通過額外的pod來訪問集群內部的服務的話,與之前使用訪問單獨pod的方法一樣,可以使用API服務器提供的相同代理屬性來訪問。
代理請求到Service的URL路徑格式如下:
/api/v1/namespaces/<namespace>/services/<service name>/proxy/<path>
因此可以在本地機器上運行curl命令,通過kubectl代理來訪問服務(之前啟動過kubectl proxy,現在它應該還在運行著):
$ kubectl proxy --address='0.0.0.0' --port=8001 --accept-hosts='.*'
$ curl localhost:8001/api/v1/namespaces/custom/services/kubia-public/proxy/
客戶端(集群內部)同樣可以通過kubia-public服務來存儲或者讀取你的集群中的數據。當然,每個請求會隨機分配到一個集群節點上,所以每次都會隨機獲取一個節點上的數據。后面我們會改進它。