為了恰當(dāng)?shù)卣故維tatefulset的行為诈茧,將會創(chuàng)建一個小的集群數(shù)據(jù)存儲叠荠。沒有太多功能墓拜,就像石器時代的一個數(shù)據(jù)存儲潮售。
10.3.1 創(chuàng)建應(yīng)用和容器鏡像
你將使用書中一直使用的kubia應(yīng)用作為你的基礎(chǔ)來擴展它,達(dá)到它的每個pod實例都能用來存儲和接收一個數(shù)據(jù)項动壤。 下面列舉了你的數(shù)據(jù)存儲的關(guān)鍵代碼萝喘。
代碼清單10.1 一個簡單的有狀態(tài)應(yīng)用: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); #存儲到一個數(shù)據(jù)文件中
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"; #返回主機名和數(shù)據(jù)文件名稱
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);
當(dāng)應(yīng)用接收到一個POST請求時,它把請求中的body數(shù)據(jù)內(nèi)容寫入 /var/data/kubia.txt
文件中狼电。而在收到GET請求時蜒灰,它返回主機名和存儲數(shù)據(jù)(文件中的內(nèi)容)。是不是很簡單呢肩碟?這是你的應(yīng)用的第一版本强窖。它還不是一個集群應(yīng)用,但它足夠讓你可以開始工作削祈。在本章的后面翅溺,你會來擴展這個應(yīng)用脑漫。
用來構(gòu)建這個容器鏡像的Dockerfile文件與之前的一樣,如下面的代碼清單所示咙崎。
代碼清單10.2 有狀態(tài)應(yīng)用的Dockerfile:kubia-pet-image/Dockerfile
FROM node:7
ADD app.js /app.js
ENTRYPOINT ["node", "app.js"]
現(xiàn)在來構(gòu)建容器鏡像优幸,或者使用筆者上傳的鏡像:docker.io/luksa/kubia-pet
。
10.3.2 通過Statefulset部署應(yīng)用
為了部署你的應(yīng)用褪猛,需要創(chuàng)建兩個(或三個)不同類型的對象:
- 存儲你數(shù)據(jù)文件的持久卷(當(dāng)集群不支持持久卷的動態(tài)供應(yīng)時网杆,需要手動創(chuàng)建)
- Statefulset必需的一個控制Service
- Statefulset本身
對于每一個pod實例,Statefulset都會創(chuàng)建一個綁定到一個持久卷上的持久卷聲明伊滋。如果你的集群支持動態(tài)供應(yīng)碳却,就不需要手動創(chuàng)建持久卷(可跳過下一節(jié))。如果不支持的話笑旺,可以按照下一節(jié)所述創(chuàng)建它們昼浦。
創(chuàng)建持久化存儲卷
因為你會調(diào)度Statefulset創(chuàng)建三個副本,所以這里需要三個持久卷筒主。如果你計劃調(diào)度創(chuàng)建更多副本关噪,那么需要創(chuàng)建更多持久卷。
如果你使用Minikube乌妙,請參考本書代碼附件中的 Chapter06/persistentvolumes-hostpath.yaml
來部署持久卷使兔。
如果你在使用谷歌的Kubernetes引擎,需要首先創(chuàng)建實際的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
注意 保證創(chuàng)建的持久磁盤和運行的節(jié)點在同一區(qū)域藤韵。
然后通過 persistent-volumes-hostpath.yaml
文件創(chuàng)建需要的持久卷火诸,如下面的代碼清單所示。
代碼清單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 #當(dāng)卷聲明釋放后,空間會被回收利用
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
注意 在上一節(jié)通過在同一YAML文件中添加三個橫杠(---)來區(qū)分定義多個資源荠察,這里使用另外一種方法,定義一個List對象奈搜,然后把各個資源作為List對象的各個項目悉盆。上述兩種方法的效果是一樣的。
通過上訴文件創(chuàng)建了pv-a馋吗、pv-b和pv-c三個持久卷焕盟。
創(chuàng)建控制Service
如我們之前所述,在部署一個Statefulset之前宏粤,需要創(chuàng)建一個用于在有狀態(tài)的pod之間提供網(wǎng)絡(luò)標(biāo)識的headless Service脚翘。下面的代碼顯示了Service的詳細(xì)信息。
代碼清單10.4 在Statefulset中使用的 kubia-service-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None #Statefulset的控制Service必須是headless模式
selector:
app: kubia #標(biāo)簽選擇器
ports:
- name: http
port: 80
上面指定了clusterIP為None绍哎,這就標(biāo)記了它是一個headless Service来农。它使得你的pod之間可以彼此發(fā)現(xiàn)(后續(xù)會用到這個功能)。創(chuàng)建完這個Service之后崇堰,就可以繼續(xù)往下創(chuàng)建實際的Statefulset了沃于。
創(chuàng)建Statefulset詳單
最后可以創(chuàng)建Statefulset了涩咖,下面的代碼清單顯示了其詳細(xì)信息。
代碼清單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 #定義標(biāo)簽
spec:
containers:
- name: kubia
image: luksa/kubia-pet
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: data
mountPath: /var/data #pvc數(shù)據(jù)卷嵌入指定目錄
volumeClaimTemplates:
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce
這個Statefulset詳單與之前創(chuàng)建的ReplicaSet和Deployment的詳單沒太多區(qū)別繁莹,這里使用的新組件是volumeClaimTemplates列表檩互。其中僅僅定義了一個名為data的卷聲明,會依據(jù)這個模板為每個pod都創(chuàng)建一個持久卷聲明咨演。如之前在第6章中介紹的闸昨,pod通過在其詳單中包含一個PersistentVolumeClaim卷來關(guān)聯(lián)一個聲明。但在上面的pod模板中并沒有這樣的卷薄风,這是因為在Statefulset創(chuàng)建指定pod時饵较,會自動將PersistentVolumeClaim卷添加到pod詳述中,然后將這個卷關(guān)聯(lián)到一個聲明上村刨。
創(chuàng)建Statefulset
現(xiàn)在就要創(chuàng)建Statefulset了:
$ kubectl create -f kubia-statefulset.yaml
現(xiàn)在列出你的pod:
$ kubectl get po
有沒有發(fā)現(xiàn)不同之處告抄?是否記得一個ReplicaSet會同時創(chuàng)建所有的pod實例?你的Statefulset配置去創(chuàng)建兩個副本嵌牺,但是它僅僅創(chuàng)建了單個pod打洼。
不要擔(dān)心,這里沒有出錯逆粹。第二個pod會在第一個pod運行并且處于就緒狀態(tài)后創(chuàng)建募疮。Statefulset這樣的行為是因為:狀態(tài)明確的集群應(yīng)用對同時有兩個集群成員啟動引起的競爭情況是非常敏感的。所以依次啟動每個成員是比較安全可靠的僻弹。特定的有狀態(tài)應(yīng)用集群在兩個或多個集群成員同時啟動時引起的競態(tài)條件是非常敏感的阿浓,所以在每個成員完全啟動后再啟動剩下的會更加安全。
再次列出pod并查看pod的創(chuàng)建過程:
$ kubectl get poNAME READY STATUS RESTARTS AGEkubia-0 1/1 Running 0 2m11skubia-1 1/1 Running 0 81s
可以看到蹋绽,第一個啟動的pod狀態(tài)是running芭毙,第二個pod已經(jīng)創(chuàng)建并在啟動過程中。
檢查生成的有狀態(tài)pod
現(xiàn)在讓我們看一下第一個pod的詳細(xì)參數(shù)卸耘,看一下Statefulset如何從pod模板和持久卷聲明模板來構(gòu)建pod退敦,如下面的代碼清單所示。
代碼清單10.6 Statefulset創(chuàng)建的有狀態(tài)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創(chuàng)建的數(shù)據(jù)卷 claimName: data-kubia-0 - name: kube-api-access-fb4qg #數(shù)據(jù)卷相關(guān)聲明 projected: defaultMode: 420 sources:
通過持久卷聲明模板來創(chuàng)建持久卷聲明和pod中使用的與持久卷聲明相關(guān)的數(shù)據(jù)卷蚣抗。
檢查生成的持久卷聲明
現(xiàn)在列出生成的持久卷聲明來確定它們被創(chuàng)建了:
$ kubectl get pvc
生成的持久卷聲明的名稱由在volumeClaimTemplate字段中定義的名稱和每個pod的名稱組成侈百。可以檢查聲明的YAML文件來確認(rèn)它們符合模板的定義翰铡。
10.3.3 使用你的pod
現(xiàn)在你的數(shù)據(jù)存儲集群的節(jié)點都已經(jīng)運行钝域,可以開始使用它們了。因為之前創(chuàng)建的Service處于headless模式锭魔,所以不能通過它來訪問你的pod例证。需要直接連接每個單獨的pod來訪問(或者創(chuàng)建一個普通的Service,但是這樣還是不允許你訪問指定的pod)迷捧。
前面已經(jīng)介紹過如何直接訪問pod:借助另一個pod战虏,然后在里面運行curl命令或者使用端口轉(zhuǎn)發(fā)拣宰。這次來介紹另外一種方法,通過API服務(wù)器作為代理烦感。
通過API服務(wù)器與pod通信
API服務(wù)器的一個很有用的功能就是通過代理直接連接到指定的pod巡社。如果想請求當(dāng)前的kubia-0 pod,可以通過如下URL:
<apiServerHost>:<port>/api/v1/namespaces/default/pods/kubia-0/proxy/<path>
因為API服務(wù)器是有安全保障的手趣,所以通過API服務(wù)器發(fā)送請求到pod是煩瑣的(需要額外在每次請求中添加授權(quán)令牌)晌该。幸運的是,在第8章中已經(jīng)學(xué)習(xí)了如何使用kubectl proxy來與API服務(wù)器通信绿渣,而不必使用麻煩的授權(quán)和SSL證書朝群。再次運行代理如下:
$ kubectl proxy
現(xiàn)在,因為要通過kubectl代理來與API服務(wù)器通信中符,將使用 localhost:8001
來代替實際的API服務(wù)器主機地址和端口姜胖。你將發(fā)送一個如下所示的請求到kubia-0 pod:
$ curl localhost:8001/api/v1/namespaces/custom/pods/kubia-0/proxy/
返回的消息表明你的請求被正確收到,并在kubia-0 pod的應(yīng)用中被正確處理淀散。
注意 如果你收到一個空的回應(yīng)右莱,請確保在URL的最后沒有忘記輸入/符號(或者用curl的-L選項來允許重定向)
因為你正在使用代理的方式,通過API服務(wù)器與pod通信档插,每個請求都會經(jīng)過兩個代理(第一個是kubectl代理漓雅,第二個是把請求代理到pod的API服務(wù)器)非区。詳細(xì)的描述如圖10.10所示岭粤。
image
圖10.10 通過kubectl代理和API服務(wù)器代理來與一個pod通信
上面介紹的是發(fā)送一個GET請求到pod星著,也可以通過API服務(wù)器發(fā)送POST請求。發(fā)送POST請求使用的代理URL與發(fā)送GET請求一致则剃。
當(dāng)你的應(yīng)用收到一個POST請求時耘柱,它把請求的主體內(nèi)容保存到本地一個文件中。發(fā)送一個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/
你發(fā)送的數(shù)據(jù)現(xiàn)在已經(jīng)保存到pod中棍现,那讓我們檢查一下當(dāng)你再次發(fā)送一個GET請求時帆谍,它是否返回存儲的數(shù)據(jù):
$ curl localhost:8001/api/v1/namespaces/default/pods/kubia-0/proxy/
挺好的,到目前為止都工作正持嵩郏。現(xiàn)在讓我們看看集群其他節(jié)點(kubia-1 pod
):
$ curl localhost:8001/api/v1/namespaces/custom/pods/kubia-1/proxy/
與期望的一致烈涮,每個節(jié)點擁有獨自的狀態(tài)朴肺。那這些狀態(tài)是否是持久的呢?讓我們進(jìn)一步驗證坚洽。
刪除一個有狀態(tài)pod來檢查重新調(diào)度的pod是否關(guān)聯(lián)了相同的存儲
你將會刪除kubia-0 pod戈稿,等待它被重新調(diào)度,然后就可以檢查它是否會返回與之前一致的數(shù)據(jù):
$ kubectl delete po kubia-0
如果你列出當(dāng)前pod讶舰,可以看到該pod正在終止運行:
$ kubectl get po
當(dāng)它一旦成功終止鞍盗,Statefulset會重新創(chuàng)建一個具有相同名稱的新的pod:
$ kubectl get po
請記住需了,新的pod可能會被調(diào)度到集群中的任何一個節(jié)點,并不一定保持與舊的pod所在的節(jié)點一致般甲。舊的pod的全部標(biāo)記(名稱肋乍、主機名和存儲)實際上都會轉(zhuǎn)移到新的pod上。如果你在使用Minikube敷存,你將看不到這些墓造,因為它僅僅運行在單個節(jié)點上,但是對于多個節(jié)點的集群來說锚烦,可以看到新的pod會被調(diào)度到與之前pod不一樣的節(jié)點上觅闽。
圖10.11 一個有狀態(tài)pod會被重新調(diào)度到新的節(jié)點,但會保留它的名稱涮俄、主機名和存儲
現(xiàn)在新的pod已經(jīng)運行了蛉拙,那讓我們檢查一下它是否擁有與之前的pod一樣的標(biāo)記。pod的名稱是一樣的彻亲,那它的主機名和持久化數(shù)據(jù)呢孕锄?可以通過訪問pod來確認(rèn):
$ curl localhost:8001/api/v1/namespaces/custom/pods/kubia-0/proxy/
從pod返回的信息表明它的主機名和持久化數(shù)據(jù)與之前pod是完全一致的,所以可以確認(rèn)Statefulset會使用一個完全一致的pod來替換被刪除的pod睹栖。
擴縮容Statefulset
縮容一個Statefulset硫惕,然后在完成后再擴容它,與刪除一個pod后讓Statefulset立馬重新創(chuàng)建它的表現(xiàn)是沒有區(qū)別的野来。需要記住的是恼除,縮容一個Statefulset只會刪除對應(yīng)的pod,留下卸載后的持久卷聲明曼氛』砘裕可以嘗試縮容一個Statefulset,來進(jìn)行確認(rèn)舀患。
需要明確的關(guān)鍵點是徽级,縮容/擴容都是逐步進(jìn)行的,與Statefulset最初被創(chuàng)建時會創(chuàng)建各自的pod一樣聊浅。當(dāng)縮容超過一個實例的時候餐抢,會首先刪除擁有最高索引值的pod。只有當(dāng)這個pod被完全終止后低匙,才會開始刪除擁有次高索引值的pod旷痕。
通過一個普通的非headless的Service暴露Statefulset的pod
在閱讀這一章的最后一部分之前,需要為你的pod添加一個適當(dāng)?shù)姆莌eadless Service顽冶,這是因為客戶端通常不會直接連接pod欺抗,而是通過一個服務(wù)。
你應(yīng)該知道了如何創(chuàng)建Service强重,如果不知道的話绞呈,請看下面的代碼清單贸人。
代碼清單10.7 一個用來訪問有狀態(tài)pod的常規(guī)Service:kubia-servicepublic.yaml
apiVersion: v1kind: Servicemetadata: name: kubia-publicspec: selector: app: kubia ports: - port: 80 targetPort: 8080
因為它不是外部暴露的Service(它是一個常規(guī)的ClusterIP Service,不是一個NodePort或LoadBalancer-type Service)佃声,只能在你的集群內(nèi)部訪問它艺智。那是否需要一個pod來訪問它呢?答案是不需要秉溉。
通過API服務(wù)器訪問集群內(nèi)部的服務(wù)
不通過額外的pod來訪問集群內(nèi)部的服務(wù)的話力惯,與之前使用訪問單獨pod的方法一樣,可以使用API服務(wù)器提供的相同代理屬性來訪問召嘶。
代理請求到Service的URL路徑格式如下:
/api/v1/namespaces/<namespace>/services/<service name>/proxy/<path>
因此可以在本地機器上運行curl命令父晶,通過kubectl代理來訪問服務(wù)(之前啟動過kubectl proxy,現(xiàn)在它應(yīng)該還在運行著):
$ kubectl proxy --address='0.0.0.0' --port=8001 --accept-hosts='.*'
$ curl localhost:8001/api/v1/namespaces/custom/services/kubia-public/proxy/
客戶端(集群內(nèi)部)同樣可以通過kubia-public服務(wù)來存儲或者讀取你的集群中的數(shù)據(jù)弄跌。當(dāng)然甲喝,每個請求會隨機分配到一個集群節(jié)點上,所以每次都會隨機獲取一個節(jié)點上的數(shù)據(jù)铛只。后面我們會改進(jìn)它埠胖。