服務發現與配置管理高可用最佳實踐
作者:三辰|阿里云云原生微服務基礎架構團隊技術專家,負責 MSE 引擎高可用架構
本篇是微服務高可用最佳實踐系列分享的開篇,系列內容持續更新中,期待大家的關注。
01
引言
在開始正式內容之前,先給大家分享一個真實的案例。
某客戶在阿里雲上使用 K8s 集羣部署了許多自己的微服務,但是某一天,其中一臺節點的網卡發生了異常,最終導致服務不可用,無法調用下游,業務受損。
我們來看一下這個問題鏈是如何形成的?
-
ECS 故障節點上運行着 K8s 集羣的核心基礎組件 CoreDNS 的所有 Pod,它沒有打散,導致集羣 DNS 解析出現問題。
-
該客戶的服務發現使用了有缺陷的客戶端版本(nacos-client 的 1.4.1 版本),這個版本的缺陷就是跟 DNS 有關——心跳請求在域名解析失敗後,會導致進程後續不會再續約心跳,只有重啓才能恢復。
-
這個缺陷版本實際上是已知問題,阿里雲在 5 月份推送了 nacos-client 1.4.1 存在嚴重 bug 的公告,但客戶研發收到通知,進而在生產環境中使用了這個版本。
風險環環相扣,缺一不可。
最終導致故障的原因是服務無法調用下游,可用性降低,業務受損。下圖示意的是客戶端缺陷導致問題的根因:
-
Provider 客戶端在心跳續約時發生 DNS 異常;
-
心跳線程正確地處理這個 DNS 異常,導致線程意外退出了;
-
註冊中心的正常機制是,心跳不續約,30 秒後自動下線。由於 CoreDNS 影響的是整個 K8s 集羣的 DNS 解析,所以 Provider 的所有實例都遇到相同的問題,整個服務所有實例都被下線;
-
在 Consumer 這一側,收到推送的空列表後,無法找到下游,那麼調用它的上游(比如網關)就會發生異常。
回顧整個案例,每一環每個風險看起來發生概率都很小,但是一旦發生就會造成惡劣的影響。
所以,本篇文章就來探討,微服務領域的高可用方案怎麼設計,細化到服務發現和配置管理領域,都有哪些具體的方案。
02
微服務高可用方案
首先,有一個事實不容改變:沒有任何系統是百分百沒有問題的,所以高可用架構方案就是面對失敗(風險)設計的。
風險是無處不在的,儘管有很多發生概率很小很小,卻都無法完全避免。
在微服務系統中,都有哪些風險的可能?
這只是其中一部分,但是在阿里巴巴內部十幾年的微服務實踐過程中,這些問題全部都遇到過,而且有些還不止一次。
雖然看起來坑很多,但我們依然能夠很好地保障雙十一大促的穩定,背後靠的就是成熟穩健的高可用體系建設。
我們不能完全避免風險的發生,但我們可以控制它(的影響),這就是做高可用的本質。
控制風險有哪些策略?
註冊配置中心在微服務體系的核心鏈路上,牽一髮動全身,任何一個抖動都可能會較大範圍地影響整個系統的穩定性。
1 策略一:縮小風險影響範圍
集羣高可用
多副本: 不少於 3 個節點進行實例部署。
多可用區(同城容災): 將集羣的不同節點部署在不同可用區(AZ)中。
當節點或可用區發生的故障時,影響範圍只是集羣其中的一部分,如果能夠做到迅速切換,並將故障節點自動離羣,就能儘可能減少影響。
減少上下游依賴
系統設計上應該儘可能地減少上下游依賴,越多的依賴,可能會在被依賴系統發生問題時,讓整體服務不可用(一般是一個功能塊的不可用)。如果有必要的依賴,也必須要求是高可用的架構。
變更可灰度
新版本迭代發佈,應該從最小範圍開始灰度,按用戶、按 Region 分級,逐步擴大變更範圍。一旦出現問題,也只是在灰度範圍內造成影響,縮小問題爆炸半徑。
服務可降級、限流、熔斷
-
註冊中心異常負載的情況下,降級心跳續約時間、降級一些非核心功能等
-
針對異常流量進行限流,將流量限制在容量範圍內,保護部分流量是可用的
-
客戶端側,異常時降級到使用本地緩存(推空保護也是一種降級方案),暫時犧牲列表更新的一致性,以保證可用性
如圖,微服務引擎 MSE 的同城雙活三節點的架構,經過精簡的上下游依賴,每一個都保證高可用架構。多節點的 MSE 實例,通過底層的調度能力,會自動分配到不同的可用區上,組成多副本集羣。
2 策略二:縮短風險發生持續時間
核心思路就是:儘早識別、儘快處理
識別 —— 可觀測
例如,基於 Prometheus 對實例進行監控和報警能力建設。
進一步地,在產品層面上做更強的觀測能力:包括大盤、告警收斂 / 分級(識別問題)、針對大客戶的保障、以及服務等級的建設。
MSE 註冊配置中心目前提供的服務等級是 99.95%,並且正在向 4 個 9(99.99%)邁進。
快速處理 —— 應急響應
應急響應的機制要建立,快速有效地通知到正確的人員範圍,快速執行預案的能力(意識到白屏與黑屏的效率差異),常態化地進行故障應急的演練。
預案是指不管熟不熟悉你的系統的人,都可以放心執行,這背後需要一套沉澱好有含金量的技術支撐(技術厚度)。
3 策略三:減少觸碰風險的次數
減少不必要的發佈,例如:增加迭代效率,不隨意發佈;重要事件、大促期間進行封網。
從概率角度來看,無論風險概率有多低,不斷嘗試,風險發生的聯合概率就會無限趨近於 1。
4 策略四:降低風險發生概率
架構升級,改進設計
Nacos2.0,不僅是性能做了提升,也做了架構上的升級:
-
升級數據存儲結構,Service 級粒度提升到到 Instance 級分區容錯(繞開了 Service 級數據不一致造成的服務掛的問題);
-
升級連接模型(長連接),減少對線程、連接、DNS 的依賴。
提前發現風險
-
這個「提前」是指在設計、研發、測試階段儘可能地暴露潛在風險;
-
提前通過容量評估預知容量風險水位是在哪裏;
-
通過定期的故障演練提前發現上下游環境風險,驗證系統健壯性。
如圖,阿里巴巴大促高可用體系,不斷做壓測演練、驗證系統健壯性和彈性、觀測追蹤系統問題、驗證限流、降級等預案的可執行性。
03
服務發現高可用方案
服務發現包含服務消費者(Consumer)和服務提供者(Provider)。
1 Consumer 端高可用
通過推空保護、服務降級等手段,達到 Consumer 端的容災目的。
推空保護
可以應對開頭講的案例,服務空列表推送自動降級到緩存數據。
服務消費者(Consumer)會從註冊中心上訂閱服務提供者(Provider)的實例列表。
當遇到突發情況(例如,可用區斷網,Provider 端無法上報心跳) 或 註冊中心(變配、重啓、升降級)出現非預期異常時,都有可能導致訂閱異常,影響服務消費者(Consumer)的可用性。
無推空保護
-
Provider 端註冊失敗(比如網絡、SDKbug 等原因)
-
註冊中心判斷 Provider 心跳過期
-
Consumer 訂閱到空列表,業務中斷報錯
開啓推空保護
-
同上
-
Consumer 訂閱到空列表,推空保護生效,丟棄變更,保障業務服務可用
開啓方式
開啓方式比較簡單
開源的客戶端 nacos-client 1.4.2 以上版本支持
配置項
-
SpingCloudAlibaba 在 spring 配置項裏增加:
spring.cloud.nacos.discovery.namingPushEmptyProtection=true
-
Dubbo 加上 registryUrl 的參數:
namingPushEmptyProtection=true
提空保護依賴緩存,所以需要持久化緩存目錄,避免重啓後丟失,路徑爲:
${user.home}/nacos/naming/${namespaceId}
服務降級
Consumer 端可以根據不同的策略選擇是否將某個調用接口降級,起到對業務請求流程的保護(將寶貴的下游 Provider 資源保留給重要的業務 Consumer 使用),保護重要業務的可用性。
服務降級的具體策略,包含返回 Null 值、返回 Exception 異常、返回自定義 JSON 數據和自定義回調。
MSE 微服務治理中心中默認就具備該項高可用能力。
2 Provider 端高可用
Provider 側通過註冊中心和服務治理提供的容災保護、離羣摘除、無損下線等方案提升可用性。
容災保護
容災保護主要用於避免集羣在異常流量下出現雪崩的場景。
下面我們來具體看一下:
無容災保護(默認閾值 =0)
-
突發請求量增加,容量水位較高時,個別 Provider 發生故障;
-
註冊中心將故障節點摘除,全量流量會給剩餘節點;
-
剩餘節點負載變高,大概率也會故障;
-
最後所有節點故障,100% 無****法提供服務。
開啓容災保護(閾值 = 0.6)
-
同上;
-
故障節點數達到保護閾值,流量平攤給所有機器;
-
最終保障 50% 節點能夠提供服務。
容災保護能力,在緊急情況下,能夠保存服務可用性在一定的水平之上,可以說是整體系統的兜底了。
這套方案曾經救過不少業務系統。
離羣實例摘除
心跳續約是註冊中心感知實例可用性的基本途徑。
但是在特定情況下,心跳存續並不能完全等同於服務可用。
因爲仍然存在心跳正常,但服務不可用的情況,例如:
-
Request 處理的線程池滿
-
依賴的 RDS 連接異常或慢 SQL
微服務治理中心提供離羣實例摘除
-
基於異常檢測的摘除策略:包含網絡異常和網絡異常 + 業務異常(HTTP 5xx)
-
設置異常閾值、QPS 下限、摘除比例下限
離羣實例摘除的能力是一個補充,根據特定接口的調用異常特徵,來衡量服務的可用性。
無損下線
無損下線,又叫優雅下線、或者平滑下線,都是一個意思。首先看什麼是有損下線:
Provider 實例進行升級過程中,下線後心跳在註冊中心存約以及變更生效都有一定的時間,在這個期間 Consumer 端訂閱列表仍然沒有更新到下線後的版本,如果魯莽地將 Provider 停止服務,會造成一部分的流量損失。
無損下線有很多不同的解決方案,但侵入性最低的還是服務治理中心默認提供的能力,無感地整合到發佈流程中,完成自動執行。免去繁瑣的運維腳本邏輯的維護。
04
配置管理高可用方案
配置管理主要包含配置訂閱和配置發佈兩類操作。
配置管理解決什麼問題?
多環境、多機器的配置發佈、配置動態實時推送。
1 基於配置管理做服務高可用
微服務如何基於配置管理做高可用方案?
發佈環境管理
一次管理上百臺機器、多套環境,如何正確無誤地推送、誤操作或出現線上問題如何快速回滾,發佈過程如何灰度。
業務開關動態推送
功能、活動頁面等開關。
容災降級預案的推送
預置的方案通過推送開啓,實時調整流控閾值等。
上圖是大促期間配置管理整體高可用解決方案。比如降級非核心業務、功能降級、日誌降級、禁用高風險操作。
客戶端高可用
配置管理客戶端側同樣有容災方案。
本地目錄分爲兩級,高優先級是容災目錄、低優先級是緩存目錄。
緩存目錄: 每次客戶端和配置中心進行數據交互後,會保存最新的配置內容至本地緩存目錄中,當服務端不可用狀態下,會使用本地緩存目錄中內容。
容災目錄: 當服務端不可用狀態下,可以在本地的容災目錄中手動更新配置內容,客戶端會優先加載容災目錄下的內容,模擬服務端變更推送的效果。
簡單來說,當配置中心不可用時,優先查看容災目錄的配置,否則使用之前拉取到的緩存。
容災目錄的設計,是因爲有時候不一定會有緩存過的配置,或者業務需要緊急覆蓋使用新的內容開啓一些必要的預案和配置。
整體思路就是,無法發生什麼問題,無論如何,都要能夠使客戶端能夠讀取到正確的配置,保證微服務的可用性。
服務端高可用
在配置中心側,主要是針對讀、寫的限流。
限制連接數、限制寫:
-
限連接:單機最大連接限流,單客戶端 IP 的連接限流
-
限寫接口:發佈操作 & 特定配置的秒級分鐘級數量限流
控制操作風險
控制人員做配置發佈的風險。
配置發佈的操作是可灰度、可追溯、可回滾的。
配置灰度
發佈歷史 & 回滾
變更對比
05
動手實踐
最後我們一起來做一個實踐。
場景取自前面提到的一個高可用方案,在服務提供者所有機器發生註冊異常的情況下,看服務消費者在推空保護打開的情況下的表現。
1 實驗架構和思路
上圖是本次實踐的架構,右側是一個簡單的調用場景,外部流量通過網關接入,這裏選擇了 MSE 產品矩陣中的雲原生網關,依靠它提供的可觀測能力,方便我們觀察服務調用情況。
網關的下游有 A、B、C 三個應用,支持使用配置管理的方式動態地將調用關係連接起來,後面我們會實踐到。
基本思路:
-
部署服務,調整調用關係是網關 ->A->B->C,查看網關調用成功率。
-
通過模擬網絡問題,將應用 B 與註冊中心的心跳鏈路斷開,模擬註冊異常的發生。
-
再次查看網關調用成功率,期望服務 A->B 的鏈路不受註冊異常的影響。
爲了方便對照,應用 A 會部署兩種版本,一種是開啓推空保護的,一種是沒有開啓的情況。
最終期望的結果是,推空保護開關開啓後,能夠幫助應用 A 在發生異常的情況下,繼續能夠尋址到應用 B。
網關的流量打到應用 A 之後,可以觀察到,接口的成功率應該正好在 50%。
2 開始
接下來開始動手實踐吧。這裏我選用阿里雲 MSE+ACK 組合做完整的方案。
環境準備
首先,購買好一套 MSE 註冊配置中心專業版,和一套 MSE 雲原生網關。這邊不介紹具體的購買流程。
在應用部署前,提前準備好配置。這邊我們可以先配置 A 的下游是 C,B 的下游也是 C。
部署應用
接下來我們基於 ACK 部署三個應用。可以從下面的配置看到,應用 A 這個版本 spring-cloud-a-b,推空保護開關已經打開。
這裏 demo 選用的 nacos 客戶端版本是 1.4.2,因爲推空保護在這個版本之後才支持。
配置示意(無法直接使用):
# A 應用 base 版本
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: spring-cloud-a
name: spring-cloud-a-b
spec:
replicas: 2
selector:
matchLabels:
app: spring-cloud-a
template:
metadata:
annotations:
msePilotCreateAppName: spring-cloud-a
labels:
app: spring-cloud-a
spec:
containers:
- env:
- name: LANG
value: C.UTF-8
- name: spring.cloud.nacos.discovery.server-addr
value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
- name: spring.cloud.nacos.config.server-addr
value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
- name: spring.cloud.nacos.discovery.metadata.version
value: base
- name: spring.application.name
value: sc-A
- name: spring.cloud.nacos.discovery.namingPushEmptyProtection
value: "true"
image: mse-demo/demo:1.4.2
imagePullPolicy: Always
name: spring-cloud-a
ports:
- containerPort: 8080
protocol: TCP
resources:
requests:
cpu: 250m
memory: 512Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: spring-cloud-a
name: spring-cloud-a
spec:
replicas: 2
selector:
matchLabels:
app: spring-cloud-a
template:
metadata:
annotations:
msePilotCreateAppName: spring-cloud-a
labels:
app: spring-cloud-a
spec:
containers:
- env:
- name: LANG
value: C.UTF-8
- name: spring.cloud.nacos.discovery.server-addr
value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
- name: spring.cloud.nacos.config.server-addr
value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
- name: spring.cloud.nacos.discovery.metadata.version
value: base
- name: spring.application.name
value: sc-A
image: mse-demo/demo:1.4.2
imagePullPolicy: Always
name: spring-cloud-a
ports:
- containerPort: 8080
protocol: TCP
resources:
requests:
cpu: 250m
memory: 512Mi
# B 應用 base 版本
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: spring-cloud-b
name: spring-cloud-b
spec:
replicas: 2
selector:
matchLabels:
app: spring-cloud-b
strategy:
template:
metadata:
annotations:
msePilotCreateAppName: spring-cloud-b
labels:
app: spring-cloud-b
spec:
containers:
- env:
- name: LANG
value: C.UTF-8
- name: spring.cloud.nacos.discovery.server-addr
value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
- name: spring.cloud.nacos.config.server-addr
value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
- name: spring.application.name
value: sc-B
image: mse-demo/demo:1.4.2
imagePullPolicy: Always
name: spring-cloud-b
ports:
- containerPort: 8080
protocol: TCP
resources:
requests:
cpu: 250m
memory: 512Mi
# C 應用 base 版本
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: spring-cloud-c
name: spring-cloud-c
spec:
replicas: 2
selector:
matchLabels:
app: spring-cloud-c
template:
metadata:
annotations:
msePilotCreateAppName: spring-cloud-c
labels:
app: spring-cloud-c
spec:
containers:
- env:
- name: LANG
value: C.UTF-8
- name: spring.cloud.nacos.discovery.server-addr
value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
- name: spring.cloud.nacos.config.server-addr
value: mse-xxx-nacos-ans.mse.aliyuncs.com:8848
- name: spring.application.name
value: sc-C
image: mse-demo/demo:1.4.2
imagePullPolicy: Always
name: spring-cloud-c
ports:
- containerPort: 8080
protocol: TCP
resources:
requests:
cpu: 250m
memory: 512Mi
部署應用:
在網關注冊服務
應用部署好之後,在 MSE 雲原生網關中,關聯上 MSE 的註冊中心,並將服務註冊進來。
我們設計的是網關只調用 A,所以只需要將 A 放進來註冊進來即可。
驗證和調整鏈路
基於 curl 命令驗證一下鏈路:
$ curl http://${網關IP}/ip
sc-A[192.168.1.194] --> sc-C[192.168.1.195]
驗證一下鏈路。 可以看到這時候 A 調用的是 C,我們將配置做一下變更,實時地將 A 的下游改爲 B。
再看一下,這時三個應用的調用關係是 ABC,符合我們之前的計劃。
$ curl http://${網關IP}/ip
sc-A[192.168.1.194] --> sc-B[192.168.1.191] --> sc-C[192.168.1.180]
接下來,我們通過一段命令,連續地調用接口,模擬真實場景下不間斷的業務流量。
$ while true; do sleep .1 ; curl -so /dev/null http://${網關IP}/ip ;done
觀測調用
通過網關監控大盤,可以觀察到成功率。
注入故障
一切正常,現在我們可以開始注入故障。
這裏我們可以使用 K8s 的 NetworkPolicy 的機制,模擬出口網絡異常。
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: block-registry-from-b
spec:
podSelector:
matchLabels:
app: spring-cloud-b
ingress:
- {}
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 8080
這個 8080 端口的意思是,不影響內網調用下游的應用端口,只禁用其它出口流量(比如到達註冊中心的 8848 端口就被禁用了)。這裏 B 的下游是 C。
網絡切斷後,註冊中心的心跳續約不上,過一會兒(30 秒後)就會將應用 B 的所有 IP 摘除。
再次觀測
再觀察大盤數據庫,成功率開始下降,這時候,在控制檯上已經看不到應用 B 的 IP 了。
回到大盤,成功率在 50% 附近不再波動。
05
小結
通過實踐,我們模擬了一次真實的風險發生的場景,並且通過客戶端的高可用方案(推空保護),成功實現了對風險的控制,防止服務調用的發生異常。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4kigzgRx85eqz064u_YJOA