微服務體系架構中,服務之間的依賴關系錯綜複雜,我們往往會使用負載均衡元件配合注冊中心來實作服務間的感覺。而這種感覺行為需要調用方、負載均衡元件、注冊中心、被調用方互相配合才能夠實作,在出現問題時我們又可能很難确定是哪一部分的問題,在正常場景中,注冊中心會有對應的控制台可以檢視,而調用方、負載均衡元件、被調用方處則需要我們手動增加日志列印語句并重新開機應用才能得到相關的資訊,而有些元件又難以找到合适的位置添加我們日志代碼,使得這類問題的排查效率低下。
負載均衡原理剖析
Cloud Native
我們以 Spring Cloud 應用為例分析一下,微服務負載均衡到底是怎麼一回事?
本文的 demo 包含 log-demo-spring-cloud-zuul、log-demo-spring-cloud-a、log-demo-spring-cloud-b、log-demo-spring-cloud-c 四個應用,采用最簡單的 Spring Cloud 标準用法依次調用,可以直接在 項目上檢視源碼:
https://github.com/aliyun/alibabacloud-microservice-demo/tree/master/mse-simple-demo
以 Spring Cloud 常用的用戶端負載均衡元件 Ribbon 作為示例,其工作原理如下圖所示。
Ribbon 位于用戶端一側,通過服務注冊中心(本文中為 Nacos)擷取到一份服務端提供的可用服務清單。随後,在用戶端發送請求時通過負載均衡算法選擇一個服務端執行個體再進行通路,以達到負載均衡的目的。在這個過程中為了感覺服務注冊中心的可用服務清單的變化,Ribbon 會在構造 com.netflix.loadbalancer.DynamicServerListLoadBalancer 時,啟動一個定時線程去循環調用 com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateListOfServers 方法更新自己持有的可用服務清單。
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
//從注冊中心擷取可用服務清單
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
//根據加載的過濾器過濾位址
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
//更新可用服務清單
updateAllServerList(servers);
}
通過代碼可以發現,updateAllServerList(servers)方法的參數 servers 就是更新後可用服務清單,不過為了確定獲得真實的現場,我們随着調用鍊繼續往下。
protected void updateAllServerList(List<T> ls) {
// other threads might be doing this - in which case, we pass
if (serverListUpdateInProgress.compareAndSet(false, true)) {
try {
for (T s : ls) {
s.setAlive(true); // set so that clients can start using these
// servers right away instead
// of having to wait out the ping cycle.
}
setServersList(ls);
super.forceQuickPing();
} finally {
serverListUpdateInProgress.set(false);
}
}
}
可以看到隻有一個線程能夠調用 setServersList(ls)方法去更新可用服務清單,之後的調用鍊還有一些處理邏輯。
com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateAllServerList
-> com.netflix.loadbalancer.DynamicServerListLoadBalancer#setServersList
-> com.netflix.loadbalancer.DynamicServerListLoadBalancer#setServerListForZones
-> com.netflix.loadbalancer.LoadBalancerStats#updateZoneServerMapping
其中 updateZoneServerMapping 方法的參數 Map<String, List<Server>> map 基本等同于本次更新動作最後所更新的可用服務清單。也就是說,隻要能列印出這個方法的參數,我們就能夠知道每次更新可用服務清單的結果,這就能夠幫助我們了解調用方以及負載均衡元件在這個場景下的真實作場。
無侵入的微服務洞察能力
Cloud Native
我們是否可以提供一種能力,我們能夠動态的在任意代碼的關鍵位置動态地列印需要的日志,觀測到任何一部分當下的真實作場,進而輔助我們排查問題。考慮到分布式微服務應用的複雜度,這種能力需要以下一些特點:
- 分布式特性 :滿足在分布式場景下,即使是複雜微服務體系架構下,該能力需要打通微服務鍊路、日志調整、流量條件比對,上下遊關聯等一系列分布式場景下的能力。
- 無侵入特性 :無需重新開機應用,動态增強與解除安裝,可以動态增強整個應用或者是任意節點。
- 完整的現場保留能力 :可以将抓取到的現場上下文等資訊,自動保留至遠端的日志系統中。
- 靈活的規則配 置 :可以靈活比對任意流量,增強任意方法點位,可以靈活控制所需的保留上下文内容。
基于以上思考,我們提供了無侵入的微服務洞察能力,可以迅速幫助我們解決微服務場景下的複雜問題的定位與診斷,可以更好地為我們的治理提供思路與幫助,助力于企業建構完整的微服務治理體系。
洞察 loadbalancer 還原服務發現第一現場
下面來看看如何解決微服務負載均衡能力?
将借助微服務洞察能力,在我們尋找到的位置上儲存列印目标方法包含入參的現場。我們首先選擇 log-demo-spring-cloud-a 應用,在接口清單處選擇自定義埋點,并且填入我們所确定的目标類和目标方法。
目标類:
com.netflix.loadbalancer.LoadBalancerStats
目标方法:
updateZoneServerMapping(java.util.Map)
由于在這個場景下不需要過濾條件,流量過濾條件部分保持預設關閉即可。在列印内容部分,由于我們所關注的内容是該方法的入參,是以勾選通用分類中的請求參數,其餘選項可以根據需求勾選。
目标執行個體根據實際需要選擇全部或是指定執行個體,最後開啟規則并點選确定。
觀察服務發現結果
在完成上述配置後,我們可以在對應的 SLS 的 LogStore 中檢視收集的日志,其結構大緻如下所示。
appName:log-demo-spring-cloud-a
destinationEndpoint:
end:1662541729796
endpoint:10.0.0.24
hostname:log-demo-spring-cloud-a-58b8b7ccc9-gnmsv
interface:com.netflix.loadbalancer.LoadBalancerStats:updateZoneServerMapping(java.util.Map)
ip:10.0.0.24
parameters:[{"unknown":[{"alive":true,"host":"10.0.0.125","hostPort":"10.0.0.125:20002","id":"10.0.0.125:20002","instance":{"clusterName":"DEFAULT","enabled":true,"ephemeral":true,"healthy":true,"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000,"instanceId":"10.0.0.125#20002#DEFAULT#DEFAULT_GROUP@@sc-B","ip":"10.0.0.125","ipDeleteTimeout":30000,"metadata":{"__micro.service.app.id__":"hkhon1po62@622bd5a9ab6ab48","preserved.register.source":"SPRING_CLOUD"},"port":20002,"serviceName":"DEFAULT_GROUP@@sc-B","weight":1.0},"metaInfo":{"appName":"DEFAULT_GROUP@@sc-B","instanceId":"10.0.0.125#20002#DEFAULT#DEFAULT_GROUP@@sc-B"},"metadata":{"$ref":"$[0].unknown[0].instance.metadata"},"port":20002,"readyToServe":true,"zone":"UNKNOWN"},{"alive":true,"host":"10.0.0.52","hostPort":"10.0.0.52:20002","id":"10.0.0.52:20002","instance":{"clusterName":"DEFAULT","enabled":true,"ephemeral":true,"healthy":true,"instanceHeartBeatInterval":5000,"instanceHeartBeatTimeOut":15000,"instanceId":"10.0.0.52#200...展開
parentSpanID:-1
ruleName:[237]
serviceType:DYNAMIC
spanID:4096
start:1662541729795
success:true
tag:_base
traceID:ea1a00001816625413997651001d0001
userId:1784327288677274
parameters 部分是被包裝成 JSON 格式的入參,将其格式化後可以看到這便是我們想要擷取的可用服務清單。
[
{
"unknown": [
{
"alive": true,
"host": "10.0.0.125",
"hostPort": "10.0.0.125:20002",
"id": "10.0.0.125:20002",
"instance": {
"clusterName": "DEFAULT",
"enabled": true,
"ephemeral": true,
"healthy": true,
"instanceHeartBeatInterval": 5000,
"instanceHeartBeatTimeOut": 15000,
"instanceId": "10.0.0.125#20002#DEFAULT#DEFAULT_GROUP@@sc-B",
"ip": "10.0.0.125",
"ipDeleteTimeout": 30000,
"metadata": {
"__micro.service.app.id__": "hkhon1po62@622bd5a9ab6ab48",
"preserved.register.source": "SPRING_CLOUD"
},
"port": 20002,
"serviceName": "DEFAULT_GROUP@@sc-B",
"weight": 1.0
},
"metaInfo": {
"appName": "DEFAULT_GROUP@@sc-B",
"instanceId": "10.0.0.125#20002#DEFAULT#DEFAULT_GROUP@@sc-B"
},
"metadata": {
"$ref": "$[0].unknown[0].instance.metadata"
},
"port": 20002,
"readyToServe": true,
"zone": "UNKNOWN"
},
{
"alive": true,
"host": "10.0.0.52",
"hostPort": "10.0.0.52:20002",
"id": "10.0.0.52:20002",
"instance": {
"clusterName": "DEFAULT",
"enabled": true,
"ephemeral": true,
"healthy": true,
"instanceHeartBeatInterval": 5000,
"instanceHeartBeatTimeOut": 15000,
"instanceId": "10.0.0.52#20002#DEFAULT#DEFAULT_GROUP@@sc-B",
"ip": "10.0.0.52",
"ipDeleteTimeout": 30000,
"metadata": {
"__micro.service.app.id__": "hkhon1po62@622bd5a9ab6ab48",
"preserved.register.source": "SPRING_CLOUD",
"__micro.service.env__": "[{\"desc\":\"k8s-pod-label\",\"priority\":100,\"tag\":\"gray\",\"type\":\"tag\"}]"
},
"port": 20002,
"serviceName": "DEFAULT_GROUP@@sc-B",
"weight": 1.0
},
"metaInfo": {
"appName": "DEFAULT_GROUP@@sc-B",
"instanceId": "10.0.0.52#20002#DEFAULT#DEFAULT_GROUP@@sc-B"
},
"metadata": {
"$ref": "$[0].unknown[1].instance.metadata"
},
"port": 20002,
"readyToServe": true,
"zone": "UNKNOWN"
}
]
}
]
為了證明所擷取的是真實的現場,我們通過容器控制台,将該應用所調用的被調用方,伸縮至 3 個節點。在完成擴容後,檢視日志發現,可用服務清單按照預期由原來的 2 個執行個體變更為 3 個執行個體。
一鍵解決全鍊路灰階流量逃逸問題
Cloud Native
有 時某個功能發版依賴多個服務同時更新上線。 我們希望可以對這些服務的新版本同時進行小流量灰階驗證,這就是微服務架構中特有的全鍊路灰階場景,通過建構從網關到整個後端服務的環境隔離來對多個不同版本的服務進行灰階驗證。 在釋出過程中,我們隻需部署服務的灰階版本,流量在調用鍊路上流轉時,由流經的網關、各個中間件以及各個微服務來識别灰階流量,并動态轉發至對應服務的灰階版本。 如下圖:
上圖可以很好展示這種方案的效果,我們用不同的顔色來表示不同版本的灰階流量,可以看出無論是微服務網關還是微服務本身都需要識别流量,根據治理規則做出動态決策。當服務版本發生變化時,這個調用鍊路的轉發也會實時改變。相比于利用機器搭建的灰階環境,這種方案不僅可以節省大量的機器成本和運維人力,而且可以幫助開發者實時快速的對線上流量進行精細化的全鍊路控制。
在我們生産環境使用全鍊路灰階的過程中,我們常常會遇到一些問題:
- 我們配置全鍊路灰階的流量流向是否符合預期,我們的流量是否按照我們配置的灰階規則進行比對。
- 我們灰階的流量出現了大量的慢調用、異常,我該如何确定是我們新版本代碼的業務問題還是因為我們在流量灰階過程中考慮不全導緻的系統問題,如何快速定位問題,進而實作高效的疊代。
- 在我們設計灰階系統的過程中,我們需要考慮如何對我們的灰階流量進行打标,有些時候在入口應用、微服務接口處可能難以找到合适的流量特征(參數、headers 等攜帶的具備業務語義的辨別),在這樣的場景下我們如何快捷地對我們的流量進行打标。
基于以上一些列的問題,也是我們在支援雲上客戶落地全鍊路灰階的過程中不斷碰到的問題。微服務洞察能力也就是我們在這個過程中抽象設計出來的一個能力。針對上訴的問題,我們的微服務洞察能力都能夠很好地解決。
洞察灰階流量,流量逃逸問題無所遁形
關于灰階流量,我們在使用全鍊路灰階中往往會關注以下三個問題:
- 我們配置全鍊路灰階的流量流向是否符合預期,有沒有流量打到了非灰階應用
- 符合灰階規則的流量是否被打上了對應的灰階标簽
- 不符合灰階規則的流量是否存在被誤打上灰階标簽的情況
因為如果發生灰階流量沒能按照預期排程,或者非灰階流量被錯誤地排程到灰階應用上的情況,不僅會影響灰階功能的測試,甚至會影響非灰階應用的正常運作。
為了回答上述的三個問題,我們需要觀測灰階流量在系統中真實的标簽和路徑的能力。為此我們可以增加自定義流量規則,在配置規則的流量過濾條件部分選中對應的全鍊路灰階标簽,并在列印内容中對應我們配置的灰階規則勾選請求參數、Headers 等資訊,所有具有該灰階标簽的流量的日志會被自動采集并且列印。
通過在控制台觀察采集的日志中的參數、Headers 等資訊可以判斷流量比對是否正确,不符合灰階規則的流量是否存在被誤打上灰階标簽的情況。而通過觀察灰階流量日志中的 appName 資訊可以判斷其所經過的應用是否全部都是灰階版本,進而判斷全鍊路灰階流量的路徑是否符合預期。
對于是否有比對的流量沒有被打上标簽這個問題,我們可以去除标簽的流量過濾條件,進而采集全部的流量,并且通過在控制台對參數等資訊的篩選,觀察是否有符合條件的流量沒有被打上灰階标簽。
定位灰階流量的問題
在全鍊路灰階中,由于整體系統中運作着灰階應用和非灰階應用,相較于日常場景更加複雜。在灰階流量出現慢調用或者異常時,快速定位的難度也會更大,而微服務洞察的動态列印日志能力能夠加速這一過程。
在灰階開始前,如果我們對可能會出現的問題沒有很好的預期,可以先行在入口應用處配置較粗粒度的日志規則,友善我們觀察到問題的出現。
建立規則時選中入口應用,在目标接口清單中,我們可以添加全部的 web、rpc 接口,也可以隻添加我們所關注的。随後在流量過濾條件中選中對應的灰階标簽、開啟慢調用并輸入慢調用門檻值(在此處慢調用可以是相對原版本較慢的調用而非絕對意義上的慢)或是開啟異常,随後在列印内容中選中定位所需的資訊,比如請求參數、錯誤資訊、調用堆棧等。
由于該規則應用于入口應用,我們需要打開後續鍊路日志的開關,以列印該流量路徑上的所有日志。
在開啟規則後,我們可以點選該規則對應的大盤,來觀察所采集的日志。
符合過濾條件的請求和它後續鍊路的日志都會被采集,我們可以在請求清單中選擇檢視某一請求的鍊路詳情,進而發現出現問題的具體位置。對于某些問題也可以看到其異常堆棧,進而确定發生異常的方法調用和導緻該異常的可能的内部方法,随後我們可以通過配置自定義流量規則,在目标接口中選中自定義埋點,并輸入這些嫌疑方法。
在列印内容中可以選擇請求參數,傳回值等資訊輔助判斷。在開啟規則後便可以列印這些方法的日志,進而判斷問題産生的原因。
總結
Cloud Native
本文基于常見的服務調用場景,以 Ribbon 負載均衡元件為例,展示了微服務洞察能力能夠在關鍵的位置為我們還原與記錄豐富的現場資訊,使得原有的黑盒場景能夠便捷直覺地被觀測到,在微服務架構下,類似的不便觀測的重要場景還有非常多,都可以借助微服務洞察能力來監測或是在異常時輔助排查。同時,全鍊路灰階是微服務治理中比較重要的一個場景,我們在落地全鍊路灰階的過程中最讓人頭大的兩個問題就是流量路由不生效以及流量逃逸,我們借助于微服務洞察能力可以快速定位與解決全鍊路灰階相關的問題。
MSE 微服務洞察能力我們還在持續地打磨與完善,旨在幫助我們更好地治理我們的微服務應用,助力于雲上幫助企業建構完整的微服務體系。歡迎大家嘗鮮與體驗~