六. 掛載卷 Volume
通過學習前五章,我們已經可以部署一個安全穩定的臨時應用程序了。為什么說是臨時的應用程序呢?
眾所周知,程序=代碼+數據,現在代碼已經可以運行並提供服務,同時可以產生或讀取數據了,但每次重啟服務或者 K8S 幫助我們調度 Pod 導致容器重啟以后,之前運行的一些有價值無價值的數據也就不存在了,在本章,我們將介紹怎么處理這份有價值的數據。
原文地址 https://www.cnblogs.com/clockq/p/12297728.html
6.1 Volume 介紹
Kubernetes 通過定義 Volume 來滿足這個需求,Volume 被定義為 Pod 這類頂級資源的一部分,並和 Pod 共享生命周期。
也就是 Pod 啟動時創建卷,Pod 刪除時銷毀卷,期間卷的內容不會消失,所以 Pod 因為各種原因重啟容器都不會影響卷的內容,如果一個 Pod 內包含多個容器,多個容器共享此卷。
Volume 類型
- emptyDir: 用於存儲臨時數據的空目錄
- hostPath: 用於將工作節點的目錄掛載到 Pod 中
- gitRepo: 通過檢出 Git 倉庫內容來初始化的掛載卷
- nfs: 用於掛載 nfs 共享卷到 Pod 中
- configMap、secret、downwardAPI: K8S 內置的用於持久化存儲的特殊類型資源
- persistentVolumeClaim: K8S 的持久存儲類型
- gcePersistentDisk: 谷歌高效磁盤存儲卷
- awsElasticBlockStore: 亞馬遜彈性塊型存儲卷
6.2 Volume 用例
1、最簡單的 emptyDir
雖然 emptyDir 只能作為臨時數據存儲,不過利用容器共享卷的這一特性,在 Pod 的多個容器中共享文件還是很有效的。
apiVersion: v1
kind: Pod
metadata:
name: fortune
spec:
containers:
- image: luksa/fortune
name: html-generator
volumeMounts:
- name: html # 使用名為 html 的卷
mountPath: /var/htdocs # 卷掛載位置
- image: nginx:alpine
name: html-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html # 聲明一個名為 html 的卷,Pod 創建時創建,自動綁定 Pod 的生命周期
emptyDir: {} # 空配置,配置項下面說
empytDir 的存儲介質由運行 Pod 的節點提供,並且默認實在磁盤上創建一個空目錄。我們可以通過修改emptyDir
的配置項,實現在工作節點的內存創建目錄。
apiVersion: v1
kind: Pod
metadata:
name: fortune
spec:
...
volumes:
- name: html
emptyDir:
medium: Memory
2、進一大步的 gitRepo
gitRepo 是 emptyDir 的進化版,它通過克隆一個 Git 倉庫的特定分支版本來初始化目錄的內容(不會同步更新倉庫內容)。
apiVersion: v1
kind: Pod
metadata:
name: fortune-for-gitRepo
spec:
...
volumes:
- name: html
gitRepo:
repository: https://github.com/clockq/***.git # 一個 git 倉庫的路徑
reversion: master # 檢出 git 的主分支
directory: . # git clone 后放在 volume 的根目錄,如果不寫,會在根目錄下創建一個 *** 項目名的子文件夾,實際倉庫內容會放到 *** 子文件夾內
6.3 可持久化的 Volume
上述例子主要實現的功能是 Pod 內的容器重啟后,卷中數據可復用。以及 Pod 內的多個容器利用卷共享卷中數據。但因為上述 Volume 的生命周期和 Pod 同步,也導致了 Volume 無法做到真正的持久化。
什么是真正的持久化呢?
當 Pod 重啟后數據依舊存在,不論 Pod 是再次調度到上次的工作節點還是其他工作節點都可以加載到之前生成的持久化數據。
注意:如果需要 Volume 可以跨工作節點訪問,就需要存儲介質可以在所有工作節點可以訪問。
1、最簡單的例子 hostPath
只是將持久化數據放到工作節點的存儲介質中,如果 Pod 重新調度后出現在別的工作節點,那么之前持久化的數據就沒有用到,所以謹慎使用。
一般用來某些系統級別的應用(比如由 DaemonSet 管理的 Pod)讀取工作節點的文件系統時使用。即便可以但也不建議用來做多個 Pod 之間的文件同步。
apiVersion: v1
kind: Pod
metadata:
name: fortune-for-hostPath
spec:
...
volumes:
- name: html
hostPath:
path: /data # 工作節點的目錄位置
type: Directory # 掛載的文件類型
2、使用 GCE 持久化卷
GCE(Google Compute Engine)是 Google 提供的雲計算平台,對於使用方式日新月異,但國內不方便,所以有興趣的看官網吧。
https://kubernetes.io/zh/docs/concepts/storage/volumes/#gcepersistentdisk
工作原理大致如下:
其他雲平台大同小異。常見的雲平台和接口如下:
- GCE(Google) => gcePersistentDisk
- AWS EC2(Amazon) => awsElasticBlockStore
- Microsoft Azure(Microsoft) => azureFile、azureDisk
- NFS(Linux) => nfs
需要了解更多的支持和配置信息,可以使用
$ kubectl explain
自行查詢
6.4 從 Pod 解耦底層存儲
掌握上面的內容可以綁定大多數的持久化存儲了,但一個 Pod 的發布者(或者說服務的開發者)其實並不需要知道所使用的的存儲介質,以及存儲介質的具體配置,這些應該交給集群管理員來處理。
利用 Kubernetes Volume 屏蔽實際的存儲技術不就是 K8S 所推崇的嗎!否則就導致一個 Pod 與某種雲平台產生了強依賴關系。
理想情況是,當開發人員需要一定一定數量的持久化存儲是,向 K8S 請求,就好像請求 CPU,Memory等資源一樣。而集群管理員的工作只需將存儲介質配置好,並加入到集群的資源池中。
1、介紹持久卷與持久卷生命
為了使應用能夠正常請求存儲資源,同時避免處理基礎設施細節,所以引入了持久卷和持久卷聲明。
- 集群管理員只需要創建和管理某種存儲介質。
- 然后創建 PV(PersistentVolume,持久卷)來抽象存儲介質,此時可以設定存儲大小和訪問模式,PV 代理的存儲能力會自動加入到 K8S 的資源池中。
- 當應用發布者需要使用持久卷時,只需創建一個 PVC(PersistentVolumeClaim,持久卷聲明),指定所需的最小存儲容量要求和訪問模式。
- K8S 會自動找到可匹配的 PV,並綁定到此 PVC。
- 持久卷聲明即可作為一個普通卷使用,並掛載到 Pod 上。
- 已經掛載的 PV 不能掛載在多個 PVC 上,只能等待之前的 PVC刪除后釋放,PV 才可以掛載到其他 PVC 上。
2、創建持久卷
apiVersion: v1
kind: PersistentVolume
metadata:
name: demo-pv
spec:
capacity:
storage: 1Gi # 定義存儲卷大小
accessModes: # 定義存儲訪問模式
- ReadWriteOnce # 可以被一個用戶綁定為讀寫模式
- ReadOnlyMany # 也可以被多個節點(而非Pod)綁定為只讀模式
persistentVolumeReclaimPolicy: Retain # 定義回收策略
# Retain => 當 PV 被刪除后 PV 的內容會被保留。相應策略還有 Delete 和 Recycle,回收策略可以動態改變
local: # 持久卷類型,使用本地存儲
path: /tmp/hostpath_pv/demo-pv # 本地目錄位置
3、通過持久卷聲明綁定持久卷
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
namespace: demo-ns # pvc 歸屬於某個 ns
spec:
accessModes:
- ReadWriteOnce # 可以被一個用戶綁定為讀寫模式
storageClassName: "" # 動態類配置
resources:
requests:
storage: 1Gi # 申請1Gi的存儲空間
如上圖一樣,K8S 根據 PVC 申請的資源,去所有 PV 中找到能滿足所有要求的 PV,然后兩者綁定。此時列舉 PVC 打印信息如下:
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
demo-pvc Bound demo-pv 1Gi RWO 13s
4、Pod 通過持久卷和持久卷聲明來使用存儲介質
apiVersion: v1
kind: Pod
metadata:
name: fortune-for-pvc
spec:
...
volumes:
- name: html
persistentVolumeClaim:
claimName: demo-pv # 通過名字引用之前創建的 pvc
5、回收持久卷
持久化的回收,so easy,刪就完了
$ kubectl delete pvc demo-pvc
$ kubectl delete pv demo-pv
PV、PVC、Pod 三者的生命周期如下:
6、總結
兩種持久卷的使用方式對比如下圖。
好處顯而易見,通過中間加了一層抽象,使開發者的持久化工作更加簡單,且對存儲介質解耦。雖然需要集群管理員做更多的配置工作,但這是值得的。
6.5 持久卷的動態配置
通過上面章節,我們已經可以很好很方便的使用存儲資源,但每次使用都需要集群管理員配置好 PV 來支持實際的存儲。
不過還好,K8S 提供 持久卷配置
來自動創建 PV。集群管理員只需定義一個或多個 SC(StorageClass)資源,用戶在創建 PVC 時就可以指定 SC,K8S 就會使用 SC 的置備程序(provisioner)自動創建 PV。
Kubernetes 包括最流行的雲服務提供商的置備程序(provisioner),所以管理員不需手動創建。但如果 K8S 部署在本地就需要配置一個定制的置備程序。
1、定義 SC(StorageClass)
下面是 minikube 環境下使用 hostpath PV 的一個 SC,對於其他雲平台的 SC,內容會更簡單,但大同小異。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
annotations:
storageclass.kubernetes.io/is-default-class: "false" # 是否設置為默認 SC
creationTimestamp: "2019-12-18"
resourceVersion: "v0.1"
provisioner: kubernetes.io/minikube-hostpath # 調用此 SC 時使用的置備程序,對於不同的雲平台選擇不同內容
reclaimPolicy: Retain # Supported policies: Delete, Retain
parameters: # 傳遞給 provisioner 的參數
type: pd-ssd
2、PVC 使用 SC 動態創建 PV
當創建下面 PVC 時就會引入上面定義的 SC,因此調用里面配置的 provisioner 來自動創建一個 PV。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
namespace: demo-ns
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast # 選擇之前創建的 SC
resources:
requests:
storage: 1Gi
如果引入的 SC 不存在,則 PV 配置失敗(ProvisioningFailed)
3、不指定存儲類的例子
上面使用 SC 的例子,在某些場景下,又一次的提高了應用 Pod 和 PVC 的移植性,因為不需要集群管理員一個一個的創建 PV 了。
在上面例子中,我們顯式的指定了 storageClassName: fast
才實現了我們要的效果,如果我們把這句話刪除,則 PVC 會使用默認的 SC (使用 $ kubectl get sc
列出所有 SC 就會看到默認的)。
如果想要 PVC 不通過 provisioner 創建,而是綁定到之手動配置的 PV 時,就要想最開始的例子一樣,storageClassName: ""
將動態類顯式的配置為空字符串。
最后,附上動態持久卷的完整圖例
關於 local PV,推薦兩篇不錯的文章,看的我清醒不少
七. ConfigMap和Secret來配置應用
7.1 配置容器化應用程序
為了服務的可擴展性和可配置性,我們寫好的服務通常會有許多的配置項。配置項的表現形式通常有
- 啟動時的命令行參數
- 配置文件
- 環境變量
推薦環境變量優先,因為使用配置文件常常需要打入鏡像或掛載到容器上,同時配置文件中包含的敏感信息也不方便透露。
下面我們就來了解一下 K8S 推薦的更優雅的解決方法。ConfigMap和Secret。
7.2 向容器傳遞命令行參數
1、在 Docker 中定義命令加參數
首先需要了解,Dockerfile 中定義的兩個指令:
- ENTERPOINT:定義容器啟動時候調用的可執行程序。類型為[ ]。
- CMD:定義傳遞給 ENTERPOINT 的參數。類型為[ ]。
更具體的內容請查看 https://aws.amazon.com/cn/blogs/china/demystifying-entrypoint-cmd-docker/
ok,了解了兩個指令后,使用方式也是很簡單
$ docker run [--entrypoint entrypoint] image [cmd] [args]
2、在 K8S 中配置啟動命令和參數
apiVersion: v1
kind: Pod
metadata:
name: demo-args
namespace: demo-ns
spec:
containers:
- image: luksa/fortune:args
command: ["/bin/command"] # 相當於 ENTERPOINT
args: ["arg1", "arg2", "arg3"] # 相當於 CMD
通常不需要設置 command
,除非是一些鏡像中未設置 ENTERPOINT
的服務。
上面的內容轉換為 Docker 啟動語句docker run -d --entrypoint /bin/command luksa/fortune arg1 arg2 arg3
注意,
command
和args
在 Pod 啟動后無法就無法修改。
7.3 為容器設置環境變量
apiVersion: v1
kind: Pod
metadata:
name: demo-env
namespace: demo-ns
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL # 定義的環境變量的Key
value: "30"
- name: INTERVAL_DESC
value: "$(INTERVAL) seconds"
7.4 利用 CM(ConfigMap)解耦配置
ConfigMap 是環境變量方式的一種優化,主要作用是可以在多個環境中區分配置選項。
1、CM 介紹
Kubernetes 允許將配置選項分離到單獨的資源對象 ConfigMap 中,CM 本質上就是一個 KV 對。V 可以是短字面量,也可以是一個完整的配置文件。
應用無需知道 CM 存儲的內容,甚至不需要知道這種資源的存在。CM 中的內容可以直接通過持久卷和環境變量的方式傳遞到容器中。
2、創建 CM
最快的創建方式
$ kubectl create configmap fortune-config --from-literal=sleep-interval=25 --from-literal=sleep-interval-desc="25 seconds"
$ kubectl create configmap fortune-config-file --from-file=customkey=config-file.conf
$ kubectl create configmap fortune-config-dir --from-file=/path/dir
注意: ConfigMap 中的鍵名必須是一個合法的 DNS 子域,僅包含數字、字母、破折號、下划線以及小數點。而環境變量不允許有破折號。
yaml 文件創建方式
apiVersion: v1
kind: ConfigMap
metadata:
name: fortune-config
namespace: demo-ns
data:
sleep-interval: 25
sleep-interval-desc: 25 seconds
3、CM 作為環境變量
apiVersion: v1
kind: Pod
metadata:
name: demo-env-configmap
namespace: demo-ns
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL # 定義的環境變量的Key
valueFrom: # 定義 Value 的來源
configMapKeyRef: # 從 CM 中取值
name: fortune-config # 查找的 CM
key: sleep-interval # 獲得的 Key
optional: true # 當這個 Key 不存在時,Pod 也能正常啟動,而不是等待 CM 的創建
4、一次性將 CM 所有內容作為環境變量
一個個添加容易出錯,批量也能搞。
apiVersion: v1
kind: Pod
metadata:
name: demo-env-configmap
namespace: demo-ns
spec:
containers:
- image: luksa/fortune:env
envFrom:
- prefix: CONFIG_ # 可為空,容器內的環境變量名為 prefix + CM.Key
configMapRef:
name: fortune-config # 提取的 CM
注意,K8S 不會自動轉換非法的環境變量名。也就是如果一個 Key="FOO-BAR" 在 CM 中是合法的,但是在容器內被自動轉為 "CONFIG_FOO-BAR" 因為包含 "-" 是非法環境變量名,所以直接就不會導入他。
5、CM 作為容器啟動的命令行參數
pod.spec.containers.args
中無法直接引用 CM 的條目,但可以使用 CM 條目初始化某個環境變量,然后再在參數字段中引用該環境變量。
apiVersion: v1
kind: Pod
metadata:
name: demo-args-configmap
namespace: demo-ns
spec:
containers:
- image: luksa/fortune:args
args: ["$(INTERVAL)"] # 引用環境變量
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
6、CM 作為掛載文件
需要使用 ConfigMap Volume 將 CM 中的文件掛載到容器中。示意圖如下:
刪除之前創建的 fortune-config CM,並通過 file 重新創建。
apiVersion: v1
kind: Pod
metadata:
name: demo-configmap-volume
namespace: demo-ns
spec:
containers:
- image: nginx:alpine
name: web-server
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d # 掛載位置,會覆蓋此目錄下所有文件
readOnly: true
volumes:
- name: config # 掛載卷名稱
configMap: # 通過 CM 創建掛載卷
name: fortune-config # 提取的 CM 名稱
defaultMode: "660" # 設置文件訪問權限,默認為644
items: # 指定要暴露的條目,不指定則全部添加
- key: nginx-config.conf # 暴露的 Key
path: gzip.conf # 條目暴露文件重命名
CM 文件掛載不覆蓋
按照上面的方式掛載 CM 中的文件到容器內的某個文件夾,會導致文件夾原有的文件被隱藏。可以使用 volumeMount 額外的 subPath 字段只掛載部分卷,來解決此問題。示意圖如下:
...
spec:
containers:
- image: nginx:alpine
name: web-server
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d # 掛載位置
subPath: gzip.conf # 只掛載卷中的一部分,這樣就不會覆蓋原有內容
readOnly: true
volumes:
...
7、通過熱更新 CM 更新 Pod
注意,command
和 args
在 Pod 啟動后無法就無法修改,但將 ConfigMap 暴露為卷是可以達到熱更新效果的。
但
subPath
掛載的卷不會自動更新。
CM 被更新后,卷中引用他的所有文件也會相應更新(因為網絡原因可能有延遲),進程發現文件改動(根據代碼邏輯,不自動發現的需要手動通知)后會進行重載。
但是熱更新的耗時會出乎意料的長。
7.5 使用 Secret 配置敏感信息
到目前為止我們傳遞給 K8S 的配置信息都是非敏感數據,而敏感信息如秘鑰和證書等,需要使用 Secret 資源確保數據的安全性。
1、Secret 介紹
Secret 和 CM 類似,均是 KV 存儲,使用方式也類似,可以將 Secret 作為環境變量傳遞給容器,或者以卷的方式掛載。
另外,Secret 只會存儲在節點的內存中,永遠不會寫入物理存儲。
我們可以看到任意一個 Pod 內都自動掛載了一個 Secret 卷,這個卷中包含了從 Pod 內部安全訪問 K8S API 服務器所需的認證信息。雖然我們希望 Pod 對集群信息無感知,但在一下別無他法的情況下,還是會用到的。
2、創建 Secret
- 准備工作,先創建證書和秘鑰
# 創建私鑰
$ openssl genrsa -out https.key 2048
# 生成 CERTIFICATE
$ openssl req -new -x509 -key https.key -out https.cert -days 360 -subj/CN=kubia.example.com
- 創建
generic
類型的 Secret,和 CM 基本一樣,就是多加了一個類型參數
$ kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert --from-file=foo
注意,Secret 保存的條目,最大是 1M。
3、在 Pod 中使用 Secret
apiVersion: v1
kind: Pod
metadata:
name: fortune-https
spec:
containers:
- image: luksa/fortune:env
name: html-generator
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: html-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: certs
mountPath: /etc/nginx/certs
readOnly: true
ports:
- containerPort: 80
protocol: TCP
- containerPort: 443
protocol: TCP
volumes:
- name: html # 聲明一個名為 html 的卷,Pod 創建時創建,自動綁定 Pod 的生命周期
emptyDir: {} # 空配置,配置項下面說
- name: config
configMap:
name: fortune-config
items:
- key: nginx-config.conf
path: https.conf
- name: certs
secret:
secretName: fortune-https
上面例子的示意圖如下:
通過環境變量引入
...
spec:
containers:
...
env:
- name: INTERVAL
valueFrom:
secretKeyRef:
name: fortune-https
key: foo
但這不是一個好主意,因為應用程序可能在系統出錯時打印環境變量,從而無意中暴露 Secret 信息。
子進程會繼承父進程的全部環境變量,如果 fork 第三方的二進制程序,敏感信息可能出現泄漏風險
Docker Hub 私有鏡像拉取
# 創建 docker-registry 類型的 secret
$ kubectl create secret docker-registry mydockerhubsecret --docker-username=admin --docker-password=pwd --docker-email=email@demo.com
# Pod 中使用 docker-registry 類型的 secret
...
spec:
imagePullSecrets:
- name: mydockerhubsecret
containers:
...
4、CM 和 Secret 對比
當使用 kubectl get cm(secret)
- CM 直接純文本展示內容存儲內容
- Secret 的內容會被 Base64 格式編碼打印(因為 Base64 編碼可以涵蓋二進制數據)
八. 從應用訪問 Pod 元數據及其他資源
8.1 通過 Downward API 傳遞元數據
之前學習的 CM 和 Secret 向應用傳遞預設的配置信息已經足夠,但對於不可預先獲得的信息,比如 Pod 的 IP,主機名或者 Pod 自身的動態名時就捉襟見肘了。
此外,對於已經在別處定義的 Pod 標簽和注解等,也不希望重復的保存和定義。
上述問題就可以通過 Downward API 解決。
1、Downward API 介紹
Downward API 允許我們通過環境變量或掛載卷的方式傳遞 Pod 的元數據。示意圖如下:
2、了解可用的元數據
- Pod 的名稱
- Pod 的 IP
- Pod 的標簽(只可以通過卷暴露)
- Pod 的注解(只可以通過卷暴露)
- Pod 所在的命名空間
- Pod 運行節點的名稱
- Pod 運行所歸屬的服務賬戶名稱
- 每個容器可用的 CPU 和內存的限制
- 每個容器請求的 CPU 和內存的使用量
3、通過環境變量暴露元數據
apiVersion: v1
kind: Pod
metadata:
name: downward
spec:
containers:
- name: main
image: busybox
command: ["sleep", "9999999"]
resources:
requests:
cpu: 15m
memory: 100Ki
limits:
cpu: 100m
memory: 4Mi
env:
- name: POD_NAME
valueFrom:
fieldRef: # 引用 manifest 中的元數據
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: SERVICE_ACCOUNT
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountName
- name: CONTAINER_CPU_REQUEST_MILLICORES
valueFrom:
resourceFieldRef: # 引用資源中的元數據
resource: requests.cpu
divisor: 1m # 定義一個基數(量詞)
- name: CONTAINER_MEMORY_LIMIT_KIBIBYTES
valueFrom:
resourceFieldRef:
resource: limits.memory
divisor: 1Ki # K 必須大寫
執行后的效果圖如下:
4、通過卷傳遞元數據
apiVersion: v1
kind: Pod
metadata:
name: downward
labels:
foo: bar
annotations:
key1: value1
key2: |
one
two
spec:
containers:
- name: main
image: busybox
command: ["sleep", "9999999"]
resources:
requests:
cpu: 15m
memory: 100Ki
limits:
cpu: 100m
memory: 4Mi
volumeMounts: # 掛載配置
- name: downward
mountPath: /etc/downward
volumes:
- name: downward # 定義卷
downwardAPI:
items:
- path: "podName" # 保存的文件名
fieldRef:
fieldPath: metadata.name
- path: "podNamespace"
fieldRef:
fieldPath: metadata.namespace
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations
- path: "containerCpuRequestMilliCores"
resourceFieldRef:
containerName: main # 資源對應的容器名
resource: requests.cpu
divisor: 1m
- path: "containerMemoryLimitBytes"
resourceFieldRef:
containerName: main
resource: limits.memory
divisor: 1
labels 和 annotations 可以動態更改,所以不能通過環境變量的方式傳遞,否則新的值無法暴露,請求重啟 Pod 會出錯
8.2 與 Kubernetes API 服務器交互
Downward API 僅可以獲得 Pod 自身的元數據,還無法獲得其他 Pod 的元數據信息甚至是集群的信息。
1、探究 Kubernetes REST API
可以通過 $ kubectl cluster-info
獲得 API 服務器的 URL。
因為服務器使用 HTTPS 協議並且需要授權,所以需要執行 $ kubectl proxy
啟動代理服務,負責轉發請求和處理身份認證,同時確保和我們通信的是真實的 API 服務器。
無需傳遞其他參數,因為
kubectl
已經知道服務器的全部參數。啟動后就會在本機8001端口開啟服務並轉發用戶的請求。
之后通過瀏覽器或者curl
工具訪問http://localhost:8001
即可獲得集群相關信息。
2、從 Pod 內部與 Kubernetes API 交互
交互之前需要關注三個問題:
- API 服務器的位置
- 確保 API 服務器的真實性而不是一個假冒的地址
- 通過 API 服務器的身份認證
關於問題1的解決方法如下:
- 可以通過
$ k get svc -A | grep kubernetes
獲得集群地址。 - 或者按照我們之前說的,每個容器內都有各個服務的環境變量信息
KUBERNETES_SERVICE_HOST
和KUBERNETES_SERVICE_PORT
。 - 利用集群 DNS 直接訪問
https://kubernetes
關於問題3的解決方法如下:
# 查看自動分配給每個 Pod 的身份證書
$ ls /var/run/secrets/kubernetes.io/serviceaccount/
# 信任服務器證書
$ export CURL_CA_BUNDLE=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# 生成Client Token
$ export TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# 獲得命名空間
$ export NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
# 簡單測試,獲得當前命名空間下所有 Pod
$ curl -H "Authorization: Bearer $TOKEN" https://kubernetes/api/v1/namespaces/$NS/pods
3、通過 ambassador 容器簡化與 API 交互
Pod 內部也可以使用類似 kubectl proxy
的方式繞過認證。
在啟動主容器的同時,啟動一個 ambassador 容器,並在其中運行 $ kubectl proxy
命令來實現與 API 服務器的交互。
示意圖如下:
4、使用客戶端庫與 API 交互
不用多說,推薦兩個官方Kuberbetes API 客戶端庫。
- Galang client:https://github.com/kubernetes/client-g
- Python client:https://github.com/kubernetes-incubator/client-python
九. Deployment:聲明式地升級應用
9.1 更新運行在 Pod 內的應用程序
假設有一組以 RC 或 RS 代理的 Pod 正在提供服務,Pod 一開始使用 v1 版本的鏡像運行。之后的開發生產過程中,創建了 v2 版本的鏡像並已經推送至倉庫。
接下來就要考慮怎么使用 v2 鏡像替換 v1 鏡像,但 Pod 創建之后不允許直接修改鏡像,有以下三種方法更新 Pod:
- 刪除原本的 Pod 並重新創建基於新版本鏡像的 Pod(會出現宕機時間)
- 先創建新的 Pod ,利用 SVC 代理到新的 Pod,成功發布后刪掉原本的 Pod(這就是藍綠部署,可以逐漸的調整實例數量)
- 利用代碼實現 AB 對接(可以新老代碼同時存在並運行)
方法二的示意圖如下:
9.2 使用 RC(ReplicationController) 實現自動滾動升級
如果容器鏡像使用 latest 的 tag,則 imagePullPolicy 默認為 Always,否則默認策略為 IfNotPresent。
K8S 對於上述的方法二,提供一種自動滾動升級的方式,而且非常的簡單和快捷。
$ kubectl rolling-update kubia-v1 kubia-v2 --image=luksa/kubia:v2
運行該命令后,會進行下列活動:
- 由舊的 RC 為模板,一個以 luksa/kubia:v2 為鏡像的,名為 kubia-v2 的 RC 被創建,但初始副本為 0
- 為新舊 RC 及 Pod 添加 deployment 版本的標簽
- 將 kubia-v2 副本增加 1,然后 kubia-v1 副本減少 1
- 循環第三步,直至全部升級完成
此方法為什么被舍棄呢?
- 需要添加標簽而修改了 RC,這與用戶預設不符,這種黑箱操作總是會導致一個問題累死都找不到原因。
- 這個操作實際就是對客戶端一組操作的包裝,也就意味着如果執行過程中出錯或網絡中斷,是沒有事物鎖幫助還原和重試的。
- K8S 期望使用聲明式的,如在 yaml 中修改為新版本,並運行新的 Pod 替換老的。而不是執行 rolling-update 去明確的告訴 K8S 應該如何升級。(好像 RC 的伸縮一樣)
9.3 使用 Deployment 聲明式地升級應用
Deployment 是一種更高階的資源,用於部署應用程序並以聲明的方式升級應用,而不是直接通過 RC 或 RS 進行部署。
當創建 Deployment 后,RS 會隨之創建,並負責復制和管理 Pod。示意圖如下:
有了 RC,RS 為啥還需要 Deployment?
就如同上一章說的,當應用服務使用rolling-update
滾動升級時,需要協調另一個新建的 RC 不斷增刪來達到升級目的。而 Deployment 資源是在 K8S 控制層上運行控制器進程解決滾動升級問題。
1、創建一個 Deployment
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: kubia # 因為 Deployment 可以管理多個版本,所以不需要加版本后綴
spec:
replicas: 3 # 副本實例數量
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- image: luksa/kubia:v1
name: nodejs
將上述內容保存為kubia-deployment-v1.yaml
,並
執行 kubectl create -f kubia-deployment-v1.yaml --record
即可創建 Deployment。
在創建時加入參數
--record
會記錄歷史版本號,這在之后的操作中很有用。
可以使用三種方式查看部署的進度:
$ kubectl get deployment kubia
$ kubectl describe deployment kubia
$ kubectl rollout status deployment kubia
2、升級 Deployment
現在升級應用,只需修改 spec.template
后重新 apply 即可,做到聲明式和期望型的服務發布。
至於如何達到升級狀態,則是由 Deployment 的升級策略決定的。
- 默認策略是執行滾動升級(RollingUpdate),需要應用程序支持多版本同時對外提供服務
- 另一種策略是一次性刪除所有舊的 Pod 然后創建新的 Pod(Recreate)。
需要知道,修改模板中引用的 CM 或者 Secret 並不會觸發 Deployment 更新,如果需要此效果,可以創建一個新的 CM 然后引用。
3、回滾 Deployment
如果發現新的版本不符合預期,需要回退到上個版本,可以使用下列語句:
# 回滾到上一版本
$ kubectl rollout undo deployment kubia
# 指定版本回滾
$ kubectl rollout undo deployment kubia --to-revision=1
undo 命令也可以在滾動升級過程中運行,並直接停止升級。在升級過程中創建的 Pod 會被刪除並被老版本的 Pod 替代。
版本回滾是很快的,因為在滾動升級時,老版本創建的 RS 是不會被刪除的,僅僅是將副本數量清空。
可以使用$ kubectl rollout history deployment kubia
查看升級的歷史記錄。
4、控制滾動升級速率
...
spec:
strategy:
rollingUpdate: # 滾動升級配置
maxSurge: 1 # Pod 峰值最多可以超出期望副本的數量(為控制峰值資源),默認25%,數值可以是百分數,比如期望數為4,比如25%,則最多存在5個Pod;或者是絕對值,比如2,可以再多出2個副本
maxUnavailable: 0 # 升級時相比期望數量,允許有多少 Pod 處於不可用狀態,默認25%,數據和上面一樣,比如25%,則最多一個 Pod 不可用,要保證最少有3個 Pod 處於可用狀態
type: RollingUpdate # 默認的升級策略
上面配置產生的升級過程示意圖如下:
5、暫停滾動升級
為避免系統升級出錯,還需要手動回退的問題,我們可以先升級很小的一部分 Pod,一旦升級的服務符合預期,再將剩余的 Pod 繼續升級。(金絲雀發布方式,控制最小風險)
# 1. 先修改 Deployment 並 apply 滾動升級服務
# 2. 查看更新進度,在第一個 Pod 升級完成后立刻停止升級
$ kubectl rollout status deployment kubia
$ kubectl rollout pause deployment kubia
# 3. 一旦新版本運行符合預期,就可以恢復繼續升級
$ kubectl rollout resume deployment kubia
6、阻止錯誤版本的滾動升級
首先介紹 minReadySeconds
屬性,它指定新創建的 Pod 至少要成功運行多久才被視為可用。
需要的條件:
- 容器具備正確配置的就緒探針
- 配置
minReadySeconds
屬性,並設置一個足夠高的值(10)
整個過程是這樣嬸的:
- 創建一個新版本的 Pod
- 因為
maxUnavailable = 0
的配置,新的 Pod 可用之前滾動進程不會繼續 - 如果就緒探針探測失敗,則因為步驟2導致升級被阻止
- 所有容器的就緒探針都成功返回后, Pod 標記為就緒狀態
- 如果 Pod 的就緒狀態不能維持超過
minReadySeconds
配置,則升級會被阻止 - 如果就緒狀態可以保持
minReadySeconds
以上,新的 Pod 升級成功,繼續重復執行 1-6
十. StatefulSet:部署有狀態的多副本應用
10.1 復制有狀態 Pod
之前我們介紹的類型資源,創建的副本集除了名字和 IP 不同以外,其余完全一樣,也就是模板中引用的簡單卷還是 CM 等都是一樣的。示意圖如下:
1、運行的每個實例都有獨立的存儲的多副本
- 方式一: 手動創建多個Pod,綁定各自的存儲
- 優勢:簡單
- 劣勢:沒有高級資源的統一管理,基本是廢了
- 方式二:創建多個單一副本的 RS,綁定各自的存儲
- 優勢:利用 K8S 自動調度和故障處理
- 劣勢:管理很多資源,難以維護,擴展
- 方式三:投機取巧的讓 RS 的多副本使用數據卷的不同子目錄
- 優勢:回歸一個資源管理,只需代碼中創建個一致性的子目錄
- 劣勢:公用存儲可能出現性能瓶頸,同時穩定性變的依賴代碼
2、每個 Pod 都提供穩定的標識
當一個新的實例替換掉老的實例后,可能會獲得新的主機名和網絡標識,但還使用上一個實例的數據,就可能引起一些問題。
為什么一些應用需要維護一個穩定的網絡標識呢?
這個需求在有狀態的分布式應用中很普遍。
這類應用要求管理者在每個集群成員的配置文件中列出所有其他集群成員和它們的 IP 地址或主機名(如Spark,YARN等的 slaves 配置文件)。
但是在 Kubernetes 中,每次重新調度一個 Pod, 這個新的 Pod 就有一個新的主機名和 IP 地址,這樣就要求當集群中任何一個 成員被重新調度后,整個應用集群都需要重新配置。
10.2 了解 StatefulSet
可以創建 StatefulSet 資源來代替 RS 運行此類 Pod,它是專門定制的一類應用,在 StatefulSet 中的每個實例都是不可替代的個體,且擁有穩定的名字和狀態。
1、對比 StatefulSet 和 RS
StatefulSet 和 RS 就好比寵物和牛,而且 StatefulSet 最初也叫 PetSet。
對於有記憶(存儲)的動物就好比寵物,我們無法獲得一個一模一樣的寵物,所以需要細心照料。而沒有記憶的動物就好像農場主千百頭牛中的一頭,不需要給予太多的關心,反而可以隨時的刪除和替換。
2、提供穩定的網絡標識
StatefulSet 創建的每個 Pod 都是名稱加上從零開始的索引命名,這個命名同樣體現在 Pod 的主機名,以及 Pod 對應的固定存儲上。不同於 RS 的隨機,這樣有規則的命名更加方便管理。
控制服務介紹
有狀態的 Pod 有時候需要通過其主機名來被定位。無狀態的服務在處理請求的時候,隨便選擇一個可用 Pod 即可,但對於有狀態 Pod 它們每個的內容是不同的,所以每次連接和處理的也不同。
為實現上方效果,一個 StatefulSet 需要創建一個用來記錄每個 Pod 網絡標記的 headlessService。
通過這個 Service 使每個 Pod 擁有獨立的 DNS 記錄,使集群內的伙伴或客戶端可以通過主機名找到它。
替換消失的寵物
當 StatefulSet 管理的 Pod 實例因為節點故障或人工刪除的原因消失時,StatefulSet 會和 RS 一樣重新創建一個 Pod 維持期望的實例數量。但和 RS 不同的是,它重新創建的 Pod 會和之前的 Pod 有完全一致的名稱、主機名、存儲卷。
擴縮容 StatefulSet
擴容一個 StatefulSet 會使用下一個還沒有用到的順序索引值創建一個新的 Pod 實例。縮容時也是會刪除最高索引值的實例,使的擴容縮容的結果都是可預知的。
因為 StatefulSet 縮容的時候,每次只會操作一個 Pod 實例,所以有狀態應用的縮容都會相對較慢。
舉例來說,如果縮容是線性且緩慢的,則分布式存儲應用就有機會把即將關閉的節點數據復制到其他節點,保證數據安全。
基於以上原因,StatefulSet 在存在不健康實例的情況下是不允許縮容的。
3、提供穩定的專屬存儲
一個有狀態的 Pod 需要擁有自己的存儲,即使該 Pod 被重新調度並且產生不一致的標識,也必須掛載和之前相同的存儲。
很明顯,有狀態的 Pod 的存儲必須是持久的,並且與 Pod 解耦。利用之前學習的 PVC 解耦,同時保證每個 Pod 綁定一個單獨的 PVC 就保證綁定了各自的 PV。
在 Pod 模板中添加卷聲明模板
如何保證 StatefulSet 使用同一模板,但可以綁定不同的 PVC 呢?
解決方式就像創建 Pod 一樣,也有一個 PVC 模板來創建和管理 PVC,並且這些持久卷聲明會在創建 Pod 前完成創建。
持久卷可以由集群管理員提前創建或使用 SC 自動創建。
持久卷的創建和刪除
擴容一個實例,會創建一個 Pod 和對應的 PV、PVC 兩三個資源。但當收縮一個實例時,則只會刪除 Pod 而保留 PV、PVC,原因不必多說。
如果需要釋放對應的 PV、PVC 要手動來完成,但並不建議這么做。
重新掛載持久卷
因為縮容時 StatefulSet 保留了 PV、PVC,所以在隨后的擴容中,新的 Pod 會期望使用之前的數據,也就會使用之前保留下來的 PV、PVC。
4、StatefulSet 的保障
相比 RS,StatefulSet 不僅擁有穩定的標記和獨立的存儲,還有一些其他的保障。
通常來說,無狀態的 Pod 可以被替代,但有狀態的 Pod 只能被消滅和復活。
但是,當 Pod 的狀態不能確定呢?如果 StatefulSet 又創建了一個一模一樣的 Pod 呢?
這也就意味着兩個完全一致的 Pod 會綁定同一個 PV,並同時寫相同的文件。
所以 K8S 要確保上述問題不會發生,也就要保證有狀態的 Pod 實例是 at-most-one
語義。
10.3 使用 StatefulSet
要使用 StatefulSet,需要以下幾種資源:
- 存儲數據的持久卷
- 一個控制的 Service
- StatefulSet 本身
1、手動創建 PV
分別創建 pv-a、pv-b、pv-c 三個持久卷,這里不墨跡了。
2、創建控制 Service
創建用於在有狀態的 Pod 之間提供網絡標識的 headlessService。
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None # 之前講過 headless 模式的 SVC
selector:
app: kubia
ports:
- name: http # 使用名字代理 Pod 端口
port: 80
3、創建 StatefulSet
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: kubia
spec:
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
volumeClaimTemplates: # 持久卷聲明模板,注意是 list 類型
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce
StatefulSet 創建的 Pod 會線性的緩慢的逐個創建。只有當前一個 Pod 運行並處於就緒狀態才會繼續向后創建 Pod。
4、使用服務
現在有狀態的集群已經啟動成功,但之前創建的 SVC 是 headless 模式,所以不能通過它來訪問我們的 Pod,而是要直接連接每個單獨的 Pod 來訪問。
方式一:通過 API 服務器通信
可以利用 kubectl proxy
開啟代理服務,跳過身份驗證后請求這個 URL:
<apiServerHost>:<port>/api/vl/namespaces/default/pods/kubia-0/proxy/<path>
不過這種方式會造成不必要的網絡跳轉,示意圖如下:
方式二:使用非 headless 的 SVC 暴露 Pod
就和我們掌握的普通 SVC 一樣,代理之前 StatefulSet 創建的所有 Pod。對於客戶端的所有連接,也會被 SVC 自身的負載均衡隨機分配到任意 Pod。
apiVersion: v1
kind: Service
metadata:
name: kubia-public
spec:
selector:
app: kubia
ports:
- name: http
port: 80
10.4 在 StatefulSet 中發現伙伴節點
對於集群型應用,通常需要和伙伴節點彼此通信和發現(如 Spark集群要配置主機名列表、分布式數據庫要知道各節點的 IP)
需要知道,請求 headless 類型的 SVC,會返回一個記錄着所有 Pod 主機和地址的 SRV 記錄。
如果用戶不想通過 kubia-public
輪詢訪問 Pod,而是想獲得所有 Pod 的數據,就必須發送多次請求,並且包含所有 Pod 才行。
10.5 了解 StatefulSet 如何處理節點失效
我們知道 K8S 必須保證每個有狀態的 Pod 都是獨特且唯一的。 因此 StatefulSet 要保證不會有兩個擁有完全相同標記和存儲的 Pod 在同時運行。
這就需要確保在一個有狀態 Pod 完全停止或未確定是否停止前,都不能創建它的替代者。
1、模擬一個節點的網絡斷開
在非 minikube 的環境可以嘗試關閉網絡適配器來模擬網絡中斷情況。
當節點的網絡中斷后,在此節點上的 Kubelet 服務就無法與 API 服務器通信,也就無法匯報本節點的健康情況以及節點資源的情況。
一段時間后,就可以在控制台看到中斷節點的狀態被置為NotReady
。
同時,可以看到位於中斷節點上的Pod的狀態被置為Unknown
。
如果網絡重新連通,Kubelet 會重新匯報它所在的節點狀態和資源狀態,一切回到沒有中斷之前的狀態。
如果網絡中斷幾分鍾后,還沒有連通。則 Unknown
標記的 Pod 會被標記為 Deleting
。
但這個 Pod 所在的節點網絡中斷,也就意味着負責和 API 服務器通信的 Kubelet 也無法與主節點通信,就會讓導致這個節點上的 Pod 偷偷的存活下去。
2、手動刪除 Pod
如果已經知道網絡中斷一時之間無法恢復,並且要保證服務可用的實例數量,我們就需要把 Pod 重新調度到其他節點。
按照之前所學正常的刪除 Pod 會出現前面說的問題(Pod 還在離線的節點運行),所以需要強制刪除才可以。
$ kubectl delete po kubia-0 --force --grace-period 0
進階篇小結
關於 K8S 的進階資源就介紹到這里,通過本篇的學習,就可以搭建出一個可持久化,可升級的有狀態應用集群了,這在簡單項目的運維和生產中已經足夠了。
避免篇幅太長,方便回顧閱讀,感謝大家閱讀,希望大佬指正。