基于SPI作为切入点来增强分布式系统的日志追踪能力

97 阅读12分钟

前提

  1. 微服务架构中使用TranceId来追踪日志链路是一种常见的手段,目前主流的框架是使用字节码修改的方式实现该功能的外挂(例如SkyWalking)。
    我所在的团队目前采用的是直接在系统中进行编码引入,这就带来了一些问题,例如:每一个系统都需要写一遍这套逻辑,每一个新项目也需要重新写一遍。
    从基于优化这个问题点的出发,我开始思考是否可以让这个流程更加优雅。我希望:只要添加一个pom引入,系统就有该功能。对 就是这样
  2. 那么会有一些人认为,为什么不用字节码修改的方式而是通过这种包引入的方式来进行增强。这个问题,在组内进行内容分享的时候,也提出来了。我的理解可以分有两点:
    首先,单纯的技术实现来说使用字节码修改的方式来增强是极佳的,毕竟这种方式对于系统可以认为是近乎于零侵入的增强手段,但是我们还需要从业务角度来考虑,每个公司里的不同项目都会存在异同,在许多场景下我们需要和业务更加贴合,这时候我认为包引入的方式相较于字节码修改更为合适(这里还可能是因为我内心希望这个包不仅限于这个功能,比如针对异常日志的通知处理等)。
    其次,问题类似于数学题,我希望可以用不同的方式来为解题提供不同且可行的解题思路
  3. 下文将阐述我是实现优化目前的思路,文章前半段说明的是实现过程中需要的一些理论知识后半段是具体的实现思路和内容(由于贴出了具体实现的代码,导致文章篇幅略长,阅读时可以适当忽略)。

一、SPI是什么?有什么用?

  1. 为接口寻找服务实现的机制- 在基于接口编程的系统中,如果对接口实现进行硬编码,那么代码就违反了可拔插的原则。为了解决这个问题,则引入了SPI-服务发现机制。对于程序本身不申明接口的具体实现是什么
  2. SPI约定 - 该机制约定,在jar包的META/services/目录里创建一个以服务接口命名的文件,该文件里指明实现该接口的具体实现类。通俗来讲,在加载接口实现的时候,该机制通过某种约定来寻找接口具体的实现并对其进行实例化
  3. Spring的约定是:通过SpringFactoriesLoader加载器,不仅在ClassPath路径下查找,而且会扫描所有路径下的Jar包的META-INF/spring.factories文件
  4. log扩展、jdbc扩展

「SPI的实现是:接口化编程 + 策略模式 + 配置文」

注:「策略模式」一个类的行为或其算法可以在运行时更改,属于行为型模式。例如:网关路由?
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。

二、实际运用中需要涉及到的知识点

1. 类加载机制 - 双亲委派模型

Bootstrap classloader - 用来加载Java的核心库(如rt.jar),是用原生代码而不是java来实现,并不继承自java.lang.ClassLoader,除此之外所有的类加载器都是java.lang.ClassLoader类的一个实例
Extension classloader - 它用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录(一般为%JRE_HOME%lib/ext).该类加载器在此目录里面查找并夹在Java类
System/app classloader - 它根据当前Java应用的类路径(ClassPath)来加载Java类。一般来说,Java应用的类都是由它来完成加载,可以通过ClassLoader.getSystemClassLoader()来获取它。
另一类加载器是开发人员通过继承java.lang.ClassLoader类的方式实现自己的类加载器,重写findClass()方法,以满足一些特殊的需求

2. 类加载的流程

当一个装载器被请求装载某个类时:
1. 递归委托自己的parent加载器去装载,若parent能装载,则返回这个类所对应的Class的对象
2. 若parent不能装载,则由请求者自己去装载
当加载的类引用另一个类时,会使用装载第一个类的类加载器来装载被引用的类。该模型下可以有效的防止应用程序装载不可靠甚至恶意的代码来代替可靠的代码

3. 打破双亲委派模型

线程上下文类加载器(Context Classloader)
由于现有模型,限定死了上层的加载器无法调用下层的加载器,但又会有场景需要,比如:JDBC - Java内置有关JDBC的接口,由不同的公司自己去实现那个接口。(具体:JDBC的接口是由顶层的加载器加载因为在rt核心包中,但具体的实现是由应用加载器加载)。为了解决这些场景,Java给出的解决方案就是通过线程上下文加载器来进行实现。
Java会在Launcher类(启动类加载器)启动的时候,设置线程上下文加载器默认为应用加载器(AppClassLoader)。

4. Spring加载Bean的不同生命阶段,以及各个生命阶段预留的扩展点

1. Spring/SpringBoot启动过程中主要的几个扩展节点

ApplicationContextInitializer -> BeanPostProcessor -> @PostConstruct -> SmartInitializingSingleton -> SmartLifecycle -> ApplicationRunner

ApplicationContextInitialize 实例化上下文后,spring fresh之前执行,多用于修改上下文实例的场景。可指定执行顺序
BeanPostProcessor 每一个bean初始化后回进行回调的函数 可控制执行顺序
SmartInitializingSingleton 是加载和初始化所有bean后执行的回调函数,未找到控制函数执行顺序的方式,只会执行一次。区别于SmartLifecycle 接口的地方在于前者提供程序停止回调等接口。rabbitmq中实现该接口用于实例化listener容器等操作。
SmartLifecycle 完成fresh后,spring会回调的函数 可控制执行顺序。多用于bean初始化完成后调用执行任务。如rabbitmq会在listener容器加载完成之后通过实现该接口完成监听器的启动

注:Spring启动初始化的时候,通过SpringFactoriesLoader加载器寻找ApplicationContextInitializer和ApplicationListener的实现类

2. Bean的生命周期如下图所示

image.png

三、实际工作中的使用场景

分布式系统日志跟踪TraceId与业务代码的剥离的实现

实现思路:- 核心思路就是:找到对应入口将自定义的拦截器进行外挂

1. 通过Spring或者第三方组件的自带拦截器机制,将编写的自定义拦截器进行外挂 
2. 通过了解Spring的启动流程以及Bean生命周期机制,对Bean进行自定义的修改 - 上述内容基本都是为这个思路点提供理论铺垫
3. 对类进行字节码修改

需要解决的主要问题点:

1. 如何让引入的系统加载包中的内容?
   答:SPI机制
2. 如何控制系统只加载使用的组件增强,比如:A系统只用了Dubbo组件,包肯定是所有组件都增强了,那怎么让系统只加载Dubbo组件的增强类?
   答:@ConditionalOnClass.用于判断当前系统是否存在一个特定类,存在才加载。这个注解有好几个,都是Condition开头,感兴趣的同学可以了解下。

以下是上述目标所要解决的几个场景点

1. Spring/SpringBoot项目启动

 /**
 * Spring方式启动的时候给上下文中增加traceId
 **/
 public class MyApplicationContextInitializer implements ApplicationContextInitializer {
     private final Logger log = LoggerFactory.getLogger(MyApplicationContextInitializer.class);

     public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
         try {
             // 开始初始化前就给上下文中添加traceId
             ThreadMdcUtil.setTraceId();
             log.debug("start init");
         } catch (Exception e) {
             log.debug("MDC set error,message:", e);
         }
     }
 }

2. Dubbo - 使用其框架自身的SPI机制来添加拦截器

``` java
/**
* Dubbo的拦截器 - 消费端
**/
@Activate(group = {Constants.CONSUMER})
public class DubboConsumerFilter implements Filter {
    private final Logger log = LoggerFactory.getLogger(DubboConsumerFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        try {
            //如果MDC上下文有追踪ID,则原样传递给provider端
            String traceId = MDC.get(TRACE_ID);
            if (traceId == null || "".equals(traceId)) {
                traceId = ThreadMdcUtil.createTraceId();
            }

            RpcContext.getContext().setAttachment(TRACE_ID, traceId);
        } catch (Exception ex) {
            log.debug("Trace id set error in dubbo consumer, message: ", ex);
        }

        return invoker.invoke(invocation);
    }
}

/**
* Dubbo拦截器 - 服务端
**/
@Activate(group = {Constants.PROVIDER})
public class DubboProviderFilter implements Filter {
    private final Logger log = LoggerFactory.getLogger(DubboProviderFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        try {
            String traceId = RpcContext.getContext().getAttachment(TRACE_ID);
            if (traceId == null || "".equals(traceId)) {
                traceId = ThreadMdcUtil.createTraceId();
            }
            MDC.put(TRACE_ID, traceId);
        } catch (Exception ex) {
            log.debug("Trace id set error in dubbo provider, message: ", ex);
        }

        try {
            return invoker.invoke(invocation);
        } finally {
            // 调用完成后移除MDC属性
            MDC.remove(TRACE_ID);
        }
    }
}
```

3. RabbitMQ

 /**
 * 修改RabbitMQ的Bean,增加TraceId相关模块。针对使用template和自定义声明container的场景
 **/
 @ConditionalOnClass({AbstractMessageListenerContainer.class, RabbitTemplate.class})
 public class RabbitMQBeanPostProcessor implements BeanPostProcessor {
     private final Logger log = LoggerFactory.getLogger(RabbitMQBeanPostProcessor.class);

     @Override
     public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
         return bean;
     }

     @Override
     public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
         try {
             // 使用容器方式
             if (bean instanceof AbstractMessageListenerContainer) {
                 AbstractMessageListenerContainer container = (AbstractMessageListenerContainer) bean;
                 // 考虑到某些项目的版本过低问题,所以使用set而不是add。因为add对版本有要求 旧项目版本不允许
                 container.setAfterReceivePostProcessors(new MyAfterReceivePostProcessors());
                 log.info("modify listener container bean " + bean);
             }
             if (bean instanceof RabbitTemplate) {
                 RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
                 // 与上述同理
                 rabbitTemplate.setAfterReceivePostProcessors(new MyAfterReceivePostProcessors());
                 rabbitTemplate.setBeforePublishPostProcessors(new MyBeforePublishPostProcessor());
                 log.info("modify rabbit template bean " + bean);
             }
         } catch (Exception e) {
             log.debug("modify mq listener bean error, message:", e);
         }

         return bean;
     }
 }

 /**
 * 用于修改@RabbitListener方式声明的监听器
 * 为什么这么做,可以参考 RabbitListenerAnnotationBeanPostProcessor mq对这个方式的处理
 **/
 @ConditionalOnClass(value = RabbitListenerEndpointRegistry.class)
 public class RabbitMQSmartLifecycle implements SmartLifecycle, ApplicationContextAware {
     private final Logger log = LoggerFactory.getLogger(RabbitMQSmartLifecycle.class);

     private ConfigurableApplicationContext applicationContext;
     private boolean running;

     @Override
     public void start() {
         running = true;
         try {
             // 该场景没有考虑@RabbitListener含有自定义group的场景,如果需要 则只要加上获取上下文中的contain进行修改即可
             Map<String, RabbitListenerEndpointRegistry> map = applicationContext.getBeanFactory().getBeansOfType(RabbitListenerEndpointRegistry.class);
             if (CollectionUtils.isEmpty(map)) {
                 return;
             }
             for (String key : map.keySet()) {
                 map.get(key).getListenerContainers().forEach(listener -> {
                     SimpleMessageListenerContainer simpleMessageListenerContainer = (SimpleMessageListenerContainer) listener;
                     // 考虑到某些项目的版本过低问题,所以使用set而不是add。因为add对版本有要求 旧项目版本不允许
                     simpleMessageListenerContainer.setAfterReceivePostProcessors(new MyAfterReceivePostProcessors());
                     log.info("modify bean " + simpleMessageListenerContainer);
                 });
             }
 //        this.printContainer(map);
         } catch (Exception e) {
             log.debug("modify mq listener bean for annotation type error, message:", e);
         }
     }

     // 实现ApplicationContextAware接口,spring会将bean所在的应用上下文的引用传入进来
     @Override
     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
         if (applicationContext instanceof ConfigurableApplicationContext) {
             this.applicationContext = (ConfigurableApplicationContext) applicationContext;
         }
     }

     // 打印容器名称 -- 开发调试使用
     private void printContainer(Map<String, RabbitListenerEndpointRegistry> map) {
         for (String key : map.keySet()) {
             map.get(key).getListenerContainers().forEach(a -> {
                 System.out.println("container keyName is " + a.toString());
             });
         }
     }

     public boolean isAutoStartup() {
         return true;
     }

     @Override
     public void stop(Runnable callback) {
         running = false;
     }

     @Override
     public void stop() {
         running = false;
     }

     @Override
     public boolean isRunning() {
         return running;
     }

     @Override
     public int getPhase() {
         return Integer.MAX_VALUE;
     }
 }

 /**
 * RABBIT-MQ接收消息拦截器
 **/
 public class MyAfterReceivePostProcessors implements MessagePostProcessor {
     private final Logger log = LoggerFactory.getLogger(MyAfterReceivePostProcessors.class);

     @Override
     public Message postProcessMessage(Message message) {
         try {
             // 获取traceId 并放到本地.如果没有 则自己生成一个
             ThreadMdcUtil.setTraceId(
                     Optional.ofNullable(message.getMessageProperties().getMessageId()).orElse(ThreadMdcUtil.createTraceId()));
         } catch (Exception ex) {
             // traceId设置带来的任何异常都不应该影响业务
             log.error("rabbitmq set traceId error, message:", ex);
         }
         return message;
     }
 }

 /**
 * RABBIT-MQ推送消息的拦截器
 **/
 public class MyBeforePublishPostProcessor implements MessagePostProcessor, PriorityOrdered {
     private final Logger log = LoggerFactory.getLogger(MyBeforePublishPostProcessor.class);

     @Override
     public Message postProcessMessage(Message message) {
         try {
             message.getMessageProperties().
                     setMessageId(Optional.ofNullable(MDC.get(TRACE_ID)).orElse(ThreadMdcUtil.createTraceId()));
         } catch (Exception ex) {
             // traceId设置带来的任何异常都不应该影响业务
             log.error("Rabbitmq set traceId error, message:", ex);
         }
         return message;
     }

     @Override
     public int getOrder() {
         // (PriorityOrdered > Ordered) 同级别时 按order升序排名,0第一位执行
         return 0;
     }
 }

4. ThreadPool

/**
 * 线程相关工具类的包装以支持多线程下mdc的生成记录
 **/
 public class ThreadMdcWrapper {
     /**
     * 线程池的包装
     */
     public static class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
         // 为了进行示例 且保持精简。下面把其它类型的线程池 以及方法重写都去掉了
         public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                             BlockingQueue<Runnable> workQueue) {
             super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
         }

         // 使用字节码操作来完成包装
         @Override
         public void execute(Runnable task) {
             super.execute(task);
         }

         @Override
         public <T> Future<T> submit(Runnable task, T result) {
             return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
         }
     }
 }

 /**
 * 对线程池做字节码操作
 * 拦截execute方法
 **/
 public class ThreadPoolFilter implements ApplicationContextInitializer {
     private final Logger log = LoggerFactory.getLogger(ThreadPoolFilter.class);

     @Override
     public void initialize(ConfigurableApplicationContext applicationContext) {
         try {
             this.filter();
             log.debug("end add threadPool filter");
         } catch (Exception ex) {
             log.error("an exception occurred in threadPool filter:" + ex);
         }
     }

     // 这该边对普通线程池进行字节码操作 主要目的是为了进行演示现实方式。作为一种解题思路
     public void filter() throws NotFoundException, CannotCompileException, InvocationTargetException,
             IllegalAccessException, InstantiationException, NoSuchMethodException, IOException {
         this.proxyThread("com.fingard.rh.rhf.logtracespi.wrapper.ThreadMdcWrapper$ThreadPoolExecutorMdcWrapper",
                 "execute");
     }

     private void proxyThread(String className, String methodName) throws NotFoundException, CannotCompileException {
         this.proxyThread(className, methodName, "(Ljava/lang/Runnable;)V");
     }

     private void proxyThread(String className, String methodName, String desc) throws NotFoundException,
             CannotCompileException {
         ClassPool pool = ClassPool.getDefault();
         CtClass cc = pool.get(className);

         CtMethod execute = cc.getMethod(methodName, desc);
         execute.insertBefore("$1 = com.fingard.rh.rhf.logtracespi.thread.RunnableHandler.proxyRunner($1);");
         cc.toClass();
     }
 }

 /**
 * 供外部调用方法
 **/
 public class RunnableHandler {
     // 对runner进行一层包装
     public static Runnable proxyRunner(final Runnable runner) {
         return ThreadMdcUtil.wrap(runner, MDC.getCopyOfContextMap());
     }
 }

5. Http Request - @注一


 /**
 * Spring/SpringBoot使用外部tomcat启动场景下的 servlet、listener、filter添加
 **/
 public class MyServletContainerInitializer implements ServletContainerInitializer {
 private final Logger log = LoggerFactory.getLogger(MyServletContainerInitializer.class);

 @Override
 public void onStartup(Set<Class<?>> set, ServletContext servletContext) {
     try {
             // 下面注释的本文无关 只是展示可以可以这么做
             //        // 注册servlet
             //        ServletRegistration.Dynamic servletRegistration = servletContext.addServlet("myServlet", new MyServlet());
             //        servletRegistration.addMapping("/myTest");
             //
             //        // 注册监听器
             //        servletContext.addListener(MyListener.class);

             // 注册Filter
             FilterRegistration.Dynamic filter = servletContext.addFilter("traceLogFilter", TraceLogFilter.class);
             // 配置Filter的映射信息
             filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

             log.info("load success my servlet filter in out tomcat");
         } catch (Exception e) {
             log.debug("filter set error, message:", e);
         }
     }
 }

 /**
 * 使用springboot内部tomcat容器启动
 **/
 @Configuration
 public class SPIAutoConfiguration {
     private final Logger log = LoggerFactory.getLogger(SPIAutoConfiguration.class);

     @Bean
     @ConditionalOnWebApplication
     public FilterRegistrationBean setFilterRegistration(){
         log.info("start init TraceLogFilter in springboot inner tomcat");
         FilterRegistrationBean registration = new FilterRegistrationBean();
         registration.setFilter(new TraceLogFilter());
         registration.addUrlPatterns("/*");
         registration.addInitParameter("paramName", "paramValue");
         registration.setName("TraceLogFilter");
         registration.setOrder(1);
         return registration;
     }
 }

6. 公共调用方法

 public class ThreadMdcUtil {

     public static String createTraceId() {
         String uuid = UUID.randomUUID().toString();
         // todo 可以自己设计实现一个traceid规则,例如:服务器 IP + ID 产生的时间 + 自增序列 + 当前进程号
         return DigestUtils.md5DigestAsHex(uuid.getBytes(StandardCharsets.UTF_8)).substring(8, 24);
     }

     public static void setTraceIdIfAbsent() {
         if (MDC.get(TRACE_ID) == null) {
             MDC.put(TRACE_ID, createTraceId());
         }
     }

     public static void setTraceId() {
         MDC.put(TRACE_ID, createTraceId());
     }

     public static void setTraceId(String traceId) {
         MDC.put(TRACE_ID, traceId);
     }

     public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
         return () -> {
             if (context == null) {
                 MDC.clear();
             } else {
                 MDC.setContextMap(context);
             }
             setTraceIdIfAbsent();
             try {
                 return callable.call();
             } finally {
                 MDC.clear();
             }
         };
     }

     public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
         return () -> {
             if (context == null) {
                 MDC.clear();
             } else {
                 MDC.setContextMap(context);
             }
             setTraceIdIfAbsent();
             try {
                 runnable.run();
             } finally {
                 MDC.clear();
             }
         };
     }
 }

 public class Constant {
     public static final String TRACE_ID = "traceId";
 }
注一:
Spring和SpringBoot的启动加载使用的启动类是不同的,前者使用的是WebApplicationInitializer,而后者使用的是内置的ServletContextInitializer。
WebApplicationInitializer:是spring提供的API,它的生命周期受第三方Servlet容器控制。(在Servlet容器启动时回调)
ServletContextInitializer:是springboot基于嵌入式容器提供的API,其生命周期是springboot自身控制的,使得springboot更加内聚
更多可以查看引用2的文章

四、引用

  1. www.cnblogs.com/theRhyme/p/…
  2. blog.csdn.net/yingzi19911…
  3. juejin.cn/post/684490… - 这是一篇不错的对TraceId简易实现介绍的文章