本系列文章將介紹用戶從 Spring Cloud,Dubbo 等傳統微服務框架遷移到 Istio 服務網格時的一些經驗,以及在使用 Istio 過程當中可能遇到的一些常見問題的解決方法。java
該問題的表現是安裝了 sidecar proxy 的應用在啓動後的一小段時間內沒法經過網絡訪問 pod 外部的其餘服務,例如外部的 HTTP,MySQL,Redis等服務。若是應用沒有對依賴服務的異常進行容錯處理,該問題還經常會致使應用啓動失敗。下面咱們以該問題致使的一個典型故障的分析過程爲例對該問題的緣由進行說明。git
典型案例:某運維同窗反饋:昨天晚上 Istio 環境中應用的心跳檢測報 connect reset,而後服務重啓了。懷疑是 Istio 環境中網絡不穩定致使了服務重啓。github
根據運維同窗的反饋,該 pod 曾屢次重啓。所以咱們先用 kubectl logs --previous
命令查詢 awesome-app 容器最後一次重啓前的日誌,以從日誌中查找其重啓的緣由。spring
kubectl logs --previous awesome-app-cd1234567-gzgwg -c awesome-app
從日誌中查詢到了其重啓前最後的錯誤信息以下:api
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)
從錯誤信息能夠得知,應用進程在啓動時試圖經過 HTTP 協議從配置中心拉取 logback 的配置信息,但該操做因爲網絡異常失敗了,致使應用進程啓動失敗,最終致使容器重啓。bash
是什麼致使了網絡異常呢?咱們再用 Kubectl get pod
命令查詢 Pod 的運行狀態,嘗試找到更多的線索:網絡
kubectl get pod awesome-app-cd1234567-gzgwg -oyaml
命令輸出的 pod 詳細內容以下,該 yaml 片斷省略了其餘無關的細節,只顯示了 lastState 和 state 部分的容器狀態信息。app
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 中的應用容器 awesome-app 重啓了兩次。整理該 pod 中 awesome-app 應用容器和 istio-proxy sidecar 容器的啓動和終止的時間順序,能夠獲得下面的時間線:框架
能夠看到在 istio-proxy 啓動2秒後,awesome-app 啓動,並於1秒後異常退出。結合前面的日誌信息,咱們知道此次啓動失敗的直接緣由是應用訪問配置中心失敗致使。在 istio-proxy 啓動16秒後,awesome-app 再次啓動,此次啓動成功,以後一直正常運行。運維
istio-proxy 啓動和 awesome-app 上一次異常退出的時間間隔很短,只有2秒鐘,所以咱們基本能夠判斷此時 istio-proxy 還沒有啓動初始化完成,致使 awesome-app 不能經過istio-proxy 鏈接到外部服務,致使其啓動失敗。待 awesome-app 於 2020-09-01T13:16:36Z 再次啓動時,因爲 istio-proxy 已經啓動了較長時間,完成了從 pilot 獲取動態配置的過程,所以 awesome-app 向 pod 外部的網絡訪問就正常了。
以下圖所示,Envoy 啓動後會經過 xDS 協議向 pilot 請求服務和路由配置信息,Pilot 收到請求後會根據 Envoy 所在的節點(pod或者VM)組裝配置信息,包括 Listener、Route、Cluster等,而後再經過 xDS 協議下發給 Envoy。根據 Mesh 的規模和網絡狀況,該配置下發過程須要數秒到數十秒的時間。因爲初始化容器已經在 pod 中建立了 Iptables rule 規則,所以這段時間內應用向外發送的網絡流量會被重定向到 Envoy ,而此時 Envoy 中尚沒有對這些網絡請求進行處理的監聽器和路由規則,沒法對此進行處理,致使網絡請求失敗。(關於 Envoy sidecar 初始化過程和 Istio 流量管理原理的更多內容,能夠參考這篇文章 Istio流量管理實現機制深度解析)
從前面的分析能夠得知,該問題的根本緣由是因爲應用進程對 Envoy sidecar 配置初始化的依賴致使的。所以最直接的解決思路就是:在應用進程啓動時判斷 Envoy sidecar 的初始化狀態,待其初始化完成後再啓動應用進程。
Envoy 的健康檢查接口 localhost:15020/healthz/ready
會在 xDS 配置初始化完成後才返回 200,不然將返回 503,所以能夠根據該接口判斷 Envoy 的配置初始化狀態,待其完成後再啓動應用容器。咱們能夠在應用容器的啓動命令中加入調用 Envoy 健康檢查的腳本,以下面的配置片斷所示。在其餘應用中使用時,將 start-awesome-app-cmd
改成容器中的應用啓動命令便可。
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"]
該流程的執行順序以下:
curl get localhost:15020/healthz/ready
查詢 Envoy sidcar 狀態,因爲此時 Envoy sidecar 還沒有就緒,所以該腳本會不斷重試。該方案雖然能夠規避依賴順序的問題,但須要對應用容器的啓動腳本進行修改,對 Envoy 的健康狀態進行判斷。更理想的方案應該是應用對 Envoy sidecar 不感知。
經過閱讀 Kubernetes 源碼 ,咱們能夠發現當 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 放到應用容器以前,這樣 Kubernetes 會先啓動 Envoy sidecar,再啓動應用容器。可是還有一個問題,Envoy 啓動後咱們並不能當即啓動應用容器,還須要等待 xDS 配置初始化完成。這時咱們就能夠採用容器的 postStart lifecycle hook來達成該目的。Kubernetes 會在啓動容器後調用該容器的 postStart hook,postStart hook 會阻塞 pod 中的下一個容器的啓動,直到 postStart hook 執行完成。所以若是在 Envoy sidecar 的 postStart hook 中對 Envoy 的配置初始化狀態進行判斷,待完成初始化後再返回,就能夠保證 Kubernetes 在 Envoy sidecar 配置初始化完成後再啓動應用容器。該流程的執行順序以下:
Istio 已經在 1.7 中合入了該修復方案,參見 Allow users to delay application start until proxy is ready #24737。
插入 sidecar 後的 pod spec 以下面的 yaml 片斷所示。postStart hook 配置的 pilot-agent wait
命令會持續調用 Envoy 的健康檢查接口 '/healthz/ready' 檢查其狀態,直到 Envoy 完成配置初始化。這篇文章Delaying application start until sidecar is ready中介紹了更多關於該方案的細節。
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
該方案在不對應用進行修改的狀況下比較完美地解決了應用容器和 Envoy sidecar 初始化的依賴問題。可是該解決方案對 Kubernetes 有兩個隱式依賴條件:Kubernetes 在一個線程中按定義順序依次啓動 pod 中的多個容器,以及前一個容器的 postStart hook 執行完畢後再啓動下一個容器。這兩個前提條件在目前的 Kuberenetes 代碼實現中是知足的,但因爲這並非 Kubernetes的 API 規範,所以該前提在未來 Kubernetes 升級後極可能被打破,致使該問題再次出現。
爲了完全解決該問題,避免 Kubernetes 代碼變更後該問題再次出現,更合理的方式應該是由 Kubernetes 支持顯式定義 pod 中一個容器的啓動依賴於另外一個容器的健康狀態。目前 Kubernetes 中已經有一個 issue Support startup dependencies between containers on the same Pod #65502 對該問題進行跟蹤處理。若是 Kubernetes 支持了該特性,則該流程的執行順序以下:
以上幾個解決方案的思路都是控制 pod 中容器的啓動順序,在 Envoy sidecar 初始化完成後再啓動應用容器,以確保應用容器啓動時可以經過網絡正常訪問其餘服務。但這些方案只是『頭痛醫頭,腳痛醫腳』,是治標不治本的方法。由於即便 pod 中對外的網絡訪問沒有問題,應用容器依賴的其餘服務也可能因爲還沒有啓動,或者某些問題而不能在此時正常提供服務。要完全解決該問題,咱們須要解耦應用服務之間的啓動依賴關係,使應用容器的啓動再也不強依賴其餘服務。
在一個微服務系統中,原單體應用中的各個業務模塊被拆分爲多個獨立進程(服務)。這些服務的啓動順序是隨機的,而且服務之間經過不可靠的網絡進行通訊。微服務多進程部署、跨進程網絡通訊的特定決定了服務之間的調用出現異常是一個常見的狀況。爲了應對微服務的該特色,微服務的一個基本的設計原則是 "design for failure",即須要以優雅的方式應對可能出現的各類異常狀況。當在微服務進程中不能訪問一個依賴的外部服務時,須要經過重試、降級、超時、斷路等策略對異常進行容錯處理,以儘量保證系統的正常運行。
Envoy sidecar 初始化期間網絡暫時不能訪問的狀況只是放大了微服務系統未能正確處理服務依賴的問題,即便解決了 Envoy sidecar 的依賴順序,該問題依然存在。例如在本案例中,配置中心也是一個獨立的微服務,當一個依賴配置中心的微服務啓動時,配置中心有可能還沒有啓動,或者還沒有初始化完成。在這種狀況下,若是在代碼中沒有對該異常狀況進行處理,也會致使依賴配置中心的微服務啓動失敗。在一個更爲複雜的系統中,多個微服務進程之間可能存在網狀依賴關係,若是沒有按照 "design for failure" 的原則對微服務進行容錯處理,那麼只是將整個系統啓動起來就將是一個巨大的挑戰。對於本例而言,能夠採用一個相似這樣的簡單容錯策略:先用一個缺省的 logback 配置啓動應用進程,並在啓動後對配置中心進行重試,待鏈接上配置中心後,再使用配置中心下發的配置對 logback 進行設置。
應用容器對 Envoy Sidecar 啓動依賴問題的典型表現是應用容器在剛啓動的一小段時間內調用外部服務失敗。緣由是此時 Envoy sidecar 還沒有完成 xDS 配置的初始化,所以不能爲應用容器轉發網絡請求。該調用失敗可能致使應用容器不能正常啓動。此問題的根本緣由是微服務應用中對依賴服務的調用失敗沒有進行合理的容錯處理。對於遺留系統,爲了儘可能避免對應用的影響,咱們能夠經過在應用啓動命令中判斷 Envoy 初始化狀態的方案,或者升級到 Istio 1.7 來緩解該問題。但爲了完全解決服務依賴致使的錯誤,建議參考 "design for failure" 的設計原則,解耦微服務之間的強依賴關係,在出現暫時不能訪問一個依賴的外部服務的狀況時,經過重試、降級、超時、斷路等策略進行處理,以儘量保證系統的正常運行。
【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公衆號,及時獲取更多幹貨!!