Zipkin和微服务链路跟踪

本期分享的内容是有关zipkin和分布式跟踪的内容。

首先,我们还是通过spring initializr来新建三个项目。一个zipkin service。另外两个是普通的业务应用,分别叫service和client。

zipkin service

client

service

如上我们引入了web 、zipkin client两个依赖。

新建zipkin server应用

先打开zipkin-service项目。

我们来看看依赖情况:

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

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

上面是默认的依赖。这里需要把这些依赖都换掉,否则zipkin server无法正常工作(另外就是spring boot用的版本是1.4.3.RELEASE,spring cloud版本为

Camden.SR4)。

spring boot 版本:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

spirng cloud 版本:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

依赖替换为以下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-server</artifactId>
    </dependency>
    <dependency>
        <groupId>io.zipkin.java</groupId>
        <artifactId>zipkin-autoconfigure-ui</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

现在我们就开始正式的开发吧。

先配置一个server port。

application.properties:

server.port=9411

然后在application类上添加@EnableZipkinServer注解。

@EnableZipkinServer
@SpringBootApplication
public class ServiceApplication {

   public static void main(String[] args) {
      SpringApplication.run(ServiceApplication.class, args);
   }
}

然后启动zipkin server。

http://localhost:9411/

好,现在server准备的差不多了。我们现在去准备client吧。

新建client应用

配置端口:

server.port==9876

配置应用名称:

spring.application.name=client

然后新建一个rest api :

@RestController
@SpringBootApplication
public class ClientApplication {

   @Bean
   RestTemplate restTemplate(){
      return new RestTemplate();
   }

   @GetMapping("/hi")
   public String hi(){
      return this.restTemplate().getForEntity("http://localhost:8081/hi",String.class).getBody();
   }

   public static void main(String[] args) {
      SpringApplication.run(ClientApplication.class, args);
   }
}

上面的逻辑很简单就是一个rest api,然后调用另外一个service的hi服务。

新建service应用

现在新建一个 service 服务。

配置端口:

server.port=9081

配置应用名称:

spring.application.name=service

代码:

@SpringBootApplication
@RestController
public class ServiceApplication {

   @GetMapping("/hi")
   public String hi(){
      return "Hello World";
   }

   public static void main(String[] args) {
      SpringApplication.run(ServiceApplication.class, args);
   }
}

体验之旅

zipkin server之前已启动。现在分别去启动client 和 service。

然后我们模拟调用。

在浏览器中输入:

返回了“Hello World”。

现在我们再刷新zipkin server 的ui,发现应用名称那个下拉框已由灰色变为了可用。

分别显示了我们刚才创建的那两个应用的应用名称:service和client。

现在选择client这个应用,然后看看情况:

发现已经能够查询出刚才的那次调用记录了。

然后我们点击进去查看具体的内容:

上面已经为我们展示了本次请求的深度、总共的span数量以及涉及到的服务以及总耗时。同时显示了调用链路的关系,可以发现每个服务所耗费的时间、上下关系等。

我们还可以点击具体的服务片段,也就是span,就会弹出具体的服务的细节指标展示:

服务指标展示中你可以看到服务片段所在环境的ip,该请求的http method,以及path,还有所在类名称等等。

而且还会展示该服务片段内部的每个请求阶段的细节。

上面的展示其实都是对json数据的渲染。你可以点击“JSON” ,然后查看更详细更具体的数据,同时通过此了解zipkin的数据模型:

除了上面说的trace能力,zipkin还为我们提供了依赖展示。

这里我们只涉及到两个服务的调用。所以依赖比较简单。

源码解读及参数配置

你也许纳闷,没有做任何配置,zipkin server怎么就会收到了数据然后展示呢?

这也太神奇了吧。其实一点都不神奇。让我们来看看源码吧。

先来看看我们引入的依赖:

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-zipkin</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-test</artifactId>
   <scope>test</scope>
</dependency>

一共三个,和zipkin直接有关的就是这个:

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

现在找到这个jar去看看吧:

发现没有代码,这只是个starter,很多时候starter就是这个样子,只是在pom中加入依赖而已:

去看看pom中有哪些依赖吧。

发现只有两个依赖:

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

现在进入看哪个呢?先尝试去看看spring-cloud-sleuth-zipkin吧,因为这个含有关键字zipkin,可能是个过渡:

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

来到spring-cloud-sleuth-zipkin包,发现了ZipKinAutoConfiguration。

进去看看吧:

@Configuration
@EnableConfigurationProperties({ZipkinProperties.class, SamplerProperties.class})
@ConditionalOnProperty(value = "spring.zipkin.enabled", matchIfMissing = true)
@AutoConfigureBefore(TraceAutoConfiguration.class)
public class ZipkinAutoConfiguration {

至此我们基本可以解释为什么我们没有做任何配置,zipkin client就在后台工作了,就是因为这里使用了自动配置机制,也就是AutoConfiguration,让配置自动生效。

ok,发现在该类上配置了两个Properties:

@EnableConfigurationProperties({ZipkinProperties.class, SamplerProperties.class})

先去看看ZipkinProperties吧:

ZipkinProperties

/**
 * Zipkin settings
 */
@ConfigurationProperties("spring.zipkin")
public class ZipkinProperties {
   /** URL of the zipkin query server instance. */
   private String baseUrl = "http://localhost:9411/";
   private boolean enabled = true;
   private int flushInterval = 1;
   private Compression compression = new Compression();

   private Service service = new Service();

   private Locator locator = new Locator();

...

这里只贴了field片段。 因为这就是我们能够在application.properties中配置的zipkin属性了。

配置zipkin server:

这里配置了默认值。zipkin client默认会向本地的9411端口发送数据:

 private String baseUrl = "http://localhost:9411/";

在生产中,我们就可以在application.properties中配置自己的zipkin的地址了:

spring.zipkin.base-url=http://localhost:9511/

Flush间隔

你可以通过以下修改flush间隔,默认是1秒:

spring.zipkin.flush-interval=1

数据压缩支持

你也许发现了。除了几个primitive类型的field之外,还有几个自定义的引用类型Compression、Service、Locator。现在我们去看看Compression吧:

/** When enabled, spans are gzipped before sent to the zipkin server */
public static class Compression {
   private boolean enabled = false;
....
}

哦,通过注释知道是一个支持压缩的能力。默认是false。你可以在配置文件中开启压缩,这样在发送给zipkin server之前会先把数据进行压缩:

spring.zipkin.compression.enabled=true

自定义service name

再来看看Service:

/** When set will override the default {@code spring.application.name} value of the service id */
public static class Service {
   /** The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin */
   private String name;
 ...
}

默认的service name是读取spring.application.name的值,你可以通过以下属性来覆盖默认策略定义想要的service name:

spring.zipkin.service.name=service1

服务发现定位支持

Locator:

public static class Locator {

   private Discovery discovery;

 ....//skip setter getter

   public static class Discovery {

      /** Enabling of locating the host name via service discovery */
      private boolean enabled;

    .....//skip setter getter
   }
}

这里你可以支持通过服务发现来定位host name:

spring.zipkin.locator.discovery.enabled=true

配置采样率

你也许发现了auto configuration类上有两个properties类。一个是ZipKinProperties,一个是SamplerProperties。接下来看看SamplerProperties。

/**
 * Properties related to sampling
 */
@ConfigurationProperties("spring.sleuth.sampler")
public class SamplerProperties {

   /**
    * Percentage of requests that should be sampled. E.g. 1.0 - 100% requests should be
    * sampled. The precision is whole-numbers only (i.e. there‘s no support for 0.1% of
    * the traces).
    */
   private float percentage = 0.1f;

}

看代码发现就是一个采样的配置。默认是采样10%。要求必须是全数。比如不能是0.1%。

spring.sleuth.sampler.percentage=0.2  # 修改为20%的采样率

自定义采样规则

除了上面的通过配置比率的方式。你还可以通过编程的方式自定义采样规则。比如你可以只对那些返回500的请求进行采样等等。或者你决定忽略掉那些成功的请求,只对失败的进行采样等等。下面是对所有请求的大概一半进行采样:

@Bean
Sampler customSampler() {
    return span -> Math.random() > .5;
}

另外除了以上配置,还有一些sleuth的配置,这里就不一一展开了。你可以去spring cloud sleuth core中的autoconfiguration类查看。

基本概念

调用链跟踪中有两个比较基本的概念就是:Trace和Span。Trace就是一次真实的业务请求就是一个Trace。它也许会经过很多个Span。Span对应的就是每个服务。一个trace会有一个trace id负责串联所有的span。同时每个span也有自己的id。span上又会携带一些元数据。其中最常见的就是调用开始时间和结束时间。你也可以把一些业务相关的元数据携带到span上。

支持跟踪的请求类型

Spring Cloud Sleuth(org.springframework.cloud:spring-cloud-starter-sleuth),一旦添加到CLASSPATH中,就会自动支持以下常用的组件:

  1. 通过mq技术(如Apache Kafka或RabbitMQ)(或任何其他Spring Cloud Stream binder)进行的请求。
  2. 在Spring MVC controller收到的HTTP header。
  3. 通过Netflix Zuul传过来的microroxy请求。
  4. 使用RestTemplate等进行的请求。

存储

Zipkin Server通过SpanStore将写入委托给持久层。 目前,支持使用MySQL或内存式SpanStore两种的开箱即用。默认是存储在内存中的。

SpanStore

该接口是持久化跟踪数据的持久化接口抽象。以下是接口的方法:

public interface SpanStore {
  List<List<Span>> getTraces(QueryRequest request);
  @Nullable
  List<Span> getTrace(long traceIdHigh, long traceIdLow);
  @Nullable
  List<Span> getRawTrace(long traceIdHigh, long traceIdLow);
  @Deprecated
  @Nullable
  List<Span> getTrace(long traceId);
  @Deprecated
  @Nullable
  List<Span> getRawTrace(long traceId);
  List<String> getServiceNames();
  List<String> getSpanNames(String serviceName);
  List<DependencyLink> getDependencies(long endTs, @Nullable Long lookback);
}

这里只抽取第一个接口方法来看看跟踪数据的内部结构:

List<List<Span>> getTraces(QueryRequest request);

getTraces方法的入参是一个QueryRequest。如果让你设计这个接口的话,也许你会传入参为serviceName或者多个参数。

这里使用了一个对象来把各参数传入进去。这算是多参数查询接口设计的不错范例。

getTraces方法的返回值则是一个二维list。 一个List<Span>是一个trace。多个List<Span>则抽象为了一个跟踪数据存储库。然后通过QueryRequest传入查询filter来实现查询。

QueryRequest

查询请求参数对象。负责把要查询的条件封装起来。

public final class QueryRequest {

  /**
   * 服务名称
   */
  @Nullable
  public final String serviceName;

  /** span名称,查询出包含该span名称的所有trace */
  @Nullable
  public final String spanName;

  /**
   * 根据json中的元数据annotation节点中的值查询
   */
  public final List<String> annotations;

  /**
   *根据json中的元数据binaryAnnotation进行查询
   */
  public final Map<String, String> binaryAnnotations;

  /**
   * 响应时间大于等于此值
   */
  @Nullable
  public final Long minDuration;

  /**
   * 响应时间小于等于此值
   */
  @Nullable
  public final Long maxDuration;

  /**
   * 只显示指定时间之前的,默认是到当前时间
   */
  public final long endTs;

  /**
   * 只显示指定时间之后的,默认是到endTs,也就是从lookback到endTs这段时间的
   */
  public final long lookback;

  /** 每次查询的数量,默认返回10条记录 */
  public final int limit;

InMemorySpanStore

该类是一个默认实现“持久化”存储实现。加引号是因为这不是真正持久化,只是在内存中而已。该存储方案仅仅适用于测试。

/** Internally, spans are indexed on 64-bit trace ID */
public final class InMemorySpanStore implements SpanStore {

另外zipkin支持mysql、cassandra、elasticsearch几种存储方案。mysql性能有点问题。生产也只能上后两个之一了。

trace探针埋点实现

现在默认支持如上图几种的探针埋点实现。这里就简单说下。比如web就是通过filter的方式进行埋点。而hystrix则是通过重新封装HystrixCommand来实现:

public abstract class TraceCommand<R> extends HystrixCommand<R> {

  ...
   @Override
   protected R run() throws Exception {
      String commandKeyName = getCommandKey().name();
      Span span = this.tracer.createSpan(commandKeyName, this.parentSpan);
      this.tracer.addTag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, HYSTRIX_COMPONENT);
      this.tracer.addTag(this.traceKeys.getHystrix().getPrefix() +
            this.traceKeys.getHystrix().getCommandKey(), commandKeyName);
      this.tracer.addTag(this.traceKeys.getHystrix().getPrefix() +
            this.traceKeys.getHystrix().getCommandGroup(), getCommandGroup().name());
      this.tracer.addTag(this.traceKeys.getHystrix().getPrefix() +
            this.traceKeys.getHystrix().getThreadPoolKey(), getThreadPoolKey().name());
      try {
         return doRun();
      }
      finally {
         this.tracer.close(span);
      }
   }

   public abstract R doRun() throws Exception;
}

zuul则是通过ZuulFilter实现的:

public class TracePreZuulFilter extends ZuulFilter {

  ...

   @Override
   public Object run() {
      getCurrentSpan().logEvent(Span.CLIENT_SEND);
      return null;
   }

   @Override
   public ZuulFilterResult runFilter() {
      RequestContext ctx = RequestContext.getCurrentContext();
      Span span = getCurrentSpan();
      if (log.isDebugEnabled()) {
         log.debug("Current span is " + span + "");
      }
      markRequestAsHandled(ctx);
      Span newSpan = this.tracer.createSpan(span.getName(), span);
      newSpan.tag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, ZUUL_COMPONENT);
      this.spanInjector.inject(newSpan, ctx);
      this.httpTraceKeysInjector.addRequestTags(newSpan, URI.create(ctx.getRequest().getRequestURI()), ctx.getRequest().getMethod());
      if (log.isDebugEnabled()) {
         log.debug("New Zuul Span is " + newSpan + "");
      }
      ZuulFilterResult result = super.runFilter();
      if (log.isDebugEnabled()) {
         log.debug("Result of Zuul filter is [" + result.getStatus() + "]");
      }
      if (ExecutionStatus.SUCCESS != result.getStatus()) {
         if (log.isDebugEnabled()) {
            log.debug("The result of Zuul filter execution was not successful thus "
                  + "will close the current span " + newSpan);
         }
         this.tracer.close(newSpan);
      }
      return result;
   }

   // TraceFilter will not create the "fallback" span
   private void markRequestAsHandled(RequestContext ctx) {
      ctx.getRequest().setAttribute(TraceRequestAttributes.HANDLED_SPAN_REQUEST_ATTR, "true");
   }

  ...

}

scheduling则是通过切面实现的:

@Aspect
public class TraceSchedulingAspect {
  ....
   @Around("execution (@org.springframework.scheduling.annotation.Scheduled  * *.*(..))")
   public Object traceBackgroundThread(final ProceedingJoinPoint pjp) throws Throwable {
      if (this.skipPattern.matcher(pjp.getTarget().getClass().getName()).matches()) {
         return pjp.proceed();
      }
      String spanName = SpanNameUtil.toLowerHyphen(pjp.getSignature().getName());
      Span span = this.tracer.createSpan(spanName);
      this.tracer.addTag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, SCHEDULED_COMPONENT);
      this.tracer.addTag(this.traceKeys.getAsync().getPrefix() +
            this.traceKeys.getAsync().getClassNameKey(), pjp.getTarget().getClass().getSimpleName());
      this.tracer.addTag(this.traceKeys.getAsync().getPrefix() +
            this.traceKeys.getAsync().getMethodNameKey(), pjp.getSignature().getName());
      try {
         return pjp.proceed();
      }
      finally {
         this.tracer.close(span);
      }
   }

}

消息中间件则是通过ExecutorChannelInterceptor来实现的:

abstract class AbstractTraceChannelInterceptor extends ChannelInterceptorAdapter
      implements ExecutorChannelInterceptor {

总结

分布式链路跟踪最核心的就是trace id以及span ID。基于此能够在每个span期间挖掘元数据并同span ID一同组成一条记录存入跟踪记录库。

本文首先为你展示了如何搭建一个zipkin server,然后启动了两个service。然后模拟发起调用请求。然后展示了zipkin server的基本使用。

然后通过查看入口源码了解到了你在application.yaml中可配置的那些参数。

最后还说明了有关链路跟踪调用的基本概念并展示了zipkin基本的存储结构。

原文地址:https://www.cnblogs.com/danghuijian/p/9482279.html

原文地址:https://www.cnblogs.com/jpfss/p/12001093.html

时间: 2024-10-11 00:59:49

Zipkin和微服务链路跟踪的相关文章

微服务链路之测试环境快速部署

一般公司都会有开发环境.测试环境.线上环境,测试环境主要用于给测试人员测试每次新功能开发后提交的代码.随着公司业务的增长,这时候同时测试的新功能会越来越多,之前搭建的测试环境,一般是一到三个,这时候同时测试的功能数量远远大于测试环境的个数,就会导致测试人员会等待,有点像一个双核cpu要执行4个任务,并且每个任务不能切换执行,所以只能一个一个执行完,这样的执行过程就会导致测试的效率低下.对于这个问题,通常我们有几种解决方式.其中最简单的就是再多增加几套测试环境,这个方式也是业界用的最多的,它的优势

zipkin+elk微服务日志收集分析系统

docker安装elk日志分析系统 在win10上安装docker环境 tip:win7/8 win7.win8 系统 win7.win8 等需要利用 docker toolbox 来安装,国内可以使用阿里云的镜像来下载,下载地址:http://mirrors.aliyun.com/docker-toolbox/windows/docker-toolbox/ win10 现在 Docker 有专门的 Win10 专业版系统的安装包,需要开启Hyper-V. 程序和功能->启用或关闭Windows

Go微服务全链路跟踪详解

在微服务架构中,调用链是漫长而复杂的,要了解其中的每个环节及其性能,你需要全链路跟踪. 它的原理很简单,你可以在每个请求开始时生成一个唯一的ID,并将其传递到整个调用链. 该ID称为CorrelationID¹,你可以用它来跟踪整个请求并获得各个调用环节的性能指标.简单来说有两个问题需要解决.第一,如何在应用程序内部传递ID; 第二,当你需要调用另一个微服务时,如何通过网络传递ID. 什么是OpenTracing? 现在有许多开源的分布式跟踪库可供选择,其中最受欢迎的库可能是Zipkin²和Ja

Spring Cloud 分布式链路跟踪 Sleuth + Zipkin + Elasticsear

随着业务越来越复杂,系统也随之进行各种拆分,特别是随着微服务架构的兴起,看似一个简单的应用,后台可能很多服务在支撑:一个请求可能需要多个服务的调用:当请求迟缓或不可用时,无法得知是哪个微服务引起的,这时就需要解决如何快速定位服务故障点,Zipkin 分布式跟踪系统就能很好的解决这样的问题. 那么到底怎么使用呢?接下来完成一个具体的实例来体会一把微服务链路追踪: 本文使用的 Spring Cloud Finchley 版本,和其他版本会有不同 我们使用user-service,order-serv

微服务全流程分析

转眼已经2020,距离微服务这个词落地已经过去好多年!(我记得2017年就听过这个词).然而今天我想想什么是微服务,其实并没有一个很好的定义.为什么这样说,按照微服务的定义: 微服务架构就是将一个庞大的业务系统按照业务模块拆分成若干个独立的子系统,每个子系统都是一个独立的应用,它是一种将应用构建成一系列按业务领域划分模块的,小的自治服务的软件架构方式,倡导将复杂的单体应用拆分成若干个功能单一.松偶合的服务,这样可以降低开发难度.增强扩展性.便于敏捷开发,及持续集成与交付活动. 根据这个定义,不难

Spring Cloud——什么是微服务?

一.什么是微服务架构? 1.系统拆分 2.服务各自独立运行.独立部署 3.服务之间基于HTTP的Restful API通信协作 二.为什么选择Spring Cloud? 1.基于Spring Boot实现的微服务架构工具. 2.组成 Config:配置中心,支持git存储配置,实现应用配置外部化,客户端动态修改配置信息 Eureka:服务的注册与发现 Hystrix:容错管理组件,实现服务间降级和熔断 Ribbon:实现客户端负载均衡 Feign:基于Ribbon和Hystrix的声明式服务调用

微服务熔断隔离机制及注意事项

导读:本文重点分析微服务化过程中熔断机制及应用注意事项,包括微服务调用与"雪崩效应"及解决方案.熔断机制及考虑因素.隔离机制及实现方式考量等内容. 随着企业微服务化战略的实施,业务功能细分,越来越多的服务从原有的单体应用中分解成一系列独立开发.部署.运维的微小服务,服务之间则依赖于各种RPC框架互相通信.纵然,微服务化有着很多优势,但与之伴随而来的是各种复杂性,对开发人员来说,除了业务领域本身外,还需要考虑由于服务拆分之后诸如分布式事务.服务部署及运维.rpc调用等系列问题,本文将重点

利用Spring Cloud实现微服务- 熔断机制

1. 熔断机制介绍 在介绍熔断机制之前,我们需要了解微服务的雪崩效应.在微服务架构中,微服务是完成一个单一的业务功能,这样做的好处是可以做到解耦,每个微服务可以独立演进.但是,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成.这就带来一个问题,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的"扇出".如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的&

kubernetes部署spring cloud微服务项目

微服务架构是一项在云中部署应用和服务的新技术.大部分围绕微服务的争论都集中在容器或其他技术是否能很好的实施微服务,而红帽说API应该是重点. 微服务可以在"自己的程序"中运行,并通过"轻量级设备与HTTP型API进行沟通".关键在于该服务可以在自己的程序中运行.通过这一点我们就可以将服务公开与微服务架构(在现有系统中分布一个API)区分开来.在服务公开中,许多服务都可以被内部独立进程所限制.如果其中任何一个服务需要增加某种功能,那么就必须缩小进程范围.在微服务架构中