链路追踪在开源SpringBoot/SpringCloud微服务框架的实践

前期内容导读:

  1. Java开源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介绍
  2. Java开源AES/SM4/3DES对称加密算法介绍及其实现
  3. Java开源AES/SM4/3DES对称加密算法的验证说明
  4. Java开源RSA/SM2非对称加密算法对比介绍
  5. Java开源RSA非对称加密算法实现
  6. Java开源SM2非对称加密算法实现
  7. Java开源接口微服务代码框架
  8. Json在开源SpringBoot/SpringCloud微服务框架中的最佳实践
  9. 加解密在开源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    |
                                                                            |                   |
                                                                            +-------------------+

说明:

  1. bq-encryptor:基于BouncyCastle安全框架,已开源加解密介绍
    ,支持RSA/AES/PGP/SM2/SM3/SM4/SHA-1/HMAC-SHA256/SHA-256/SHA-512/MD5等常用加解密算法,并封装好了多种使用场景、做好了为SpringBoot所用的准备;
  2. bq-base:基于Spring框架的基础代码框架,已开源 ,支持json/redis/DataSource/guava/http/tcp/thread/jasypt等常用工具API;
  3. bq-log:基于SpringBoot框架的基础日志代码,已开源 ,支持接口Access日志、调用日志、业务操作日志等日志文件持久化,可根据实际情况扩展;
  4. bq-boot-root:基于SpringBoot,已开源 ,但是不包含spring-boot-starter-web,也不包含spring-boot-starter-webflux,可通用于servletnettyweb容器场景,封装了redis/http /定时器/加密机/安全管理器等的自动注入;
  5. bq-boot-base:基于spring-boot-starter-web(servlet,BIO),已开源 ,提供常规的业务服务基础能力,支持PostgreSQL/限流/bq-log/Web框架/业务数据加密机加密等可配置自动注入;
  6. bq-service-gateway:基于spring-boot-starter-webflux(Netty,NIO),已开源 ,提供了Jwt Token安全校验能力,包括接口完整性校验/接口数据加密/Jwt Token合法性校验等;
  7. bq-service-auth:基于spring-security-oauth2-authorization-server,已开源 ,提供了JwtToken生成和刷新的能力;
  8. 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   |        |
  |       |                   |           |                   |        |
  |       +-------------------+           +-------------------+        |
  |                                                                    |
  +--------------------------------------------------------------------+

说明:

  1. bq-service-gateway:基于SpringCloud-Gateway,用作JwtToken鉴权,并提供了接口、数据加解密的安全保障能力;
  2. bq-service-auth:基于spring-security-oauth2-authorization-server,提供了JwtToken生成和刷新的能力;
  3. bq-service-biz:基于spring-boot-starter-web,业务微服务参考样例;
  4. k8s在上述微服务架构中,承担起了服务注册和服务发现的作用,鉴于k8s云原生环境构造较为复杂,实际开源的代码时,以Nacos(为主)/Eureka做服务注册和服务发现中间件;
  5. 以上所有服务都以docker容器作为载体,确保服务有较好地集群迁移和弹性能力,并能够逐步平滑迁移至k8s的终极目标;
  6. 逻辑架构不等同于物理架构(部署架构),实际业务部署时,还有DMZ区和内网区,本逻辑架构做了简化处理;

3. 链路追踪框架选型

3.1 为什么要引入链路追踪

  • 随着分布式微服务的发展,服务在小型化的同时,服务数据急剧膨胀,导致调用链条特别复杂特别长,定位问题和数据提取比较困难;
  • 微服务化也促使用平价的服务器(一般是VM,或者叫ECS)来替代价格高昂的专用服务器,所以会导致服务的稳定性变差,所以也需要关注资源的性能瓶颈;
  • 链路追踪并非必须的,在传统项目、服务数量稀少、业务相对简单的项目就没有必要使用,在云原生微服务架构中则很有必要引入;

3.2 链路追踪能做什么

  • 链路追踪是为了解决技术痛点的,其核心价值在于:评估并记录服务间的调用链数据;我们可以基于这些数据清晰地知道客户请求的来龙去脉,系统出现问题的大致位置。
  • 链路追踪不关心服务内部触发的其它调用链,比如:服务内的定时器、服务内的初始化服务等;

3.3 当下链路追踪框架对比

  • 链路追踪技术基本上都是Google Dapper,当下有2种不同的实现:

    • 代码侵入式的引用,如:zipkin/cat;
    • 代码无侵入式的引用,如:SkyWalking/Pinpoint;

    二者的区别:前者需要通过把链路追踪的Java包当做依赖加入到依赖库中;后者则是在执行启动命令时,带上链路追踪的jar包即可,链路监控完全基于字节码增强技术来实现;

  • 当下较多使用的链路追踪框架如下表所示:

    链路追踪特性CatZipkinSkyWalkingPinpoint
    调用链可视化
    聚合报表非常丰富较丰富非常丰富
    服务依赖图简单简单
    埋点方式侵入式侵入式非侵入式,字节码增强非侵入式,字节码增强
    VM监控指标
    支持语言java/.net丰富java/.net/php/go/node.jsjava/php/python
    存储机制mysql(报表),本地文件/HDFS(调用链)内存/redis/es/mysql等H2、esHBase
    社区支持主要在国内国外主流Apache支持-
    使用案例美团、携程京东、阿里定制后不开源华为、小米-
    APM
    开发基础eBay calGoogle DapperGoogle DapperGoogle Dapper
    是否支持WebFlux

    结合实际情况:

    • 我们有SpringCloud-Gateway(基于WebFlux),所以不能使用Cat/Pinpoint
    • 我们当下只要加上链路追踪即可,再加上zipkin是SpringCloud的亲儿子,对应的SpringCloud组件为SpringCloud-Sleuth,所以此框架优先选用了zipkin,暂没有必要去使用牛刀SkyWalking
  • 综上,我们选择小巧而且与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日志的格式字段,带有traceIdSpanId字段;

  • 深入研究就会发现常规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和不使用sleuth2种情况。
      • AccessLog打印出来后,就会发现会多了一些健康检查日志,注意不要把心跳检查设置得过于频繁;
  • 至此,基于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. 参考资料

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐