运行 Pinpoint 系统最简单的办法是使用 Docker。java
# 运行 Pinpoint
$ git clone https://github.com/dawidmalina/docker-pinpoint
$ cd docker-pinpoint
$ docker-compose up -d
复制代码
编译 Pinpoint 1.5.2 的源代码须要 JDK 六、JDK 7+ 以及 Maven 3.2.x+ 的支持,符合以上要求的最新版本的编译工具列表以下:git
要求 | 最新版本 | 备注 |
---|---|---|
JDK 6 | JDK 6u45 | 已经中止更新 |
JDK 7+ | JDK 8u112 | |
Maven 3.2.x+ | Maven 3.2.9 | 之因此使用 Maven 3.2.x,猜测是由于只有 Maven 3.2.x 才支持 JDK 6 |
而且还要设置两个环境变量:github
# 编译 Pinpoint
$ git clone https://github.com/naver/pinpoint
$ cd pinpoint
$ mvn install -Dmaven.test.skip=true
复制代码
使用 Docker 来运行编译会更加容易,这免去了环境配置的须要。首先将 Pinpoint 的源代码下载到本地目录,例如 /projects/pinpoint
,而后运行命令:docker
# 使用 Docker 编译 Pinpoint
$ docker run -v /projects/pinpoint:/pinpoint:rw -v </path/to/.m2>:/root/.m2:rw tangrui/pinpoint-development
复制代码
其中的两个 -v 参数是用来映射目录的。第一个 -v 参数是将本地的 /projects/pinpoint
目录映射到容器的 /pinpoint
目录;而第二个 -v 参数是将本地的 Maven 存储库映射到容器的 /root/.m2
目录,这样作的目的是让本地存储库与 Docker 中的存储库共享内容,避免每次编译的时候都要从网络上下载大量依赖包,提高运行效率。数据库
Pinpoint 主要由 3 个组件外加 Hbase 数据库构成,三个组件分别为:Agent、Collector 和 Web UI。apache
不管 Pinpoint 仍是 Zipkin,都是基于 Google Dapper 的论文实现的。其核心思想就是在服务各节点彼此调用的时候,记录并传递一个应用级别的标记,这个标记能够用来关联各个服务节点之间的关系。好比两个节点之间使用 HTTP 做为请求协议的话,那么这些标记就会被加入到 HTTP 头中。所以如何传递这些标记是与节点之间使用的通信协议有关的,有些协议就很容易加入这样的内容,但有些协议就相对困难甚至不可能,所以这一点就直接决定了实现分布式追踪系统的难度。bootstrap
Pinpoint 消息的数据结构主要包含三种类型 Span,Trace 和 TraceId。数组
Span 是最基本的调用追踪单元,当远程调用到达的时候,Span 指代处理该调用的做业,而且携带追踪数据。为了实现代码级别的可见性,Span 下面还包含一层 SpanEvent 的数据结构。每一个 Span 都包含一个 SpanId。tomcat
Trace 是一组相互关联的 Span 集合,同一个 Trace 下的 Span 共享一个 TransactionId,并且会按照 SpanId 和 ParentSpanId 排列成一棵有层级关系的树形结构。bash
TraceId 是 TransactionId、SpanId 和 ParentSpanId 的组合。TransactionId (TxId) 是一个交易下的横跨整个分布式系统收发消息的 ID,其必须在整个服务器组中是全局惟一的。也就是说 TransactionId 识别了整个调用链;SpanId (SpanId) 是处理远程调用做业的 ID,当一个调用到达一个节点的时候随即产生;ParentSpanId (pSpanId) 顾名思义,就是产生当前 Span 的调用方 Span 的 ID。若是一个节点是交易的最初发起方,其 ParentSpanId 是 -1,以标志其是整个交易的根 Span。下图可以比较直观的说明这些 ID 结构之间的关系。
Pinpoint 的优点在于其使用 Java Agent 向节点应用注入字节码,而不是直接修改源代码。所以部署一个节点就变得很是容易,只须要在程序启动的时候加入以下一些启动参数:
# 使用 Java Agent
-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
-Dpinpoint.agentId=<Agent's UniqueId> -Dpinpoint.applicationName=<The name indicating a same service (AgentId collection)> 复制代码
Pinpoint 对代码注入的封装很是相似 AOP,当一个类被加载的时候会经过 Interceptor 向指定的方法先后注入 before 和 after 逻辑,在这些逻辑中能够获取系统运行的状态,并经过 TraceContext 建立 Trace 消息,并发送给 Pinpoint 服务器。但与 AOP 不一样的是,Pinpoint 在封装接口的时候考虑到了更多与目标代码的交互能力,所以用 Pinpoint 提供的 API 来编写注入逻辑会比 AOP 看起来更加容易和专业。(这些内容后面会有更详细说明)
下图展示了两个 Tomcat 服务器应用了 Pinpoint 以后,所收集到的追踪数据。
开发 Pinpoint Agent 插件只须要关注两个接口:TraceMetadataProvider 和 ProfilerPlugin,实现类经过 Java 的服务发现机制进行加载。
Pinpoint 的插件是以 jar 包的形式部署的,为了使得 Pinpoint Agent 可以定位到 TraceMetadataProvider 和 ProfilerPlugin 两个接口的实现,须要在 META-INF/services
目录下建立两个文件:
META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider
META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin
复制代码
这两个文件中的每一行都写明对应实现类的全名称便可。
TraceMetadataProvider 提供了对 ServiceType 和 AnnotationKey 的管理。
每一个 Span 和 SpanEvent 都包含一个 ServiceType,用来标明他们属于哪个库 (例如 Jetty、MySQL JDBC Client 或者 Apache HTTP Client 等),以及追踪此类型服务的 Span 和 SpanEvent 该如何被处理。ServiceType 的数据结构以下:
属性 | 描述 |
---|---|
name | ServiceType 的名称,必须惟一 |
code | ServiceType 的编码,短整形,必须惟一 |
desc | 描述 |
properties | 附加属性 |
Pinpoint 为了尽可能压缩 Agent 到 Collector 数据包的大小,ServiceType 被设计成不是以序列化字符串的形式发送的,而是以整形数字发送的 (code 字段),这就须要创建一个映射关系,将 code 转换成对应的 ServiceType 实例,这个映射机制就是由 TraceMetadataProvider 负责的。
ServiceType 的 code 必须全局惟一,为了不冲突,Pinpoint 官方对这个映射表进行了严格的管理,若是所开发的插件想要声明新的映射关系,须要通知 Pinpoint 团队,以便对此映射表进行更新和发布。与私有 IP 地址段同样,Pinpoint 团队也保留了一段私有区域可供开发内部服务的时候使用。具体的 ID 范围参照表以下:
ServiceType Code 所有范围
类型 | 范围 |
---|---|
Internal Use | 0 ~ 999 |
Server | 1000 ~ 1999 |
DB Client | 2000 ~ 2999 |
Cache Client | 8000 ~ 8999 |
RPC Client | 9000 ~ 9999 |
Others | 5000 ~ 7999 |
ServiceType Code 私有区域范围
类型 | 范围 |
---|---|
Server | 1900 ~ 1999 |
DB Client | 2900 ~ 2999 |
Cache Client | 8900 ~ 8999 |
RPC Client | 9900 ~ 9999 |
Others | 7500 ~ 7999 |
Annotation 是包含在 Span 和 SpanEvent 中的更详尽的数据,以键值对的形式存在,键就是 AnnotationKey,值能够是字符串或字节数组。Pinpoint 内置了不少的 AnnotationKey,若是不够用的话也能够经过 TraceMetadataProvider 来自定义。AnnotationKey 的数据结构以下:
属性 | 描述 |
---|---|
name | AnnotationKey 的名称 |
code | AnnotationKey 的编码,整形,必须惟一 |
properties | 附加属性 |
同 ServiceType 的 code 字段同样,AnnotationKey 的 code 字段也是全局惟一的,Pinpoint 团队给出的私有区域范围是 900 到 999。
TraceMetadataProvider 接口只有一个 setup 方法,此方法接收一个 TraceMetadataSetupContext 类型的参数,该类型有三个方法:
方法 | 描述 |
---|---|
addServiceType(ServiceType) | 注册 ServiceType |
addServiceType(ServiceType, AnnotationKeyMatcher) | 注册 ServiceType,并将匹配 AnnotationKeyMatcher 的 AnnotationKey 做为此 ServiceType 的典型注解,这些典型注解会显示在瀑布视图的 Argument 列中 |
addAnnotationKey(AnnotationKey) | 注册 AnnotationKey,这里注册的 AnnotationKey 会被标记为 VIEW_IN_RECORD_SET,显示在瀑布视图中是以单独一行显示的,且前面有一个蓝色的 i 图标 |
详细使用方法能够参考官方提供的样例文件 SampleTraceMetadataProvider。
ProfilerPlugin 经过字节码注入的方式拦截目标代码以实现跟踪数据的收集。
plugin
目录下的插件Pinpoint 插件的工做原理看似跟 AOP 很是类似,但仍是有一些区别和自身特点的:
经过上述内容能够了解,若是要编写一个 Pinpoint 的插件,除了要对目标代码的调用逻辑有较深刻的理解,还必须得设计好上下文数据如何存储、如何传递,以及如何经过 Scope 避免信息被重复收集等问题。这些问题在 AOP 的场景下也会存在,只是 Pinpoint 提供了更加一致和便捷的解决方案,而基于 AOP 的实现就要本身去考虑这些问题了。
如前文所述,Pinpoint 插件须要实现 ProfilerPlugin 接口,该接口只有一个 setup(ProfilerPluginSetupContext) 方法。为了更容易的操做 Pinpoint 的代码注入 API,还须要实现一个 TransformTemplateAware 的接口,该接口会注入 TransformTemplate 类。
public class SamplePlugin implements ProfilerPlugin, TransformTemplateAware {
private TransformTemplate transformTemplate;
@Override
public void setup(ProfilerPluginSetupContext context) {
}
@Override
public void setTransformTemplate(TransformTemplate transformTemplate) {
this.transformTemplate = transformTemplate;
}
}
复制代码
ProfilerPluginSetupContext 有两个方法:getConfig() 和 addApplicationTypeDetector(ApplicationTypeDetector…)。第一个方法用来获取 ProfilerConfig 对象,该对象保存了全部插件的配置信息,而第二个方法用来添加 ApplicationTypeDetector。ApplicationTypeDetector 是用来自动检测节点所运行服务的类型的。例如在 pinpoint-tomcat-plugin 项目中,有 TomcatDetector 类,这个类的做用是经过以下逻辑检测来肯定当前服务是否为 Tomcat 的:
若是以上三个条件都知足,就把当前节点的 ServiceType 设置为 Tomcat。
TransformTemplate 只有一个方法 transform(String, TransformCallback),第一个参数是须要被转换的类的全名称,而第二个参数就是 4.3.1 章节中提到的 TransformCallback 接口,这个接口也只有一个方法叫 doInTransform,全部的注入逻辑都在这里完成。
public byte[] doInTransform(Instrumentor instrumentor,
ClassLoader classLoader,
String className,
Class<?>; classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws InstrumentException {
// 1. Get InstrumentClass of the target class
InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
// 2. Get InstrumentMethod of the target method.
InstrumentMethod targetMethod = target.getDeclaredMethod("targetMethod", "java.lang.String");
// 3. Add interceptor. The first argument is FQN of the interceptor class,
// followed by arguments for the interceptor's constructor. targetMethod.addInterceptor("com.navercorp.pinpoint.bootstrap.interceptor.BasicMethodInterceptor", va(SamplePluginConstants.MY_SERVICE_TYPE)); // 4. Return resulting byte code. return target.toBytecode(); } 复制代码
BasicMethodInterceptor 类仅提供了对方法调用信息的简单收集,只收集方法的名称、参数、返回值以及是否产生异常等等。在某些复杂的场景下,咱们会须要收集更多的信息,如当前登陆用户、线程池、数据库查询语句以及任何跟中间件功能有关的信息,这就须要咱们定义本身的 Interceptor 类。
以上内容请参考该样例。
Interceptor 是一个标记接口,真正有意义的是 AroundInterceptor 接口,该接口定义了以下两个方法:
public interface AroundInterceptor extends Interceptor {
void before(Object target, Object[] args);
void after(Object target, Object[] args, Object result, Throwable throwable);
}
复制代码
为了应对被拦截方法的不一样个数的参数列表,AroundInterceptor 还有若干子接口:AroundInterceptor0, AroundInterceptor1,…,AroundInterceptor5,分别对应没有参数,一个参数,到 5 个参数的方法。实现 Interceptor 接口的时候要提供一个以下的构造方法:
public RecordArgsAndReturnValueInterceptor(TraceContext traceContext,
MethodDescriptor descriptor) {
this.traceContext = traceContext;
this.descriptor = descriptor;
}
复制代码
TraceContext 和 MethodDescriptor 会被 Pinpoint Agent 在运行时注入进来,固然也能够添加额外的参数,这些额外的参数,须要在 addInterceptor 的时候指定,就像上文中关于 va 的描述那样。
有了 TraceContext 对象,就能够开始收集信息了。调用 traceContext.getCurrentTraceObject() 方法能够获取当前的 Trace,再调用 trace.traceBlockBegin() 开始记录一个新的 Trace 块 (这里我理解应该就是 Span 了)。在 traceBlockBegin 之后,能够调用 currentSpanEventRecorder 方法获取 SpanEventRecorder 对象,这个对象提供了诸如 recordServiceType、recordApi、recordException 和 recordAttribute 等方法,能够分别记录方法的有关信息。可是 SpanEventRecorder 并无提供 recordReturnValue 这样的方法,只能经过 recordAttribute 来记录。全部本身扩展的信息也是经过 recordAttribute 来记录的。最后全部信息记录完成就调用 traceBlockEnd() 方法关闭区块。
以上内容请参考该样例。
其实 Pinpint 的插件开发 API 还提供了很是丰富的能力,如拦截异步方法、调用链跟踪、拦截器之间共享数据等等,但原理都是基于上述这些内容,只是调用了更加复杂的 API 而已。具体代码能够参考官方提供的样例项目,里面有很是详尽的代码及注释,相信理解了上面这些内容,再看这个代码就不会有任何困难了。