探针LinkAgent快来了解一下!

1、简介

LinkAgent是一个基于Java Instrumentation实现的Agent,通过对Java字节码增强,在无业务代码侵入的情况下,实现对Java应用程序的数据收集和逻辑控制。同时会开放相关的数据及控制接口,供外部应用使用。整体结构如下:

探针LinkAgent快来了解一下!

LinkAgent由三个部分组成:

  • simulator-agent模块,负责对客户端的管理,如加载/卸载等,负责基础状态的注册和基础命令的订阅和调度执行。
  • instrument-simulator模块,负责运行和管理模块,字节码增强、增强安全管理。
  • instrument-modules模块,包含了中间件插件以及各种如日志推送、节点状态注册等基础功能模块。

2、启动流程

LinkAgent采用Java命令行的方式加载启动,相关参数如下:

-Xbootclasspath/a:/XXX/lib/tools.jar
-javaagent:/XXX/transmittable-thread-local-2.12.1.jar
-javaagent:/XXX/simulator-launcher-instrument.jar  // simulator-agent 
-Dpradar.project.name=XXX
-Dsimulator.agentId=XXX
-Dsimulator.delay=1    //启动延时,避免影响目标应用启动
-Dsimulator.unit=SECONDS
-Djdk.attach.allowAttachSelf=true

启动流程可分为三个阶段:探针启动、客户端启动及扩展模块加载,整体流程如下图(只列出重要类及方法):

探针LinkAgent快来了解一下!

探针模块的AgentLauncher#start()方法中,会使用attach(agentmain)或permain(伪)的方式启动客户端,客户端启动完成后会将JettyServer的端口信息写入文件.simulator.token中。探针会循环检查本地文件.simulator.token是否已被创建及内容是否正确,来确认客户端是否已加载完成以及用于后续向客户端转发命令请求。

2.1、探针启动

采用-javaagent命令行启动探针的入口方法为InstrumentLauncher#premain(),完整时序图如下:

探针LinkAgent快来了解一下!

2.1.1、类隔离

为避免与目标应用及模块间的类冲突,agent启动后,会初始化AgentClassLoader,并使用AgentClassLoader进行自身相关类加载。

//InstrumentLauncher#startInternal
private static void startInternal(final long pid, final String processName, Integer delay, TimeUnit unit, Instrumentation inst) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, java.lang.reflect.InvocationTargetException {
    File file = new File(DEFAULT_AGENT_HOME + File.separator + "core", "simulator-agent-core.jar");
    AgentClassLoader agentClassLoader = new AgentClassLoader(new URL[]{file.toURI().toURL()});
    Class coreLauncherOfClass = agentClassLoader.loadClass("com.shulie.instrument.simulator.agent.core.CoreLauncher");
    ...// other code
    startMethod.invoke(coreLauncherOfInstance);
}

2.1.2、延时启动

为避免影响目标应用的启动,agent启动任务会进行延时执行,延时可配置,默认300s。

//CoreLauncher#start
public void start() throws Throwable {
    //启动任务
    Runnable runnable = new Runnable() {...};
    if (delay <= 0) {
        runnable.run();
    } else {
        //ScheduledThreadPoolExecutor,延时执行
        this.startService.schedule(runnable, delay, unit);
    }
}

2.1.3、组件功能

探针模块中主要组件如下:

  • HttpApplicationUploader:探针启动时,通过HTTP请求向tro-web上报应用信息。详见HttpApplicationUploader#checkAndGenerateApp()方法。
  • ZookeeperRegister:心跳节点/config/log/pradar/status/${appName}/${agentId}维护,详见ZookeeperRegister#init()方法。
  • HttpAgentScheduler:负责命令订阅及调度执行,客户端安装成功后,HttpAgentScheduler会循环获取并执行命令。命令通过ExternalAPIImpl组件请求tro-web获取,然后由AgentLauncher进行执行。
private void startScheduler() {
    this.scheduledExecutorService.schedule(new Runnable() {
        @Override
        public void run() {
            //循环获取并执行任务
            while (!isShutdown && AgentStatus.doInstall()) {
                executeCommand();
            }
        }
    }, Math.max(schedulerArgs.getDelay(), 0), schedulerArgs.getDelayUnit());
}
  • AgentLauncher:通过请求客户端内的JettyServer,将命令以HTTP请求转发给客户端,共有以下6类。
AgentLauncher#startup(((StartCommand)command)) //启动客户端
AgentLauncher#shutdown((StopCommand)command)  //停止客户端
AgentLauncher#loadModule(((LoadModuleCommand)command)) //加载指定扩展模块
AgentLauncher#unloadModule(((UnloadModuleCommand)command)) //卸载指定扩展模块
AgentLauncher#reloadModule(((ReloadModuleCommand) command)) //重新加载
AgentLauncher#commandModule((HeartCommand)command) //心跳
  • ExternalAPIImpl:封装了对外部系统的HTTP请求,如对tro-web的心跳请求、命令获取、命令执行响应以及模块下载等。

2.2、客户端启动

客户端启动主要包含两部分内容:内置Jetty服务启动和模块加载。Jetty服务主要用于接收探针获取并转发的命令。启动的入口方法为com.shulie.instrument.simulator.agent.AgentLauncher#agentmain()。以下为客户端启动时序图:

探针LinkAgent快来了解一下!

2.2.1、类隔离

客户端启动时,会初始化SimulatorClassLoader,用于加载客户端相关的类,实现类隔离。

//com.shulie.instrument.simulator.agent.AgentLauncher#install()
// 构造自定义的类加载器,尽量减少Simulator对现有工程的侵蚀
final ClassLoader simulatorClassLoader = defineClassLoader(
    getSimulatorCoreJarPath(home) // SIMULATOR_CORE_JAR_PATH   
);

所有模块也会有通过各自的类加载器进行加载。

//com.shulie.instrument.simulator.core.manager.impl.ModuleJarLoader#load()
void load(final SimulatorConfig simulatorConfig, final ModuleLoadCallback moduleLoadCallback) throws IOException {

    boolean hasModuleLoadedSuccessFlag = false;
    ClassLoaderFactory classLoaderFactory = null;
    if (isInfoEnabled) {
        logger.info("SIMULATOR: prepare loading module-jar={};", moduleJarFile);
    }
    try {
        //根据模块id获取对应的工厂类
        classLoaderFactory = classLoaderService.getModuleClassLoaderFactory(moduleSpec.getModuleId());
        if (classLoaderFactory == null) {
            throw new SimulatorException("can't found ModuleClassLoaderFactory. moduleId=" + moduleSpec.getModuleId());
        }
        final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        //获取moduleClassLoader
        ClassLoader moduleClassLoader = classLoaderFactory.getDefaultClassLoader();
        Thread.currentThread().setContextClassLoader(moduleClassLoader);

        try {
            hasModuleLoadedSuccessFlag = loadingModules(simulatorConfig, classLoaderFactory, moduleLoadCallback);
        } finally {
            Thread.currentThread().setContextClassLoader(contextClassLoader);
        }

    } finally {
        if (!hasModuleLoadedSuccessFlag && null != classLoaderFactory) {
            logger.warn("SIMULATOR: loading module-jar completed, but NONE module loaded, will be close ModuleClassLoader. module-jar={};", moduleJarFile);
            classLoaderFactory.release();
        }
    }

}

2.2.2、Jetty请求处理

Jetty负责处理的请求共有5种:heartbeat、shutdown、load、unload和reload。模块相关的命令(load、unload和reload)通过扫描@Command注解匹配获取方法,然后使用反射调用。 处理逻辑如下:

//ModuleHttpServlet#doMethod
private void doMethod(final HttpServletRequest req,
                      final HttpServletResponse resp) throws ServletException, IOException {

    // 获取请求路径
    final String path = req.getPathInfo();
    //如果心跳则直接回复,不经过命令
    if (heartbeatPath.equals(path)) {
        doHeartbeat(req, resp);
        return;
    }
    if (shutdownPath.equals(path)) {
        doShutdown(req, resp);
        return;
    }
    /**
     * 如果在销毁中,则忽略除心跳外的所有一切请求
     */
if (isDestroying) {
        return;
    }

    // 获取模块ID
    final String moduleId = parseModuleId(path);
    if (StringUtils.isBlank(moduleId)) {
        logger.warn("SIMULATOR: path={} is not matched any module.", path);
        resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    // 获取模块
    final CoreModule coreModule = coreModuleManager.get(moduleId);
    if (null == coreModule) {
        logger.warn("SIMULATOR: path={} is matched module {}, but not existed.", path, moduleId);
        resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    // 匹配对应的方法 @Method注解 匹配
    final Method method = matchingModuleMethod(
            path,
            moduleId,
            coreModule.getModule().getClass()
    );

    if (null == method) {
        logger.warn("SIMULATOR: path={} is not matched any method in module {}",
                path,
                moduleId
        );
        resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    } else {
        if (logger.isDebugEnabled()) {
            logger.debug("SIMULATOR: path={} is matched method {} in module {}", path, method.getName(), moduleId);
        }
    }

    // 自动释放I/O资源
    final List<Closeable> autoCloseResources = coreModule.append(new ReleaseResource<List<Closeable>>(new ArrayList<Closeable>()) {...});

    // 生成方法调用参数
    final Object[] parameterObjectArray = generateParameterObjectArray(method, req, resp);

    final boolean isAccessible = method.isAccessible();
    final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
    try {
        method.setAccessible(true);
        //将当前的 ClassLoader置成是模块的 ClassLoader,处理模块执行时的类加载问题
        Thread.currentThread().setContextClassLoader(coreModule.getClassLoaderFactory().getDefaultClassLoader());
        Object value = method.invoke(coreModule.getModule(), parameterObjectArray);
        if (logger.isDebugEnabled()) {
            logger.debug("SIMULATOR: path={} invoke module {} method {} success.", path, moduleId, method.getName());
        }
        Thread.currentThread().setContextClassLoader(currentClassLoader); //do response之前
        doResponse(value, req, resp);
    } catch (...) {
        //异常处理
    } finally {
        Thread.currentThread().setContextClassLoader(currentClassLoader);
        method.setAccessible(isAccessible);
        coreModule.release(autoCloseResources);
    }

}

除了探针会循环从控制台获取命令并转发给客户端进行处理外,CommandChannelPlugin模块被激活(onActive)后,会向Zookeeper注册节点config/log/pradar/commands/${agentId}的Listener,新增的子节点即为待执行的命令。详见:com.pamirs.attach.plugin.command.CommandChannelPlugin#onActive()。

探针LinkAgent快来了解一下!

2.2.3、模块管理

客户端中负责管理模块的核心类为:DefaultCoreModuleManager,包括模块加载、卸载及模块生命周期回调等功能,功能实现依赖的接口有两个:com.shulie.instrument.simulator.api.ExtensionModulecom.shulie.instrument.simulator.api.ModuleLifecycle,作用分别如下:

  • ExtensionModule

所有模块均实现了ExtensionModule接口,ExtensionModule是一个空接口,配合第三方注解@MetaInfServices自动生成META-INF/service/com.shulie.instrument.simulator.api.ExtensionModule文件,使用ServiceLoader完成模块扫描。

  • ModuleLifecycle

模块生命周期接口,用于操作模块时进行回调通知,如加载模块时会依次调用onLoad()->onActive()->loadCompleted()方法。

本文来自投稿,不代表TakinTalks稳定性技术交流平台立场,如若转载,请联系原作者。

(1)
上一篇 2021年8月23日 上午10:06
下一篇 2022年4月26日 下午8:45

相关推荐

发表评论

邮箱地址不会被公开。