分布式系統(tǒng)性能監(jiān)控工具,初探Pinpoint Agent啟動源碼
作者:未完成交響曲,資深Java工程師!目前在某一線互聯(lián)網(wǎng)公司任職,架構(gòu)師社區(qū)合伙人!
本文源碼基于Pinpoint 2.0.3-SNAPSHOT版本
官方開源地址:https://github.com/naver/pinpoint
Pinpoint Agent
Pinpoint通過字節(jié)碼增強(qiáng)技術(shù)來實現(xiàn)無侵入式的調(diào)用鏈采集。其核心實現(xiàn)是基于JVM的Java Agent機(jī)制。
我們使用Pinpoint時,需要在Java應(yīng)用啟動參數(shù)上加上-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
參數(shù),這樣,當(dāng)我們的Java應(yīng)用啟動時,會同時啟動Agent。
Pinpoint Agent在啟動的時候,會加載plugin
文件夾下所有的插件,這些插件會對特定class類修改字節(jié)碼,在一些指定的方法調(diào)用前后加上鏈路采集邏輯(比如Dubbo中AbstractProxyInvoker
的invoke()
方法),這樣就實現(xiàn)了調(diào)用鏈監(jiān)控功能。
Pinpoint官方文檔中的原理描述:
在pintpoin-bootstrap模塊中,我們可以在pom文件中看到maven插件里有MANIFEST相關(guān)的配置,指定了
Premain-Class
,這個配置在打包時也會生成到MANIFEST.MF文件中:
執(zhí)行premain()方法
通過上述配置可知,當(dāng)我們啟動接入了pinpoint的Java應(yīng)用時,會先執(zhí)行PinpointBootStrap.premain()
方法:
去掉了日志等非核心邏輯代碼
public static void premain(String agentArgs, Instrumentation instrumentation) {
// 1.設(shè)置啟動狀態(tài),避免重復(fù)初始化
final boolean success = STATE.start();
if (!success) {
return;
}
// 2.初始化和解析啟動參數(shù)
final JavaAgentPathResolver javaAgentPathResolver = JavaAgentPathResolver.newJavaAgentPathResolver();
final String agentPath = javaAgentPathResolver.resolveJavaAgentPath();
final Map<String, String> agentArgsMap = argsToMap(agentArgs);
final ClassPathResolver classPathResolver = new AgentDirBaseClassPathResolver(agentPath);
// 3.查找核心jar包
final AgentDirectory agentDirectory = resolveAgentDir(classPathResolver);
BootDir bootDir = agentDirectory.getBootDir();
appendToBootstrapClassLoader(instrumentation, bootDir);
// 4.獲取類加載器,加載核心jar中的類
ClassLoader parentClassLoader = getParentClassLoader();
final ModuleBootLoader moduleBootLoader = loadModuleBootLoader(instrumentation, parentClassLoader);
PinpointStarter bootStrap = new PinpointStarter(parentClassLoader, agentArgsMap, agentDirectory, instrumentation, moduleBootLoader);
// 5.啟動bootStrap
if (!bootStrap.start()) {
logPinpointAgentLoadFail();
}
}
可以看到premain()方法有兩個參數(shù),最重要的是這個instrumentation
對象
Instrumentation
是Java提供的一個來自JVM的接口,該接口提供了一系列查看和操作Java類定義的方法,例如修改類的字節(jié)碼、向classLoader的classpath下加入jar文件等,
在PinpointBootStrap.premain()
方法中,主要完成了相關(guān)jar包的查找和加載,然后將一系列配置以及instrumentation對象構(gòu)造成PinpointStarter
對象,并執(zhí)行start()
方法完成后續(xù)的啟動:
boolean start() {
// 1.讀取agentId和applicationName
final AgentIds agentIds = resolveAgentIds();
final String agentId = agentIds.getAgentId();
final String applicationName = agentIds.getApplicationName();
final boolean isContainer = new ContainerResolver().isContainer();
try {
// 2.解析并加載配置
final Properties properties = loadProperties();
ProfilerConfig profilerConfig = new DefaultProfilerConfig(properties);
// 3.設(shè)置日志路徑和版本信息到systemProperty
saveLogFilePath(agentDirectory);
savePinpointVersion();
// 4.創(chuàng)建AgentClassLoader
URL[] urls = resolveLib(agentDirectory);
final ClassLoader agentClassLoader = createClassLoader("pinpoint.agent", urls, parentClassLoader);
if (moduleBootLoader != null) {
moduleBootLoader.defineAgentModule(agentClassLoader, urls);
}
final String bootClass = getBootClass();
AgentBootLoader agentBootLoader = new AgentBootLoader(bootClass, agentClassLoader);
final List<String> pluginJars = agentDirectory.getPlugins();
// 5.構(gòu)建AgentOption,并作為參數(shù)通過反射機(jī)制構(gòu)建Agent(DefaultAgent)
AgentOption option = createAgentOption(agentId, applicationName, isContainer, profilerConfig, instrumentation, pluginJars, agentDirectory);
Agent pinpointAgent = agentBootLoader.boot(option);
// 6.啟動死鎖監(jiān)控線程、agent數(shù)據(jù)上報線程、注冊ShutdownHook
pinpointAgent.start();
pinpointAgent.registerStopHandler();
} catch (Exception e) {
return false;
}
return true;
}
初始化上下文
上面過程其實還是加載配置并構(gòu)建一些對象,這里面最核心的邏輯是構(gòu)建Agent
對象,執(zhí)行了DefaultAgent
類的構(gòu)造器,初始化了上下文:
new DefaultAgent()
┆┈ DefaultAgent.newApplicationContext()
┆┈┈┈ new DefaultApplicationContext()
這里我們直接看DefaultApplicationContext
類的構(gòu)造器中的關(guān)鍵邏輯:
public DefaultApplicationContext(AgentOption agentOption, ModuleFactory moduleFactory) {
// 1.獲取Instrumentation對象
final Instrumentation instrumentation = agentOption.getInstrumentation();
// 2.構(gòu)建Guice ioc容器,用于依賴注入
final Module applicationContextModule = moduleFactory.newModule(agentOption);
this.injector = Guice.createInjector(Stage.PRODUCTION, applicationContextModule);
// 3.通過Guice注入一系列對象
this.profilerConfig = injector.getInstance(ProfilerConfig.class);
this.interceptorRegistryBinder = injector.getInstance(InterceptorRegistryBinder.class);
this.instrumentEngine = injector.getInstance(InstrumentEngine.class);
this.classFileTransformer = injector.getInstance(ClassFileTransformer.class);
this.dynamicTransformTrigger = injector.getInstance(DynamicTransformTrigger.class);
// 4.通過instrumentation對象注冊類轉(zhuǎn)換器
instrumentation.addTransformer(classFileTransformer, true);
...
}
綁定TransformCallback
Guice是谷歌開源的一個輕量級的依賴注入框架,pinpoint依靠Guice管理各種對象。
在初始化ioc容器的過程中,會遍歷plugin目錄下的所有插件對其進(jìn)行初始化,調(diào)用過程如下:
ApplicationServerTypeProvider.get()
|— PluginContextLoadResultProvider.get()
|—— new DefaultPluginContextLoadResult()
|——— DefaultProfilerPluginContextLoader.load()
|———— DefaultProfilerPluginContextLoader.setupPlugin()
|————— DefaultPluginSetup.setupPlugin()
|—————— XxxPlugin.setup()
(具體Plugin實現(xiàn))
以DubboPlugin
為例,在setup()
方法中主要對dubbo中的核心類進(jìn)行轉(zhuǎn)換器綁定:
@Override
public void setup(ProfilerPluginSetupContext context) {
DubboConfiguration config = new DubboConfiguration(context.getConfig());
...
this.addTransformers();
}
private void addTransformers() {
// 為dubbo核心rpc調(diào)用類綁定Transform關(guān)系
transformTemplate.transform("com.alibaba.dubbo.rpc.protocol.AbstractInvoker", AbstractInvokerTransform.class);
transformTemplate.transform("com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker", AbstractProxyInvokerTransform.class);
}
再來看看其中一個Transform類都做了些什么:
public static class AbstractInvokerTransform implements TransformCallback {
@Override
public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
// 指定目標(biāo)類(上一步綁定的類)
final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
// 指定目標(biāo)方法(方法名、參數(shù))
InstrumentMethod invokeMethod = target.getDeclaredMethod("invoke", "com.alibaba.dubbo.rpc.Invocation");
if (invokeMethod != null) {
// 為此方法添加攔截器
invokeMethod.addInterceptor(DubboConsumerInterceptor.class);
}
return target.toBytecode();
}
}
可以看到,這個類實現(xiàn)了TransformCallback
接口,這個接口從名字上可以看出是一個回調(diào)接口,而在其doInTransform()
方法中,是通過字節(jié)碼增強(qiáng)的方式,為com.alibaba.dubbo.rpc.protocol.AbstractInvoker
類的invoke()
方法添加了一個攔截器DubboConsumerInterceptor
。
DubboConsumerInterceptor
實現(xiàn)了AroundInterceptor
接口的before()
和after()
方法,這里可以看出和Spring AOP很相似了,而在攔截器中,主要是對dubbo的RPC調(diào)用進(jìn)行trace、span等鏈路追蹤信息的記錄。
動態(tài)類加載
在上下文初始化時,Pinpoint向instrumentation
注冊了一個Transformer
,該接口只定義個一個方法transform()
,該方法會在加載新class類或者重新加載class類時調(diào)用,其調(diào)用路徑如下:
DefaultClassFileTransformerDispatcher.transform()
|— BaseClassFileTransformer.transform()
|—— MatchableClassFileTransformerDelegate.transform()
|——— TransformCallback.doInTransform()
可以看到,最后執(zhí)行的就是我們在上面執(zhí)行XxxPlugin.setup()
方法時配置的回調(diào)接口,即對指定的方法進(jìn)行字節(jié)碼增強(qiáng)。而Java應(yīng)用啟動后,加載的就是我們增強(qiáng)后的類,從而實現(xiàn)鏈路監(jiān)控或其他的功能。
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!