引言
上一篇文章我們圍繞如何合理利用資源的主題做了一些最佳實踐的分享,這一次我們就如何提高服務可用性的主題來展開探討,
怎樣提高我們部署服務的可用性呢?K8S 設計本身就考慮到了各種故障的可能性,并提供了一些自愈機制以提高系統的容錯性,但有些情況還是可能導致較長時間不可用,拉低服務可用性的指標,本文將結合生產實踐經驗,為大家提供一些最佳實踐來最大化的提高服務可用性,
如何避免單點故障?
K8S 的設計就是假設節點是不可靠的,節點越多,發生軟硬體故障導致節點不可用的幾率就越高,所以我們通常需要給服務部署多個副本,根據實際情況調整 replicas 的值,如果值為 1 就必然存在單點故障,如果大于 1 但所有副本都調度到同一個節點了,那還是有單點故障,有時候還要考慮到災難,比如整個機房不可用,
所以我們不僅要有合理的副本數量,還需要讓這些不同副本調度到不同的拓撲域(節點、可用區),打散調度以避免單點故障,這個可以利用 Pod 反親和性來做到,反親和主要分強反親和與弱反親和兩種,更多親和與反親和資訊可參考官方檔案Affinity and anti-affinity,
先來看個強反親和的示例,將 DNS 服務強制打散調度到不同節點上:
affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: k8s-app operator: In values: - kube-dns topologyKey: kubernetes.io/hostname
labelSelector.matchExpressions寫該服務對應 pod 中 labels 的 key 與 value,因為 Pod 反親和性是通過判斷 replicas 的 pod label 來實作的,topologyKey指定反親和的拓撲域,即節點 label 的 key,這里用的kubernetes.io/hostname表示避免 pod 調度到同一節點,如果你有更高的要求,比如避免調度到同一個可用區,實作異地多活,可以用failure-domain.beta.kubernetes.io/zone,通常不會去避免調度到同一個地域,因為一般同一個集群的節點都在一個地域,如果跨地域,即使用專線時延也會很大,所以topologyKey一般不至于用failure-domain.beta.kubernetes.io/region,requiredDuringSchedulingIgnoredDuringExecution調度時必須滿足該反親和性條件,如果沒有節點滿足條件就不調度到任何節點 (Pending),
如果不用這種硬性條件可以使用 preferredDuringSchedulingIgnoredDuringExecution 來指示調度器盡量滿足反親和性條件,即弱反親和性,如果實在沒有滿足條件的,只要節點有足夠資源,還是可以讓其調度到某個節點,至少不會 Pending,
我們再來看個弱反親和的示例:
affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: k8s-app operator: In values: - kube-dns topologyKey: kubernetes.io/hostname
注意到了嗎?相比強反親和有些不同哦,多了一個 weight,表示此匹配條件的權重,而匹配條件被挪到了 podAffinityTerm 下面,
如何避免節點維護或升級時導致服務不可用?
有時候我們需要對節點進行維護或進行版本升級等操作,操作之前需要對節點執行驅逐 (kubectl drain),驅逐時會將節點上的 Pod 進行洗掉,以便它們漂移到其它節點上,當驅逐完畢之后,節點上的 Pod 都漂移到其它節點了,這時我們就可以放心的對節點進行操作了,
有一個問題就是,驅逐節點是一種有損操作,驅逐的原理:
- 封鎖節點 (設為不可調度,避免新的 Pod 調度上來),
- 將該節點上的 Pod 洗掉,
- ReplicaSet 控制器檢測到 Pod 減少,會重新創建一個 Pod,調度到新的節點上,
這個程序是先洗掉,再創建,并非是滾動更新,因此更新程序中,如果一個服務的所有副本都在被驅逐的節點上,則可能導致該服務不可用,
我們再來下什么情況下驅逐會導致服務不可用:
- 服務存在單點故障,所有副本都在同一個節點,驅逐該節點時,就可能造成服務不可用,
- 服務沒有單點故障,但剛好這個服務涉及的 Pod 全部都部署在這一批被驅逐的節點上,所以這個服務的所有 Pod 同時被刪,也會造成服務不可用,
- 服務沒有單點故障,也沒有全部部署到這一批被驅逐的節點上,但驅逐時造成這個服務的一部分 Pod 被刪,短時間內服務的處理能力下降導致服務過載,部分請求無法處理,也就降低了服務可用性,
針對第一點,我們可以使用前面講的反親和性來避免單點故障,
針對第二和第三點,我們可以通過配置 PDB (PodDisruptionBudget) 來避免所有副本同時被洗掉,驅逐時 K8S 會 "觀察" nginx 的當前可用與期望的副本數,根據定義的 PDB 來控制 Pod 洗掉速率,達到閥值時會等待 Pod 在其它節點上啟動并就緒后再繼續洗掉,以避免同時洗掉太多的 Pod 導致服務不可用或可用性降低,下面給出兩個示例,
示例一 (保證驅逐時 nginx 至少有 90% 的副本可用):
apiVersion: policy/v1beta1kind: PodDisruptionBudgetmetadata: name: zk-pdbspec: minAvailable: 90% selector: matchLabels: app: zookeeper
示例二 (保證驅逐時 zookeeper 最多有一個副本不可用,相當于逐個洗掉并等待在其它節點完成重建):
apiVersion: policy/v1beta1kind: PodDisruptionBudgetmetadata: name: zk-pdbspec: maxUnavailable: 1 selector: matchLabels: app: zookeeper
如何讓服務進行平滑更新?
解決了服務單點故障和驅逐節點時導致的可用性降低問題后,我們還需要考慮一種可能導致可用性降低的場景,那就是滾動更新,為什么服務正常滾動更新也可能影響服務的可用性呢?別急,下面我來解釋下原因,
假如集群記憶體在服務間呼叫:

當 server 端發生滾動更新時:

發生兩種尷尬的情況:
- 舊的副本很快銷毀,而 client 所在節點 kube-proxy 還沒更新完轉發規則,仍然將新連接調度給舊副本,造成連接例外,可能會報 "connection refused" (行程停止程序中,不再接受新請求) 或 "no route to host" (容器已經完全銷毀,網卡和 IP 已不存在),
- 新副本啟動,client 所在節點 kube-proxy 很快 watch 到了新副本,更新了轉發規則,并將新連接調度給新副本,但容器內的行程啟動很慢 (比如 Tomcat 這種 java 行程),還在啟動程序中,埠還未監聽,無法處理連接,也造成連接例外,通常會報 "connection refused" 的錯誤,
針對第一種情況,可以給 container 加 preStop,讓 Pod 真正銷毀前先 sleep 等待一段時間,等待 client 所在節點 kube-proxy 更新轉發規則,然后再真正去銷毀容器,這樣能保證在 Pod Terminating 后還能繼續正常運行一段時間,這段時間如果因為 client 側的轉發規則更新不及時導致還有新請求轉發過來,Pod 還是可以正常處理請求,避免了連接例外的發生,聽起來感覺有點不優雅,但實際效果還是比較好的,分布式的世界沒有銀彈,我們只能盡量在當前設計現狀下找到并實踐能夠解決問題的最優解,
針對第二種情況,可以給 container 加 ReadinessProbe (就緒檢查),讓容器內行程真正啟動完成后才更新 Service 的 Endpoint,然后 client 所在節點 kube-proxy 再更新轉發規則,讓流量進來,這樣能夠保證等 Pod 完全就緒了才會被轉發流量,也就避免了鏈接例外的發生,
最佳實踐 yaml 示例:
readinessProbe: httpGet: path: /healthz port: 80 httpHeaders: - name: X-Custom-Header value: Awesome initialDelaySeconds: 10 timeoutSeconds: 1 lifecycle: preStop: exec: command: ["/bin/bash", "-c", "sleep 10"]
更多資訊請參考 Specifying a Disruption Budget for your Application ,
健康檢查怎么配才好?
我們都知道,給 Pod 配置健康檢查也是提高服務可用性的一種手段,配置 ReadinessProbe (就緒檢查) 可以避免將流量轉發給還沒啟動完全或出現例外的 Pod;配置 LivenessProbe (存活檢查) 可以讓存在 bug 導致死鎖或 hang 住的應用重啟來恢復,但是,如果配置配置不好,也可能引發其它問題,這里根據一些踩坑經驗總結了一些指導性的建議:
- 不要輕易使用 LivenessProbe,除非你了解后果并且明白為什么你需要它,參考 Liveness Probes are Dangerous
- 如果使用 LivenessProbe,不要和 ReadinessProbe 設定成一樣 (failureThreshold 更大)
- 探測邏輯里不要有外部依賴 (db, 其它 pod 等),避免抖動導致級聯故障
- 業務程式應盡量暴露 HTTP 探測介面來適配健康檢查,避免使用 TCP 探測,因為程式 hang 死時, TCP 探測仍然能通過 (TCP 的 SYN 包探測埠是否存活在內核態完成,應用層不感知)
【騰訊云原生】云說新品、云研新術、云游新活、云賞資訊,掃碼關注同名公眾號,及時獲取更多干貨!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/3049.html
標籤:其他

