英文原文:https://naver.github.io/pinpoint/1.8.4/techdetail.html
说明:【】中内容为方便解释自己加的

在这篇文章中,我们描述了Pinpoint的技术细节比如请求追踪(transaction tracing)和字节码插装(bytecode instrumentation),同时说明了应用于Pinpoint agent中的优化方法,它可以修改字节码并记录性能数据。

分布式事务追踪,基于Google的Dapper

基于Google的Dapper,Pinpoint可以追踪到一个事务(transaction)中的分布式请求【这里说白了即指Pinpoint能够追踪到从应用A到应用B的一次分布式请求中的所有数据】

Google Dapper 中分布式事务追踪是如何工作的

分布式追踪系统的目的就是对于分布式系统中的两个节点Node1和Node2,当有一条消息从Node1发送到Node2时,分布式追踪系统能够识别Node1和Node2之间的关系(如图1所示)
图1. 分布式系统中的消息关系
图1. 分布式系统中的消息关系

问题在于没有方法能够识别消息之间的关系,比如我们无法辨别从Node1发送的N条消息和Node2接收到的N条消息之间的关系,换句话说,当从Node1发送第X条消息时,你无法在Node2收到的N条消息里分辨出Node1发送的第X条消息。有系统尝试在操作系统或者TCP级别追踪这些消息,然而实现复杂度高,同时性能低,因为需要针对每个协议单独实现。此外,很难准确地跟踪消息。

Google的Dapper提供了一种解决上述问题的简单方法。方案便是当发送消息的时候,在应用级别给这些消息加上标签,从而能够使这些消息关联起来。例如,对于Http请求,它在Http请求头中加入标签信息,并使用这个标签信息追踪消息。

可以查看论文了解Dapper :https://ai.google/research/pubs/pub36356

Pinpoint 基于Google的Dapper中的追踪技术,同时进行了修改,在远程调用的时候,pinpoint会在调用方加入应用级别的标签数据来追踪分布式事务(请求),标签数据由一系列的key组成,在pinpoint中定义为 TraceId

Pinpoint中的数据结构

在pinpoint中,核心的数据结构包括:Spans,Traces 以及 TraceIds

  • Span: RPC(远程过程调用)追踪的基本单元,它代表了远程调用到达时处理的工作并且包含了追踪数据【直白点说,你可以把一个应用当作一个Span】,为了确保代码级别的可见性,Span有子节点,用SpanEvent【这个就相当于一次方法调用,故一个Span可能大部分都会有多个SpanEvent】表示,每个Span都包含一个TraceId,每个Span有一个SpanIdParentSpanId,如果这个Span为请求的最原始的发起者,ParentSpanId为-1。
  • TraceSpan的集合,由关联的RPC调用组成【这个也就是说在分布式系统中,每个应用代表一个Span,不同应用的一次全链路的请求表示一个Trace,即一个Trace会包含多个Span】,在同一个链路中的Spans共享同一个TransactionIdTrace通过SpanIds和ParentSpanIds排序成分层树结构。
  • TraceId: 一系列key的集合,这些key包括:TransactionIdSpanIdParentSpanIdTransactionId代表消息的id,这个id在一次追踪过程中唯一,SpanIdParentSpanId代表远程调用的父子关系。
  • TransactionId(TxId): 一次事务(请求)中分布式系统发送/接收的消息id,在整个请求关联的所有应用服务中必须唯一
  • SpanId: 处理接收RPC消息的应用id,在RPC消息到达某个节点时生成
  • ParentSpanId(pSpanId): 发起RPC调用的父span的 SpandId,如果某个节点时整个事务请求的发起者,那么它没有父span,对于这种情况,我们使用 -1表示这是整个事务请求的根span

Google的Dapper和NAVER的Pinpoint的术语的不同处

Pinpoint中的术语 TransactionId和Google的Dapper中的术语TraceId意思相同,TraceId在Pinpoint中表示的是一系列key的集合【前面提到的TransactionId,SpanId等】

TraceId如何工作?

下图展示的是TraceId的行为,里面包括了三次RPC请求以及4个节点【这个就相当于一次请求,即一次transaction】
在这里插入图片描述
图2. TraceId行为样例

在图2中,TransactionId(TxId)表示的是三次不同的调用通过TransactionId(TxId)关联在一起作为一次事务请求(transaction),然而,TransactionId自身并不能明显的描述RPC之间的关系。为了识别RPC之间的关系,我们需要SpanId以及ParentSpanId(pSpanId)。假设节点是tomcat,你可以想象成 SpanId是处理http请求的线程,parentSpanId代表的是发起这次RPC请求的SpanId【这里假设应用A和B,A调B,A和B都是一个Node,同时A是SpanIdA,B是SpanIdB,因为是A调B,那么B的parentSpanId就是SpanIdA了】

Pinpoint通过TransactionId查找关联的多个Span,同时根据SpanId和ParentSpanId对他们进行层次关系排序。

SpanId和ParentSpanId都是64位的长整型,由于数字是随机产生的,因此可能会有冲突,但是考虑到64位长整型的范围是 -9223372036854775808到9223372036854775807,冲突发生的几率很小。如果真的产生冲突,Pinpoint和Google的Dapper一样,会让开发者知道发生的情况而不是解决冲突。

TransactionId由 agentId,JVM启动时间以及一个序列号组成

  • agentId: JVM启动时用户创建的id;在安装了Pinpoint的整个服务器组内必须唯一,最简单的方式是使用主机名hostname,因为通常主机名不会重复,如果你需要在服务器组运行多个JVM,可以在主机名前加下前缀避免重复
  • jvm启动时间:用于保证生成的唯一序列号(SequenceNumber)从0开始【这里没怎么懂】,这个值用来当用户不小心创建了相同的agentId时避免transactionId冲突
  • SequenceNumber: Pinpoint agent产生的id,从0开始递增,每条消息都会产生一个

Dapper和Zipkin(Twitter的一个分布式追踪平台),随机生成TraceIds(对应Pinpoint中的TransactionIds),认为id冲突是很常见场景。但是,在Pinpoint中,我们努力避免这种冲突,有两个选择:一是id中的数量量小但是冲突的可能性很高;另外一种是id的数据量大但是冲突的可能性低,Pinpoint选择的是后一种【也就是说pinpoint为了使TransactionId的冲突减少,TransactionId的数据会大点】

可能有更好的方法来解决这个问题,我们提出几个想法,比如通过一个中心key服务器来产生key【这里指的应该就是前面的TransactionId】,但是由于性能和网络问题没有实现。目前我们仍然在考虑批量产生key作为替代解决方案。因此在将来,可能会开发出这样的方法,但就目前而言,我们采用了一种简单的方法。在Pinpoint中,TransactionId是可变的数据。

字节码注入,无需修改代码

前面我们解释了分布式事务追踪。一种方法是让开发者修改自己的代码,当发生RPC调用的时候允许开发者增加标记信息,然后修改代码负担比较重。

Twitter的Zipkin使用修改后的类库以及容器(Finagle)来提供分布式追踪的功能,然后它同样需要开发者修改代码,我们想要的是不修改代码就能实现追踪功能,同时提供代码级别的可见性,Pinpoint使用了字节码注入技术(bytecode instrumentation),Pinpoint的Agent干预(拦截)调用RPC的代码以便自动处理标记信息。

克服字节码注入的缺点

如下所说,有两种方式实现分布式追踪。字节码注入是一种自动的方式

  • 手动方式:开发者使用Pinpoint提供的api在关键位置记录数据
  • 自动方式:开发者无需修改代码,Pinpoint决定那些代码需要干预和增强
    两种方式的优缺点如下
    方法优点缺点
    手动追踪需要较少的开发资源;API可以变得简单因此bug也少开发者需要修改代码;跟踪级别低【这里的跟踪级别高低怎么理解?】
    自动追踪开发者无需修改代码;可以收集到更精确的数据需要高素质的开发人员可以快速的识别哪些代码需要跟踪,以及在哪些跟踪点需要处理;由于使用了字节码注入等高级开发技巧,bug也会多

字节码注入技术会引入更高的难度和风险,但是有很多益处。

尽管字节码注入需要很多的开发资源,但是部署应用的时候几乎不需要(即开发难,部署运行简单)
【中间一段无关痛痒的分析字节码注入方式的好处就不翻译了】

字节码注入的价值

我们使用字节码注入(自动方法)来实现除了上面提到的还有下面一些理由

  • 隐藏API
    如果API是暴露给开发者使用,作为API的提供者,想根据我们的需要修改API时往往会受到限制,这种限制会给我们很多压力
    我们可能会修改API来纠正错误或者添加新功能,然而如果修改受到限制的话,我们很难完善API,解决此类问题的最佳答案是可扩展的系统设计,这并不是每个人都知道的简单选择。 创建完美的API设计几乎是不可能的,因为我们无法预测未来。
    通过字节码注入技术,我们不用担心暴露追踪API导致的这些问题,也可以持续完善设计而不用考虑依赖关系。想要使用Pinpoint开发应用的开发者需要注意API可能会修改,因为我们的第一优先级是提高性能和设计。
  • 容易开始或者关闭
    使用字节码注入的缺点是当分析的类库或者Pinpoint本身有问题时,你的应用程序会受到影响,但是你不许需要修改代码,只需要禁用Pinpoint即可解决这种问题

通过在JVM启动脚本中添加以下三行(与Pinpoint Agent的配置相关联),可以轻松地为应用程序启用Pinpoint:

-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
-Dpinpoint.agentId=<Agent's UniqueId>
-Dpinpoint.applicationName=<The name indicating a same service (AgentId collection)>
  • 1
  • 2
  • 3

如果因为Pinpoint导致什么问题,只需要删除启动参数里的配置

字节码注入是如何工作

由于字节码注入需要处理Java字节码,往往会增加开发风险同时降低开发效率,此外,开发人员容易犯错误。在Pinpoint中,我们通过抽象出拦截器提高开发效率和可访问性,Pinpoint在类加载时干预应用程序代码,注入必要的代码来跟踪分布式请求以及性能信息,由于跟踪代码直接注入应用程序代码,因此这提高了性能。
在这里插入图片描述
图3. 字节码注入的基本原理
在Pinpoint中,API拦截部分和数据记录部分是分开的。 拦截器被注入到我们想要跟踪的方法中,并调用before()和after()方法来处理数据记录。 通过字节码检测,Pinpoint Agent只从必要的方法记录数据,这使得分析数据的大小变得紧凑。

优化Pinpoint Agent的性能

下面我们描述下如何优化Pinpoint agent的性能

  • 使用二进制格式(Thrift)
    你可以使用二进制格式(Thrift)加快编码速度,尽管它很难使用和调试,但是可以提高网络利用率以及减小生成的数据大小
  • 使用可变长度编码和格式优化记录数据
    如果你把一个长整型转化成定长的字符串,数据大小是8字节,但是,如果你使用可变长度编码,跟你长整型数字的大小,转化后的数据长度可能是1到10字节。为了减少数据大小,Pinpoint通过Compact Protocol of Thrift将数据编码为可变长度字符串,并记录要针对编码格式进行优化的数据。 Pinpoint Agent通过将基于根方法的剩余时间转换为矢量值来减少数据大小。
    可以通过Google开发者的 Base 128 Varints来了解更多关于可变长度编码
    在这里插入图片描述
    图4.定长编码和可变长度编码的比较
    如图4,有三个不同的方法开始调用和完成调用,你需要在这6处记录时间,采用定长编码,需要48字节(6处*8字节)。Pinpoint agent使用可变长度编码,并根据其对应的格式记录数据,同时以根方法的起始时间为基准,计算其他方法处与基准的差异(矢量值)来计算时间,由于向量值是一个小数字,它消耗少量字节,因此只消耗13个字节而不是48字节
    如果执行方法需要更多时间,即使使用可变长度编码,也会增加字节数。 但是,它仍然比固定长度编码更有效。
  • 使用常量表替换重复的API信息,SQL语句以及字符串
    我们希望pinpoint能够实现代码级别的追踪,然而会造成数据变大的问题,每次高精度的数据发送到服务端,数据变大会增加带宽开销。
    为了解决这种问题,我们采用了一种策略,通过在远程的HBase服务中创建一张常量表,既然每次发送“方法A”的数据到Pinpoint的Collector都有负载,Pinpoint Agent把"方法A"的数据转化成一个ID并在HBase的常量表中存储方法A的信息,并且使用这个ID继续后面的追踪,当用户在网站上检索跟踪数据时,Pinpoint Web会在常量表中搜索相应ID的方法信息并重新组织它们。 使用同样的方法用于减少SQL或常用字符串中的数据大小
  • 处理批量请求的采样
    Naver提供的在线门户服务请求非常庞大。 单个服务每天处理超过200亿个请求。 跟踪此类请求的一种简单方法是根据需要扩展网络基础架构和服务器以满足请求的数量。 但是,这不是处理这种情况的经济有效的方法。
    在Pinpoint中,您可以只收集采样数据,而不是跟踪每个请求。 在请求很少的开发环境中,每个数据都会被收集。 在请求很大的生产环境中,只收集整个数据的1~5%,这足以分析整个应用程序的状态。 通过采样,您可以最大限度地减少应用程序中的网络开销,并降低网络和服务器等基础设施的花费。
    • Pinpoint中的采样方法:Pinpoint支持一个计数采样器(Counting Sampler),如果设置为10,那么只会收集十分之一的请求。我们计划添加新的采样器来更高效地收集数据
  • 使用异步数据传输最小化被终止的应用线程
    Pinpoint不会干挠应用程序线程,因为数据编码和远程消息传输是通过异步线程实现的
  • 通过UDP传输数据
    与Google的Dapper不同,Pinpoint通过网络传输数据以确保数据速度。 当数据流量突然爆发时,与你的服务共享网络可能是一个问题。 在这种情况下,Pinpoint Agent开始使用UDP协议这样你的服务便有网络连接优先级【这里也是说如果你的服务使用TCP,pinpoint 使用UDP的话,TCP优先级高于UDP?】
    • 注意: 数据传输的API可以被替换,因为他是独立的接口,你可以替换成不同的实现,比如通过本地文件

Pinpoint应用样例

下面是如何在你的应用程序中获取数据的例子,这样你能全面理解上面描述的内容。
图5展示当你的TomcatA和TomcatB安装了Pinpoint你可以看到什么,你可以看到单个节点作为单个请求事务的追踪数据,表示的是分布式事务追踪的流
在这里插入图片描述
图5 实践中的pinpoint样例
下面描述了Pinpoint在每个方法中做的事情

  1. 当请求到达TomcatA时,Pinpoint Agent产生了一个TraceId
    • TX_ID: TomcatA^ TIME^1
    • SpanId: 10
    • ParentSpanId: -1(根)
  2. 从Spring MVC controllers中记录数据
  3. 干预 HttpClient.execute()方法的调用,并在HttpGet中组装TraceId
    • 创建子 TraceId
    • TX_ID: TomcatA^ TIME^ 1 -> TomcatA^ TIME^1
    • SPAN_ID: 10->20
    • PARENT_SPAN_ID: -1->10(父SpanId) ——在HTTP头部中配置子TraceId
    • HttpGet.setHeader(PINPOINT_TX_ID,“TomcatA^ TIME^1”)
    • HttpGet.setHeader(PINPOINT_SPAN_ID,“20”)
    • HttpGet.setHeader(PINPOINT_PARENT_SPAN_ID,“10”)
  4. 将打了标记的请求传输到TomcatB
    • TomcatB检查传输过来的请求头部
    • HttpServletRequest.getHeader(PINPOINT_TX_ID)——TomcatB成了子节点,因为它识别到了header中的TraceId
    • TX_ID: TOMCATA^ TIME^1
    • SPAN_ID: 20
    • PARENT_SPAN_ID: 10
      注意到子节点的SPAN_ID其实是由父节点创建好的
  5. 从Spring MVC controllers中记录数据并完成请求

在这里插入图片描述

  1. 当TomcatB中的请求完成时Pinpoint agent将追踪数据发送到Pinpoint Collector存储到HBase
  2. 来自TomcatB的HTTP调用终止后,TomcatA的请求完成。 Pinpoint Agent将跟踪数据发送到Pinpoint Collector以将其存储在HBase中
  3. UI从HBase读取跟踪数据,并通过对树进行排序来创建调用堆栈

总结

Pinpoint是另一个与你的应用程序一起运行的应用程序。 使用字节码检测使Pinpoint看起来像是不需要修改代码。 通常,字节码检测技术使应用程序容易受到风险的影响; 如果Pinpoint出现问题,它也会影响你的应用程序。 但就目前而言,我们并没有摆脱这些威胁,而是专注于提高Pinpoint的性能和设计。 因为我们认为这使得Pinpoint更有价值。 因此,是否使用Pinpoint是由你决定的。

我们仍然需要做大量的工作来改进Pinpoint。 尽管Pinpoint不完整,但它作为一个开源项目被发布了; 我们不断努力开发和改进Pinpoint,以满足你的期望

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐