作者:未完成交响曲,资深Java工程师!目前在某一线互联网公司任职,架构师社区合伙人!

本文源码基于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.启动bootStrapif (!bootStrap.start()) {logPinpointAgentLoadFail();}
}

可以看到premain()方法有两个参数,最重要的是这个instrumentation对象

Instrumentation是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader的classpath下加入jar文件等,

PinpointBootStrap.premain()方法中,主要完成了相关jar包的查找和加载,然后将一系列配置以及instrumentation对象构造成PinpointStarter对象,并执行start()方法完成后续的启动:

boolean start() {// 1.读取agentId和applicationNamefinal 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.设置日志路径和版本信息到systemPropertysaveLogFilePath(agentDirectory);savePinpointVersion();// 4.创建AgentClassLoaderURL[] 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数据上报线程、注册ShutdownHookpinpointAgent.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 {@Overridepublic 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应用启动后,加载的就是我们增强后的类,从而实现链路监控或其他的功能。

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢

分布式系统性能监控工具,初探Pinpoint Agent启动源码相关推荐

  1. vscode中安装webpack_leaflet-webpack 入门开发系列一初探篇(附源码下载)

    前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载 webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址w ...

  2. XXL-Job启动源码详解

    目录 1.xxl目录结构 1.1 xxl-job-admin 1.2 xxl-job-core 1.3 xxl-job-executor-samples 2.架构设计 2.1  整体架构设计 2.2 ...

  3. Myth源码解析系列之五- 服务启动源码解析

    通过前面几篇文章,我们搭建了环境,也进行了分布式事务服务的体验,相信大家对myth也有了一个大体直观的了解,接下来我们将正式步入源码解析之旅~~ order服务启动源码解析(myth-demo-spr ...

  4. spring-boot启动源码学习-1

    2019独角兽企业重金招聘Python工程师标准>>> spring-boot启动源码分析-启动初始化 主要对spring-boot的启动流程中的启动初始化进行学习,学习spring ...

  5. 【Linux 内核 内存管理】Linux 内核内存布局 ④ ( ARM64 架构体系内存分布 | 内核启动源码 start_kernel | 内存初始化 mm_init | mem_init )

    文章目录 一.ARM64 架构体系内存分布 二.Linux 内核启动源码 start_kernel 三.内存初始化源码 mm_init 四.内存初始化源码 mem_init 一.ARM64 架构体系内 ...

  6. 【Android 启动过程】Activity 启动源码分析 ( ActivityThread 流程分析 二 )

    文章目录 前言 一.ActivityManagerService.attachApplicationLocked 二.ActivityStackSupervisor.attachApplication ...

  7. 【Android 启动过程】Activity 启动源码分析 ( ActivityThread -> Activity、主线程阶段 二 )

    文章目录 前言 一.ActivityThread 类 handleLaunchActivity -> performLaunchActivity 方法 二.Instrumentation.new ...

  8. 【Android 启动过程】Activity 启动源码分析 ( ActivityThread -> Activity、主线程阶段 一 )

    文章目录 前言 一.ClientTransactionHandler.scheduleTransaction 二.ActivityThread.H 处理 EXECUTE_TRANSACTION 消息 ...

  9. 【Android 启动过程】Activity 启动源码分析 ( AMS -> ActivityThread、AMS 线程阶段 二 )

    文章目录 前言 一.热启动与冷启动选择 二.AMS 进程中执行的相关操作 三.通过 Binder 机制转到 ActivityThread 中执行的操作 总结 前言 上一篇博客 [Android 启动过 ...

最新文章

  1. stm32使用flymcu烧写程序
  2. mysql 游标的使用
  3. aspx ttf文件加载不出来_加载页面信息,刷不出来心态都崩了
  4. 省选考试防爆0注意事项(PART1考试习惯)
  5. 关于TensorFlow开发者证书,你想要的资源都在这里!
  6. 工作几天被裁员,难受
  7. dataloader 源码_[莫烦 PyTorch 系列教程] 3.5 – 数据读取 (Data Loader)
  8. [LeetCode]Balanced Binary Tree
  9. redis学习总结2
  10. LAb3-自行车码表
  11. 金蝶K3案例实验实际成本后台配置
  12. 社会信用编码的验证(18位)
  13. streaming mr
  14. 4年小Java的心路历程,工作感悟
  15. jenkins shell 权限_Jenkins在shell脚本运行docker权限报错解决
  16. mysql 截取身份证出生日期
  17. RabbitMQ-2-工作模式及参数配置
  18. ubuntu ufw firewall防火墙端口设置 (防火墙的的开启、禁用、开放端口、关闭端口、重置、重启...)
  19. 动态输入数据并生成表格,带删除操作javascript
  20. linux打开icmp端口,linux – ICMP – 目的地不可达(端口不可达)

热门文章

  1. mysql导出csv数据_mysql 导出 csv数据命令
  2. jq 数组不重复_一道简单的数组遍历题,加上四个条件后感觉无从下手
  3. 洛谷 P4463 [集训队互测 2012] calc(拉格朗日插值优化DP)
  4. I-Matrix Power Series POJ - 3233 矩阵快速幂+分治
  5. D - Triangle Partition HDU - 6300 sort(cmp)
  6. 服务器500_BTA齐聚新基建——2030年百度智能云服务器数量超500万台
  7. python的前端怎么实现_Bootstrap、Python、Flask 做简单的前端
  8. 基于wincc的虚拟电梯设计_基于WINCC的模拟电梯设计
  9. oracle convertobject,oracle.sql進行轉換。TIMESTAMPLTZ@71d9d55b Java時間戳
  10. java线程间通信管道_通过管道进行线程间通信