![cover](https://img-blog.csdnimg.cn/e956fc2979a54f38b4ac30c686731b0d.png)
链路追踪在开源SpringBoot/SpringCloud微服务框架的最简实践
·
目录导读
链路追踪在开源SpringBoot/SpringCloud微服务框架的实践
前期内容导读:
- 在前面详细介绍的基础上,且代码全部开源后,这次来完整介绍下链路追踪在SpringBoot/SpringCloud微服务中到底是如何应用的。
- 应该是先有业务,才会有微服务设计。此开源的微服务设计见Java开源接口微服务代码框架 文章,现把核心设计摘录如下:
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日志、调用日志、业务操作日志等日志文件持久化,可根据实际情况扩展;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),已开源 ,提供常规的业务服务基础能力,支持PostgreSQL
/限流
/bq-log
/Web框架
/业务数据加密机加密
等可配置自动注入;bq-service-gateway
:基于spring-boot-starter-webflux
(Netty,NIO),已开源 ,提供了Jwt Token安全校验能力,包括接口完整性校验
/接口数据加密
/Jwt Token合法性校验等;bq-service-auth
:基于spring-security-oauth2-authorization-server
,已开源 ,提供了JwtToken生成和刷新的能力;bq-service-biz
:业务微服务参考样例,已开源 ;
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鉴权,并提供了接口、数据加解密的安全保障能力;bq-service-auth
:基于spring-security-oauth2-authorization-server
,提供了JwtToken生成和刷新的能力;bq-service-biz
:基于spring-boot-starter-web
,业务微服务参考样例;k8s
在上述微服务架构中,承担起了服务注册和服务发现的作用,鉴于k8s
云原生环境构造较为复杂,实际开源的代码时,以Nacos
(为主)/Eureka
做服务注册和服务发现中间件;- 以上所有服务都以docker容器作为载体,确保服务有较好地集群迁移和弹性能力,并能够逐步平滑迁移至k8s的终极目标;
- 逻辑架构不等同于物理架构(部署架构),实际业务部署时,还有DMZ区和内网区,本逻辑架构做了简化处理;
3. 链路追踪框架选型
3.1 为什么要引入链路追踪
- 随着分布式微服务的发展,服务在小型化的同时,服务数据急剧膨胀,导致调用链条特别复杂特别长,定位问题和数据提取比较困难;
- 微服务化也促使用平价的服务器(一般是VM,或者叫ECS)来替代价格高昂的专用服务器,所以会导致服务的稳定性变差,所以也需要关注资源的性能瓶颈;
- 链路追踪并非必须的,在传统项目、服务数量稀少、业务相对简单的项目就没有必要使用,在云原生微服务架构中则很有必要引入;
3.2 链路追踪能做什么
- 链路追踪是为了解决技术痛点的,其核心价值在于:评估并记录服务间的调用链数据;我们可以基于这些数据清晰地知道客户请求的来龙去脉,系统出现问题的大致位置。
- 链路追踪不关心服务内部触发的其它调用链,比如:服务内的定时器、服务内的初始化服务等;
3.3 当下链路追踪框架对比
-
链路追踪技术基本上都是Google Dapper,当下有2种不同的实现:
- 代码侵入式的引用,如:zipkin/cat;
- 代码无侵入式的引用,如:SkyWalking/Pinpoint;
二者的区别:前者需要通过把链路追踪的Java包当做依赖加入到依赖库中;后者则是在执行启动命令时,带上链路追踪的jar包即可,链路监控完全基于字节码增强技术来实现;
-
当下较多使用的链路追踪框架如下表所示:
链路追踪特性 Cat Zipkin SkyWalking Pinpoint 调用链可视化 有 有 有 有 聚合报表 非常丰富 少 较丰富 非常丰富 服务依赖图 简单 简单 好 好 埋点方式 侵入式 侵入式 非侵入式,字节码增强 非侵入式,字节码增强 VM监控指标 好 无 有 好 支持语言 java/.net 丰富 java/.net/php/go/node.js java/php/python 存储机制 mysql(报表),本地文件/HDFS(调用链) 内存/redis/es/mysql等 H2、es HBase 社区支持 主要在国内 国外主流 Apache支持 - 使用案例 美团、携程 京东、阿里定制后不开源 华为、小米 - APM 是 否 是 是 开发基础 eBay cal Google Dapper Google Dapper Google Dapper 是否支持WebFlux 否 是 是 否 结合实际情况:
- 我们有SpringCloud-Gateway(基于WebFlux),所以不能使用
Cat
/Pinpoint
; - 我们当下只要加上链路追踪即可,再加上
zipkin
是SpringCloud的亲儿子,对应的SpringCloud组件为SpringCloud-Sleuth,所以此框架优先选用了zipkin
,暂没有必要去使用牛刀SkyWalking
;
- 我们有SpringCloud-Gateway(基于WebFlux),所以不能使用
-
综上,我们选择小巧而且与SpringCloud框架最密切的
zipkin
作为我们的链路追踪框架,缺点就是它是代码侵入式的,它的变更可能会影响业务稳定。
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目录,执行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日志,另一种是运行日志。我们现在就要保证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"?> <!--日志级别以及优先级排序: 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"/> <!--单机直接运行时这样区分--> <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
字段; - 深入研究就会发现常规SpringBoot微服务(基于tomcat),access log并没有链路信息,还需要对框架进一步改造。
- 需要先定制Tomcat的Access Log工厂类,因此新增一个日志的配置服务LogConfigurer ,代码如下:
@Configuration public class LogConfigurer { /** * 在tomcat日志中实现trace id * <p> * 参考: https://www.appsloveworld.com/springboot/100/36/mdc-related-content-in-tomcat-access-logs * * @param env 运行环境变量 * @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打印出来后,就会发现会多了一些健康检查日志,注意不要把心跳检查设置得过于频繁;
- 需要先定制Tomcat的Access Log工厂类,因此新增一个日志的配置服务LogConfigurer ,代码如下:
- 至此,基于SpringBoot常规的微服务大部分情况下有链路ID了。但是由于定制了线程池和异步任务池,存在如下2种异常情况:
- 在接收到请求后,用线程池起多线程执行任务,在多线程日志里面没有链路ID;
- 在接收到请求后,起了异步任务,在异步任务日志里面没有链路ID;
- 由于sleuth底层还是用了MDC来做线程间的日志数据隔离,解决办法是继续在自定义的线程工厂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
其实是扩展源码最多的的一个开源代码,后续再单独讲述。
6. Spring-Cloud-Gateway引入zipkin
- Spring-Cloud-Gateway是基于Spring-Boot-WebFlux,而Spring-Boot-WebFlux是基于Netty Web容器的,与Tomcat容器的日志配置差异较大。
- 基于WebFlux的服务在启动时,需要添加启动参数
-Dreactor.netty.http.server.accessLogEnabled=true -Dproject.name=bq-gateway
; - 网关的logback-spring.xml 配置差异部分如下:
<?xml version="1.0" encoding="UTF-8"?> <!--日志级别以及优先级排序: 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()); })); }; } }
- 配置完毕后,发现日志只在第一个网关过滤器中有TraceId,后面的过滤器都没有了,因此想到添加切面NettyTraceLogAop 来实现过滤器间的传递:
@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中获取然后放入全局的参数中去,也可以不放入Header头。
- 测试过程中发现,后面会重复出现前面出现过的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. 参考资料
更多推荐
所有评论(0)