问题背景
在实际生产中,我们有可能会碰到有些接口,有些URI我们不想追踪,不想产生追踪数据,这个时候可以使用sleuth的追踪跳过的机制,默认sleuth已经禁用了如下URI,下面的URI访问系统的时候是不会被追踪的。
spring:sleuth:web:skip-pattern:"/api-docs.*|/autoconfig|/configprops|/dump|/health|/info|/metrics.*|/mappings|/trace|/swagger.*|.*\\.png|.*\\.css|.*\\.js|.*\\.html|/favicon.ico|/hystrix.stream"
每个URI通过| 隔开,由于sleuth生成span的地方实在traceFilter中生成的,traceFilter默认是所有路径都会拦截的,只要拦截到了就会生成span,只不过这个span会不会上报,这就要看两个方面:
1.yaml中skip-pattern的配置,如果uri在这个里面配置了,那么就不会上报。
2.Sampler defaultTraceSampler()的配置,这个类里面有个isSample方法,如果返回false,那么就不会上报,后面会单独讲这一块。
效果演示
情景1
A 调用 B,再调用C
在系统A里面添加skip-pattern配置
spring:sleuth:web:skip-pattern:"/api-docs.*|/autoconfig|/configprops|/dump|/health|/info|/metrics.*|/mappings|/trace|/swagger.*|.*\\.png|.*\\.css|.*\\.js|.*\\.html|/favicon.ico|/hystrix.stream|/test"
skip-pattern的最后面添加了test, 也就是说uri为test的接口调用不追踪。 通过测试发现,A>B>C这一次调用链都没有被收集到。
情景2
A 调用 B,再调用C
在系统B里面添加skip-pattern配置,配置我就不贴上了。
测试结果: 调用链仅收集到A>B , 因为在B系统里面,请求B的URI过滤掉了,因此整个追踪链路在B系统就断掉了,C也不会上传他的span了。
原理解析
代码入口:org.springframework.cloud.sleuth.instrument.web.TraceFilter
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
if (!(servletRequest instanceof HttpServletRequest) || !(servletResponse instanceof HttpServletResponse)) {
throw new ServletException("Filter just supports HTTP requests");
}
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 获取请求的URL String uri = this.urlPathHelper.getPathWithinApplication(request);
// 判断是否需要跳过,跳过的意思就是创建的span不上报 boolean skip = this.skipPattern.matcher(uri).matches()
|| Span.SPAN_NOT_SAMPLED.equals(ServletUtils.getHeader(request, response, Span.SAMPLED_NAME));
// 省略代码。。。。。
try {
// 创建span spanFromRequest = createSpan(request, skip, spanFromRequest, name);
filterChain.doFilter(request, new TraceHttpServletResponse(response, spanFromRequest));
} catch (Throwable e) {
// 省略代码。。 } finally {
// 省略代码。。。。 }
当一个请求进入系统的时候,首先会经过TraceFilter , 进行span的创建和上报
步骤说明:
1.解析当前请求的URI
2.this.skipPattern.matcher(uri).matches(),这行代码的意思就是判断当前请求的URI是否在skip-pattern里面,匹配到了就返回true。
3.获取请求头里面X-B3-Sampled的值,当X-B3-Sampled==0的时候表示不上报,比如我们上面举的例子,系统A调用系统B的时候,系统A本身就已经
被限制了,说是不上报span, 那么他在请求系统B的时候,就会往请求头里面塞个值,X-B3-Sampled=0 , 这样系统A往下调用的那些系统都不会进行上报span了
4.上面三步,主要是确定可skip的值,true还是false,具体应用的时候还是在createSpan的时候会使用
createSpan
private Span createSpan(HttpServletRequest request,
boolean skip, Span spanFromRequest, String name) {
if (spanFromRequest != null) {
if (log.isDebugEnabled()) {
log.debug("Span has already been created - continuing with the previous one");
}
return spanFromRequest;
}
// 解析出请求里面的traceID,spanId ,parentId Span parent = spanExtractor().joinTrace(new HttpServletRequestTextMap(request));
// parent不为空,表示该请求是从上游系统调用过来的。 if (parent != null) {
if (log.isDebugEnabled()) {
log.debug("Found a parent span " + parent + " in the request");
}
// 添加请求tag进入span addRequestTagsForParentSpan(request, parent);
spanFromRequest = parent;
// 将spanFromRequest放入ThreadLocal中,并且记录日志 tracer().continueSpan(spanFromRequest);
if (parent.isRemote()) { // 是否是远程调用过来的。只要parent不为空,这个基本上就是true了 // 设置事件,server receive ,表示服务收到了,记录服务收到请求的时间 parent.logEvent(Span.SERVER_RECV);
}
request.setAttribute(TRACE_REQUEST_ATTR, spanFromRequest);
if (log.isDebugEnabled()) {
log.debug("Parent span is " + parent + "");
}
} else {
// 如果skip==true, 那么设置当前span的上报器为NeverSampler.INSTANCE , 表示不上报span if (skip) {
spanFromRequest = tracer().createSpan(name, NeverSampler.INSTANCE);
}
else {
// skip==false , 设置他的上报器为new AlwaysSampler() , 表示本次的span是会上报的 String header = request.getHeader(Span.SPAN_FLAGS);
if (Span.SPAN_SAMPLED.equals(header)) {
spanFromRequest = tracer().createSpan(name, new AlwaysSampler());
} else {
spanFromRequest = tracer().createSpan(name);
}
}
// server receive时间,记录服务收到请求的时间 spanFromRequest.logEvent(Span.SERVER_RECV);
request.setAttribute(TRACE_REQUEST_ATTR, spanFromRequest);
if (log.isDebugEnabled()) {
log.debug("No parent span present - creating a new span");
}
}
return spanFromRequest;
}
上面的那一大串代码,跟本次分析的相关的,就是当parent等于null的那个else里面的代码, parent不等于空的时候,是否上报请求,跟parent一样,parent上报,那就上报。
当skip==true的时候,会将span的上报器设置为naverSampler,也就是从不上报span。
设置好Sampler之后,程序在哪里使用呢? 在createSpan里面会调用下面这个方法
private Span sampledSpan(Span span, Sampler sampler) {
if (!sampler.isSampled(span)) {
// Copy everything, except set exportable to false return Span.builder()
.begin(span.getBegin())
.traceIdHigh(span.getTraceIdHigh())
.traceId(span.getTraceId())
.spanId(span.getSpanId())
.name(span.getName())
.exportable(false).build();
}
return span;
}
调用sampler的isSampled方法,如果返回false,那么这是exportable = false
NeverSampler的isSampled至始至终都是返回false
public class NeverSampler implements Sampler {
public static final NeverSampler INSTANCE = new NeverSampler();
@Override
public boolean isSampled(Span span) {
return false;
}
}
设置好span的属性exportable 等于false之后,在最后closeSpan时,准备将其放入队列里面供异步线程(从队列里面取数据,发送给kafka)消费时。会对span的exportable属性做判断。
代码入口:org.springframework.cloud.sleuth.stream.StreamSpanReporter
@Override
public void report(Span span) {
Span spanToReport = span;
// 判断exportable 的属性值,只有等于true的时候才会进入if结构 if (spanToReport.isExportable()) {
try {
if (this.environment != null) {
processLogs(spanToReport);
}
// 调用span的调整器,对span做最后的调整 for (SpanAdjuster adjuster : this.spanAdjusters) {
spanToReport = adjuster.adjust(spanToReport);
}
// 加入队列供异步线程消费 this.queue.add(spanToReport);
} catch (Exception e) {
this.spanMetricReporter.incrementDroppedSpans(1);
if (log.isDebugEnabled()) {
log.debug("The span " + spanToReport + " will not be sent to Zipkin due to [" + e + "]");
}
}
} else {
// 等于false,不做任何处理 if (log.isDebugEnabled()) {
log.debug("The span " + spanToReport + " will not be sent to Zipkin due to sampling");
}
}
}