1、简介
LinkAgent是一个基于Java Instrumentation实现的Agent,通过对Java字节码增强,在无业务代码侵入的情况下,实现对Java应用程序的数据收集和逻辑控制。同时会开放相关的数据及控制接口,供外部应用使用。整体结构如下:
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
启动流程可分为三个阶段:探针启动、客户端启动及扩展模块加载,整体流程如下图(只列出重要类及方法):
探针模块的AgentLauncher#start()方法中,会使用attach(agentmain)或permain(伪)的方式启动客户端,客户端启动完成后会将JettyServer的端口信息写入文件.simulator.token
中。探针会循环检查本地文件.simulator.token
是否已被创建及内容是否正确,来确认客户端是否已加载完成以及用于后续向客户端转发命令请求。
2.1、探针启动
采用-javaagent命令行启动探针的入口方法为InstrumentLauncher#premain(),完整时序图如下:
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()。以下为客户端启动时序图:
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()。
2.2.3、模块管理
客户端中负责管理模块的核心类为:DefaultCoreModuleManager,包括模块加载、卸载及模块生命周期回调等功能,功能实现依赖的接口有两个:com.shulie.instrument.simulator.api.ExtensionModule
和com.shulie.instrument.simulator.api.ModuleLifecycle
,作用分别如下:
ExtensionModule
所有模块均实现了ExtensionModule
接口,ExtensionModule是一个空接口,配合第三方注解@MetaInfServices
自动生成META-INF/service/com.shulie.instrument.simulator.api.ExtensionModule
文件,使用ServiceLoader完成模块扫描。
ModuleLifecycle
模块生命周期接口,用于操作模块时进行回调通知,如加载模块时会依次调用onLoad()->onActive()->loadCompleted()方法。
本文来自投稿,不代表TakinTalks稳定性技术交流平台立场,如若转载,请联系原作者。