鏈路追蹤在開源SpringBoot/SpringCloud微服務框架的實踐
前期內容導讀:
- Java開源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介紹
- Java開源AES/SM4/3DES對稱加密算法介紹及其實現(xiàn)
- Java開源AES/SM4/3DES對稱加密算法的驗證說明
- Java開源RSA/SM2非對稱加密算法對比介紹
- Java開源RSA非對稱加密算法實現(xiàn)
- Java開源SM2非對稱加密算法實現(xiàn)
- Java開源接口微服務代碼框架
- Json在開源SpringBoot/SpringCloud微服務框架中的最佳實踐
- 加解密在開源SpringBoot/SpringCloud微服務框架的最佳實踐
- 在前面詳細介紹的基礎上,且代碼全部開源后,這次來完整介紹下鏈路追蹤在SpringBoot/SpringCloud微服務中到底是如何應用的。
- 應該是先有業(yè)務,才會有微服務設計。此開源的微服務設計見Java開源接口微服務代碼框架 文章,現(xiàn)把核心設計摘錄如下:
1. 開源代碼整體設計
+------------+
| bq-log |
| |
+------------+
Based on SpringBoot
|
|
v
+------------+ +------------+ +------------+ +-------------------+
|bq-encryptor| +-----> | bq-base | +-----> |bq-boot-root| +-----> | bq-service-gateway|
| | | | | | | |
+------------+ +------------+ +------------+ +-------------------+
Based on BouncyCastle Based on Spring Based on SpringBoot Based on SpringBoot-WebFlux
+
|
v
+------------+ +-------------------+
|bq-boot-base| +-----> | bq-service-auth |
| | | | |
+------------+ | +-------------------+
ased on SpringBoot-Web | Based on SpringSecurity-Authorization-Server
|
|
|
| +-------------------+
+-> | bq-service-biz |
| |
+-------------------+
說明:
bq-encryptor
:基于BouncyCastle
安全框架,已開源 ,加解密介紹
,支持RSA
/AES
/PGP
/SM2
/SM3
/SM4
/SHA-1
/HMAC-SHA256
/SHA-256
/SHA-512
/MD5
等常用加解密算法,并封裝好了多種使用場景、做好了為SpringBoot所用的準備;bq-base
:基于Spring框架的基礎代碼框架,已開源 ,支持json
/redis
/DataSource
/guava
/http
/tcp
/thread
/jasypt
等常用工具API;bq-log
:基于SpringBoot框架的基礎日志代碼,已開源 ,支持接口Access日志、調用日志、業(yè)務操作日志等日志文件持久化,可根據(jù)實際情況擴展;bq-boot-root
:基于SpringBoot,已開源 ,但是不包含spring-boot-starter-web
,也不包含spring-boot-starter-webflux
,可通用于servlet
和netty
web容器場景,封裝了redis
/http
/定時器
/加密機
/安全管理器
等的自動注入;bq-boot-base
:基于spring-boot-starter-web
(servlet,BIO),已開源 ,提供常規(guī)的業(yè)務服務基礎能力,支持PostgreSQL
/限流
/bq-log
/Web框架
/業(yè)務數(shù)據(jù)加密機加密
等可配置自動注入;bq-service-gateway
:基于spring-boot-starter-webflux
(Netty,NIO),已開源 ,提供了Jwt Token安全校驗能力,包括接口完整性校驗
/接口數(shù)據(jù)加密
/Jwt Token合法性校驗等;bq-service-auth
:基于spring-security-oauth2-authorization-server
,已開源 ,提供了JwtToken生成和刷新的能力;bq-service-biz
:業(yè)務微服務參考樣例,已開源 ;
2. 微服務邏輯架構設計
+-------------------+
| Web/App Client |
| |
+-------------------+
|
|
v
+--------------------------------------------------------------------+
| | Based On K8S |
| |1 |
| v |
| +-------------------+ 2 +-------------------+ |
| | bq-service-gateway| +-------> | bq-service-auth | |
| | | | | |
| +-------------------+ +-------------------+ |
| |3 |
| +-------------------------------+ |
| v v |
| +-------------------+ +-------------------+ |
| | bq-service-biz1 | | bq-service-biz2 | |
| | | | | |
| +-------------------+ +-------------------+ |
| |
+--------------------------------------------------------------------+
說明:
bq-service-gateway
:基于SpringCloud-Gateway
,用作JwtToken鑒權,并提供了接口、數(shù)據(jù)加解密的安全保障能力;bq-service-auth
:基于spring-security-oauth2-authorization-server
,提供了JwtToken生成和刷新的能力;bq-service-biz
:基于spring-boot-starter-web
,業(yè)務微服務參考樣例;k8s
在上述微服務架構中,承擔起了服務注冊和服務發(fā)現(xiàn)的作用,鑒于k8s
云原生環(huán)境構造較為復雜,實際開源的代碼時,以Nacos
(為主)/Eureka
做服務注冊和服務發(fā)現(xiàn)中間件;- 以上所有服務都以docker容器作為載體,確保服務有較好地集群遷移和彈性能力,并能夠逐步平滑遷移至k8s的終極目標;
- 邏輯架構不等同于物理架構(部署架構),實際業(yè)務部署時,還有DMZ區(qū)和內網區(qū),本邏輯架構做了簡化處理;
3. 鏈路追蹤框架選型
3.1 為什么要引入鏈路追蹤
- 隨著分布式微服務的發(fā)展,服務在小型化的同時,服務數(shù)據(jù)急劇膨脹,導致調用鏈條特別復雜特別長,定位問題和數(shù)據(jù)提取比較困難;
- 微服務化也促使用平價的服務器(一般是VM,或者叫ECS)來替代價格高昂的專用服務器,所以會導致服務的穩(wěn)定性變差,所以也需要關注資源的性能瓶頸;
- 鏈路追蹤并非必須的,在傳統(tǒng)項目、服務數(shù)量稀少、業(yè)務相對簡單的項目就沒有必要使用,在云原生微服務架構中則很有必要引入;
3.2 鏈路追蹤能做什么
- 鏈路追蹤是為了解決技術痛點的,其核心價值在于:評估并記錄服務間的調用鏈數(shù)據(jù);我們可以基于這些數(shù)據(jù)清晰地知道客戶請求的來龍去脈,系統(tǒng)出現(xiàn)問題的大致位置。
- 鏈路追蹤不關心服務內部觸發(fā)的其它調用鏈,比如:服務內的定時器、服務內的初始化服務等;
3.3 當下鏈路追蹤框架對比
-
鏈路追蹤技術基本上都是Google Dapper,當下有2種不同的實現(xiàn):
- 代碼侵入式的引用,如:zipkin/cat;
- 代碼無侵入式的引用,如:SkyWalking/Pinpoint;
二者的區(qū)別:前者需要通過把鏈路追蹤的Java包當做依賴加入到依賴庫中;后者則是在執(zhí)行啟動命令時,帶上鏈路追蹤的jar包即可,鏈路監(jiān)控完全基于字節(jié)碼增強技術來實現(xiàn);
-
當下較多使用的鏈路追蹤框架如下表所示:
鏈路追蹤特性 Cat Zipkin SkyWalking Pinpoint 調用鏈可視化 有 有 有 有 聚合報表 非常豐富 少 較豐富 非常豐富 服務依賴圖 簡單 簡單 好 好 埋點方式 侵入式 侵入式 非侵入式,字節(jié)碼增強 非侵入式,字節(jié)碼增強 VM監(jiān)控指標 好 無 有 好 支持語言 java/.net 豐富 java/.net/php/go/node.js java/php/python 存儲機制 mysql(報表),本地文件/HDFS(調用鏈) 內存/redis/es/mysql等 H2、es HBase 社區(qū)支持 主要在國內 國外主流 Apache支持 - 使用案例 美團、攜程 京東、阿里定制后不開源 華為、小米 - APM 是 否 是 是 開發(fā)基礎 eBay cal Google Dapper Google Dapper Google Dapper 是否支持WebFlux 否 是 是 否 結合實際情況:
- 我們有SpringCloud-Gateway(基于WebFlux),所以不能使用
Cat
/Pinpoint
; - 我們當下只要加上鏈路追蹤即可,再加上
zipkin
是SpringCloud的親兒子,對應的SpringCloud組件為SpringCloud-Sleuth,所以此框架優(yōu)先選用了zipkin
,暫沒有必要去使用牛刀SkyWalking
;
- 我們有SpringCloud-Gateway(基于WebFlux),所以不能使用
-
綜上,我們選擇小巧而且與SpringCloud框架最密切的
zipkin
作為我們的鏈路追蹤框架,缺點就是它是代碼侵入式的,它的變更可能會影響業(yè)務穩(wěn)定。
3.4 在項目中引入zipkin
- 引入maven依賴 :
<!--for trace id--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> <version>3.1.7</version> </dependency> <!--for monitor trace--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> <version>3.1.7</version> </dependency>
工程引入sleuth和zipkin時,可能會存在jar包依賴沖突,尤其是要兼顧webflux時,有興趣可以看看工程中的真實引用關系。maven沖突的問題就不單獨講了。
- 下載zipkin 源碼,按照指導文檔進行編譯(不要最新版本,要找和SpringCloud匹配的版本)。編譯成功后,進入zipkin-server目錄,執(zhí)行zipkin啟動命令:
java -jar ./zipkin-server/target/zipkin-server-*exec.jar
- 在spring yaml 配置文件中配置zipkin信息:
spring: sleuth: sampler: #采樣率值介于0到1之間,1則表示全部采集 probability: 1 zipkin: #Zipkin的訪問地址 base-url: http://localhost:9411
- 為了讓日志格式在Spring中定制,logback日志配置文件最好是使用logback-spring.xml,不要使用logback.xml;
- 日志分為2種,一種是Access日志,另一種是運行日志。我們現(xiàn)在就要保證2種日志都有鏈路ID;
- SpringBoot的yaml配置 為:
logging: name: ${spring.application.name} config: classpath:logback-spring.xml basedir: /***/logs/${spring.application.name}/ format: "%d{yy-MM-dd HH:mm:ss.SSS}[${spring.application.name}][Tid:%X{traceId:-},Sid:%X{spanId:-}][%level][%logger{20}_%M] - %msg%n"
4. SpringBoot服務引入zipkin
- 對應的logback-spring.xml :
<?xml version="1.0" encoding="UTF-8"?> <!--日志級別以及優(yōu)先級排序: FATAL > ERROR > WARN > INFO > DEBUG--> <configuration debug="false"> <springProperty scope="context" name="LOG_SERVICE" source="spring.application.name" defaultValue="bq-service"/> <springProperty scope="context" name="INSTANCE_ID" source="server.port" defaultValue="8080"/> <springProperty scope="context" name="BASE_LOG_PATH" source="logging.basedir" defaultValue="/temp/${LOG_SERVICE}"/> <!-- 日志默認輸出級別 --> <springProperty scope="context" name="LOG_LEVEL" source="log.level.ROOT" defaultValue="INFO"/> <!-- 日志文件默認輸出格式,不帶行號輸出(行號顯示會影響日志輸出性能);%C:大寫,類名;%M:方法名;%m:錯誤信息;%n:換行 --> <!--%d{yy-MM-dd HH:mm:ss.SSS}[TxId:%X{PtxId},SpanId:%X{PspanId}][${LOG_SERVICE}][%level][%logger{20}_%M] - %msg%n--> <springProperty scope="context" name="LOG_PATTERN" source="logging.format" defaultValue="%msg%n"/> <!-- 日志默認切割的最小單位 --> <springProperty scope="context" name="MAX_FILE_SIZE" source="logging.file-size" defaultValue="100MB"/> <!--單機直接運行時這樣區(qū)分--> <property name="LOG_PATH" value="${BASE_LOG_PATH}/${LOG_SERVICE}_${INSTANCE_ID}"/> <!--使用自定義的access日志 --> <appender name="accessAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/access.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/%d{yy-MM-dd}/access-%d{yy-MM-dd}.log</FileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${AUDIT_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--控制臺日志--> <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--default日志 --> <appender name="defaultAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/default.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/%d{yy-MM-dd}/default-%d{yy-MM-dd}.log</FileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--error日志 --> <appender name="errorAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/%d{yy-MM-dd}/error-%d{yy-MM-dd}.log</FileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender> <!--默認日志--> <logger name="com.biuqu" additivity="false"> <appender-ref ref="consoleAppender"/> <appender-ref ref="defaultAppender"/> </logger> <!--access日志--> <logger name="com.biuqu.boot.model.MdcAccessLogValve" additivity="false"> <appender-ref ref="accessAppender"/> </logger> <!--全局異常日志--> <logger name="com.biuqu.boot.handler.GlobalExceptionHandler" additivity="false"> <appender-ref ref="errorAppender"/> <appender-ref ref="defaultAppender"/> </logger> <!--建立一個默認的root的logger --> <root level="${LOG_LEVEL}"> <appender-ref ref="consoleAppender"/> <appender-ref ref="defaultAppender"/> </root> </configuration>
仔細觀察就可以看出
logging.format
是從SpringBoot yaml配置中傳入logback的,是Access Log/運行日志/錯誤日志/Console日志的格式字段,帶有traceId
和SpanId
字段; - 深入研究就會發(fā)現(xiàn)常規(guī)SpringBoot微服務(基于tomcat),access log并沒有鏈路信息,還需要對框架進一步改造。
- 需要先定制Tomcat的Access Log工廠類,因此新增一個日志的配置服務LogConfigurer ,代碼如下:
@Configuration public class LogConfigurer { /** * 在tomcat日志中實現(xiàn)trace id * <p> * 參考: https://www.appsloveworld.com/springboot/100/36/mdc-related-content-in-tomcat-access-logs * * @param env 運行環(huán)境變量 * @return 定制的AccessLog工廠 */ @Bean public WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory> accessLog(Environment env) { return factory -> { final AccessLogValve valve = new MdcAccessLogValve(); valve.setPattern(env.getProperty("server.tomcat.accesslog.pattern")); //直接覆蓋原生的日志對象 if (factory instanceof TomcatServletWebServerFactory) { TomcatServletWebServerFactory tsFactory = (TomcatServletWebServerFactory)factory; tsFactory.setEngineValves(Lists.newArrayList(valve)); } }; } }
- 在LogConfigurer 中自定義了一個MdcAccessLogValve 對象,代碼如下:
@Slf4j public class MdcAccessLogValve extends AccessLogValve { @Override public void log(CharArrayWriter message) { log.info(message.toString()); } @Override protected AccessLogElement createAccessLogElement(String name, char pattern) { if (pattern == CommonBootConst.TRACE_TAG) { return (buf, date, request, response, time) -> { //兼容沒有sleuth時的場景 boolean existTrace = ClassUtils.isPresent(SLEUTH_TYPE, this.getClass().getClassLoader()); if (!existTrace) { buf.append(Const.MID_LINK); return; } Object context = request.getRequest().getAttribute(TraceContext.class.getName()); if (!(context instanceof TraceContext)) { return; } TraceContext traceContext = (TraceContext)context; if (CommonBootConst.TRACE_ID.equalsIgnoreCase(name)) { buf.append(traceContext.traceId()); } else if (CommonBootConst.SPAN_ID.equalsIgnoreCase(name)) { buf.append(traceContext.spanId()); } }; } return super.createAccessLogElement(name, pattern); } /** * Sleuth存在的key */ private static final String SLEUTH_TYPE = "org.springframework.cloud.sleuth.TraceContext"; }
-
MdcAccessLogValve
設計時,兼容了使用sleuth
和不使用sleuth
2種情況。 - AccessLog打印出來后,就會發(fā)現(xiàn)會多了一些健康檢查日志,注意不要把心跳檢查設置得過于頻繁;
-
- 需要先定制Tomcat的Access Log工廠類,因此新增一個日志的配置服務LogConfigurer ,代碼如下:
- 至此,基于SpringBoot常規(guī)的微服務大部分情況下有鏈路ID了。但是由于定制了線程池和異步任務池,存在如下2種異常情況:
- 在接收到請求后,用線程池起多線程執(zhí)行任務,在多線程日志里面沒有鏈路ID;
- 在接收到請求后,起了異步任務,在異步任務日志里面沒有鏈路ID;
- 由于sleuth底層還是用了MDC來做線程間的日志數(shù)據(jù)隔離,解決辦法是繼續(xù)在自定義的線程工廠CommonThreadFactory 時,從主線程中設置到子線程中去:
public class CommonThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { //獲取主線程的鏈路信息 MDCAdapter mdc = MDC.getMDCAdapter(); Map<String, String> map = mdc.getCopyOfContextMap(); Thread t = new Thread(r, this.poolPrefix + "-thread-" + THREAD_ID.getAndIncrement()) { @Override public void run() { try { //把鏈路追蹤設置到線程池中的線程 if (null != map) { MDC.getMDCAdapter().setContextMap(map); } super.run(); } finally { //使用完畢后,清理緩存,避免內存溢出 MDC.clear(); } } }; return t; } }
- 同樣,把異步任務池的線程池也換成我們自定義的線程池。擴展異步任務池的配置服務ThreadPoolConfigurer 如下:
@Configuration @EnableAsync public class ThreadPoolConfigurer implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { return CommonThreadPool.getExecutor("asyncPool", CORE_NUM, MAX_NUM); } }
- 至此,基于Tomcat的SpringBoot鏈路追蹤全部改造完畢。
5. Spring-Security-OAuth2-Authorization-Server引入zipkin
- 本來最開始使用的是oauth2框架是
Spring Authorization Server
,很不幸該項目2021年被下線了,考慮到該框架不可維護,需要替換成繼任者Spring-Security-OAuth2-Authorization-Server
,但是二者的代碼差異非常大; - 項目一直使用的JDK是1.8,使用的
SpringBoot/SpringCloud
版本為2.7.x+
/3.1.x+
,可支持JDK1.8的Spring-Security-OAuth2-Authorization-Server
最高版本只有0.2.3
,而0.2.3
最多支持的SpringBoot/SpringCloud版本為2.5.x+
/3.0.x+
,而兩個版本的SpringBoot/SpringCloud存在較大的兼容性問題,搞得人快崩潰了。 - 嘗試了非常久,才搞定這個版本不匹配的問題。主要思路是在
bq-service-auth
的根pom 配置去降級SpringBoot/SpringCloud的版本,配置如下:<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <packaging>pom</packaging> <artifactId>bq-service-auth</artifactId> <version>1.0.0</version> <dependencyManagement> <dependencies> <!--引入基礎依賴--> <dependency> <groupId>com.biuqu</groupId> <artifactId>bq-parent</artifactId> <version>${bq.version}</version> <scope>import</scope> </dependency> <!---引入微服務基礎jar,并把高版本spring cloud和springboot換成低版本--> <dependency> <groupId>com.biuqu</groupId> <artifactId>bq-boot-base</artifactId> <version>1.0.4</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </exclusion> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> </exclusion> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </exclusion> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </exclusion> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> </exclusion> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </exclusion> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-brave</artifactId> </exclusion> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </exclusion> </exclusions> </dependency> <!--低版本spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-brave</artifactId> <version>${spring.cloud.security.version}</version> <exclusions> <exclusion> <groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-mongodb</artifactId> </exclusion> <exclusion> <groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-kafka-clients</artifactId> </exclusion> <exclusion> <groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-kafka-streams</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> <version>${spring.cloud.security.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> <version>${spring.cloud.security.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> <version>${spring.cloud.security.version}</version> </dependency> <!--低版本spring boot--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>${spring.boot.security.version}</version> </dependency> <!--為了兼容security-server,此處autoconfigure必須用匹配的低版本--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>${spring.boot.security.version}</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot</artifactId> </exclusion> </exclusions> </dependency> <!--為了兼容低版本的autoconfigure,此處actuator必須用匹配的低版本--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator-autoconfigure</artifactId> <version>${spring.boot.security.version}</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </dependencyManagement> </project>
- 驗證的oauth access log日志如下:
23-06-14 09:36:35.122|0f7969b428e4bb69|0f7969b428e4bb69|0:0:0:0:0:0:0:1|0:0:0:0:0:0:0:1|HTTP/1.1|POST /auth/user/get HTTP/1.1|200|235B|1027ms|-|forward:-|refer:-|PostmanRuntime/7.31.3 23-06-14 20:24:24.734|9f70c1d26fa9e9aa|9f70c1d26fa9e9aa|0:0:0:0:0:0:0:1|0:0:0:0:0:0:0:1|HTTP/1.1|POST /auth/user/add HTTP/1.1|200|216B|237ms|-|forward:-|refer:-|PostmanRuntime/7.31.3 23-06-14 20:24:31.246|39705b997ca54c70|39705b997ca54c70|127.0.0.1|127.0.0.1|HTTP/1.1|POST /oauth/token?scope=read&grant_type=client_credentials HTTP/1.1|200|1659B|235ms|-|forward:-|refer:-|PostmanRuntime/7.31.3 23-06-14 20:38:04.965|714f135ca51d2b65|714f135ca51d2b65|127.0.0.1|127.0.0.1|HTTP/1.1|GET /oauth/jwk HTTP/1.1|200|425B|12ms|-|forward:-|refer:-|Apache-HttpClient/4.5.13 (Java/1.8.0_144) 23-06-14 20:38:59.282|bee49f5e7bdfe536|708815f91d8f5fdf|127.0.0.1|127.0.0.1|HTTP/1.1|POST /oauth/token?scope=read&grant_type=client_credentials HTTP/1.1|200|1659B|220ms|-|forward:127.0.0.1|refer:-|PostmanRuntime/7.31.3
bq-service-auth
其實是擴展源碼最多的的一個開源代碼,后續(xù)再單獨講述。文章來源:http://www.zghlxwxcb.cn/news/detail-487372.html
6. Spring-Cloud-Gateway引入zipkin
- Spring-Cloud-Gateway是基于Spring-Boot-WebFlux,而Spring-Boot-WebFlux是基于Netty Web容器的,與Tomcat容器的日志配置差異較大。
- 基于WebFlux的服務在啟動時,需要添加啟動參數(shù)
-Dreactor.netty.http.server.accessLogEnabled=true -Dproject.name=bq-gateway
; - 網關的logback-spring.xml 配置差異部分如下:
<?xml version="1.0" encoding="UTF-8"?> <!--日志級別以及優(yōu)先級排序: FATAL > ERROR > WARN > INFO > DEBUG--> <configuration debug="false"> <!--控制臺日志--> <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!--spring-cloud-gateway access log--> <appender name="accessLog" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/access.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_PATH}/%d{yy-MM-dd}/access-%d{yy-MM-dd}.log</FileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${AUDIT_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <appender name="asyncAccessLog" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="accessLog"/> </appender> <appender name="asyncNettyLog" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="consoleAppender"/> <appender-ref ref="defaultAppender"/> </appender> <!--gateway access-log, with jvm env:'-Dreactor.netty.http.server.accessLogEnabled=true'--> <logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false"> <appender-ref ref="asyncAccessLog"/> </logger> <!--記錄netty日志--> <logger name="reactor.netty.http.server.HttpServer" level="DEBUG" additivity="false" includeLocation="true"> <appender-ref ref="asyncNettyLog"/> </logger> </configuration>
- 網關的AccessLog工廠NettyConfigurer 定制如下:
@Slf4j @Configuration public class NettyConfigurer { /** * 配置自定義的AccessLog * * @return Netty定制工廠 */ @Bean public WebServerFactoryCustomizer<NettyReactiveWebServerFactory> nettyServerFactory() { return factory -> { //配置access log factory.addServerCustomizers(httpServer -> httpServer.accessLog(true, x -> { List<String> params = Lists.newArrayList(); params.add(x.accessDateTime().format(DateTimeFormatter.ofPattern(TimeUtil.SIMPLE_TIME_FORMAT))); String traceId = Const.MID_LINK; if (null != x.responseHeader(CommonBootConst.TRACE_ID)) { traceId = x.responseHeader(CommonBootConst.TRACE_ID).toString(); } params.add(traceId); String spanId = Const.MID_LINK; if (null != x.responseHeader(CommonBootConst.SPAN_ID)) { spanId = x.responseHeader(CommonBootConst.SPAN_ID).toString(); } params.add(spanId); params.add(x.method().toString()); params.add(x.protocol()); params.add(x.connectionInformation().remoteAddress().toString()); params.add(x.connectionInformation().hostAddress().toString()); params.add(x.status() + StringUtils.EMPTY); params.add(x.uri().toString()); params.add(x.contentLength() + "B"); params.add(x.duration() + "ms"); String format = StringUtils.repeat("{}|", params.size()); return AccessLog.create(format, params.toArray()); })); }; } }
- 配置完畢后,發(fā)現(xiàn)日志只在第一個網關過濾器中有TraceId,后面的過濾器都沒有了,因此想到添加切面NettyTraceLogAop 來實現(xiàn)過濾器間的傳遞:
@Slf4j @Component @Aspect public class NettyTraceLogAop extends BaseAop { @Before(BEFORE_PATTERN) @Override public void before(JoinPoint joinPoint) { super.before(joinPoint); } @Override protected void doBefore(Method method, Object[] args) { Object webServerObj = args[0]; if (webServerObj instanceof ServerWebExchange) { ServerWebExchange exchange = (ServerWebExchange)webServerObj; MDCAdapter mdc = MDC.getMDCAdapter(); Map<String, String> map = mdc.getCopyOfContextMap(); if (!MapUtils.isEmpty(map)) { //獲取并緩存鏈路信息 exchange.getAttributes().put(GatewayConst.TRACE_LOG_KEY, map); HttpHeaders headers = exchange.getResponse().getHeaders(); //把鏈路信息緩存至exchange的response對象header for (String traceKey : map.keySet()) { String value = map.get(traceKey); if (!headers.containsKey(traceKey)) { headers.add(traceKey, value); } } } else { //從緩存中提取并設置給過濾器 Map<String, String> cachedMap = exchange.getAttribute(GatewayConst.TRACE_LOG_KEY); if (!MapUtils.isEmpty(cachedMap)) { mdc.setContextMap(cachedMap); } } } } /** * 攔截所有過濾器匹配表達式 */ private static final String BEFORE_PATTERN = "(execution (* com.biuqu.boot.*.*.filter.*GatewayFilter.filter(..)))"; }
當前的策略是從MDC中獲取然后放入全局的參數(shù)中去,也可以不放入Header頭。文章來源地址http://www.zghlxwxcb.cn/news/detail-487372.html
- 測試過程中發(fā)現(xiàn),后面會重復出現(xiàn)前面出現(xiàn)過的TraceId,因此在最后一個過濾器RemovingGatewayFilter 中清除下整個鏈路的緩存,包括MDC的緩存;
@Slf4j @Component public class RemovingGatewayFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //從緩存中提取并設置給過濾器 Map<String, String> cachedMap = exchange.getAttribute(GatewayConst.TRACE_LOG_KEY); return chain.filter(exchange).doFinally(s -> { if (!MapUtils.isEmpty(cachedMap)) { MDC.getMDCAdapter().setContextMap(cachedMap); } long start = System.currentTimeMillis(); Map<String, Object> attributes = exchange.getAttributes(); if (attributes.containsKey(GatewayConst.TRACE_LOG_KEY)) { attributes.remove(GatewayConst.TRACE_LOG_KEY); } log.info("finally cost:{}ms", System.currentTimeMillis() - start); MDC.getMDCAdapter().clear(); }); } }
- 網關的Access Log運行效果如下:
23-06-14 20:38:21.160|063e2bc1e5223c6d|063e2bc1e5223c6d|POST|HTTP/1.1|/127.0.0.1:63787|/127.0.0.1:9992|500|/oauth/token?scope=read&grant_type=client_credentials|54B|225ms|
23-06-14 20:38:59.003|bee49f5e7bdfe536|bee49f5e7bdfe536|POST|HTTP/1.1|/127.0.0.1:63787|/127.0.0.1:9992|200|/oauth/token?scope=read&grant_type=client_credentials|1647B|304ms|
23-06-14 20:39:41.736|77b9d62ebaafec48|77b9d62ebaafec48|POST|HTTP/1.1|/127.0.0.1:63787|/127.0.0.1:9992|200|/oauth/token?scope=read&grant_type=client_credentials|1673B|251ms|
23-06-14 20:40:55.359|b2d83c74c2911355|b2d83c74c2911355|POST|HTTP/1.1|/0:0:0:0:0:0:0:1:63867|/0:0:0:0:0:0:0:1:9992|200|/oauth/token?scope=read&grant_type=client_credentials|1673B|239ms|
23-06-14 20:41:10.096|0637881570dec847|0637881570dec847|POST|HTTP/1.1|/0:0:0:0:0:0:0:1:63867|/0:0:0:0:0:0:0:1:9992|500|/oauth/enc/token?scope=read&grant_type=client_credentials|53B|11ms|
7. 參考資料
- [1] SkyWalking 鏈路追蹤
- [2] Pinpoint 應用性能管理工具
- [3] 使用Pinpoint作分布式鏈路跟蹤系統(tǒng)
- [4] 從MDC說分布式鏈路追蹤的前世今生
- [5] Java學習錄
- [6] Spring Authorization Server 正式遷移至 spring-projects
到了這里,關于鏈路追蹤在開源SpringBoot/SpringCloud微服務框架的實踐的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!