1. 故事背景

前面有说到过,我们正在进行微服务改造,其中曾经选型Spring Cloud,种种原因放弃了,最后选择了SpringBoot + Istio

原来在Spring Cloud中拥有的分布式追踪功能,就寄希望于Istio了。但原来在Spring Cloud全家桶中,有一个叫做Feign的家伙,专门用于调用API,我们觉得挺好用,所以保留了他。并且他还和Hystrix(熔断库)集成了,在我们的业务场景里也需要。

使用分布式追踪很简单啊,把对应的Tracing Headers传播下去就好啦!真的是这样吗?!真的!对于Feign和Hystrix还真有点儿不一样!

2. 提前准备

  • 使用了Feign和Hystrix的环境,使用了如下版本:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
        <version>1.4.3.RELEASE</version>
    </dependency>
    

3. 实践

Istio - 分布式跟踪实践中,提到了集成的方式,我们一步步来。

3.1 增加拦截器

使用拦截器,把所有的Tracing Headers放入ThreadLocal里,代码如下:

public class TracingInterceptor extends HandlerInterceptorAdapter {
	
	public final static ThreadLocal<Map> headerThreadLocal = new ThreadLocal();

	private static List<String> tracingHeaderKeys = Arrays.asList("x-request-id", "x-b3-traceid", "x-b3-spanid", "x-b3-parentspanid",
			"x-b3-sampled", "x-b3-flags", "x-ot-span-context");

	@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        this.handleHeader(request, tracingHeaderKeys);
        response.setHeader("traceid", request.getHeader("x-b3-traceid"));
		return true;
	}

    private void handleHeader(HttpServletRequest request, List<String> tracingHeaderKeys) {

        Map<String, List> tracingHeaders = new HashMap();

	    for(String key: tracingHeaderKeys) {
	        String val = request.getHeader(key);
	        if ( null != val ) {
				tracingHeaders.put(key, Arrays.asList(val));
			}
        }

	    if (tracingHeaders.size() > 0) {
			headerThreadLocal.set(tracingHeaders);
		}
    }

}

3.2 增加Feign拦截器

Feign作为调用API的工具,这里也使用TA的拦截器,把上面的Tracing Headers往下传播,其实就是从ThreadLocal里取出来,代码如下:

@Configuration
public class FeignInterceptorConfiguration {

    @Bean
    public RequestInterceptor headerInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Map<String, Collection<String>> tracingHeaders = TracingInterceptor.headerThreadLocal.get();
                if (null != tracingHeaders) {
                    requestTemplate.headers(tracingHeaders);
                }
            }
        };
    }

}

3.3 一阶段测试

兴高采烈的测试,一切完美,在Jaeger可以正常看到所有的调用链,相当顺利!直到有个同事说,好像Hystrix的熔断没生效!

哦,那也没事,配置加上就成!如下:

feign:
  hystrix:
    enabled: true

But,第一个问题出现了,Tracing好像传递不去了,在Feign拦截器中ThreadLocal取不到相关信息了。

🤔为啥??

原因在于,Hystrix提供了基于信号量和线程两种隔离模式,默认是线程模式,这就是解释了为什么取不到了,因为Hystrix使用了线程,和请求的主线程不是同一个线程,那是肯定取不到值的。

那该怎么解决??方法有二:

  1. 使用信号量模式(但官方不推荐)
  2. 使用Hystrix的插件模式,添加自定义并发策略

既然第一种方法,官方不推荐,我们尝试第二种。

3.4 增加Hystrix自定义并发策略

参考官方文档:Concurrency Strategy

1. 添加自定义并发策略

代码如下:

public class TracingHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        return new TracingAwareCallable<>(callable, TracingInterceptor.headerThreadLocal.get());
    }

    static class TracingAwareCallable<T> implements Callable<T> {

        private final Callable<T> delegate;
        private final Map tracingMap;

        public TracingAwareCallable(Callable<T> callable, Map tracingMap) {
            this.delegate = callable;
            this.tracingMap = tracingMap;
        }

        @Override
        public T call() throws Exception {
            try {
                TracingInterceptor.headerThreadLocal.set(this.tracingMap);
                return delegate.call();
            } finally {
                TracingInterceptor.headerThreadLocal.set(null);
            }
        }
    }
}
  1. 实现Callable接口,在构造函数里,增加Tracing信息,然后在call方法里,再把对应的Tracing信息放置于ThreadLocal里(为了在Feign拦截器里可以用一致的方式获取)。
  2. 实现HystrixConcurrencyStrategy类,覆盖方法wrapCallable,实例化刚刚实现的Callable类,把当前的Tracing信息传入。

2. 注册

注册自定义同步策略

@Configuration
public class HystrixConfiguration {

    @PostConstruct
    public void init() {
        HystrixPlugins.getInstance().registerConcurrencyStrategy(new TracingHystrixConcurrencyStrategy());
    }

}

3.5 二阶段测试

Tracing信息终于又正常传递下去了,所以如果需要熔断的人儿,可以尝试这种方法。

4. 总结

总的来说,为了达到分布式追踪,对于代码的入侵性还是很小的,主要都是一些拦截器,并不影响业务代码。

我相信在别的语言上,整合这个功能会比JAVA容易的多。

比起Spring Cloud,TA有个最大的优势,语言无关性,对于以后的扩展性最好,不会那么局限。