基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

當閘道器和服務在實施全鏈路分散式灰度釋出和路由時候,我們需要一款追蹤系統來監控閘道器和服務走的是哪個灰度組,哪個灰度版本,哪個灰度區域,甚至監控從Http Header頭部全程傳遞的灰度規則和路由策略。這個功能意義在於:

可以監控全鏈路中基本的呼叫資訊,也可以監控額外的灰度資訊,有助於我們判斷灰度釋出和路由是否執行準確,一旦有問題,也可以快速定位

可以監控流量何時切換到新版本,或者新的區域,或者新的機器上

可以監控灰度規則和路由策略是否配置準確

可以監控閘道器和服務灰度上下級樹狀關係

可以監控全鏈路流量拓撲圖

筆者嘗試調研了一系列分散式追蹤系統和中介軟體,包括Opentracing、Uber Jaeger、Twitter Zipkin、Apache Skywalking、Pinpoint、CAT等,並結合業界動向,CNCF技術委員會透過OpenTelemetry規範整合基於Tracing的OpenTracing規範(官方推薦Jaeger做Backend)和基於Metrics的OpenSensus規範(官方推薦Prometheus做Backend),最後決定採用Opentracing + Uber Jaeger方式來實現,重要原因除了易用性和可擴充套件性外,Opentracing支援WebMvc和WebFlux兩種方式,業界的追蹤系統能支援WebFlux相對較少

OpenTracing

] OpenTracing已進入CNCF,正在為全球的分散式追蹤系統提供統一的概念、規範、架構和資料標準。它透過提供平臺無關、廠商無關的API,使得開發人員能夠方便的新增(或更換)追蹤系統的實現。對於存在多樣化的技術棧共存的呼叫鏈中,Opentracing適配Java、C、Go和。Net等技術棧,實現全鏈路分散式追蹤功能。迄今為止,Uber Jaeger、Twitter Zipkin和Apache Skywalking已經適配了Opentracing規範

筆者以Nepxion社群的Discovery開源框架(對該開源框架感興趣的同學,請訪問如下連結)為例子展開整合

原始碼主頁,請訪問

https://github。com/Nepxion/Discovery

指南主頁,請訪問

https://github。com/Nepxion/DiscoveryGuide

文件主頁,請訪問

https://gitee。com/Nepxion/Docs/tree/master/web-doc

整合的效果圖

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

整合Sentinel + 灰度全鏈路監控

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

整合主流中介軟體 + 灰度全鏈路監控

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

基於Opentracing + Uber Jaeger實現全鏈路灰度呼叫鏈

基本概念

灰度呼叫鏈主要包括如下11個引數。使用者可以自行定義要傳遞的呼叫鏈引數,例如:traceId, spanId等;也可以自行定義要傳遞的業務呼叫鏈引數,例如:mobile, user等

1n-d-service-group - 服務所屬組或者應用 2n-d-service-type - 服務型別,分為“閘道器”和“服務” 3n-d-service-id - 服務ID 4n-d-service-address - 服務地址,包括Host和Port 5n-d-service-version - 服務版本 6n-d-service-region - 服務所屬區域 7n-d-version - 版本路由值 8n-d-region - 區域路由值 9n-d-address - 地址路由值10n-d-version-weight - 版本權重路由值11n-d-region-weight - 區域權重路由值

核心實現

Opentracing通用模組

原始碼參考

https://github。com/Nepxion/Discovery/tree/master/discovery-plugin-strategy-opentracing

由於OpenTracing擴充套件需要兼顧到Spring Cloud Gateway、Zuul和服務,它的核心邏輯存在著一定的可封裝性,所以筆者抽取出一個公共模組discovery-plugin-strategy-opentracing,包含configuration、operation、context等模組,著重闡述operation模組,其它比較簡單,不一一贅述了

在闡述前,筆者需要解釋一個配置,該配置將決定核心實現以及終端介面的顯示

如果開啟,灰度資訊輸出到獨立的Span節點中,意味著在介面顯示中,灰度資訊透過獨立的GRAY Span節點來顯示。優點是資訊簡潔明瞭,缺點是Span節點會增長一倍。我們可以稱呼它為【模式A】

如果關閉,灰度資訊輸出到原生的Span節點中,意味著在介面顯示中,灰度資訊會和原生Span節點的呼叫資訊、協議資訊等混在一起,缺點是資訊龐雜混合,優點是Span節點數不會增長。我們可以稱呼它為【模式B】

1# 啟動和關閉呼叫鏈的灰度資訊在Opentracing中以獨立的Span節點輸出,如果關閉,則灰度資訊輸出到原生的Span節點中。缺失則預設為true2spring。application。strategy。trace。opentracing。separate。span。enabled=true

Opentracing公共操作類 - StrategyOpentracingOperation。java

1- 裝配注入Opentracing的Tracer物件 2- opentracingInitialize方法,提供給閘道器和服務的Span節點初始化 3 - 【模式A】下,tracer。buildSpan(。。。)。start()實現新建一個Span,並把它放置到儲存上下文的StrategyOpentracingContext的ThreadLocal裡 4 - 【模式B】下,不需要做任何工作 5- opentracingHeader方法,提供給閘道器的灰度呼叫鏈輸出 6 - 【模式A】下,首先從StrategyOpentracingContext的ThreadLocal裡獲取Span物件,其次把customizationMap(自定義的呼叫鏈引數)的元素都放入到Tag中,最後把灰度呼叫鏈主11個引數(透過strategyContextHolder。getHeader(。。。)獲取)和更多上下文資訊放入到Tag中 7 - 【模式B】下,跟【模式A】類似,唯一區別的是Tags。COMPONENT的處理,由於原生的Span節點已經帶有該資訊,所以不需要放入到Tag中 8- opentracingLocal方法,提供給服務的灰度呼叫鏈輸出 9 - 【模式A】下,首先從StrategyOpentracingContext的ThreadLocal裡獲取Span物件,其次把customizationMap(自定義的呼叫鏈引數)的元素都放入到Tag中,最後把灰度呼叫鏈主11個引數(透過pluginAdapter。getXXX()獲取)和更多上下文資訊放入到Tag中10 - 【模式B】下,跟【模式A】類似,唯一區別的是Tags。COMPONENT的處理,由於原生的Span節點已經帶有該資訊,所以不需要放入到Tag中11- opentracingError方法,提供給服務的灰度呼叫鏈異常輸出12 - 【模式A】下,首先從StrategyOpentracingContext的ThreadLocal裡獲取Span物件,其次span。log(。。。)方法實現異常輸出13 - 【模式B】下,不需要做任何工作14- opentracingClear方法,灰度呼叫鏈的Span上報和清除15 - 【模式A】下,首先從StrategyOpentracingContext的ThreadLocal裡獲取Span物件,其次span。finish()方法實現Span上報,最後StrategyOpentracingContext。clearCurrentContext()方法實現Span清除16 - 【模式B】下,不需要做任何工作 17- getCurrentSpan方法18 - 【模式A】下,返回StrategyOpentracingContext。getCurrentContext()。getSpan(),即opentracingInitialize新建的Span物件19 - 【模式B】下,返回tracer。activeSpan(),即原生的Span物件

程式碼如下

1public class StrategyOpentracingOperation { 2 @Autowired 3 protected PluginAdapter pluginAdapter; 4 5 @Autowired 6 protected StrategyContextHolder strategyContextHolder; 7 8 @Autowired 9 private Tracer tracer; 10 11 @Value(“${” + StrategyOpentracingConstant。SPRING_APPLICATION_STRATEGY_TRACE_OPENTRACING_ENABLED + “:false}”) 12 protected Boolean traceOpentracingEnabled; 13 14 @Value(“${” + StrategyOpentracingConstant。SPRING_APPLICATION_STRATEGY_TRACE_OPENTRACING_SEPARATE_SPAN_ENABLED + “:true}”) 15 protected Boolean traceOpentracingSeparateSpanEnabled; 16 17 public void opentracingInitialize() { 18 if (!traceOpentracingEnabled) { 19 return; 20 } 21 22 if (!traceOpentracingSeparateSpanEnabled) { 23 return; 24 } 25 26 Span span = tracer。buildSpan(DiscoveryConstant。SPAN_VALUE)。start(); 27 StrategyOpentracingContext。getCurrentContext()。setSpan(span); 28 } 29 30 public void opentracingHeader(Map customizationMap) { 31 if (!traceOpentracingEnabled) { 32 return; 33 } 34 35 Span span = getCurrentSpan(); 36 if (span == null) { 37 return; 38 } 39 40 if (MapUtils。isNotEmpty(customizationMap)) { 41 for (Map。Entry entry : customizationMap。entrySet()) { 42 span。setTag(entry。getKey(), entry。getValue()); 43 } 44 } 45 46 span。setTag(DiscoveryConstant。TRACE_ID, span。context()。toTraceId()); 47 span。setTag(DiscoveryConstant。SPAN_ID, span。context()。toSpanId()); 48 span。setTag(DiscoveryConstant。N_D_SERVICE_GROUP, strategyContextHolder。getHeader(DiscoveryConstant。N_D_SERVICE_GROUP)); 49 。。。 50 51 String routeVersion = strategyContextHolder。getHeader(DiscoveryConstant。N_D_VERSION); 52 if (StringUtils。isNotEmpty(routeVersion)) { 53 span。setTag(DiscoveryConstant。N_D_VERSION, routeVersion); 54 } 55 。。。 56 } 57 58 public void opentracingLocal(String className, String methodName, Map customizationMap) { 59 if (!traceOpentracingEnabled) { 60 return; 61 } 62 63 Span span = getCurrentSpan(); 64 if (span == null) { 65 return; 66 } 67 68 if (MapUtils。isNotEmpty(customizationMap)) { 69 for (Map。Entry entry : customizationMap。entrySet()) { 70 span。setTag(entry。getKey(), entry。getValue()); 71 } 72 } 73 74 span。setTag(DiscoveryConstant。TRACE_ID, span。context()。toTraceId()); 75 span。setTag(DiscoveryConstant。SPAN_ID, span。context()。toSpanId()); 76 span。setTag(DiscoveryConstant。N_D_SERVICE_GROUP, pluginAdapter。getGroup()); 77 。。。 78 79 String routeVersion = strategyContextHolder。getHeader(DiscoveryConstant。N_D_VERSION); 80 if (StringUtils。isNotEmpty(routeVersion)) { 81 span。setTag(DiscoveryConstant。N_D_VERSION, routeVersion); 82 } 83 。。。 84 } 85 86 public void opentracingError(String className, String methodName, Throwable e) { 87 if (!traceOpentracingEnabled) { 88 return; 89 } 90 91 if (!traceOpentracingSeparateSpanEnabled) { 92 return; 93 } 94 95 Span span = getCurrentSpan(); 96 if (span == null) { 97 return; 98 } 99100 span。log(new ImmutableMap。Builder()101 。put(DiscoveryConstant。CLASS, className)102 。put(DiscoveryConstant。METHOD, methodName)103 。put(DiscoveryConstant。EVENT, Tags。ERROR。getKey())104 。put(DiscoveryConstant。ERROR_OBJECT, e)105 。build());106 }107108 public void opentracingClear() {109 if (!traceOpentracingEnabled) {110 return;111 }112113 if (!traceOpentracingSeparateSpanEnabled) {114 return;115 }116117 Span span = getCurrentSpan();118 if (span != null) {119 span。finish();120 }121 StrategyOpentracingContext。clearCurrentContext();122 }123124 public Span getCurrentSpan() {125 return traceOpentracingSeparateSpanEnabled ? StrategyOpentracingContext。getCurrentContext()。getSpan() : tracer。activeSpan();126 }127128 public String getTraceId() {129 if (!traceOpentracingEnabled) {130 return null;131 }132133 Span span = getCurrentSpan();134 if (span != null) {135 return span。context()。toTraceId();136 }137138 return null;139 }140141 public String getSpanId() {142 if (!traceOpentracingEnabled) {143 return null;144 }145146 Span span = getCurrentSpan();147 if (span != null) {148 return span。context()。toSpanId();149 }150151 return null;152 }153}

Opentracing Service模組

原始碼參考

https://github。com/Nepxion/Discovery/tree/master/discovery-plugin-strategy-starter-service-opentracing

實現OpenTracing對服務的擴充套件,包含configuration、tracer等模組,著重闡述tracer模組,其它比較簡單,不一一贅述了

Opentracing的服務追蹤類 - DefaultServiceStrategyOpentracingTracer。java

1- 繼承DefaultServiceStrategyTracer,並注入StrategyOpentracingOperation2- trace方法裡先執行opentracingInitialize初始化Span,這樣可以讓後面的邏輯都可以從Span中拿到traceId和spanId,執行opentracingLocal實現服務的灰度呼叫鏈輸出3- error方法裡執行opentracingError實現服務的灰度呼叫鏈異常輸出4- release方法裡執行opentracingClear實現灰度呼叫鏈的Span上報和清除

程式碼如下

1public class DefaultServiceStrategyOpentracingTracer extends DefaultServiceStrategyTracer { 2 @Autowired 3 private StrategyOpentracingOperation strategyOpentracingOperation; 4 5 @Override 6 public void trace(ServiceStrategyTracerInterceptor interceptor, MethodInvocation invocation) { 7 strategyOpentracingOperation。opentracingInitialize(); 8 9 super。trace(interceptor, invocation);1011 strategyOpentracingOperation。opentracingLocal(interceptor。getMethod(invocation)。getDeclaringClass()。getName(), interceptor。getMethodName(invocation), getCustomizationMap());12 }1314 @Override15 public void error(ServiceStrategyTracerInterceptor interceptor, MethodInvocation invocation, Throwable e) {16 super。error(interceptor, invocation, e);1718 strategyOpentracingOperation。opentracingError(interceptor。getMethod(invocation)。getDeclaringClass()。getName(), interceptor。getMethodName(invocation), e);19 }2021 @Override22 public void release(ServiceStrategyTracerInterceptor interceptor, MethodInvocation invocation) {23 super。release(interceptor, invocation);2425 strategyOpentracingOperation。opentracingClear();26 }2728 @Override29 public String getTraceId() {30 return strategyOpentracingOperation。getTraceId();31 }3233 @Override34 public String getSpanId() {35 return strategyOpentracingOperation。getSpanId();36 }37}

Opentracing Gateway模組

原始碼參考

https://github。com/Nepxion/Discovery/tree/master/discovery-plugin-strategy-starter-gateway-opentracing

實現OpenTracing對Spring Cloud Gateway的擴充套件,跟discovery-plugin-strategy-starter-service-opentracing模組類似,不一一贅述了

Opentracing Zuul模組

原始碼參考

https://github。com/Nepxion/Discovery/tree/master/discovery-plugin-strategy-starter-zuul-opentracing

實現OpenTracing對Zuul的擴充套件,跟discovery-plugin-strategy-starter-service-opentracing模組類似,不一一贅述了

擴充套件其它呼叫鏈中介軟體

如果使用者不希望採用Opentracing + Uber Jaeger方式,希望自己擴充套件其它呼叫鏈中介軟體,那麼也很簡單,仿造原始碼,繼承實現下面的類:

DefaultServiceStrategyTracer,擴充套件服務側的灰度呼叫鏈

DefaultGatewayStrategyTracer,擴充套件Spring Cloud Gatway側的灰度呼叫鏈

DefaultZuulStrategyTracer,擴充套件Zuul側的灰度呼叫鏈

使用說明

示例參考

https://github。com/Nepxion/DiscoveryGuide

使用方式

Opentracing輸出方式以Uber Jaeger為例來說明,步驟非常簡單

從https://pan。baidu。com/s/1i57rXaNKPuhGRqZ2MONZOA獲取Jaeger-1。14。0。zip,Windows作業系統下解壓後執行jaeger。bat,Mac和Lunix作業系統請自行研究

執行Postman呼叫後,訪問http://localhost:16686檢視灰度呼叫鏈

灰度呼叫鏈支援WebMvc和WebFlux兩種方式,以GRAY字樣的標記來標識

開關控制

對於Opentracing呼叫鏈功能的開啟和關閉,需要透過如下開關做控制:

1# 啟動和關閉呼叫鏈。缺失則預設為false2spring。application。strategy。trace。enabled=true3# 啟動和關閉呼叫鏈的Opentracing輸出,支援F版或更高版本的配置,其它版本不需要該行配置。缺失則預設為false4spring。application。strategy。trace。opentracing。enabled=true5# 啟動和關閉呼叫鏈的灰度資訊在Opentracing中以獨立的Span節點輸出,如果關閉,則灰度資訊輸出到原生的Span節點中。缺失則預設為true6spring。application。strategy。trace。opentracing。separate。span。enabled=true

可選功能

自定義呼叫鏈上下文引數的建立(該類不是必須的),繼承DefaultStrategyTracerAdapter

1// 自定義呼叫鏈上下文引數的建立 2// 對於getTraceId和getSpanId方法,在Opentracing等呼叫鏈中介軟體引入的情況下,由呼叫鏈中介軟體決定,在這裡定義不會起作用;在Opentracing等呼叫鏈中介軟體未引入的情況下,在這裡定義才有效,下面程式碼中表示從Http Header中獲取,並全鏈路傳遞 3// 對於getCustomizationMap方法,表示輸出到呼叫鏈中的定製化業務引數,可以同時輸出到日誌和Opentracing等呼叫鏈中介軟體,下面程式碼中表示從Http Header中獲取,並全鏈路傳遞 4public class MyStrategyTracerAdapter extends DefaultStrategyTracerAdapter { 5 @Override 6 public String getTraceId() { 7 return StringUtils。isNotEmpty(strategyContextHolder。getHeader(DiscoveryConstant。TRACE_ID)) ? strategyContextHolder。getHeader(DiscoveryConstant。TRACE_ID) : StringUtils。EMPTY; 8 } 910 @Override11 public String getSpanId() {12 return StringUtils。isNotEmpty(strategyContextHolder。getHeader(DiscoveryConstant。SPAN_ID)) ? strategyContextHolder。getHeader(DiscoveryConstant。SPAN_ID) : StringUtils。EMPTY;13 }1415 @Override16 public Map getCustomizationMap() {17 return new ImmutableMap。Builder()18 。put(“mobile”, StringUtils。isNotEmpty(strategyContextHolder。getHeader(“mobile”)) ? strategyContextHolder。getHeader(“mobile”) : StringUtils。EMPTY)19 。put(“user”, StringUtils。isNotEmpty(strategyContextHolder。getHeader(“user”)) ? strategyContextHolder。getHeader(“user”) : StringUtils。EMPTY)20 。build();21 }22}

在配置類裡@Bean方式進行呼叫鏈類建立,覆蓋框架內建的呼叫鏈類

1@Bean2public StrategyTracerAdapter strategyTracerAdapter() {3 return new MyStrategyTracerAdapter();4}

本文作者

任浩軍, 10 多年開源經歷,Github ID:@HaojunRen,Nepxion 開源社群創始人,Nacos Group Member,Spring Cloud Alibaba & Nacos & Sentinel Committer ,曾就職於平安銀行平臺架構部,負責銀行 PaaS 系統基礎服務框架研發。現就職掌門1對1,負責基礎架構部研發。

掌門技術

出處:https://mp。weixin。qq。com/s/THA5RycQlaUsfVaicRvUHQ