SpringCloud--Sleuth日志跟踪(十四)

一、Sleuth简介

  Spring Cloud Sleuth[sluːθ]是Spring Cloud提供的分布式系统服务链追踪组件,它大量借用了Google的Dapper,Twitter的Zipkin。
场景:

在分布式系统中,一个集群中有几十个微服务;
微服务调用微服务,一个或多个微服务的网络环境问题、硬件问题导致服务提供失败;

疑问:

我们怎么去定位这个出问题的服务?
怎么定位出问题接口?
怎么分析行为诱因?
难道要逐个的看log?
肯定不是,sleuth 可以给我们提供很好的方案,用服务链路追踪来快速查看。sleuth 集成了Zipkin、HTrace 几种链路追踪工具,我选择zipkin。

  在微服务中,通常根据业务模块分服务,项目中前端发起一个请求,后端可能跨几个服务调用才能完成这个请求(如下图)。


Sleuth

  如果系统越来越庞大,服务之间的调用与被调用关系就会变得很复杂,假如一个请求中需要跨几个服务调用,其中一个服务由于网络延迟等原因挂掉了,那么这时候我们需要分析具体哪一个服务出问题了就会显得很困难。Spring Cloud Sleuth服务链路跟踪功能,就可以帮助我们快速的发现错误根源以及监控分析每条请求链路上的性能等等。
  Spring Cloud Sleuth是一个在应用中实现日志跟踪的强有力的工具。使用Sleuth库可以应用于计划任务 、多线程服务或复杂的Web请求,尤其是在一个由多个服务组成的系统中。当我们在这些应用中来诊断问题时,即使有日志记录也很难判断出一个请求需要将哪些操作关联在一起。
  如果想要诊断复杂操作,通常的解决方案是在请求中传递唯一的ID到每个方法来识别日志。 而Sleuth可以与日志框架Logback、SLF4J轻松地集成,通过添加独特的标识符来使用日志跟踪和诊断问题。
  微服务跟踪(sleuth)其实是一个工具,它在整个分布式系统中能跟踪一个用户请求的过程(包括数据采集,数据传输,数据存储,数据分析,数据可视化),捕获这些跟踪数据,就能构建微服务的整个调用链的视图,这是调试和监控微服务的关键工具。
SpringCloudSleuth有4个特点:

特点 说明
提供链路追踪 通过sleuth可以很清楚的看出一个请求经过了哪些服务,可以方便的理清服务局的调用关系
性能分析 通过sleuth可以很方便的看出每个采集请求的耗时,分析出哪些服务调用比较耗时,当服务调用的耗时,随着请求量的增大而增大时,也可以对服务的扩容提供一定的提醒作用
数据分析优化链路 对于频繁地调用一个服务,或者并行地调用等,可以针对业务做一些优化措施
可视化 对于程序未捕获的异常,可以在zipkpin界面上看到

二、Maven依赖

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

三、 Application.properties配置

配置应用名称,以便日志输出。

# 应用程序名称
spring.cloud.config.name=goods-service

四、日志输出测试

  1. 编写控制器
@RestController
@RequestMapping("goods")
@Slf4j
public class GoodsController {
    @Autowired
    private RestTemplate restTemplate;

    /**
     * 注入Rest模板
     * @return
     */
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
    @GetMapping
    public String getGoodsByUser() {
        String response = (String) restTemplate.exchange("http://localhost:8080/user",
                HttpMethod.GET, null, new ParameterizedTypeReference<String>() {
                }).getBody();
        log.info("测试sleuth");
        return  response;
    }
}
  1. 查看日志结果:
    日志的格式为:[application name, traceId, spanId, export]
[goods-client,5a43ec515cf49d23,5a43ec515cf49d23,true] 18076 --- [nio-8081-exec-7] c.xtsz.goods.controller.GoodsController  : 测试sleuth

说明:
application name — 应用的名称,也就是application.properties中的spring.application.name参数配置的属性。
traceId — 为一个请求分配的ID号,用来标识一条请求链路。
spanId — 表示一个基本的工作单元,一个请求可以包含多个步骤,每个步骤都拥有自己的spanId。一个请求包含一个TraceId,多个SpanId
export — 布尔类型。表示是否要将该信息输出到类似Zipkin这样的聚合器进行收集和展示。

四、异步线程池

  1. 配置类
@Configuration
@EnableAsync
public class ThreadConfig extends AsyncConfigurerSupport {
    @Autowired
    private BeanFactory beanFactory;

    @Override
    public Executor getAsyncExecutor() {
        // 线程池定义
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(7);
        threadPoolTaskExecutor.setMaxPoolSize(42);
        threadPoolTaskExecutor.setQueueCapacity(11);
        threadPoolTaskExecutor.setThreadNamePrefix("MyExecutor-");
        threadPoolTaskExecutor.initialize();
        // sleuth 中延时跟踪执行
        return new LazyTraceExecutor(beanFactory, threadPoolTaskExecutor);
    }
}

  1. 业务类
@Service
@Slf4j
public class UserService {
    @Async
    public void asyncMethod() throws InterruptedException {
        log.info("开始执行异步方法");
        Thread.sleep(1000L);
        log.info("异步方法执行结束");
    }
}
  1. 控制器
@RestController
@RequestMapping("user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/async")
    public String async() throws InterruptedException {
        System.out.println("####控制器输出####   1");
        log.info("####异步方法调用前####");
        userService.asyncMethod();
        log.info("####异步方法调用后####");
        return "success";
    }
}

  1. 测试
    地址:http://localhost:8080/user/async
####控制器输出####   1
2019-09-18 11:15:18.335  INFO [user-client,3f575919b0df993a,3f575919b0df993a,true] 19040 --- [nio-8080-exec-5] com.xtsz.user.controller.UserController  : ####异步方法调用前####
2019-09-18 11:15:18.335  INFO [user-client,3f575919b0df993a,3f575919b0df993a,true] 19040 --- [nio-8080-exec-5] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService
2019-09-18 11:15:18.338  INFO [user-client,3f575919b0df993a,3f575919b0df993a,true] 19040 --- [nio-8080-exec-5] com.xtsz.user.controller.UserController  : ####异步方法调用后####
2019-09-18 11:15:18.342  INFO [user-client,3f575919b0df993a,05fddf5ed5bbbf59,true] 19040 --- [   MyExecutor-1] com.xtsz.user.service.UserService        : 开始执行异步方法
2019-09-18 11:15:19.020  INFO [user-client,,,] 19040 --- [ender@5ae1c281}] c.netflix.config.ChainedDynamicProperty  : Flipping property: localhost.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2019-09-18 11:15:19.036  INFO [user-client,,,] 19040 --- [ender@5ae1c281}] c.n.u.concurrent.ShutdownEnabledTimer    : Shutdown hook installed for: NFLoadBalancer-PingTimer-localhost
2019-09-18 11:15:19.036  INFO [user-client,,,] 19040 --- [ender@5ae1c281}] c.netflix.loadbalancer.BaseLoadBalancer  : Client: localhost instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=localhost,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2019-09-18 11:15:19.039  INFO [user-client,,,] 19040 --- [ender@5ae1c281}] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2019-09-18 11:15:19.041  INFO [user-client,,,] 19040 --- [ender@5ae1c281}] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client localhost initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=localhost,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@69326b62
2019-09-18 11:15:19.343  INFO [user-client,3f575919b0df993a,05fddf5ed5bbbf59,true] 19040 --- [   MyExecutor-1] com.xtsz.user.service.UserService        : 异步方法执行结束

3f575919b0df993a:traceId相同
3f575919b0df993a,05fddf5ed5bbbf59: spanId不同。
Sleuth将traceId传入了异步方法并创建了以新的spanId,代表这是同一个请求但进入了另一个处理阶段,由一个异步线程来执行。

五、计划任务

  1. 计划任务配置类
/**
 * 实现了SchedulingConfigurer 接口并重写了configureTasks 方法,
 * 同时在类上面添加了@EnableScheduling 注解。
 */
@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfig extends AsyncConfigurerSupport implements SchedulingConfigurer {
    /**
     * 注册计划任务
     * @param scheduledTaskRegistrar
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(schedulingExecutor());
    }

    @Bean
    public Executor schedulingExecutor() {
        // 生成一个线程池
        return Executors.newScheduledThreadPool(1);
    }
}

  1. 业务类
@Service
@Slf4j
public class UserService {
    @Async
    public void asyncMethod() throws InterruptedException {
        log.info("开始执行异步方法");
        Thread.sleep(1000L);
        log.info("异步方法执行结束");
    }
}

  1. 计划任务类
@Service
@Slf4j
public class SchedulingService {
    private final UserService userService;

    /**
     * 初始化对象
     * @param userService
     */
    @Autowired
    public SchedulingService(UserService userService) {
        this.userService = userService;
    }

    @Scheduled(fixedDelay = 30000)
    public void scheduledWork() throws InterruptedException {
        log.info("开始计划任务");
        userService.asyncMethod();
        log.info("结束计划任务");
    }
}

  1. 控制器类
@RestController
@RequestMapping("user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/async")
    public String async() throws InterruptedException {
        System.out.println("####控制器输出####   1");
        log.info("####异步方法调用前####");
        userService.asyncMethod();
        log.info("####异步方法调用后####");
        return "success";
    }
}

  1. 测试
    结果:
2019-09-18 11:27:46.311  INFO [user-client,621cbb0fca03c421,621cbb0fca03c421,true] 13584 --- [pool-2-thread-1] com.xtsz.user.service.SchedulingService  : 开始计划任务
2019-09-18 11:27:46.312  INFO [user-client,621cbb0fca03c421,621cbb0fca03c421,true] 13584 --- [pool-2-thread-1] com.xtsz.user.service.SchedulingService  : 结束计划任务
2019-09-18 11:27:46.312  INFO [user-client,80d6ab72b300f539,80d6ab72b300f539,true] 13584 --- [cTaskExecutor-2] com.xtsz.user.service.UserService        : 开始执行异步方法
2019-09-18 11:27:47.313  INFO [user-client,80d6ab72b300f539,80d6ab72b300f539,true] 13584 --- [cTaskExecutor-2] com.xtsz.user.service.UserService        : 异步方法执行结束

可以看到Sleuth为每个任务实例都创建一个新的traceId和spanId。
Spring Cloud Sleuth可以应用在各种各样的单一Web应用中。我们可以使用这项技术轻松地为一个请求采集日志,即使请求跨越多个线程。帮助我们在多线程环境下进行清晰的调试,通过识别traceId和spanId来确定每一个操作和操作中的每一步,这样可以减轻我们做日志分析的复杂性。

六、logback-spring.xml 配置

# 配置日志文件
logging.config=classpath:logback-spring.xml  

注:当文件名为logback-spring.xml时 不需要配置logging.config

<?xml version="1.0" encoding="UTF-8"?>
<!--该日志将日志级别不同的log信息保存到不同的文件中 -->
<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{yyyy-MM-dd 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="console" 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>

    <!-- 为logstash输出的JSON格式的Appender -->
    <appender name="logstash"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}.json</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名 -->
            <fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
            <!--日志文件保留天数 -->
            <MaxHistory>3</MaxHistory>
        </rollingPolicy>
        <!-- 日志输出编码 -->
        <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-Span-Export:-}",
                        "pid": "${PID:-}",
                        "thread": "%thread",
                        "class": "%logger{40}",
                        "rest": "%message"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>

    <!-- 为logstash输出的JSON格式的Appender -->
    <appender name="logstash2"
              class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>localhost:9600</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-Span-Export:-}",
                        "pid": "${PID:-}",
                        "thread": "%thread",
                        "class": "%logger{40}",
                        "rest": "%message"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>
    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="console" />
        <appender-ref ref="logstash" />
        <appender-ref ref="logstash2" />
    </root>
</configuration>

sleuth中的一些术语:

  1. Span:基本工作单元,例如,在一个新建的span中发送一个RPC等同于发送一个回应请求给RPC,span通过一个64位ID唯一标识,trace以另一个64位ID表示,span还有其他数据信息,比如摘要、时间戳事件、关键值注释(tags)、span的ID、以及进度ID(通常是IP地址)
    span在不断的启动和停止,同时记录了时间信息,当你创建了一个span,你必须在未来的某个时刻停止它。
  1. Trace:一系列spans组成的一个树状结构,例如,如果你正在跑一个分布式工程,你可能需要创建一个trace。
  2. Annotation:用来及时记录一个事件的存在,一些核心annotations用来定义一个请求的开始和结束
  3. cs - Client Sent -客户端发起一个请求,这个annotion描述了这个span的开始
  4. sr - Server Received -服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络延迟
  5. ss - Server Sent -注解表明请求处理的完成(当请求返回客户端),如果ss减去sr时间戳便可得到服务端需要的处理请求时间
  6. cr - Client Received -表明span的结束,客户端成功接收到服务端的回复,如果cr减去cs时间戳便可得到客户端从服务端获取回复的所有所需时间
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容