10.3 使用Statefulset

為了恰當地展示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所示。

img

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不一樣的節點上。

img

圖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服務來存儲或者讀取你的集群中的數據。當然,每個請求會隨機分配到一個集群節點上,所以每次都會隨機獲取一個節點上的數據。后面我們會改進它。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372