zoukankan      html  css  js  c++  java
  • Zipkin和微服务链路跟踪

    https://cloud.tencent.com/developer/article/1082821

    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基本的存储结构。

  • 相关阅读:
    ubuntu下无法在目录下创建文件夹,权限不足解决办法
    mongo中的模糊查询
    mysql中的模糊查询
    mysql安装与配置详情
    Django model中的class Meta详解
    kafka集群搭建
    myeclipse/eclipse添加Spket插件实现ExtJs4.2/ExtJs3智能提示
    博客园自定义标题、阅读目录、导航栏、活动的推荐&反对按钮
    IntelliJ IDEA 14 创建maven项目二
    EXT4.2--Ext Designer 使用
  • 原文地址:https://www.cnblogs.com/danghuijian/p/9482279.html
Copyright © 2011-2022 走看看