初探Pinpoint Agent 啓動源碼

本文源碼基於Pinpoint 2.0.3-SNAPSHOT版本
官方開源地址:https://github.com/naver/pinpoint

Pinpoint Agent

Pinpoint通過字節碼增強技術來實現無侵入式的調用鏈採集。其核心實現是基於JVM的Java Agent機制。

我們使用Pinpoint時,需要在Java應用啓動參數上加上-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar參數,這樣,當我們的Java應用啓動時,會同時啓動Agent。

Pinpoint Agent在啓動的時候,會加載plugin文件夾下所有的插件,這些插件會對特定class類修改字節碼,在一些指定的方法調用前後加上鍊路採集邏輯(比如Dubbo中AbstractProxyInvokerinvoke()方法),這樣就實現了調用鏈監控功能。

Pinpoint官方文檔中的原理描述:
在這裏插入圖片描述
在pintpoin-bootstrap模塊中,我們可以在pom文件中看到maven插件裏有MANIFEST相關的配置,指定了Premain-Class,這個配置在打包時也會生成到MANIFEST.MF文件中:
在這裏插入圖片描述

執行premain()方法

通過上述配置可知,當我們啓動接入了pinpoint的Java應用時,會先執行PinpointBootStrap.premain()方法:

去掉了日誌等非核心邏輯代碼

public static void premain(String agentArgs, Instrumentation instrumentation) {
    // 1.設置啓動狀態,避免重複初始化
    final boolean success = STATE.start();
    if (!success) {
        return;
    }

    // 2.初始化和解析啓動參數
    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()方法有兩個參數,最重要的是這個instrumentation對象

Instrumentation是Java提供的一個來自JVM的接口,該接口提供了一系列查看和操作Java類定義的方法,例如修改類的字節碼、向classLoader的classpath下加入jar文件等,

PinpointBootStrap.premain()方法中,主要完成了相關jar包的查找和加載,然後將一系列配置以及instrumentation對象構造成PinpointStarter對象,並執行start()方法完成後續的啓動:

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.設置日誌路徑和版本信息到systemProperty
        saveLogFilePath(agentDirectory);
        savePinpointVersion();

        // 4.創建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.構建AgentOption,並作爲參數通過反射機制構建Agent(DefaultAgent)
        AgentOption option = createAgentOption(agentId, applicationName, isContainer, profilerConfig, instrumentation, pluginJars, agentDirectory);
        Agent pinpointAgent = agentBootLoader.boot(option);

        // 6.啓動死鎖監控線程、agent數據上報線程、註冊ShutdownHook
        pinpointAgent.start();
        pinpointAgent.registerStopHandler();

    } catch (Exception e) {
        return false;
    }
    return true;
}

初始化上下文

上面過程其實還是加載配置並構建一些對象,這裏面最核心的邏輯是構建Agent對象,執行了DefaultAgent類的構造器,初始化了上下文:

new DefaultAgent()

┆┈ DefaultAgent.newApplicationContext()

┆┈┈┈ new DefaultApplicationContext()

這裏我們直接看DefaultApplicationContext類的構造器中的關鍵邏輯:

public DefaultApplicationContext(AgentOption agentOption, ModuleFactory moduleFactory) {
    // 1.獲取Instrumentation對象
    final Instrumentation instrumentation = agentOption.getInstrumentation();

    // 2.構建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對象註冊類轉換器
    instrumentation.addTransformer(classFileTransformer, true);

    ...
}

綁定TransformCallback

Guice是谷歌開源的一個輕量級的依賴注入框架,pinpoint依靠Guice管理各種對象。

在初始化ioc容器的過程中,會遍歷plugin目錄下的所有插件對其進行初始化,調用過程如下:

ApplicationServerTypeProvider.get()

|— PluginContextLoadResultProvider.get()

|—— new DefaultPluginContextLoadResult()

|——— DefaultProfilerPluginContextLoader.load()

|———— DefaultProfilerPluginContextLoader.setupPlugin()

|————— DefaultPluginSetup.setupPlugin()

|—————— XxxPlugin.setup()(具體Plugin實現)

DubboPlugin爲例,在setup()方法中主要對dubbo中的核心類進行轉換器綁定:

@Override
public void setup(ProfilerPluginSetupContext context) {
    DubboConfiguration config = new DubboConfiguration(context.getConfig());
    ...
    this.addTransformers();
}

private void addTransformers() {
    // 爲dubbo核心rpc調用類綁定Transform關係
    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 {
        // 指定目標類(上一步綁定的類)
        final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
        // 指定目標方法(方法名、參數)
        InstrumentMethod invokeMethod = target.getDeclaredMethod("invoke", "com.alibaba.dubbo.rpc.Invocation");
        if (invokeMethod != null) {
            // 爲此方法添加攔截器
            invokeMethod.addInterceptor(DubboConsumerInterceptor.class);
        }
        return target.toBytecode();
    }
}

可以看到,這個類實現了TransformCallback接口,這個接口從名字上可以看出是一個回調接口,而在其doInTransform()方法中,是通過字節碼增強的方式,爲com.alibaba.dubbo.rpc.protocol.AbstractInvoker類的invoke()方法添加了一個攔截器DubboConsumerInterceptor

DubboConsumerInterceptor實現了AroundInterceptor接口的before()after()方法,這裏可以看出和Spring AOP很相似了,而在攔截器中,主要是對dubbo的RPC調用進行trace、span等鏈路追蹤信息的記錄。

動態類加載

在上下文初始化時,Pinpoint向instrumentation註冊了一個Transformer類轉換器,該接口只定義了一個方法transform(),該方法會在加載新class類或者重新加載class類時調用,其調用路徑如下:

DefaultClassFileTransformerDispatcher.transform()

|— BaseClassFileTransformer.transform()

|—— MatchableClassFileTransformerDelegate.transform()

|——— TransformCallback.doInTransform()

可以看到,最後執行的就是我們在上面執行XxxPlugin.setup()方法時配置的回調接口,即對指定的方法進行字節碼增強。而Java應用啓動後,加載的就是我們增強後的類,從而實現鏈路監控或其他的功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章