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基本的存儲結構。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM