K8s有損發佈問題探究

問題提出

流量有損是在應用發佈時的常見問題,其現象通常會反饋到流量監控上,如下圖所示,發佈過程中服務RT突然升高,造成部分業務響應變慢,給用戶的最直觀體驗就是卡頓;或是請求的500錯誤數突增,在用戶側可能感受到服務降級或服務不可用,從而影響用戶體驗。

 

因爲應用發佈會伴隨流量有損,所以我們往往需要將發佈計劃移到業務低谷期,並嚴格限制應用發佈的持續時間,儘管如此,還是不能完全避免發佈帶來的風險,有時甚至不得不選擇停機發布。EDAS作爲一個通用應用管理系統,應用發佈是其最基本的功能之一,而K8s 應用是EDAS中最普遍的應用的形態,下文將通過對EDAS客戶真實場景的歸納,從K8s的流量路徑入手,分析有損發佈產生的原因,並提供實用的解決方案。

流量路徑分析

K8s中,流量通常可以從以下幾種路徑進入到應用Pod中,每條路徑大相徑庭,流量損失的原因各不相同。我們將分情況探究每種路徑的路由機制,以及Pod變更對流量路徑的影響。

LB Service流量

通過LoadBalancer類型Service訪問應用時,流量路徑中核心組件是LoadBalancer和ipvs/iptables。LoadBalancer負責接收K8s集羣外部流量並轉發到Node節點上,ipvs/iptables負責將節點接收到的流量轉發到Pod中。核心組件的動作由CCM(cloud-controller-manager)和kube-proxy驅動,分別負責更新LoadBalancer後端和ipvs/iptables規則。

在應用發佈時,就緒的Pod會被添加到Endpoint後端,Terminating狀態的Pod會從Endpoint中移除。kube-proxy組件會更新各節點的ipvs/iptables規則,CCM組件監聽到了Endpoint的變更後會調用雲廠商API更新負載均衡器後端,將Node IP和端口更新到後端列表中。流量進入後,會根據負載均衡器配置的監聽後端列表轉發到對應的節點,再由節點ipvs/iptables轉發到實際Pod。

Service支持設置externalTrafficPolicy,根據該參數的不同,節點kube-proxy組件更新ipvs/iptables列表及CCM更新負載均衡器後端的行爲會有所不同:

  • Local模式:CCM 僅會將目標服務所在節點添加入負載均衡後端地址列表。流量到達該節點後僅會轉發到本節點的Pod中。
  • Cluster模式:CCM會將所有節點都添加到負載均衡後端地址列表。流量到達該節點後允許被轉發到其他節點的Pod中。

Nginx Ingress流量

通過Nginx Ingress提供的SLB訪問應用時,流量路徑核心組件爲Ingress Controller,它不但作爲代理服務器負責將流量轉發到後端服務的Pod中,還負責根據Endpoint更新網關代理的路由規則。

在應用發佈時,Ingress Controller會監聽Endpoint的變化,並更新Ingress網關路由後端,流量進入後會根據流量特徵轉發到匹配規則上游,並根據上游後端列表選擇一個後端將請求轉發過去。

默認情況下,Controller在監聽到Service的Endpoint變更後,會調用Nginx中的動態配置後端接口,更新Nginx網關上游後端列表爲服務Endpoint列表,即Pod的IP和端口列表。因此,流量進入Ingress Controller後會被直接轉發到後端Pod IP和端口。

微服務流量

使用微服務方式訪問應用時,核心組件爲註冊中心。Provider啓動後會將服務註冊到註冊中心,Consumer會訂閱註冊中心中服務的地址列表。

在應用發佈時,Provider啓動後會將Pod IP和端口註冊到註冊中心,下線的Pod會從註冊中心移除。服務端列表的變更會被消費者訂閱,並更新緩存的服務後端Pod IP和端口列表。流量進入後,消費者會根據服務地址列表由客戶端負載均衡轉發到對應的Provider Pod中。

原因分析與通用解決方案

應用發佈過程其實是新Pod上線和舊Pod下線的過程,當流量路由規則的更新與應用Pod上下線配合出現問題時,就會出現流量損失。我們可以將應用發佈中造成的流量損失歸類爲上線有損和下線有損,總的來看,上線和下線有損的原因如下,後文將分情況做更深入討論:

  • 上線有損:新Pod上線後過早被加入路由後端,流量被過早路由到了未準備好的Pod。
  • 下線有損:舊Pod下線後路由規則沒有及時將後端移除,流量仍路由到了停止中的Pod。

上線有損分析與對策

K8s中Pod上線流程如下圖所示:

如果在Pod上線時,不對Pod中服務進行可用性檢查,這會使得Pod啓動後過早被添加到Endpoint後端,後被其他網關控制器添加到網關路由規則中,那麼流量被轉發到該Pod後就會出現連接被拒絕的錯誤。因此,健康檢查尤爲重要,我們需要確保Pod啓動完成再讓其分攤線上流量,避免流量損失。K8s爲Pod提供了readinessProbe用於校驗新Pod是否就緒,設置合理的就緒探針對應用實際的啓動狀態進行檢查,進而能夠控制其在Service後端Endpoint中上線的時機。

基於Endpoint流量場景

對於基於Endpoint控制流量路徑的場景,如LB Service流量和Nginx Ingress流量,配置合適的就緒探針檢查就能夠保證服務健康檢查通過後,纔將其添加到Endpoint後端分攤流量,以避免流量損失。例如,在Spring Boot 2.3.0以上版本中增加了健康檢查接口/actuator/health/readiness和/actuator/health/liveness以支持配置應用部署在K8S環境下的就緒探針和存活探針配置:

readinessProbe: 
  ...
  httpGet:
    path: /actuator/health/readiness
    port: ${server.port}

微服務流量場景

對於微服務應用,服務的註冊和發現由註冊中心管理,而註冊中心並沒有如K8s就緒探針的檢查機制。並且由於JAVA應用通常啓動較慢,服務註冊成功後所需資源均仍然可能在初始化中,比如數據庫連接池、線程池、JIT編譯等。如果此時有大量微服務請求湧入,那麼很可能造成請求RT過高或超時等異常。

針對上述問題,Dubbo提供了延遲註冊、服務預熱的解決方案,功能概述如下:

  • 延遲註冊功能允許用戶指定一段時長,程序在啓動後,會先完成設定的等待,再將服務發佈到註冊中心,在等待期間,程序有機會完成初始化,避免了服務請求的湧入。
  • 服務預熱功能允許用戶設定預熱時長,Provider在向註冊中⼼註冊服務時,將⾃身的預熱時⻓、服務啓動時間通過元數據的形式註冊到註冊中⼼中,Consumer在註冊中⼼訂閱相關服務實例列表,根據實例的預熱時長,結合Provider啓動時間計算調用權重,以控制剛啓動實例分配更少的流量。通過小流量預熱,能夠讓程序在較低負載的情況下完成類加載、JIT編譯等操作,以支持預熱結束後讓新實例穩定均攤流量。

我們可以通過爲程序增加如下配置來開啓延遲註冊和服務預熱功能:

dubbo:
    provider:
        warmup: 120000
        delay: 5000

配置以上參數後,我們通過爲Provider應用擴容一個Pod,來查看新Pod啓動過程中的QPS曲線以驗證流量預熱效果。QPS數據如下圖所示:

根據Pod接收流量的QPS曲線可以看出,在Pod啓動後沒有直接均攤線上的流量,而是在設定的預熱時長120秒內,每秒處理的流量呈線性增長趨勢,並在120秒後趨於穩定,符合流量預熱的預期效果。

下線有損分析與對策

在K8s中,Pod下線流程如下圖所示:

從圖中我們可以看到,Pod被刪除後,狀態被endpoint-controller和kubelet訂閱,並分別執行移除Endpoint和刪除Pod操作。而這兩個組件的操作是同時進行的,並非我們預期的按順序先移除Endpoint後再刪除Pod,因此有可能會出現在Pod已經接收到了SIGTERM信號,但仍然有流量進入的情況。

K8s在Pod下線流程中提供了preStop Hook機制,可以讓kubelet在發現Pod狀態爲Terminating時,不立即向容器發送SIGTERM信號,而允許其做一些停止前操作。對於上述問題的通用方案,可以在preStop中設置sleep一段時間,讓SIGTERM延遲一段時間再發送到應用中,可以避免在這段時間內流入的流量損失。此外,也能允許已被Pod接收的流量繼續處理完成。

上面介紹了在變更時,由於Pod下線和Endpoint更新時機不符合預期順序可能會導致的流量有損問題,在應用接入了多種類型網關後,流量路徑的複雜度增加了,在其他路由環節也會出現流量損失的可能。

LB Service流量場景

在使用LoadBalancer類型Service訪問應用時,配置Local模式的externalTrafficPolicy可以避免流量被二次轉發並且能夠保留請求包源IP地址。

應用發佈過程中,Pod下線並且已經從節點的ipvs列表中刪除,但是由CCM監聽Endpoint變更並調用雲廠商API更新負載均衡器後端的操作可能會存在延遲。如果新Pod被調度到了其他的節點,且原節點不存在可用Pod時,若負載均衡器路由規則沒有及時更新,那麼負載均衡器仍然會將流量轉發到原節點上,而這條路徑沒有可用後端,導致流量有損。

此時,在Pod的preStop中配置sleep雖然能夠讓Pod在LoadBalancer後端更新前正常運行一段時間,但卻無法保證kube-proxy在CCM移除LoadBalancer後端前不刪除節點中ipvs/iptables規則的。場景如上圖所示,在Pod下線過程中,請求路徑2已經刪除,而請求路徑1還沒有及時更新,即使sleep能夠讓Pod繼續提供一段時間服務,但由於轉發規則的不完整,流量沒有被轉發到Pod就已經被丟棄了。

解決方案:

  • 設置externalTrafficPolicy爲Cluster能夠避免流量下線損失。因爲Cluster模式下集羣所有節點均被加入負載均衡器後端,且節點中ipvs維護了集羣所有可用Pod列表,當本節點中不存在可用Pod時,可以二次轉發到其他節點上的Pod中,但是會導致二次轉發損耗,並且無法保留源IP地址。
  • 設置Pod原地升級,通過爲Node打特定標籤的方式,設置新Pod仍然被調度到本節點上。那麼該流程無需調用雲廠商API進行負載均衡器後端更新,流量進入後會轉發到新Pod中。

Nginx Ingress流量場景

對於Nginx Ingress,默認情況下流量是通過網關直接轉發到後端PodIP而非Service的ClusterIP。在應用發佈過程中,Pod下線,並由Ingress Controller監聽Endpoint變更並更新到Nginx網關的操作存在延遲,流量進入後仍然可能被轉發到已下線的Pod,此時就會出現流量損失。

解決方案:

  • Ingress註解“http://nginx.ingress.kubernetes.io/service-upstream”支持配置爲“true”或“false”,默認爲“false”。設置該註解爲“true”時,路由規則使用ClusterIP爲Ingress上游服務地址;設置爲“false”時,路由規則使用Pod IP爲Ingress上游服務地址。由於Service的ClusterIP總是不變的,當Pod上下線時,無需考慮Nginx網關配置的變更,不會出現上述流量下線有損問題。但需要注意的是,當使用該特性時流量負載均衡均由K8s控制,一些Ingress Controller特性將會失效,如會話保持、重試策略等。
  • 在Pod的preStop設置sleep一段時間,讓Pod接收SIGTERM信號前等待一段時間,允許接收並處理這段時間內的流量,避免流量損失。

微服務流量場景

在Provider應用發佈的過程中,Pod下線並從註冊中心註銷,但消費者訂閱服務端列表變更存在一定的延遲,此時流量進入Consumer後,若Consumer仍沒有刷新serverList,仍然可能會訪問到已下線的Pod。

對於微服務應用Pod的下線,服務註冊發現是通過註冊中心而非不依賴於Endpoint,上述endpoint-controller移除Endpoint並不能實現Pod IP從註冊中心下線。僅僅在preStop中sleep仍然無法解決消費者serverList緩存刷新延遲問題。爲了舊Pod能夠優雅下線,在preStop中需要首先從註冊中心下線,並能夠處理已經接收的流量,還需要保證下線前消費者已經將其客戶端緩存的Provider實例列表刷新。下線實例可以通過調用註冊中心接口,或在程序中調用服務框架所提供的接口並設置到preStop以達到上述效果,在EDAS中可以直接使用http://localhost:54199/offline:

lifecycle:
  preStop:
     exec:
       command:
         - /bin/sh
         - -c
         - curl http://localhost:54199/offline; sleep 30;

企業級一站式解決方案

上面我們對應用發佈過程中三種常用流量路徑的流量有損問題進行了原因分析並給出瞭解決方案。總的來說,爲了保證流量無損,需要從網關參數和Pod生命週期探針和鉤子來保證流量路徑和Pod上下線的默契配合。EDAS在面對上述問題時,提供了無侵入式的解決方案,無需更改程序代碼或參數配置,在EDAS控制檯即可實現應用無損上下線。如下圖所示:

  • LB Service支持配置外部流量策略(externalTrafficPolicy)

  • 微服務應用配置無損上線參數

除此之外,EDAS還提供了多種流量網關管理方式,如Nginx Ingress、ALB Ingress、雲原生網關,也爲應用的發佈提供了多種部署方式,如分批發布、金絲雀發佈,還提供了不同維度的可觀測手段,如Ingress監控、應用監控。在EDAS平臺管理應用,能夠輕鬆實現多種部署場景下的無損上下線。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章