jvm-sandbox-repeater源码解析-配置管理

一、配置初见

源码里提供的控制台截图如下:(怎么搭建自己去百度)

从中取出对应的配置如下:

json 复制代码
{
  "degrade": false,  //阻断能力
  "exceptionThreshold": 1000, //异常采样率
  "httpEntrancePatterns": [
    "^/app/v1/order/.*$"
  ],
  "javaEntranceBehaviors": [ //主调用配置
    {
      "classPattern": "com.test.order.service.pay.PayService",
      "includeSubClasses": false,  //是否对内部类生效
      "methodPatterns": [
        "payCallback"
      ]
    }
  ],
  "javaSubInvokeBehaviors": [ //子调用配置
    {
      "classPattern": "com.test.backend.util.RedisUtil",
      "includeSubClasses": false,
      "methodPatterns": [
        "*"
      ]
    }
  ],
  "pluginIdentities": [  //加载的插件
      "redis",
     "http",
    "java-entrance",
    "java-subInvoke",
    "mybatis",
    "ibatis",
    "dubbo-provider",
    "dubbo-consumer"
  ],
  "repeatIdentities": [ //回放插件
    "java",
    "http",
    "dubbo"
  ],
  "sampleRate": 10000, //采样率,最高10000
  "useTtl": true  //开启ttl主子线程变量同步,主要应对多线程场景下的采集
}

接下来,我们通过源码,一步一步来看下具体的配置过程

二、启动过程

repeater插件的启动入口见com.alibaba.jvm.sandbox.repeater.module.RepeaterModule#loadCompleted, 基本流程就是:

  1. 拉取配置
  2. 初始化配置
  3. 发起心跳
java 复制代码
@Override
    public void loadCompleted() {
        //这里启动一个线程去进行做加载,主要是为了不阻断sandbox的模块加载流程
        ExecutorInner.execute(new Runnable() {
            @Override
            public void run() {
                //standalone模式 我们在项目应用过程中是不需要考虑的,所以configManager就是DefaultConfigManager
                configManager = StandaloneSwitch.instance().getConfigManager();
                broadcaster = StandaloneSwitch.instance().getBroadcaster();
                invocationListener = new DefaultInvocationListener(broadcaster);
                RepeaterResult<RepeaterConfig> pr = configManager.pullConfig();
                if (pr.isSuccess()) {
                    log.info("pull repeater config success,config={}", pr.getData());
                    ClassloaderBridge.init(loadedClassDataSource);
                    
                    //启动的核心逻辑就在这里
                    initialize(pr.getData());
                }
            }
        });
        
        //心跳机制
        heartbeatHandler = new HeartbeatHandler(configInfo, moduleManager);
        heartbeatHandler.start();
    }

所以,配置相关的核心使用逻辑在 com.alibaba.jvm.sandbox.repeater.module.RepeaterModule#initialize里

  1. 读取配置
  2. 初始化invokePlugin插件列表
  3. 初始化repeater插件列表(目前实现的有http、java、dubbo)
  4. SubscribeSupporter插件初始化(默认实现 RepeatSubscribeSupporter )
java 复制代码
private synchronized void initialize(RepeaterConfig config) {
        //确保一个周期里面,只初始化一次
        if (initialized.compareAndSet(false, true)) {
            try {
                ApplicationModel.instance().setConfig(config);
                // 特殊路由表; 这个特殊路由表是方便插件需要访问repeater不支持的类
                PluginClassLoader.Routing[] routingArray = PluginClassRouting.wellKnownRouting(configInfo.getMode() == Mode.AGENT, 20L);
                String pluginsPath;
                if (StringUtils.isEmpty(config.getPluginsPath())) {
                    pluginsPath = PathUtils.getPluginPath();
                } else {
                    pluginsPath = config.getPluginsPath();
                }
                //这个其实是一个类加载器
                lifecycleManager = new JarFileLifeCycleManager(pluginsPath, routingArray);
                // 装载插件
                invokePlugins = lifecycleManager.loadInvokePlugins();
                for (InvokePlugin invokePlugin : invokePlugins) {
                    try {
                        // 根据配置中的pluginIdentities,判断是否要注入相关拦截机制
                        if (invokePlugin.enable(config)) {
                            log.info("enable plugin {} success", invokePlugin.identity());
                            invokePlugin.watch(eventWatcher, invocationListener);
                            invokePlugin.onConfigChange(config);
                        }
                    } catch (PluginLifeCycleException e) {
                        log.info("watch plugin occurred error", e);
                    }
                }
                // 装载回放器
                List<Repeater> repeaters = lifecycleManager.loadRepeaters();
                for (Repeater repeater : repeaters) {
                    if (repeater.enable(config)) {
                        repeater.setBroadcast(broadcaster);
                    }
                }
                RepeaterBridge.instance().build(repeaters);
                // 装载消息订阅器
                List<SubscribeSupporter> subscribes = lifecycleManager.loadSubscribes();
                for (SubscribeSupporter subscribe : subscribes) {
                    subscribe.register();
                }
                
                //用于开启ttl的
                TtlConcurrentAdvice.watcher(eventWatcher).watch(config);
            } catch (Throwable throwable) {
                initialized.compareAndSet(true, false);
                log.error("error occurred when initialize module", throwable);
            }
        }
    }

repeater配置生效过程

所以回过头再看配置信息里,启动配置用到的是pluginIdentities、repeatIdentities;

三、java插件启动读取配置javaEntranceBehaviors、javaSubInvokeBehaviors

我们发现配置里还有javaEntranceBehaviors 和 javaSubInvokeBehaviors,这2个配置顾名思义就是java主子调用的意思;

我们去看代码com.alibaba.jvm.sandbox.repeater.plugin.java.JavaEntrancePlugin的实现,发现JavaEntrancePlugin重写了getEnhanceModels

java 复制代码
@MetaInfServices(InvokePlugin.class)
public class JavaEntrancePlugin extends AbstractInvokePluginAdapter {

    private RepeaterConfig config;

    @Override
    protected List<EnhanceModel> getEnhanceModels() {
         //在这个地方读取对应配置的
        if (config == null || CollectionUtils.isEmpty(config.getJavaEntranceBehaviors())) { return null;}
        List<EnhanceModel> ems = Lists.newArrayList();
        for (Behavior behavior : config.getJavaEntranceBehaviors()) {
            ems.add(EnhanceModel.convert(behavior));
        }
        return ems;
    }
    
    ... 其余代码省略
}

同理 JavaSubInvokePlugin 也重写了getEnhanceModels()方法; 这里关注一个点,java主子调用配置的生效,最终是在 com.alibaba.jvm.sandbox.repeater.plugin.core.impl.AbstractInvokePluginAdapter#watchIfNecessary里,通过调用sandbox的接口com.alibaba.jvm.sandbox.api.listener.ext.EventWatchBuilder.IBuildingForBehavior#onWatch()来生效的,也就意味着需要真正字节码注入之后才会生效,所以本质上它的配置,也是启动配置的一部分;

四、http插件的配置 httpEntrancePatterns

首先肯定是看http插件关于 植入点的实现, 我们发现它的注入点是固定的javax.servlet.http.HttpServlet#service()方法,那有关httpEntrancePatterns是在哪里生效的呢?

java 复制代码
@Override
    protected List<EnhanceModel> getEnhanceModels() {
        // 拦截javax.servlet.http.HttpServlet#service(HttpServletRequest req, HttpServletResponse resp)
        EnhanceModel.MethodPattern mp = EnhanceModel.MethodPattern.builder()
                .methodName("service")
                .parameterType(new String[]{"javax.servlet.http.HttpServletRequest", "javax.servlet.http.HttpServletResponse"})
                .build();
        EnhanceModel em = EnhanceModel.builder()
                .classPattern("javax.servlet.http.HttpServlet")
                .methodPatterns(new EnhanceModel.MethodPattern[]{mp})
                .watchTypes(Event.Type.BEFORE, Event.Type.RETURN, Event.Type.THROWS)
                .build();
        return Lists.newArrayList(em);
    }

通过代码最终,我们最后发现是在com.alibaba.jvm.sandbox.repater.plugin.http.HttpStandaloneListener#doBefore里,也就是真正的 切点通知逻辑做的, 在拦截http的每次请求后判断是否需要采集;

java 复制代码
@Override
protected void doBefore(BeforeEvent event) throws ProcessControlException {
        ... 冗余代码省略
        
        Object request = event.argumentArray[0];
        Object response = event.argumentArray[1];
        if (!(request instanceof HttpServletRequest && response instanceof HttpServletResponse)) {
            return;
        }
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        // 根据 requestURI 进行采样匹配, 这里取的配置
        List<String> patterns = ApplicationModel.instance().getConfig().getHttpEntrancePatterns();
        if (!matchRequestURI(patterns, req.getRequestURI())) {
            LogUtil.debug("current uri {} can't match any httpEntrancePatterns, ignore this request", req.getRequestURI());
            Tracer.getContext().setSampled(false);
            return;
        }
        
        ...冗余代码省略
  }

四、可优化的点

通过上述源码阅读,配置这块我觉得有以下几个可优化的点:

  1. 像sampleRate采样率配置,是经常需要手动调整的,这类配置可以跟启动配置pluginIdentities等区分开;启动配置的修改是需要重启生效或者重新attach生效的
  2. http的配置 以及 java的配置也可以区分开; 以下是我这边优化后的界面, 有需要欢迎多交流
相关推荐
李慕婉学姐10 分钟前
【开题答辩过程】以《基于Spring Boot和大数据的医院挂号系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
大数据·spring boot·后端
源代码•宸1 小时前
Leetcode—3. 无重复字符的最长子串【中等】
经验分享·后端·算法·leetcode·面试·golang·string
0和1的舞者1 小时前
基于Spring的论坛系统-前置知识
java·后端·spring·系统·开发·知识
invicinble2 小时前
对于springboot
java·spring boot·后端
码界奇点3 小时前
基于Spring Boot与Vue的校园后台管理系统设计与实现
vue.js·spring boot·后端·毕业设计·源代码管理
爱编程的小庄3 小时前
Rust 发行版本及工具介绍
开发语言·后端·rust
Apifox.4 小时前
测试用例越堆越多?用 Apifox 测试套件让自动化回归更易维护
运维·前端·后端·测试工具·单元测试·自动化·测试用例
sunnyday04265 小时前
Nginx与Spring Cloud Gateway QPS统计全攻略
java·spring boot·后端·nginx
康王有点困5 小时前
Link入门
后端·flink
海南java第二人5 小时前
Spring Boot全局异常处理终极指南:打造优雅的API错误响应体系
java·spring boot·后端