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

这篇具有很好参考价值的文章主要介绍了链路追踪在开源SpringBoot/SpringCloud微服务框架的最简实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

链路追踪在开源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包即可,链路监控完全基于字节码增强技术来实现;

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

    链路追踪特性 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框架最密切的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头。文章来源地址https://www.toymoban.com/news/detail-517403.html

  • 测试过程中发现,后面会重复出现前面出现过的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作分布式链路跟踪系统
  • [4] 从MDC说分布式链路追踪的前世今生
  • [5] Java学习录
  • [6] Spring Authorization Server 正式迁移至 spring-projects

到了这里,关于链路追踪在开源SpringBoot/SpringCloud微服务框架的最简实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringCloud搭建微服务之Micrometer分布式链路追踪

    由于Spring Cloud Sleuth最新版本只支持Spring Boot 2.7.x,核心项目已经迁移到Micrometer Traceing项目,Spring Boot 3.x版本要实现分布式链路追踪需要集成Micrometer。更多详情可以参阅Micrometer官网 本文将以Spring Boot 3.2.x和Spring Cloud 2023.0.x版本和JDK 17实现分布式链路追踪,有需要了解Spring Bo

    2024年03月22日
    浏览(56)
  • 【微服务】springcloud集成sleuth与zipkin实现链路追踪

    目录 一、前言 二、分布式链路调用问题 三、链路追踪中的几个概念

    2024年01月22日
    浏览(37)
  • 加解密在开源SpringBoot/SpringCloud微服务框架的最佳实践

    前期内容导读: Java开源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介绍 Java开源AES/SM4/3DES对称加密算法介绍及其实现 Java开源AES/SM4/3DES对称加密算法的验证说明 Java开源RSA/SM2非对称加密算法对比介绍 Java开源RSA非对称加密算法实现 Java开源SM2非对称加密算法实现 Java开源接口微服务代码框架

    2024年02月09日
    浏览(42)
  • 异常处理在开源SpringBoot/SpringCloud微服务框架的最佳实践

    前期内容导读: Java开源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介绍 Java开源AES/SM4/3DES对称加密算法介绍及其实现 Java开源AES/SM4/3DES对称加密算法的验证说明 Java开源RSA/SM2非对称加密算法对比介绍 Java开源RSA非对称加密算法实现 Java开源SM2非对称加密算法实现 Java开源接口微服务代码框架

    2024年02月12日
    浏览(57)
  • OAuth2在开源SpringBoot/SpringCloud微服务框架的最佳实践

    前期内容导读: Java开源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介绍 Java开源AES/SM4/3DES对称加密算法介绍及其实现 Java开源AES/SM4/3DES对称加密算法的验证说明 Java开源RSA/SM2非对称加密算法对比介绍 Java开源RSA非对称加密算法实现 Java开源SM2非对称加密算法实现 Java开源接口微服务代码框架

    2024年02月11日
    浏览(39)
  • 熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践

    前期内容导读: Java开源RSA/AES/SHA1/PGP/SM2/SM3/SM4加密算法介绍 Java开源AES/SM4/3DES对称加密算法介绍及其实现 Java开源AES/SM4/3DES对称加密算法的验证说明 Java开源RSA/SM2非对称加密算法对比介绍 Java开源RSA非对称加密算法实现 Java开源SM2非对称加密算法实现 Java开源接口微服务代码框架

    2024年02月12日
    浏览(48)
  • k8s部署elk+filebeat;springCloud集成elk+filebeat+kafka+zipkin实现多个服务日志链路追踪聚合到es

    如今2023了,大多数javaweb架构都是springboot微服务,一个前端功能请求后台可能是多个不同的服务共同协做完成的。例如用户下单功能,js转发到后台 网关gateway服务 ,然后到 鉴权spring-sercurity服务 ,然后到 业务订单服务 ,然后到 支付服务 ,后续还有发货、客户标签等等服务

    2024年02月16日
    浏览(43)
  • day09-SpringCloud Sleuth+Zipkin-链路追踪

    官网:spring-cloud/spring-cloud-sleuth: Distributed tracing for spring cloud (github.com) 分布式链路追踪之Spring Cloud Sleuth+Zipkin最全教程! - bucaichenmou - 博客园 (cnblogs.com) 在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用,来协同产生最后的请求结果,

    2024年02月08日
    浏览(37)
  • Zipkin开源的分布式链路追踪系统

    Zipkin是一款开源的分布式链路追踪系统,主要功能包括: 1. 采集跟踪数据 - Zipkin client库负责收集并上报各服务的请求信息。 2. 存储跟踪数据 - 存储层默认采用Zipkin自带的基于内存的快速存储,也支持整合MySQL、Cassandra等外部存储。 3. 查询接口 - 提供RESTful API进行跟踪数据查询。

    2024年02月11日
    浏览(42)
  • java+springboot 做日志链路追踪

    日志链路追踪(Log Path Tracing)是Spring Boot项目的一项关键功能,它通过将日志消息的源头与其对应的请求或响应路径相关联,实现对日志数据的可视化跟踪。 随着项目规模的扩大和复杂性的增加,追踪和管理日志数据变得越来越重要。通过实现日志链路追踪,我们可以更好地

    2024年02月05日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包