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的配置也可以区分开; 以下是我这边优化后的界面, 有需要欢迎多交流
相关推荐
goTsHgo26 分钟前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
waicsdn_haha37 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
Q_19284999061 小时前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
求知若饥1 小时前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
左羊1 小时前
【代码备忘录】复杂SQL写法案例(一)
后端
gb42152872 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶2 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
颜淡慕潇2 小时前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes