今天就跟大家聊聊有關(guān)應(yīng)用容器對Envoy Sidecar的啟動依賴問題怎么解決,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
10余年的德陽網(wǎng)站建設(shè)經(jīng)驗(yàn),針對設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時及時工作處理。成都全網(wǎng)營銷的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整德陽建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。成都創(chuàng)新互聯(lián)從事“德陽網(wǎng)站設(shè)計(jì)”,“德陽網(wǎng)站推廣”以來,每個客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
下面將介紹用戶從 Spring Cloud,Dubbo 等傳統(tǒng)微服務(wù)框架遷移到 Istio 服務(wù)網(wǎng)格時的一些經(jīng)驗(yàn),以及在使用 Istio 過程中可能遇到的一些常見問題的解決方法。
該問題的表現(xiàn)是安裝了 sidecar proxy 的應(yīng)用在啟動后的一小段時間內(nèi)無法通過網(wǎng)絡(luò)訪問 pod 外部的其他服務(wù),例如外部的 HTTP,MySQL,redis等服務(wù)。如果應(yīng)用沒有對依賴服務(wù)的異常進(jìn)行容錯處理,該問題還常常會導(dǎo)致應(yīng)用啟動失敗。下面我們以該問題導(dǎo)致的一個典型故障的分析過程為例對該問題的原因進(jìn)行說明。
典型案例:某運(yùn)維同學(xué)反饋:昨天晚上 Istio 環(huán)境中應(yīng)用的心跳檢測報 connect reset,然后服務(wù)重啟了。懷疑是 Istio 環(huán)境中網(wǎng)絡(luò)不穩(wěn)定導(dǎo)致了服務(wù)重啟。
根據(jù)運(yùn)維同學(xué)的反饋,該 pod 曾多次重啟。因此我們先用 kubectl logs --previous
命令查詢 awesome-app 容器最后一次重啟前的日志,以從日志中查找其重啟的原因。
kubectl logs --previous awesome-app-cd1234567-gzgwg -c awesome-app
從日志中查詢到了其重啟前最后的錯誤信息如下:
Logging system failed to initialize using configuration from 'http://log-config-server:12345/******/logback-spring.xml' java.net.ConnectException: Connection refused (Connection refused) at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
從錯誤信息可以得知,應(yīng)用進(jìn)程在啟動時試圖通過 HTTP 協(xié)議從配置中心拉取 logback 的配置信息,但該操作由于網(wǎng)絡(luò)異常失敗了,導(dǎo)致應(yīng)用進(jìn)程啟動失敗,最終導(dǎo)致容器重啟。
是什么導(dǎo)致了網(wǎng)絡(luò)異常呢?我們再用 Kubectl get pod
命令查詢 Pod 的運(yùn)行狀態(tài),嘗試找到更多的線索:
kubectl get pod awesome-app-cd1234567-gzgwg -oyaml
命令輸出的 pod 詳細(xì)內(nèi)容如下,該 yaml 片段省略了其他無關(guān)的細(xì)節(jié),只顯示了 lastState 和 state 部分的容器狀態(tài)信息。
containerStatuses: - containerID: lastState: terminated: containerID: exitCode: 1 finishedAt: 2020-09-01T13:16:23Z reason: Error startedAt: 2020-09-01T13:16:22Z name: awesome-app ready: true restartCount: 2 state: running: startedAt: 2020-09-01T13:16:36Z - containerID: lastState: {} name: istio-proxy ready: true restartCount: 0 state: running: startedAt: 2020-09-01T13:16:20Z hostIP: 10.0.6.161
從該輸出可以看到 pod 中的應(yīng)用容器 awesome-app 重啟了兩次。整理該 pod 中 awesome-app 應(yīng)用容器和 istio-proxy sidecar 容器的啟動和終止的時間順序,可以得到下面的時間線:
2020-09-01T13:16:20Z istio-proxy 啟動
2020-09-01T13:16:22Z awesome-app 上一次啟動時間
2020-09-01T13:16:23Z awesome-app 上一次異常退出時間
2020-09-01T13:16:36Z awesome-app 最后一次啟動,以后就一直正常運(yùn)行
可以看到在 istio-proxy 啟動2秒后,awesome-app 啟動,并于1秒后異常退出。結(jié)合前面的日志信息,我們知道這次啟動失敗的直接原因是應(yīng)用訪問配置中心失敗導(dǎo)致。在 istio-proxy 啟動16秒后,awesome-app 再次啟動,這次啟動成功,之后一直正常運(yùn)行。
istio-proxy 啟動和 awesome-app 上一次異常退出的時間間隔很短,只有2秒鐘,因此我們基本可以判斷此時 istio-proxy 尚未啟動初始化完成,導(dǎo)致 awesome-app 不能通過istio-proxy 連接到外部服務(wù),導(dǎo)致其啟動失敗。待 awesome-app 于 2020-09-01T13:16:36Z 再次啟動時,由于 istio-proxy 已經(jīng)啟動了較長時間,完成了從 pilot 獲取動態(tài)配置的過程,因此 awesome-app 向 pod 外部的網(wǎng)絡(luò)訪問就正常了。
如下圖所示,Envoy 啟動后會通過 xDS 協(xié)議向 pilot 請求服務(wù)和路由配置信息,Pilot 收到請求后會根據(jù) Envoy 所在的節(jié)點(diǎn)(pod或者VM)組裝配置信息,包括 Listener、Route、Cluster等,然后再通過 xDS 協(xié)議下發(fā)給 Envoy。根據(jù) Mesh 的規(guī)模和網(wǎng)絡(luò)情況,該配置下發(fā)過程需要數(shù)秒到數(shù)十秒的時間。由于初始化容器已經(jīng)在 pod 中創(chuàng)建了 Iptables rule 規(guī)則,因此這段時間內(nèi)應(yīng)用向外發(fā)送的網(wǎng)絡(luò)流量會被重定向到 Envoy ,而此時 Envoy 中尚沒有對這些網(wǎng)絡(luò)請求進(jìn)行處理的監(jiān)聽器和路由規(guī)則,無法對此進(jìn)行處理,導(dǎo)致網(wǎng)絡(luò)請求失敗。(關(guān)于 Envoy sidecar 初始化過程和 Istio 流量管理原理的更多內(nèi)容,可以參考這篇文章 Istio流量管理實(shí)現(xiàn)機(jī)制深度解析)
從前面的分析可以得知,該問題的根本原因是由于應(yīng)用進(jìn)程對 Envoy sidecar 配置初始化的依賴導(dǎo)致的。因此最直接的解決思路就是:在應(yīng)用進(jìn)程啟動時判斷 Envoy sidecar 的初始化狀態(tài),待其初始化完成后再啟動應(yīng)用進(jìn)程。
Envoy 的健康檢查接口 localhost:15020/healthz/ready
會在 xDS 配置初始化完成后才返回 200,否則將返回 503,因此可以根據(jù)該接口判斷 Envoy 的配置初始化狀態(tài),待其完成后再啟動應(yīng)用容器。我們可以在應(yīng)用容器的啟動命令中加入調(diào)用 Envoy 健康檢查的腳本,如下面的配置片段所示。在其他應(yīng)用中使用時,將 start-awesome-app-cmd
改為容器中的應(yīng)用啟動命令即可。
apiVersion: apps/v1 kind: Deployment metadata: name: awesome-app-deployment spec: selector: matchLabels: app: awesome-app replicas: 1 template: metadata: labels: app: awesome-app spec: containers: - name: awesome-app image: awesome-app ports: - containerPort: 80 command: ["/bin/bash", "-c"] args: ["while [[ \"$(curl -s -o /dev/null -w ''%{http_code}'' localhost:15020/healthz/ready)\" != '200' ]]; do echo Waiting for Sidecar;sleep 1; done; echo Sidecar available; start-awesome-app-cmd"]
該流程的執(zhí)行順序如下:
Kubernetes 啟動 應(yīng)用容器。
應(yīng)用容器啟動腳本中通過 curl get localhost:15020/healthz/ready
查詢 Envoy sidcar 狀態(tài),由于此時 Envoy sidecar 尚未就緒,因此該腳本會不斷重試。
Kubernetes 啟動 Envoy sidecar。
Envoy sidecar 通過 xDS 連接 Pilot,進(jìn)行配置初始化。
應(yīng)用容器啟動腳本通過 Envoy sidecar 的健康檢查接口判斷其初始化已經(jīng)完成,啟動應(yīng)用進(jìn)程。
該方案雖然可以規(guī)避依賴順序的問題,但需要對應(yīng)用容器的啟動腳本進(jìn)行修改,對 Envoy 的健康狀態(tài)進(jìn)行判斷。更理想的方案應(yīng)該是應(yīng)用對 Envoy sidecar 不感知。
通過閱讀 Kubernetes 源碼 ,我們可以發(fā)現(xiàn)當(dāng) pod 中有多個容器時,Kubernetes 會在一個線程中依次啟動這些容器,如下面的代碼片段所示:
// Step 7: start containers in podContainerChanges.ContainersToStart. for _, idx := range podContainerChanges.ContainersToStart { start("container", containerStartSpec(&pod.Spec.Containers[idx])) }
因此我們可以在向 pod 中注入 Envoy sidecar 時將 Envoy sidecar 放到應(yīng)用容器之前,這樣 Kubernetes 會先啟動 Envoy sidecar,再啟動應(yīng)用容器。但是還有一個問題,Envoy 啟動后我們并不能立即啟動應(yīng)用容器,還需要等待 xDS 配置初始化完成。這時我們就可以采用容器的 postStart lifecycle hook來達(dá)成該目的。Kubernetes 會在啟動容器后調(diào)用該容器的 postStart hook,postStart hook 會阻塞 pod 中的下一個容器的啟動,直到 postStart hook 執(zhí)行完成。因此如果在 Envoy sidecar 的 postStart hook 中對 Envoy 的配置初始化狀態(tài)進(jìn)行判斷,待完成初始化后再返回,就可以保證 Kubernetes 在 Envoy sidecar 配置初始化完成后再啟動應(yīng)用容器。該流程的執(zhí)行順序如下:
Kubernetes 啟動 Envoy sidecar 。
Kubernetes 執(zhí)行 postStart hook。
postStart hook 通過 Envoy 健康檢查接口判斷其配置初始化狀態(tài),直到 Envoy 啟動完成 。
Kubernetes 啟動應(yīng)用容器。
Istio 已經(jīng)在 1.7 中合入了該修復(fù)方案,參見 Allow users to delay application start until proxy is ready #24737。
插入 sidecar 后的 pod spec 如下面的 yaml 片段所示。postStart hook 配置的 pilot-agent wait
命令會持續(xù)調(diào)用 Envoy 的健康檢查接口 '/healthz/ready' 檢查其狀態(tài),直到 Envoy 完成配置初始化。這篇文章Delaying application start until sidecar is ready中介紹了更多關(guān)于該方案的細(xì)節(jié)。
apiVersion: v1 kind: Pod metadata: name: sidecar-starts-first spec: containers: - name: istio-proxy image: lifecycle: postStart: exec: command: - pilot-agent - wait - name: application image: my-application
該方案在不對應(yīng)用進(jìn)行修改的情況下比較完美地解決了應(yīng)用容器和 Envoy sidecar 初始化的依賴問題。但是該解決方案對 Kubernetes 有兩個隱式依賴條件:Kubernetes 在一個線程中按定義順序依次啟動 pod 中的多個容器,以及前一個容器的 postStart hook 執(zhí)行完畢后再啟動下一個容器。這兩個前提條件在目前的 Kuberenetes 代碼實(shí)現(xiàn)中是滿足的,但由于這并不是 Kubernetes的 API 規(guī)范,因此該前提在將來 Kubernetes 升級后很可能被打破,導(dǎo)致該問題再次出現(xiàn)。
為了徹底解決該問題,避免 Kubernetes 代碼變動后該問題再次出現(xiàn),更合理的方式應(yīng)該是由 Kubernetes 支持顯式定義 pod 中一個容器的啟動依賴于另一個容器的健康狀態(tài)。目前 Kubernetes 中已經(jīng)有一個 issue Support startup dependencies between containers on the same Pod #65502 對該問題進(jìn)行跟蹤處理。如果 Kubernetes 支持了該特性,則該流程的執(zhí)行順序如下:
Kubernetes 啟動 Envoy sidecar 容器。
Kubernetes 通過 Envoy sidecar 容器的 readiness probe 檢查其狀態(tài),直到 readiness probe 反饋 Envoy sidecar 已經(jīng) ready,即已經(jīng)初始化完畢。
Kubernetes 啟動應(yīng)用容器。
以上幾個解決方案的思路都是控制 pod 中容器的啟動順序,在 Envoy sidecar 初始化完成后再啟動應(yīng)用容器,以確保應(yīng)用容器啟動時能夠通過網(wǎng)絡(luò)正常訪問其他服務(wù)。但這些方案只是『頭痛醫(yī)頭,腳痛醫(yī)腳』,是治標(biāo)不治本的方法。因?yàn)榧词?pod 中對外的網(wǎng)絡(luò)訪問沒有問題,應(yīng)用容器依賴的其他服務(wù)也可能由于尚未啟動,或者某些問題而不能在此時正常提供服務(wù)。要徹底解決該問題,我們需要解耦應(yīng)用服務(wù)之間的啟動依賴關(guān)系,使應(yīng)用容器的啟動不再強(qiáng)依賴其他服務(wù)。
在一個微服務(wù)系統(tǒng)中,原單體應(yīng)用中的各個業(yè)務(wù)模塊被拆分為多個獨(dú)立進(jìn)程(服務(wù))。這些服務(wù)的啟動順序是隨機(jī)的,并且服務(wù)之間通過不可靠的網(wǎng)絡(luò)進(jìn)行通信。微服務(wù)多進(jìn)程部署、跨進(jìn)程網(wǎng)絡(luò)通信的特定決定了服務(wù)之間的調(diào)用出現(xiàn)異常是一個常見的情況。為了應(yīng)對微服務(wù)的該特點(diǎn),微服務(wù)的一個基本的設(shè)計(jì)原則是 "design for failure",即需要以優(yōu)雅的方式應(yīng)對可能出現(xiàn)的各種異常情況。當(dāng)在微服務(wù)進(jìn)程中不能訪問一個依賴的外部服務(wù)時,需要通過重試、降級、超時、斷路等策略對異常進(jìn)行容錯處理,以盡可能保證系統(tǒng)的正常運(yùn)行。
Envoy sidecar 初始化期間網(wǎng)絡(luò)暫時不能訪問的情況只是放大了微服務(wù)系統(tǒng)未能正確處理服務(wù)依賴的問題,即使解決了 Envoy sidecar 的依賴順序,該問題依然存在。例如在本案例中,配置中心也是一個獨(dú)立的微服務(wù),當(dāng)一個依賴配置中心的微服務(wù)啟動時,配置中心有可能尚未啟動,或者尚未初始化完成。在這種情況下,如果在代碼中沒有對該異常情況進(jìn)行處理,也會導(dǎo)致依賴配置中心的微服務(wù)啟動失敗。在一個更為復(fù)雜的系統(tǒng)中,多個微服務(wù)進(jìn)程之間可能存在網(wǎng)狀依賴關(guān)系,如果沒有按照 "design for failure" 的原則對微服務(wù)進(jìn)行容錯處理,那么只是將整個系統(tǒng)啟動起來就將是一個巨大的挑戰(zhàn)。對于本例而言,可以采用一個類似這樣的簡單容錯策略:先用一個缺省的 logback 配置啟動應(yīng)用進(jìn)程,并在啟動后對配置中心進(jìn)行重試,待連接上配置中心后,再使用配置中心下發(fā)的配置對 logback 進(jìn)行設(shè)置。
應(yīng)用容器對 Envoy Sidecar 啟動依賴問題的典型表現(xiàn)是應(yīng)用容器在剛啟動的一小段時間內(nèi)調(diào)用外部服務(wù)失敗。原因是此時 Envoy sidecar 尚未完成 xDS 配置的初始化,因此不能為應(yīng)用容器轉(zhuǎn)發(fā)網(wǎng)絡(luò)請求。該調(diào)用失敗可能導(dǎo)致應(yīng)用容器不能正常啟動。此問題的根本原因是微服務(wù)應(yīng)用中對依賴服務(wù)的調(diào)用失敗沒有進(jìn)行合理的容錯處理。對于遺留系統(tǒng),為了盡量避免對應(yīng)用的影響,我們可以通過在應(yīng)用啟動命令中判斷 Envoy 初始化狀態(tài)的方案,或者升級到 Istio 1.7 來緩解該問題。但為了徹底解決服務(wù)依賴導(dǎo)致的錯誤,建議參考 "design for failure" 的設(shè)計(jì)原則,解耦微服務(wù)之間的強(qiáng)依賴關(guān)系,在出現(xiàn)暫時不能訪問一個依賴的外部服務(wù)的情況時,通過重試、降級、超時、斷路等策略進(jìn)行處理,以盡可能保證系統(tǒng)的正常運(yùn)行。
看完上述內(nèi)容,你們對應(yīng)用容器對Envoy Sidecar的啟動依賴問題怎么解決有進(jìn)一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。