Zipkin和微服務鏈路跟蹤

Zipkin和微服務鏈路跟蹤

2018-04-03閱讀 6.2K0

本期分享的內容是有關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基本的存儲結構。

篇幅所限不能再寫了,公號最大限是二萬字。

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