在 Kubernetes 容器集群,微服務(wù)項目最佳實踐
本文主要介紹我個人在使用 Kubernetes 的過程中,總結(jié)出的一套「Kubernetes 配置」,是我個人的「最佳實踐」。其中大部分內(nèi)容都經(jīng)歷過線上環(huán)境的考驗,但是也有少部分還只在我腦子里模擬過,請謹(jǐn)慎參考。
閱讀前的幾個注意事項:
這份文檔比較長,囊括了很多內(nèi)容,建議當(dāng)成參考手冊使用,先參照目錄簡單讀一讀,有需要再細(xì)讀相關(guān)內(nèi)容。 這份文檔需要一定的 Kubernetes 基礎(chǔ)才能理解,而且如果沒有過實踐經(jīng)驗的話,看上去可能會比較枯燥。 而有過實踐經(jīng)驗的大佬,可能會跟我有不同的見解,歡迎各路大佬評論~
首先,這里給出一些本文遵守的前提,這些前提只是契合我遇到的場景,可靈活變通:
這里只討論無狀態(tài)服務(wù),有狀態(tài)服務(wù)不在討論范圍內(nèi) 我們不使用 Deployment 的滾動更新能力,而是為每個服務(wù)的每個版本,都創(chuàng)建不同的 Deployment + HPA + PodDisruptionBudget,這是為了方便做金絲雀/灰度發(fā)布 我們的服務(wù)可能會使用 IngressController / Service Mesh 來進(jìn)行服務(wù)的負(fù)載均衡、流量切分
下面先給出一個 Deployment + HPA + PodDisruptionBudget 的 demo,后面再拆開詳細(xì)說下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-v3
namespace: prod # 建議按業(yè)務(wù)邏輯劃分名字空間,prod 僅為示例
labels:
app: my-app
spec:
replicas: 3
strategy:
type: RollingUpdate
# 因為服務(wù)的每個版本都使用各自的 Deployment,服務(wù)更新時其實是用不上這里的滾動更新策略的
# 這個配置應(yīng)該只在 SRE 手動修改 Deployment 配置時才會生效(通常不應(yīng)該發(fā)生這種事)
rollingUpdate:
maxSurge: 10% # 滾動更新時,每次最多更新 10% 的 Pods
maxUnavailable: 0 # 滾動更新時,不允許出現(xiàn)不可用的 Pods,也就是說始終要維持 3 個可用副本
selector:
matchLabels:
app: my-app
version: v3
template:
metadata:
labels:
app: my-app
version: v3
spec:
affinity:
# 注意,podAffinity/podAntiAffinity 可能不是最佳方案,這部分配置待更新
# topologySpreadConstraints 可能是更好的選擇
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution: # 非強(qiáng)制性條件
- weight: 100 # weight 用于為節(jié)點評分,會優(yōu)先選擇評分最高的節(jié)點(只有一條規(guī)則的情況下,這個值沒啥意義)
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- my-app
- key: version
operator: In
values:
- v3
# pod 盡量使用同一種節(jié)點類型,也就是盡量保證節(jié)點的性能一致
topologyKey: node.kubernetes.io/instance-type
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution: # 非強(qiáng)制性條件
- weight: 100 # weight 用于為節(jié)點評分,會優(yōu)先選擇評分最高的節(jié)點(只有一條規(guī)則的情況下,這個值沒啥意義)
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- my-app
- key: version
operator: In
values:
- v3
# 將 pod 盡量打散在多個可用區(qū)
topologyKey: topology.kubernetes.io/zone
requiredDuringSchedulingIgnoredDuringExecution: # 強(qiáng)制性要求(這個建議按需添加)
# 注意這個沒有 weights,必須滿足列表中的所有條件
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- my-app
- key: version
operator: In
values:
- v3
# Pod 必須運行在不同的節(jié)點上
topologyKey: kubernetes.io/hostname
securityContext:
# runAsUser: 1000 # 設(shè)定用戶
# runAsGroup: 1000 # 設(shè)定用戶組
runAsNonRoot: true # Pod 必須以非 root 用戶運行
seccompProfile: # security compute mode
type: RuntimeDefault
nodeSelector:
nodegroup: common # 使用專用節(jié)點組,如果希望使用多個節(jié)點組,可改用節(jié)點親和性
volumes:
- name: tmp-dir
emptyDir: {}
containers:
- name: my-app-v3
image: my-app:v3 # 建議使用私有鏡像倉庫,規(guī)避 docker.io 的鏡像拉取限制
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /tmp
name: tmp-dir
lifecycle:
preStop: # 在容器被 kill 之前執(zhí)行
exec:
command:
- /bin/sh
- -c
- "while [ $(netstat -plunt | grep tcp | wc -l | xargs) -ne 0 ]; do sleep 1; done"
resources: # 資源請求與限制
# 對于核心服務(wù),建議設(shè)置 requests = limits,避免資源競爭
requests:
# HPA 會使用 requests 計算資源利用率
# 建議將 requests 設(shè)為服務(wù)正常狀態(tài)下的 CPU 使用率,HPA 的目前指標(biāo)設(shè)為 80%
# 所有容器的 requests 總量不建議為 2c/4G 4c/8G 等常見值,因為節(jié)點通常也是這個配置,這會導(dǎo)致 Pod 只能調(diào)度到更大的節(jié)點上,適當(dāng)調(diào)小 requests 等擴(kuò)充可用的節(jié)點類型,從而擴(kuò)充節(jié)點池。
cpu: 1000m
memory: 1Gi
limits:
# limits - requests 為允許超賣的資源量,建議為 requests 的 1 到 2 倍,酌情配置。
cpu: 1000m
memory: 1Gi
securityContext:
# 將容器層設(shè)為只讀,防止容器文件被篡改
## 如果需要寫入臨時文件,建議額外掛載 emptyDir 來提供可讀寫的數(shù)據(jù)卷
readOnlyRootFilesystem: true
# 禁止 Pod 做任何權(quán)限提升
allowPrivilegeEscalation: false
capabilities:
# drop ALL 的權(quán)限比較嚴(yán)格,可按需修改
drop:
- ALL
startupProbe: # 要求 kubernetes 1.18+
httpGet:
path: /actuator/health # 直接使用健康檢查接口即可
port: 8080
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 20 # 最多提供給服務(wù) 5s * 20 的啟動時間
successThreshold: 1
livenessProbe:
httpGet:
path: /actuator/health # spring 的通用健康檢查路徑
port: 8080
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 5
successThreshold: 1
# Readiness probes are very important for a RollingUpdate to work properly,
readinessProbe:
httpGet:
path: /actuator/health # 簡單起見可直接使用 livenessProbe 相同的接口,當(dāng)然也可額外定義
port: 8080
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 5
successThreshold: 1
---
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
labels:
app: my-app
name: my-app-v3
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app-v3
maxReplicas: 50
minReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-v3
namespace: prod
labels:
app: my-app
spec:
minAvailable: 75%
selector:
matchLabels:
app: my-app
version: v3
一、優(yōu)雅停止(Gracful Shutdown)與 502/504 報錯
如果 Pod 正在處理大量請求(比如 1000 QPS+)時,因為節(jié)點故障或「競價節(jié)點」被回收等原因被重新調(diào)度, 你可能會觀察到在容器被 terminate 的一段時間內(nèi)出現(xiàn)少量 502/504。
為了搞清楚這個問題,需要先理解清楚 terminate 一個 Pod 的流程:
Pod 的狀態(tài)被設(shè)為 Terminating,(幾乎)同時該 Pod 被從所有關(guān)聯(lián)的 Service Endpoints 中移除preStop鉤子被執(zhí)行它的執(zhí)行階段很好理解:在容器被 stop 之前執(zhí)行 它可以是一個命令,或者一個對 Pod 中容器的 http 調(diào)用 如果在收到 SIGTERM 信號時,無法優(yōu)雅退出,要支持優(yōu)雅退出比較麻煩的話,用 preStop實現(xiàn)優(yōu)雅退出是一個非常好的方式preStop 的定義位置:https://github.com/kubernetes/api/blob/master/core/v1/types.go#L2515 preStop執(zhí)行完畢后,SIGTERM 信號被發(fā)送給 Pod 中的所有容器繼續(xù)等待,直到容器停止,或者超時 spec.terminationGracePeriodSeconds,這個值默認(rèn)為 30s需要注意的是,這個優(yōu)雅退出的等待計時是與 preStop同步開始的!而且它也不會等待preStop結(jié)束!如果超過了 spec.terminationGracePeriodSeconds容器仍然沒有停止,k8s 將會發(fā)送 SIGKILL 信號給容器進(jìn)程全部終止后,整個 Pod 完全被清理掉 Docker+K8s+Jenkins 主流技術(shù)全解視頻資料【干貨免費分享】
注意:1 跟 2 兩個工作是異步發(fā)生的,所以在未設(shè)置 preStop 時,可能會出現(xiàn)「Pod 還在 Service Endpoints 中,但是 SIGTERM 已經(jīng)被發(fā)送給 Pod 導(dǎo)致容器都掛掉」的情況,我們需要考慮到這種狀況的發(fā)生。
了解了上面的流程后,我們就能分析出兩種錯誤碼出現(xiàn)的原因:
502:應(yīng)用程序在收到 SIGTERM 信號后直接終止了運行,導(dǎo)致部分還沒有被處理完的請求直接中斷,代理層返回 502 表示這種情況 504:Service Endpoints 移除不夠及時,在 Pod 已經(jīng)被終止后,仍然有個別請求被路由到了該 Pod,得不到響應(yīng)導(dǎo)致 504
通常的解決方案是,在 Pod 的 preStop 步驟加一個 15s 的等待時間。其原理是:在 Pod 處理 terminating 狀態(tài)的時候,就會被從 Service Endpoints 中移除,也就不會再有新的請求過來了。在 preStop 等待 15s,基本就能保證所有的請求都在容器死掉之前被處理完成(一般來說,絕大部分請求的處理時間都在 300ms 以內(nèi)吧)。
一個簡單的示例如下,它使 Pod 被 Terminate 時,總是在 stop 前先等待 15s,再發(fā)送 SIGTERM 信號給容器:
containers:
- name: my-app
# 添加下面這部分
lifecycle:
preStop:
exec:
command:
- /bin/sleep
- "15"
更好的解決辦法,是直接等待所有 tcp 連接都關(guān)閉(需要鏡像中有 netstat):
containers:
- name: my-app
# 添加下面這部分
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "while [ $(netstat -plunt | grep tcp | wc -l | xargs) -ne 0 ]; do sleep 1; done"
如果我的服務(wù)還使用了 Sidecar 代理網(wǎng)絡(luò)請求,該怎么處理?
以服務(wù)網(wǎng)格 Istio 為例,在 Envoy 代理了 Pod 流量的情況下,502/504 的問題會變得更復(fù)雜一點——還需要考慮 Sidecar 與主容器的關(guān)閉順序:
如果在 Envoy 已關(guān)閉后,有新的請求再進(jìn)來,將會導(dǎo)致 504(沒人響應(yīng)這個請求了) 所以 Envoy 最好在 Terminating 至少 3s 后才能關(guān),確保 Istio 網(wǎng)格配置已完全更新 如果在 Envoy 還沒停止時,主容器先關(guān)閉,然后又有新的請求再進(jìn)來,Envoy 將因為無法連接到 upstream 導(dǎo)致 503 所以主容器也最好在 Terminating 至少 3s 后,才能關(guān)閉。 如果主容器處理還未處理完遺留請求時,Envoy 或者主容器的其中一個停止了,會因為 tcp 連接直接斷開連接導(dǎo)致 502 因此 Envoy 必須在主容器處理完遺留請求后(即沒有 tcp 連接時),才能關(guān)閉
所以總結(jié)下:Envoy 及主容器的 preStop 都至少得設(shè)成 3s,并且在「沒有 tcp 連接」時,才能關(guān)閉,避免出現(xiàn) 502/503/504.
主容器的修改方法在前文中已經(jīng)寫過了,下面介紹下 Envoy 的修改方法。
和主容器一樣,Envoy 也能直接加 preStop,修改 istio-sidecar-injector 這個 configmap,在 sidecar 里添加 preStop sleep 命令:
containers:
- name: istio-proxy
# 添加下面這部分
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "while [ $(netstat -plunt | grep tcp | grep -v envoy | wc -l | xargs) -ne 0 ]; do sleep 1; done"
參考
Kubernetes best practices: terminating with grace Graceful shutdown in Kubernetes is not always trivial
二、服務(wù)的伸縮配置 - HPA
Kubernetes 官方主要支持基于 Pod CPU 的伸縮,這是應(yīng)用最為廣泛的伸縮指標(biāo),需要部署 metrics-server 才可使用。
先回顧下前面給出的,基于 Pod CPU 使用率進(jìn)行伸縮的示例:
apiVersion: autoscaling/v2beta2 # k8s 1.23+ 此 API 已經(jīng) GA
kind: HorizontalPodAutoscaler
metadata:
labels:
app: my-app
name: my-app-v3
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app-v3
maxReplicas: 50
minReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
1. 當(dāng)前指標(biāo)值的計算方式
提前總結(jié):每個 Pod 的指標(biāo)是其中所有容器指標(biāo)之和,如果計算百分比,就再除以 Pod 的 requests.
HPA 默認(rèn)使用 Pod 的當(dāng)前指標(biāo)進(jìn)行計算,以 CPU 使用率為例,其計算公式為:
「Pod 的 CPU 使用率」= 100% * 「所有 Container 的 CPU 用量之和」/「所有 Container 的 CPU requests 之和」
注意分母是總的 requests 量,而不是 limits.
1.1 存在的問題與解決方法
在 Pod 只有一個容器時這沒啥問題,但是當(dāng) Pod 注入了 envoy 等 sidecar 時,這就會有問題了。
因為 Istio 的 Sidecar requests 默認(rèn)為 100m 也就是 0.1 核。在未 tuning 的情況下,服務(wù)負(fù)載一高,sidecar 的實際用量很容易就能漲到 0.2-0.4 核。把這兩個值代入前面的公式,會發(fā)現(xiàn) 對于 QPS 較高的服務(wù),添加 Sidecar 后,「Pod 的 CPU 利用率」可能會高于「應(yīng)用容器的 CPU 利用率」,造成不必要的擴(kuò)容。
即使改用「Pod 的 CPU 用量」而非百分比來進(jìn)行擴(kuò)縮容,也解決不了這個問題。
解決方法:
最佳解決方案:使用絕對度量指標(biāo),而非百分比。 方法一:針對每個服務(wù)的 CPU 使用情況,為每個服務(wù)的 sidecar 設(shè)置不同的 requests/limits. 感覺這個方案太麻煩了 方法二:使用 KEDA 等第三方組件,獲取到應(yīng)用程序的 CPU 利用率(排除掉 Sidecar),使用它進(jìn)行擴(kuò)縮容 方法三:使用 k8s 1.20 提供的 alpha 特性:Container Resourse Metrics.
2. HPA 的擴(kuò)縮容算法
HPA 什么時候會擴(kuò)容,這一點是很好理解的。但是 HPA 的縮容策略,會有些迷惑,下面簡單分析下。
HPA 的「目標(biāo)指標(biāo)」可以使用兩種形式:絕對度量指標(biāo)和資源利用率。 絕對度量指標(biāo):比如 CPU,就是指 CPU 的使用量 資源利用率(資源使用量/資源請求 * 100%):在 Pod 設(shè)置了資源請求時,可以使用資源利用率進(jìn)行 Pod 伸縮 HPA 的「當(dāng)前指標(biāo)」是一段時間內(nèi)所有 Pods 的平均值,不是峰值。
HPA 的擴(kuò)縮容算法為:
期望副本數(shù) = ceil[當(dāng)前副本數(shù) * ( 當(dāng)前指標(biāo) / 目標(biāo)指標(biāo) )]
從上面的參數(shù)可以看到:
只要「當(dāng)前指標(biāo)」超過了目標(biāo)指標(biāo),就一定會發(fā)生擴(kuò)容。 當(dāng)前指標(biāo) / 目標(biāo)指標(biāo)要小到一定的程度,才會觸發(fā)縮容。比如雙副本的情況下,上述比值要小于等于 1/2,才會縮容到單副本。 三副本的情況下,上述比值的臨界點是 2/3。 五副本時臨界值是 4/5,100 副本時臨界值是 99/100,依此類推。 如果 當(dāng)前指標(biāo) / 目標(biāo)指標(biāo)從 1 降到 0.5,副本的數(shù)量將會減半。(雖然說副本數(shù)越多,發(fā)生這么大變化的可能性就越小。)當(dāng)前副本數(shù) / 目標(biāo)指標(biāo)的值越大,「當(dāng)前指標(biāo)」的波動對「期望副本數(shù)」的影響就越大。
為了防止擴(kuò)縮容過于敏感,HPA 有幾個相關(guān)參數(shù):
Hardcoded 參數(shù) HPA Loop 延時:默認(rèn) 15 秒,每 15 秒鐘進(jìn)行一次 HPA 掃描。 縮容冷卻時間:默認(rèn) 5 分鐘。 對于 K8s 1.18+,HPA 通過 spec.behavior提供了多種控制擴(kuò)縮容行為的參數(shù),后面會具體介紹。
3. HPA 的期望值設(shè)成多少合適
這個需要針對每個服務(wù)的具體情況,具體分析。
以最常用的按 CPU 值伸縮為例,
核心服務(wù) 需要注意 CPU 跟 Memory 的 limits 限制策略是不同的,CPU 是真正地限制了上限,而 Memory 是用超了就干掉容器(OOMKilled) k8s 一直使用 cgroups v1 ( cpu_shares/memory.limit_in_bytes)來限制 cpu/memory,但是對于Guaranteed的 Pods 而言,內(nèi)存并不能完全預(yù)留,資源競爭總是有可能發(fā)生的。1.22 有 alpha 特性改用 cgroups v2,可以關(guān)注下。requests/limits 值: 建議設(shè)成相等的,保證服務(wù)質(zhì)量等級為 Guaranteed HPA: 一般來說,期望值設(shè)為 60% 到 70% 可能是比較合適的,最小副本數(shù)建議設(shè)為 2 - 5. (僅供參考) PodDisruptionBudget: 建議按服務(wù)的健壯性與 HPA 期望值,來設(shè)置 PDB,后面會詳細(xì)介紹,這里就先略過了 非核心服務(wù) 也就是超賣了資源,這樣做主要的考量點是,很多非核心服務(wù)負(fù)載都很低,根本跑不到 limits 這么高,降低 requests 可以提高集群資源利用率,也不會損害服務(wù)穩(wěn)定性。 requests/limits 值: 建議 requests 設(shè)為 limits 的 0.6 - 0.9 倍(僅供參考),對應(yīng)的服務(wù)質(zhì)量等級為 Burstable HPA: 因為 requests 降低了,而 HPA 是以 requests 為 100% 計算使用率的,我們可以提高 HPA 的期望值(如果使用百分比為期望值的話),比如 80% ~ 90%,最小副本數(shù)建議設(shè)為 1 - 3. (僅供參考) PodDisruptionBudget: 非核心服務(wù)嘛,保證最少副本數(shù)為 1 就行了。
4. HPA 的常見問題
4.1. Pod 擴(kuò)容 - 預(yù)熱陷阱
預(yù)熱:Java/C# 這類運行在虛擬機(jī)上的語言,第一次使用到某些功能時,往往需要初始化一些資源,例如「JIT 即時編譯」。如果代碼里還應(yīng)用了動態(tài)類加載之類的功能,就很可能導(dǎo)致微服務(wù)某些 API 第一次被調(diào)用時,響應(yīng)特別慢(要動態(tài)編譯 class)。因此 Pod 在提供服務(wù)前,需要提前「預(yù)熱(slow_start)」一次這些接口,將需要用到的資源提前初始化好。
在負(fù)載很高的情況下,HPA 會自動擴(kuò)容。但是如果擴(kuò)容的 Pod 需要預(yù)熱,就可能會遇到「預(yù)熱陷阱」。
在有大量用戶訪問的時候,不論使用何種負(fù)載均衡策略,只要請求被轉(zhuǎn)發(fā)到新建的 Pod 上,這個請求就會「卡住」。如果請求速度太快,Pod 啟動的瞬間「卡住」的請求就越多,這將會導(dǎo)致新建 Pod 因為壓力過大而垮掉。然后 Pod 一重啟就被壓垮,進(jìn)入 CrashLoopBackoff 循環(huán)。
如果是在使用多線程做負(fù)載測試時,效果更明顯:50 個線程在不間斷地請求, 別的 Pod 響應(yīng)時間是「毫秒級」,而新建的 Pod 的首次響應(yīng)是「秒級」。幾乎是一瞬間,50 個線程就會全部陷在新建的 Pod 這里。而新建的 Pod 在啟動的瞬間可能特別脆弱,瞬間的 50 個并發(fā)請求就可以將它壓垮。然后 Pod 一重啟就被壓垮,進(jìn)入 CrashLoopBackoff 循環(huán)。
解決方法:
可以在「應(yīng)用層面」解決:
在啟動探針 API 的后端控制器里面,依次調(diào)用所有需要預(yù)熱的接口或者其他方式,提前初始化好所有資源。 啟動探針的控制器中,可以通過 localhost回環(huán)地址調(diào)用它自身的接口。使用「AOT 預(yù)編譯」技術(shù):預(yù)熱,通常都是因為「JIT 即時編譯」導(dǎo)致的問題,在需要用到時它才編譯。而 AOT 是預(yù)先編譯,在使用前完成編譯,因此 AOT 能解決預(yù)熱的問題。
也可以在「基礎(chǔ)設(shè)施層面」解決:
像 AWS ALB TargetGroup 以及其他云服務(wù)商的 ALB 服務(wù),通常都可以設(shè)置 slow_start時長,即對新加入的實例,使用一定時間慢慢地把流量切過去,最終達(dá)到預(yù)期的負(fù)載均衡狀態(tài)。這個可以解決服務(wù)預(yù)熱問題。Envoy 也已經(jīng)支持 slow_start模式,支持在一個設(shè)置好的時間窗口內(nèi),把流量慢慢負(fù)載到新加入的實例上,達(dá)成預(yù)熱效果。
4.2. HPA 擴(kuò)縮容過于敏感,導(dǎo)致 Pod 數(shù)量震蕩
通常來講,K8s 上絕大部分負(fù)載都應(yīng)該選擇使用 CPU 進(jìn)行擴(kuò)縮容。因為 CPU 通常能很好的反映服務(wù)的負(fù)載情況
但是有些服務(wù)會存在其他影響 CPU 使用率的因素,導(dǎo)致使用 CPU 擴(kuò)縮容變得不那么可靠,比如:
有些 Java 服務(wù)堆內(nèi)存設(shè)得很大,GC pause 也設(shè)得比較長,因此內(nèi)存 GC 會造成 CPU 間歇性飆升,CPU 監(jiān)控會有大量的尖峰。 有些服務(wù)有定時任務(wù),定時任務(wù)一運行 CPU 就漲,但是這跟服務(wù)的 QPS 是無關(guān)的 有些服務(wù)可能一運行 CPU 就會立即處于一個高位狀態(tài),它可能希望使用別的業(yè)務(wù)側(cè)指標(biāo)來進(jìn)行擴(kuò)容,而不是 CPU.
因為上述問題存在,使用 CPU 擴(kuò)縮容,就可能會造成服務(wù)頻繁的擴(kuò)容然后縮容,或者無限擴(kuò)容。而有些服務(wù)(如我們的「推薦服務(wù)」),對「擴(kuò)容」和「縮容」都是比較敏感的,每次擴(kuò)縮都會造成服務(wù)可用率抖動。
對這類服務(wù)而言,HPA 有這幾種調(diào)整策略:
選擇使用 QPS 等相對比較平滑,沒有 GC 這類干擾的指標(biāo)來進(jìn)行擴(kuò)縮容,這需要借助 KEDA 等社區(qū)組件。 對 kubernetes 1.18+,可以直接使用 HPA 的 behavior.scaleDown和behavior.scaleUp兩個參數(shù),控制每次擴(kuò)縮容的最多 pod 數(shù)量或者比例。示例如下:
---
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: podinfo
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: podinfo
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # 期望的 CPU 平均值
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # 默認(rèn)為 0,只使用當(dāng)前值進(jìn)行擴(kuò)縮容
policies:
- periodSeconds: 180 # 每 3 分鐘最多擴(kuò)容 5% 的 Pods
type: Percent
value: 5
- periodSeconds: 60 # 每分鐘最多擴(kuò)容 1 個 Pod,擴(kuò)的慢一點主要是為了一個個地預(yù)熱,避免一次擴(kuò)容太多未預(yù)熱的 Pods 導(dǎo)致服務(wù)可用率劇烈抖動
type: Pods
value: 1
selectPolicy: Min # 選擇最小的策略
# 以下的一切配置,都是為了更平滑地縮容
scaleDown:
stabilizationWindowSeconds: 600 # 使用過去 10 mins 的最大 cpu 值進(jìn)行縮容計算,避免過快縮容
policies:
- type: Percent # 每 3 mins 最多縮容 `ceil[當(dāng)前副本數(shù) * 5%]` 個 pod(20 個 pod 以內(nèi),一次只縮容 1 個 pod)
value: 5
periodSeconds: 180
- type: Pods # 每 1 mins 最多縮容 1 個 pod
value: 1
periodSeconds: 60
selectPolicy: Min # 上面的 policies 列表,只生效其中最小的值作為縮容限制(保證平滑縮容)
而對于擴(kuò)容不夠平滑這個問題,可以考慮提供類似 AWS ALB TargetGroup slow_start 的功能,在擴(kuò)容時緩慢將流量切到新 Pod 上,以實現(xiàn)預(yù)熱服務(wù)(JVM 預(yù)熱以及本地緩存預(yù)熱),這樣就能達(dá)到比較好的平滑擴(kuò)容效果。
5. HPA 注意事項
注意 kubectl 1.23 以下的版本,默認(rèn)使用 hpa.v1.autoscaling 來查詢 HPA 配置,v2beta2 相關(guān)的參數(shù)會被編碼到 metadata.annotations 中。
比如 behavior 就會被編碼到 autoscaling.alpha.kubernetes.io/behavior 這個 key 所對應(yīng)的值中。
因此如果使用了 v2beta2 的 HPA,一定要明確指定使用 v2beta2 版本的 HPA:
kubectl get hpa.v2beta2.autoscaling
否則不小心動到 annotations 中編碼的某些參數(shù),可能會產(chǎn)生意料之外的效果,甚至直接把控制面搞崩… 比如這個 issue: Nil pointer dereference in KCM after v1 HPA patch request
6. 參考
Pod 水平自動伸縮 - Kubernetes Docs Horizontal Pod Autoscaler 演練 - Kubernetes Docs
三、節(jié)點維護(hù)與 Pod 干擾預(yù)算
在我們通過 kubectl drain 將某個節(jié)點上的容器驅(qū)逐走的時候, kubernetes 會依據(jù) Pod 的「PodDistruptionBuget」來進(jìn)行 Pod 的驅(qū)逐。
如果不設(shè)置任何明確的 PodDistruptionBuget,Pod 將會被直接殺死,然后在別的節(jié)點重新調(diào)度,這可能導(dǎo)致服務(wù)中斷!
PDB 是一個單獨的 CR 自定義資源,示例如下:
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: podinfo-pdb
spec:
# 如果不滿足 PDB,Pod 驅(qū)逐將會失??!
minAvailable: 1 # 最少也要維持一個 Pod 可用
# maxUnavailable: 1 # 最大不可用的 Pod 數(shù),與 minAvailable 不能同時配置!二選一
selector:
matchLabels:
app: podinfo
如果在進(jìn)行節(jié)點維護(hù)時(kubectl drain),Pod 不滿足 PDB,drain 將會失敗,示例:
> kubectl drain node-205 --ignore-daemonsets --delete-local-data
node/node-205 cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/calico-node-nfhj7, kube-system/kube-proxy-94dz5
evicting pod default/podinfo-7c84d8c94d-h9brq
evicting pod default/podinfo-7c84d8c94d-gw6qf
error when evicting pod "podinfo-7c84d8c94d-h9brq" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/podinfo-7c84d8c94d-h9brq
error when evicting pod "podinfo-7c84d8c94d-h9brq" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/podinfo-7c84d8c94d-h9brq
error when evicting pod "podinfo-7c84d8c94d-h9brq" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/podinfo-7c84d8c94d-h9brq
pod/podinfo-7c84d8c94d-gw6qf evicted
pod/podinfo-7c84d8c94d-h9brq evicted
node/node-205 evicted
上面的示例中,podinfo 一共有兩個副本,都運行在 node-205 上面。我給它設(shè)置了干擾預(yù)算 PDB minAvailable: 1。
然后使用 kubectl drain 驅(qū)逐 Pod 時,其中一個 Pod 被立即驅(qū)逐走了,而另一個 Pod 大概在 15 秒內(nèi)一直驅(qū)逐失敗。因為第一個 Pod 還沒有在新的節(jié)點上啟動完成,它不滿足干擾預(yù)算 PDB minAvailable: 1 這個條件。
大約 15 秒后,最先被驅(qū)逐走的 Pod 在新節(jié)點上啟動完成了,另一個 Pod 滿足了 PDB 所以終于也被驅(qū)逐了。這才完成了一個節(jié)點的 drain 操作。
ClusterAutoscaler 等集群節(jié)點伸縮組件,在縮容節(jié)點時也會考慮 PodDisruptionBudget. 如果你的集群使用了 ClusterAutoscaler 等動態(tài)擴(kuò)縮容節(jié)點的組件,強(qiáng)烈建議設(shè)置為所有服務(wù)設(shè)置 PodDisruptionBudget.
在 PDB 中使用百分比的注意事項
在使用百分比時,計算出的實例數(shù)都會被向上取整,這會造成兩個現(xiàn)象:
如果使用 minAvailable,實例數(shù)較少的情況下,可能會導(dǎo)致 ALLOWED DISRUPTIONS 為 0,所有實例都無法被驅(qū)逐了。如果使用 maxUnavailable,因為是向上取整,ALLOWED DISRUPTIONS 的值一定不會低于 1,至少有 1 個實例可以被驅(qū)逐。
因此從「便于驅(qū)逐」的角度看,如果你的服務(wù)至少有 2-3 個實例,建議在 PDB 中使用百分比配置 maxUnavailable,而不是 minAvailable. 相對的從「確保服務(wù)穩(wěn)定性」的角度看,我們則應(yīng)該使用 minAvailable,確保至少有 1 個實例可用。
最佳實踐 Deployment + HPA + PodDisruptionBudget
一般而言,一個服務(wù)的每個版本,都應(yīng)該包含如下三個資源:
Deployment: 管理服務(wù)自身的 Pods 嘛 HPA: 負(fù)責(zé) Pods 的擴(kuò)縮容,通常使用 CPU 指標(biāo)進(jìn)行擴(kuò)縮容 PodDisruptionBudget(PDB): 建議按照 HPA 的目標(biāo)值,來設(shè)置 PDB. 比如 HPA CPU 目標(biāo)值為 60%,就可以考慮設(shè)置 PDB minAvailable=65%,保證至少有 65% 的 Pod 可用。這樣理論上極限情況下 QPS 均攤到剩下 65% 的 Pods 上也不會造成雪崩(這里假設(shè) QPS 和 CPU 是完全的線性關(guān)系)
四、節(jié)點親和性與節(jié)點組
我們一個集群,通常會使用不同的標(biāo)簽為節(jié)點組進(jìn)行分類,比如 kubernetes 自動生成的一些節(jié)點標(biāo)簽:
kubernetes.io/os: 通常都用linuxkubernetes.io/arch:amd64,arm64topology.kubernetes.io/region和topology.kubernetes.io/zone: 云服務(wù)的區(qū)域及可用區(qū)
我們使用得比較多的,是「節(jié)點親和性」以及「Pod 反親和性」,另外兩個策略視情況使用。
1. 節(jié)點親和性
如果你使用的是 aws,那 aws 有一些自定義的節(jié)點標(biāo)簽:
eks.amazonaws.com/nodegroup: aws eks 節(jié)點組的名稱,同一個節(jié)點組使用同樣的 aws ec2 實例模板比如 arm64 節(jié)點組、amd64/x64 節(jié)點組 內(nèi)存比例高的節(jié)點組如 m 系實例,計算性能高的節(jié)點組如 c 系列 競價實例節(jié)點組:這個省錢啊,但是動態(tài)性很高,隨時可能被回收 按量付費節(jié)點組:這類實例貴,但是穩(wěn)定。
假設(shè)你希望優(yōu)先選擇競價實例跑你的 Pod,如果競價實例暫時跑滿了,就選擇按量付費實例。那 nodeSelector 就滿足不了你的需求了,你需要使用 nodeAffinity,示例如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: xxx
namespace: xxx
spec:
# ...
template:
# ...
spec:
affinity:
nodeAffinity:
# 優(yōu)先選擇 spot-group-c 的節(jié)點
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
matchExpressions:
- key: eks.amazonaws.com/nodegroup
operator: In
values:
- spot-group-c
weight: 80 # weight 用于為節(jié)點評分,會優(yōu)先選擇評分最高的節(jié)點
- preference:
matchExpressions:
# 優(yōu)先選擇 aws c6i 的機(jī)器
- key: node.kubernetes.io/instance-type
operator: In
values:
- "c6i.xlarge"
- "c6i.2xlarge"
- "c6i.4xlarge"
- "c6i.8xlarge"
weight: 70
- preference:
matchExpressions:
# 其次選擇 aws c5 的機(jī)器
- key: node.kubernetes.io/instance-type
operator: In
values:
- "c5.xlarge"
- "c5.2xlarge"
- "c5.4xlarge"
- "c5.9xlarge"
weight: 60
# 如果沒 spot-group-c 可用,也可選擇 ondemand-group-c 的節(jié)點跑
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: eks.amazonaws.com/nodegroup
operator: In
values:
- spot-group-c
- ondemand-group-c
containers:
# ...
2. Pod 反親和性
Pod 親和性與反親和性可能不是最佳的實現(xiàn)手段,這部分內(nèi)容待更新
相關(guān) Issue: https://github.com/kubernetes/kubernetes/issues/72479
相關(guān)替代方案:https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/
通常建議為每個 Deployment 的 template 配置 Pod 反親和性,把 Pods 打散在所有節(jié)點上:
apiVersion: apps/v1
kind: Deployment
metadata:
name: xxx
namespace: xxx
spec:
# ...
template:
# ...
spec:
replicas: 3
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution: # 非強(qiáng)制性條件
- weight: 100 # weight 用于為節(jié)點評分,會優(yōu)先選擇評分最高的節(jié)點
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- xxx
- key: version
operator: In
values:
- v12
# 將 pod 盡量打散在多個可用區(qū)
topologyKey: topology.kubernetes.io/zone
requiredDuringSchedulingIgnoredDuringExecution: # 強(qiáng)制性要求
# 注意這個沒有 weights,必須滿足列表中的所有條件
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- xxx
- key: version
operator: In
values:
- v12
# Pod 必須運行在不同的節(jié)點上
topologyKey: kubernetes.io/hostname五、Pod 的就緒探針、存活探針與啟動探針
Pod 提供如下三種探針,均支持使用 Command、HTTP API、TCP Socket 這三種手段來進(jìn)行服務(wù)可用性探測。
startupProbe啟動探針(Kubernetes v1.18 [beta]): 此探針通過后,「就緒探針」與「存活探針」才會進(jìn)行存活性與就緒檢查startupProbe 顯然比 livenessProbe 的 initialDelaySeconds 參數(shù)更靈活。 同時它也能延遲 readinessProbe 的生效時間,這主要是為了避免無意義的探測。容器都還沒 startUp,顯然是不可能就緒的。 用于對慢啟動容器進(jìn)行存活性檢測,避免它們在啟動運行之前就被殺掉 程序?qū)⒆疃嘤? failureThreshold * periodSeconds的時間用于啟動,比如設(shè)置failureThreshold=20、periodSeconds=5,程序啟動時間最長就為 100s,如果超過 100s 仍然未通過「啟動探測」,容器會被殺死。readinessProbe就緒探針:就緒探針失敗次數(shù)超過 failureThreshold限制(默認(rèn)三次),服務(wù)將被暫時從 Service 的 Endpoints 中踢出,直到服務(wù)再次滿足successThreshold.livenessProbe存活探針: 檢測服務(wù)是否存活,它可以捕捉到死鎖等情況,及時殺死這種容器。kubectl describe pod會顯示重啟原因為State.Last State.Reason = Error, Exit Code=137,同時 Events 中會有Liveness probe failed: ...這樣的描述。服務(wù)發(fā)生死鎖,對所有請求均無響應(yīng) 服務(wù)線程全部卡在對外部 redis/mysql 等外部依賴的等待中,導(dǎo)致請求無響應(yīng) 存活探針失敗可能的原因: 存活探針失敗次數(shù)超過 failureThreshold限制(默認(rèn)三次),容器將被殺死,隨后根據(jù)重啟策略執(zhí)行重啟。
上述三類探測器的參數(shù)都是通用的,五個時間相關(guān)的參數(shù)列舉如下:
# 下面的值就是 k8s 的默認(rèn)值
initialDelaySeconds: 0 # 默認(rèn)沒有 delay 時間
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 3
successThreshold: 1
示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-v3
spec:
# ...
template:
# ...
spec:
containers:
- name: my-app-v3
image: xxx.com/app/my-app:v3
imagePullPolicy: IfNotPresent
# ... 省略若干配置
startupProbe:
httpGet:
path: /actuator/health # 直接使用健康檢查接口即可
port: 8080
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 20 # 最多提供給服務(wù) 5s * 20 的啟動時間
successThreshold: 1
livenessProbe:
httpGet:
path: /actuator/health # spring 的通用健康檢查路徑
port: 8080
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 5
successThreshold: 1
# Readiness probes are very important for a RollingUpdate to work properly,
readinessProbe:
httpGet:
path: /actuator/health # 簡單起見可直接使用 livenessProbe 相同的接口,當(dāng)然也可額外定義
port: 8080
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 5
successThreshold: 1
在 Kubernetes 1.18 之前,通用的手段是為「就緒探針」添加較長的 initialDelaySeconds 來實現(xiàn)類似「啟動探針」的功能動,避免容器因為啟動太慢,存活探針失敗導(dǎo)致容器被重啟。示例如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-v3
spec:
# ...
template:
# ...
spec:
containers:
- name: my-app-v3
image: xxx.com/app/my-app:v3
imagePullPolicy: IfNotPresent
# ... 省略若干配置
livenessProbe:
httpGet:
path: /actuator/health # spring 的通用健康檢查路徑
port: 8080
initialDelaySeconds: 120 # 前兩分鐘,都假設(shè)服務(wù)健康,避免 livenessProbe 失敗導(dǎo)致服務(wù)重啟
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 5
successThreshold: 1
# 容器一啟動,Readiness probes 就會不斷進(jìn)行檢測
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 3 # readiness probe 不需要設(shè)太長時間,使 Pod 盡快加入到 Endpoints.
periodSeconds: 5
timeoutSeconds: 1
failureThreshold: 5
successThreshold: 1六、Pod 安全
這里只介紹 Pod 中安全相關(guān)的參數(shù),其他諸如集群全局的安全策略,不在這里討論。
Pod SecurityContext
通過設(shè)置 Pod 的 SecurityContext,可以為每個 Pod 設(shè)置特定的安全策略。
SecurityContext 有兩種類型:
spec.securityContext: 這是一個 PodSecurityContext 對象顧名思義,它對 Pod 中的所有 contaienrs 都有效。 spec.containers[*].securityContext: 這是一個 SecurityContext 對象container 私有的 SecurityContext
這兩個 SecurityContext 的參數(shù)只有部分重疊,重疊的部分 spec.containers[*].securityContext 優(yōu)先級更高。
我們比較常遇到的一些提升權(quán)限的安全策略:
特權(quán)容器: spec.containers[*].securityContext.privileged添加(Capabilities)可選的系統(tǒng)級能力: spec.containers[*].securityContext.capabilities.add只有 ntp 同步服務(wù)等少數(shù)容器,可以開啟這項功能。請注意這非常危險。 Sysctls: 系統(tǒng)參數(shù): spec.securityContext.sysctls
權(quán)限限制相關(guān)的安全策略有(強(qiáng)烈建議在所有 Pod 上按需配置如下安全策略!):
spec.volumes: 所有的數(shù)據(jù)卷都可以設(shè)定讀寫權(quán)限spec.securityContext.runAsNonRoot: truePod 必須以非 root 用戶運行spec.containers[*].securityContext.readOnlyRootFileSystem:true將容器層設(shè)為只讀,防止容器文件被篡改。如果微服務(wù)需要讀寫文件,建議額外掛載 emptydir類型的數(shù)據(jù)卷。spec.containers[*].securityContext.allowPrivilegeEscalation: false不允許 Pod 做任何權(quán)限提升!spec.containers[*].securityContext.capabilities.drop: 移除(Capabilities)可選的系統(tǒng)級能力
還有其他諸如指定容器的運行用戶(user)/用戶組(group)等功能未列出,請自行查閱 Kubernetes 相關(guān)文檔。
一個無狀態(tài)的微服務(wù) Pod 配置舉例:
apiVersion: v1
kind: Pod
metadata:
name: <Pod name>
spec:
containers:
- name: <container name>
image: <image>
imagePullPolicy: IfNotPresent
# ......此處省略 500 字
securityContext:
readOnlyRootFilesystem: true # 將容器層設(shè)為只讀,防止容器文件被篡改。
allowPrivilegeEscalation: false # 禁止 Pod 做任何權(quán)限提升
capabilities:
drop:
# 禁止容器使用 raw 套接字,通常只有 hacker 才會用到 raw 套接字。
# raw_socket 可自定義網(wǎng)絡(luò)層數(shù)據(jù),避開 tcp/udp 協(xié)議棧,直接操作底層的 ip/icmp 數(shù)據(jù)包。可實現(xiàn) ip 偽裝、自定義協(xié)議等功能。
# 去掉 net_raw 會導(dǎo)致 tcpdump 無法使用,無法進(jìn)行容器內(nèi)抓包。需要抓包時可臨時去除這項配置
- NET_RAW
# 更好的選擇:直接禁用所有 capabilities
# - ALL
securityContext:
# runAsUser: 1000 # 設(shè)定用戶
# runAsGroup: 1000 # 設(shè)定用戶組
runAsNonRoot: true # Pod 必須以非 root 用戶運行
seccompProfile: # security compute mode
type: RuntimeDefault
2. seccomp: security compute mode
seccomp 和 seccomp-bpf 允許對系統(tǒng)調(diào)用進(jìn)行過濾,可以防止用戶的二進(jìn)制文對主機(jī)操作系統(tǒng)件執(zhí)行通常情況下并不需要的危險操作。它和 Falco 有些類似,不過 Seccomp 沒有為容器提供特別的支持。
視頻:
Seccomp: What Can It Do For You? - Justin Cormack, Docker
六、隔離性
推薦按業(yè)務(wù)線或者業(yè)務(wù)團(tuán)隊進(jìn)行名字空間劃分,方便對每個業(yè)務(wù)線/業(yè)務(wù)團(tuán)隊分別進(jìn)行資源限制 推薦使用 network policy 對服務(wù)實施強(qiáng)力的網(wǎng)絡(luò)管控,避免長期發(fā)展過程中,業(yè)務(wù)服務(wù)之間出現(xiàn)混亂的跨業(yè)務(wù)線相互調(diào)用關(guān)系,也避免服務(wù)被黑后,往未知地址發(fā)送數(shù)據(jù)。
其他問題
不同節(jié)點類型的性能有差距,導(dǎo)致 QPS 均衡的情況下,CPU 負(fù)載不均衡 盡量使用性能相同的實例類型:通過 podAffinity及nodeAffinity添加節(jié)點類型的親和性解決辦法(未驗證):
- END -
推薦閱讀 2022年最火的 Kubernetes 認(rèn)證 2022 年要考慮的 7 種 Docker 替代方案 Linux 運維工程師的 6 類好習(xí)慣和 23 個教訓(xùn) Keepalived+HAProxy 搭建高可用負(fù)載均衡 網(wǎng)絡(luò)丟包,網(wǎng)絡(luò)延遲?這款神器幫你搞定所有! 頂級 DevOps 工具鏈大盤點 某外企從 0 建設(shè) SRE 運維體系經(jīng)驗分享 Nginx+Redis:高性能緩存利器 主流監(jiān)控系統(tǒng) Prometheus 學(xué)習(xí)指南 基于 eBPF 的 Kubernetes 問題排查全景圖發(fā)布 一文掌握 Ansible 自動化運維 Linux的10個最危險命令 Kubernetes網(wǎng)絡(luò)難懂?可能是沒看到這篇文章 24 個 Docker 常見問題處理技巧 這篇文章帶你全面掌握 Nginx ! 一文搞懂 Kubernetes 網(wǎng)絡(luò)通信原理 搭建一套完整的企業(yè)級 K8s 集群(kubeadm方式) 點亮,服務(wù)器三年不宕機(jī)


