服务链路追踪-Sleuth :解决分布式部署下最头疼的溯源问题

一、服务调用链追踪

  微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。

二、核心功能和体系架构

  Sleuth的意思是大侦探,顾名思义,侦探就是查找信息搜集线索,顺着线索链找到所有上下游的关联,这恰好正是Seluth在Spring Cloud中的工作

1、核心功能

借助Sleuth的链路追踪能力,我们还可以完成一些其他的任务,比如说:

  • 线上故障定位:结合Tracking ID寻找上下游链路中所有的日志信息(这一步还需要借助一些其他开源组件,后面会有这部分的Demo)
  • 依赖分析梳理:梳理上下游依赖关系,理清整个系统中所有微服务之间的依赖关系
  • 链路优化:比如说目前我们有三种途径可以导流到下单接口,通过对链路调用情况的统计分析,我们可以识别出转化率最高的业务场景,从而为以后的产品设计提供指导意见。
  • 性能分析:梳理各个环节的时间消耗,找到性能瓶颈,为性能优化、软硬件资源调配指明方向

2、设计理念

  • 无业务侵入:如果说接入某个监控组件还需要改动业务代码,那么我们认为这是一个“高侵入性”的组件。Sleuth在设计上秉承“低侵入”的理念,不需要对业务代码做任何改动,即可静默接入链路追踪功能
  • 高性能:一般认为在代码里加入完善的log(10行代码对应2条log)会对降低5%左右接口性能(针对非异步log框架),而通过链路追踪技术在log里做埋点多多少少也会影响性能。Sleuth在埋点过程中力求对性能影响降低到最小,同时还提供了“采样率配置”来进一步降低开销(比如说开发人员可以设置只对20%的请求进行采样)

3、数据埋点

  每一个微服务都有自己的Log组件(slf4j,lockback等各不相同),当我们集成了Sleuth之后,它便会将链路信息传递给底层Log组件,同时Log组件会在每行Log的头部输出这些数据,这个埋点动作主要会记录两个关键信息:

  • 链路ID(Trace ID):当前调用链的唯一ID,在这次调用请求开始到结束的过程中,所有经过的节点都拥有一个相同的链路ID
  • 单元ID(Event ID): 在一次链路调用中会访问不同服务器节点上的服务,每一次服务调用都相当于一个独立单元,也就是说会有一个独立的单元ID。同时每一个独立单元都要知道调用请求来自哪里(就是对当前服务发起直接调用的那一方的单元ID,我们记为Parent ID)

4、Sleuth与Log系统集成原理

  我们需要把链路追踪信息加入到业务Log中,这些业务Log是我们研发人员写在具体服务里的,而不是Sleuth单独打印的log,因此Sleuth需要找到一个合适的切入点,让底层Log组件可以获取链路信息,并且我们的业务代码还不需要做任何改动。
  如果有对Log框架做过深度定制的同学可能一下就能想到实现方式,就是使用MDC + Format Pattern的方式输出信息,我们先来看一下Log组件打印信息到文件的过程:
在这里插入图片描述
  当我们使用"log.info"打印日志的时候,Log组件会将“写入”动作封装成一个LogEvent事件,而这个事件的具体表现形式由Log Format和MDC共同控制,Format决定了Log的输出格式,而MDC决定了输出什么内容。

1)Log Format Pattern

Log组件定义了日志输出格式,这和我们平时使用“String.format”的方式差不多,集成了Sleuth后的Log输出格式是下面这个样子:

%5p [sleuth-traceA,%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]

同学们发现上面有几个X开头的占位符,这就是我们需要写入Log的链路追踪信息

2)MDC

  MDC是通过InheritableThreadLocal来实现的,它可以携带当前线程的上下文信息。它的底层是一个Map结构,存储了一系列Key-Value的值。Sleuth就是借助Spring的AOP机制,在方法调用的时候配置了切面,将链路追踪数据加入到了MDC中,这样在打印Log的时候,就能从MDC中获取这些值,填入到Log Format中的占位符里。
  由于MDC基于InheritableThreadLocal而不是ThreadLocal实现,因此假如在当前线程中又开启了新的子线程,那么子线程依然会保留父线程的上下文信息。

5、Sleuth数据结构

  • Trace:它就是从头到尾贯穿整个调用链的ID,我们叫它Trace ID,不管调用链路中途访问了多少服务节点,在每个节点的log中都会打印同一个Trace ID
  • Span:它标识了Sleuth下面一个基本的工作单元,每个单元都有一个独一无二的ID。比如服务A发起对服务B的调用,这个事件就可以看做一个独立单元,生成一个独立的ID
      Span不单单只是一个ID,它还包含一些其他信息,比如时间戳,它标识了一个事件从开始到结束经过的时间,我们可以用这个信息来统计接口的执行时间。每个Span还有一系列特殊的“标记”,也就是接下来要介绍的“Annotation”,它标识了这个Span在执行过程中发起的一些特殊事件。

1)Annotation标记

一个Span可以包含多个Annotation,每个Annotation表示一个特殊事件,比如:

  • cs Client Sent:客户端发送了一个调用请求
  • sr Server Received:服务端收到了来自客户端的调用
  • ss Server Sent:服务端将Response发送给客户端
  • cr Client Received:客户端收到了服务端发来的Response

  每个Annotation同样有一个时间戳字段,这样我们就能分析一个Span内部每个事件的起始和结束时间。这里我选取了Spring Cloud官网的一张图来展示Trace、Span和Annotation的关系。

2)服务节点间的ID传递

  我们知道了Trace ID和Span ID,眼下的问题就是如何在不同服务节点之间传递这些ID。我想这一步大家很容易猜到是怎么做的,因为在Eureka的服务治理下所有调用请求都是基于HTTP的,那我们的链路追踪ID也一定是HTTP请求中的一部分。可是把ID加在HTTP哪里好呢?Body里可以吗?NoNoNo,一来GET请求压根就没有Body,二来加入Body还有可能影响后台服务的反序列化。那加在URL后面呢?似乎也不妥,因为某些服务组件对URL的长度可能做了限制(比如Nginx可以设置最大URL长度)。
  那剩下的只有Header了!Sleuth正是通过Filter向Header中添加追踪信息,我们来看下面表格中Header Name和Trace Data的对应关系:

HTTP Header Name Trace Data
X-B3-TraceId Trace ID 链路全局唯一ID
X-B3-SpanId Span ID 当前Span的ID
X-B3-ParentSpanId Parent Span ID 前一个Span的ID
X-Span-Export Can be exported for sampling or not 是否可以被采样

三、整合Sleuth追踪调用链路

1、创建Sleuth项目

1)创建一个模块命名为sleuth-traceA,修改pom文件

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--sleuth-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
</dependencies>

2)修改启动文件

@EnableDiscoveryClient
@SpringBootApplication
public class SleuthTraceA{
    public static void main(String[] args){
        new SpringApplicationBuilder(SleuthTraceA.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
    
    @LoadBalanced
    @Bean
    public RestTemplate lb(){
        return new RestTemplate();
    }
}

3)创建配置文件

spring.application.name=sleuth-traceA
server.port=62000

eureka.client.serviceUrl.defaultZone=http://localhost:20000/eureka/

logging.file=${spring.application.name}.log

#日志采样率, 1:所有的日志都会被采样
spring.sleuth.sampler.probability=1

info.app.name=sleuth-traceA
info.app.description=test

management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

4)在resources中添加日志配置文件logback-spring.xml

<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    
    <springProperty scope="context" name="springAppName" source="spring.application.name"/>
    <!--日志输出位置-->
    <property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>
    <!--日志输出格式-->
    <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />

    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref = "consoleLog"/>
    </root>
</configuration>

5)编写controller

//lombok的注解  用来打印日志,用其他的方式打印日志也是一样的
@Slf4j
@RestController
public class ControllerA{
    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping(value = "/traceA")
    public String traceA(){
        log.info("--------Trace A");
        return restTemplate.getForEntity("http://sleuth-traceB/traceB",String.class).getBody();
    }
}

6)创建一个新的模块sleuth-traceB,和之前的步骤一样

1.修改以下配置文件中的3个参数

spring.application.name=sleuth-traceB
server.port=62001

info.app.name=sleuth-traceB

2.修改一下Controller

//lombok的注解  用来打印日志,用其他的方式打印日志也是一样的
@Slf4j
@RestController
public class ControllerB{
    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping(value = "/traceB")
    public String traceB(){
        log.info("--------Trace B");
        return "traceB";
    }
}

7)测试

1.启动服务发现中心(示例中没有需要自己配置下)
2.启动traceB和traceA
3.调用/traceA接口参看日志

四、Zipkin

  Zipkin是一套分布式实时数据追踪系统,它主要关注的是时间维度的监控数据,比如某个调用链路下各个阶段所花费的时间,同时还可以从可视化的角度帮我们梳理上下游系统之间的依赖关系。
  Sleuth为什么需要一个搭档?大家难道没发现Sleuth空有一身本领,可是没个页面可以show出来吗?而且Sleuth似乎只是自娱自乐在log里埋点,却没有一个汇聚信息的能力,不方便对整个集群的调用链路进行分析。Sleuth目前的情形就像Hystrix一样,也需要一个类似Turbine的组件做信息聚合+展示的功能。在这个背景下,Zipkin就是一个不错的选择。

1、Zipkin的核心功能

Zipkin的主要作用是收集Timing维度的数据,以供查找调用延迟等线上问题。所谓Timing其实就是开始时间+结束时间的标记,有了这两个时间信息,我们就能计算得出调用链路每个步骤的耗时。Zipkin的核心功能有以下两点

  • 数据收集:聚合客户端数据
  • 数据查找:通过不同维度对调用链路进行查找

  Zipkin分为服务端和客户端,服务端是一个专门负责收集数据、查找数据的中心Portal,而每个客户端负责把结构化的Timing数据发送到服务端,供服务端做索引和分析。这里我们重点关注一下“Timing数据”到底用来做什么,前面我们说过Zipkin主要解决调用延迟情况的线上排查,它通过收集一个调用链上下游所有工作单元的独立用时,Zipkin就能知道每个环节在服务总用时中所占的比重,再通过图形化界面的形式,让开发人员知道性能瓶颈出在哪里。
  Zipkin提供了多种维度的查找功能用来检索Span的耗时,最直观的是通过Trace ID查找整个Trace链路上所有Span的前后调用关系和每阶段的用时,还可以根据Service Name或者访问路径等维度进行查找

2、Zipkin的组件

  • Collector:很多人以为Collector是一个客户端组件,其实它是Zipkin Server的守护进程,用来验证客户端发送来的链路数据,并在存储结构中建立索引。守护进程就是指一类用于执行特定任务的后台进程,它独立于Zipkin Server的控制终端,一直等待接收客户端数据。
  • Storage:Zipkin支持ElasticSearch和MySQL等存储介质用来保存链路信息,本章demo中采用默认的Cassandra作为存储方式
  • Search Engine:提供基于JSON API的接口来查找信息
  • Dashboard:一个大盘监控页面,后台调用Search Engine来获取展示信息。大家如果本地启动Zipkin会每次刷新主页后系统日志会打印Error信息,这个是Zipkin的一个小问题,直接跳过即可

3、Zipkin与Sleuth集成,日志分析和链路追踪

1)创建一个模块,起名zipkin-server,并修改pom文件

<dependencies>
    <!--后端工具包-->
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-server</artifactId>
        <version>2.8.4</version>
    </dependency>
    <!--用于前端展示的包-->
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-autoconfigure-ui</artifactId>
        <version>2.8.4</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <!--指定启动方法-->
                <mainClass>>com.test.spring.ZipkinApplication</mainClass>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <!-- zipkin是可以直接使用他自己的jar部署的,我们这里需要做一些修改,然后再次打包 -->
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

2)修改启动类

//选哪个没有被标记为Deprecated的
@EnableZipkinServer
@SpringBootApplication
public class ZipkinApplication{
    public static void main(String[] args){
        new SpringApplicationBuilder(ZipkinApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

3)创建配置文件

spring.application.name=zipkin-server
server.port=62100

#允许bean重载
spring.main.allow-bean-definition-overriding=true
management.metrics.web.server.auto-time-requests=false

4)访问zipkin地址

localhost:62100/zipkin/

5)修改之前写的sleuth-traceA和B,修改pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

6)修改之前写的sleuth-traceA和B,修改配置文件

#zipkin的地址
spring.zipkin.base-url=http://localhost:62100
#使用http上传数据到zipkin
#因为有可能后引入bus依赖导入了rabbitmq的依赖,zipkin会自动切换到RabbitMq上
spring.zipkin.sender.type=web

五、Sleuth集成ELK实现日志搜索

1)使用docker快速搭建ELK

  • ElasticSearch:存储Log信息,并提供搜索功能
  • Logstash:log信息收集过滤
  • Kibana:Log信息的查询报表

1.使用docker下载elk镜像(很慢需要耐心)

docker pull sebp/elk

2.创建Docker容器(第一次使用的时候才需要创建,启动很慢 5-10分钟)

        #kibana的端口   ElasticSearch端口   LogStash端口                                                                 容器名称   启动的镜像
docker run -p 5601:5601 -p 9200:9200 -p5044:5044 -e ES_MIN_MEM=128m -e ES_MAX_MEM=1025m  -it --name elk sebp/elk

3.再次启动的命令docker容器(启动很慢 5-10分钟)
docker start elk
4. 进入docker容器:

docker exec -it elk /bin/bash

5.修改配置文件
配置文件位置: /etc/logstash/conf.d/02-beats-input.conf
将内容全部删除,替换成下面的配置

input{
    tcp{
        port =>5044
        codec => json_lines
    }
}
output{
    elasticsearch{
        hosts => ["localhost:9200"]
    }
}

6.重启docker容器(启动很慢 5-10分钟)
docker restart elk
7.访问Kibana
http://localhost:5601/

2)Sleuth日志推送给ELK

1.修改之前写的sleuth-traceA和B,修改pom文件

<!-- Logstash for ELK -->
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>5.2</version>
</dependency>

2.修改日志配置文件logback-spring.xml,下面是完整的配置,添加logstashLog了

<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    
    <springProperty scope="context" name="springAppName" source="spring.application.name"/>
    <!--日志输出位置-->
    <property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>
    <!--日志输出格式-->
    <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />

    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 远程把日志以json格式输出给logstash -->
    <appender name="logstashLog" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <!-- logstash的地址 -->
        <destination>127.0.0.1:5044</destination>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {
                        "severity":"%level",
                        "service":"${springAppName:-}",
                        "trace":"%X{X-B3-TraceId:-}",
                        "span":"%X{X-B3-SpanId:-}",
                        "exportable":"%X{X-SpanId-Export:-}",
                        "pid":"${PID:-}",
                        "thread":"%thread",
                        "class":"%logger{40}",
                        "rest":"%message"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref = "consoleLog"/>
        <appender-ref ref = "logstashLog"/>
    </root>
</configuration>

3.配置完成,发起一个调用,查看Kibana

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章