zipkin:實現zipkin-spring-boot-starter(三)

文章篇幅有點長,先一句話總結一下:

在Spring啓動過程中,自動注入鏈路配置;並且利用SpringBoot的EnableAutoConfiguration機制,實現了開發無感知、無代碼侵入的zipkin鏈路跟蹤框架

背景

基於zipkin打造的鏈路跟蹤系統上線之後收到了開發同學的好評,尤其是定位跨系統的調用問題時非常方便,效率提升許多。但也有美中不足的地方:第一個版本接入方式是基本上是按照zipkin官方demo進行接入的。這就導致很多地方都需要開發同學進行自行配置、感知到框架底層的東西:如鏈路上報、採集相關配置以及各種組件需要手動開啓,無法做到代碼配置無侵入

具有侵入性且繁雜的配置項會造成很多問題:

  1. 降低開發同學接入的積極性;
  2. 不同環境的server配置不一樣,每個環境都需要修改,提高了使用成本。

於是下一步的工作重點放到了怎麼做才能讓開發同學降低接入和使用成本,將鏈路跟蹤打造成基礎設施,讓上層應用系統接入無感知?

因爲zipkin底層技術架構的原因,很難實現skywalking的模式:將鏈路跟蹤能力下沉到應用運行時環境。 某次閱讀師兄代碼時,發現師兄使用了BeanPostProcessor這個接口去對spring 容器中的bean進行二次加工處理。突然想到:能否利用BeanPostProcessor在Spring啓動的過程中對鏈路配置進行處理從而實現對中間件的鏈路跟蹤?

銀彈:BeanPostProcessor

先看看BeanPostPorcessor作用的時間點: spring初始化過程

再來看看BeanPostProcessor接口的方法:只有兩個方法,分別會在bean初始化之前和之後進行調用

public interface BeanPostProcessor {

	/**
	 * Apply this BeanPostProcessor to the given new bean instance <i>before</i> any bean
	 * initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
	 * or a custom init-method). The bean will already be populated with property values.
	 */
	Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

	/**
	 * Apply this BeanPostProcessor to the given new bean instance <i>after</i> any bean
	 * initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
	 * or a custom init-method). The bean will already be populated with property values.
	 */
	Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

想要在Spring啓動過程中啓動對中間件的鏈路跟蹤,那麼就需要將對應的中間件的配置對象交由Spring進行管理,換句話說就是需要注入到Spring容器中成爲一個bean。現在來梳理一下需要追蹤的中間件:

  1. mysql
  2. dubbo
  3. incoming http:即對外提供的http接口
  4. outcoming http:OkHttp、HttpClient、RestTemplate
  5. 線程池:Executor、ExecutorService、@Scheduled註解

其中mysql、dubbo、incoming http、restTemplate這幾個中間件的正常使用方式默認會注入到Spring容器中,處理起來還算方便。

主要的適配工作在HttpClient,有機會單獨寫一篇文章聊一聊我的適配實現思路。

具體實現

具體的實現邏輯

在Spring啓動過程中注入鏈路跟蹤相關配置

繼承BeanPostProcessor接口實現處理鏈路跟蹤配置的ZipkinBeanPostProcessor:在ZipkinBeanPostProcessor中的postProcessBeforeInitialization或者postProcessBeforeInitialization利用instanceof關鍵字對需要進行鏈路跟蹤的中間件配置實例對象進行過濾並進行加工處理;

利用SpringBoot的EnableAutoConfiguration機制,實現無感接入

設置ZipkinConfiguration對properties文件監聽以及掃描並注入ZipkinBeanPostProcessor。如何製作spring-boot-starter請參見附錄:SpringBoot 創建自己的 Starter

打好zipkin-spring-boot-starterjar包以後,SpringBoot應用只需要引入這個starter jar包就可以絕大部分鏈路進行跟蹤了。實現了代碼配置無侵入、接入零成本的目標。

demo1:跟蹤dubbo鏈路

/**
 * dubbo鏈路配置
 *
 * @author liumian 
 */
@Component
public class DubboConfigProcessor extends AbstractZipkinPostProcessor {

    @Resource
    private ZipkinTracingProperties properties;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (!properties.enable.getDubbo()) {
            return bean;
        }

        if (bean instanceof ProviderConfig) {
            ProviderConfig config = (ProviderConfig)bean;
            return addProviderTracingFilter(config);
        } else if (bean instanceof ConsumerConfig) {
            ConsumerConfig consumerConfig = (ConsumerConfig)bean;
            return addConsumerTracingFilter(consumerConfig);
        } else {
            return bean;
        }
    }

    private final String tracingFilterName = "tracing";

    private Object addProviderTracingFilter(ProviderConfig config) {
        String filter = config.getFilter();
        if (StringUtils.isNotBlank(filter)) {
            if (!StringUtils.contains(filter, tracingFilterName)) {
                String tracedFilter = StringUtils.join(tracingFilterName, ",", filter);
                config.setFilter(tracedFilter);
            }
        } else {
            config.setFilter(tracingFilterName);
        }
        return config;
    }

    private Object addConsumerTracingFilter(ConsumerConfig config) {
        String filter = config.getFilter();
        if (StringUtils.isNotBlank(filter)) {
            if (!StringUtils.contains(filter, tracingFilterName)) {
                String tracedFilter = StringUtils.join(tracingFilterName, ",", filter);
                config.setFilter(tracedFilter);
            }
        } else {
            config.setFilter(tracingFilterName);
        }
        return config;
    }
}

對於跟蹤dubbo鏈路主要提3點:

  1. 跟蹤dubbo鏈路的核心邏輯是在filter配置中加上tracingFilter (zipkin官方實現的dubbo鏈路跟蹤filter),所以只要拿到ProviderConfig和ConsumerConfig實例後併爲其加上tracingFilter即可;
  2. 爲什麼沒有使用ProviderConfig和ConsumerConfig的父類AbstractInterfaceConfig,將添加tracing filter配置的兩個方法合爲一個? 答案是爲了兼容2.7.x版本的dubbo,在2.7.x版本中AbstractInterfaceConfig的包路徑已經變成org.apache.dubbo
  3. 爲什麼要使用postProcessBeforeInitialization而不是postProcessAfterInitialization? 因爲必須在dubbo客戶端註冊到註冊中心之前將tracingFilter配置到filter鏈路中。

demo2:跟蹤線程池鏈路

/**
 * spring 線程池鏈路配置
 *
 * @author liumian 
 */
@Component
public class ExecutorConfigProcessor extends AbstractZipkinBeanPostProcessor {

    @Autowired
    private Tracing tracing;

    @Autowired
    private GodeyeTracingProperties properties;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (properties.enable.geteExecutor() &amp;&amp; bean instanceof Executor) {
            return decorateExecutor((Executor)bean);
        } else {
            return bean;
        }
    }

    private Executor decorateExecutor(Executor delegate) {

        class TracedExecutor implements Executor {
            @Override
            public void execute(Runnable command) {
                String name = Thread.currentThread().getName();
                TraceUtil.newChildTrace(tracing, name, span -&gt; {
                    delegate.execute(tracing.currentTraceContext().wrap(command));
                    return null;
                });
            }
        }
        return new TracedExecutor();
    }
}

線程池的鏈路跟蹤本質上是將線程池內運行的每個task與線程池外部的鏈路上下文銜接起來,比如異步處理http請求等。這裏除了支持Executor線程池外,還支持ExecutorService@Scheduled等常用的線程池和註解。爲了降低代碼的篇幅,這裏只列舉Executor,另外兩種線程池鏈路跟蹤方式也是類似的。

爲什麼要使用postProcessAfterInitialization而不是postProcessBeforeInitialization? 因爲接口被抹去的原因,所以等bean初始化之後再進行包裝;下一個章節會討論這個問題。

遺留的問題

接口被“抹去”

demo2:跟蹤線程池鏈路這個例子中可以瞭解到對線程池進行鏈路跟蹤的步驟:

  1. 需要將實例對象A強制轉換成Executor接口的實例B;
  2. 對上一步強制轉換的ExecutorB實例進行包裝。

這會造成一個問題,如果A不僅僅實現了Executor接口還實現了其他接口,那麼其他的接口信息就會丟失。如果某些框架依賴這些接口類型去獲取bean實例,那麼就會造成奇怪的問題。

解法:使用cglib等動態代理庫進行方法增強而不是進行強制轉換的方式進行包裝


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