Pinpoint–基础–04–请求追踪和字节码插装
备注、背景
英文原文:https://naver.github.io/pinpoint/1.8.4/techdetail.html
Dapper原文 :https://ai.google/research/pubs/pub36356
说明:【】中内容为方便解释自己加的
1、分布式事务追踪,基于Google的Dapper
- 基于Google的Dapper,Pinpoint可以追踪到一个事务(transaction)中的分布式请求【这里说白了即指Pinpoint能够追踪到从应用A到应用B的一次分布式请求中的所有数据】
1.1、Google Dapper的 分布式事务追踪 是如何工作的
假设 分布式系统中的两个节点(Node1,Node2),当有一条消息从Node1发送到Node2时,分布式追踪系统能够识别Node1和Node2之间的关系
1.2、分布式系统中的消息关系
1.2.1、问题
问题在于没有方法能够识别消息之间的关系,比如我们无法辨别从Node1发送的N条消息和Node2接收到的N条消息之间的关系,换句话说,当从Node1发送第X条消息时,你无法在Node2收到的N条消息里分辨出Node1发送的第X条消息。
有系统尝试在操作系统或者TCP级别追踪这些消息,然而实现复杂度高,同时性能低,因为需要针对每个协议单独实现。此外,很难准确地跟踪消息。
1.2.2、解决
Google的Dapper提供了一种解决上述问题的简单方法。方法便是当发送消息的时候,在应用级别给这些消息加上标签,从而能够使这些消息关联起来。
例如,对于Http请求,它在Http请求头中加入标签信息,并使用这个标签信息追踪消息。
2、PinPoint
- Pinpoint 基于Google的Dapper中的追踪技术,同时进行了修改,在远程调用的时候,pinpoint会在调用方 加入应用级别的标签数据 来追踪分布式事务(请求)
- 标签数据由一系列的key组成,在pinpoint中定义为 TraceId
2.1、数据结构
2.1.1、Span
- RPC(远程过程调用)追踪的基本单元
- 代表了远程调用到达时处理的工作并且包含了追踪数据【直白点说,你可以把一个应用当作一个Span】,为了确保代码级别的可见性,Span有子节点,用SpanEvent【这个就相当于一次方法调用,故一个Span可能大部分都会有多个SpanEvent】表示,每个Span都包含一个TraceId,每个Span有一个SpanId和ParentSpanId,如果这个Span为请求的最原始的发起者,ParentSpanId为-1。
2.1.2、Trace
- Span的集合,由关联的RPC调用组成【这个也就是说在分布式系统中,每个应用代表一个Span,不同应用的一次全链路的请求表示一个Trace,即一个Trace会包含多个Span】
- 在同一个链路中的Spans共享同一个TransactionId
- Trace通过SpanIds和ParentSpanIds排序成分层树结构
2.1.3、TraceId
-
一系列key的集合,这些key包括
- TransactionId
- SpanId
- ParentSpanId
-
TransactionId
- 代表消息id,来自单个事务的分布式系统发送、接收的消息id
- 这个id在一次追踪过程中唯一
-
SpanId和ParentSpanId代表远程调用的父子关系
2.1.4、TransactionId(TxId)
一次事务(请求)中分布式系统发送/接收的消息id,在整个请求关联的所有应用服务中必须唯一
2.1.5、SpanId
处理接收RPC消息的应用id,在RPC消息到达某个节点时生成
2.1.6、ParentSpanId(pSpanId)
发起RPC调用的父span的 SpandId,如果某个节点是整个事务请求的发起者,那么它没有父span,对于这种情况,我们使用 -1表示这是整个事务请求的根span
2.2、TraceId如何工作?
2.2.1、图示
下图展示的是TraceId的行为,里面包括了三次RPC请求以及4个节点【这个就相当于一次请求,即一次transaction】
2.2.2、TraceId行为样例
在上图中,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对他们进行层次关系排序。
2.2.2.1、TransactionId
由agentId,JVM启动时间以及一个序列号组成
2.2.2.2、agentId
- JVM启动时用户创建的id
- 在安装了Pinpoint的整个服务器组内必须唯一,最简单的方式是使用主机名hostname,因为通常主机名不会重复,如果你需要在服务器组运行多个JVM,可以在主机名前加下前缀避免重复
2.2.2.3、jvm启动时间
- 用于保证生成的序列号是唯一的(SequenceNumber)
- 这个值用来当用户不小心创建了相同的agentId时避免transactionId冲突
2.2.2.4、SequenceNumber
Pinpoint agent产生的id,从0开始递增,每条消息都会产生一个
2.2.2.4、TransactionId 冲突
- Dapper和Zipkin(Twitter的一个分布式追踪平台),随机生成TraceIds(对应Pinpoint中的TransactionIds),认为id冲突是很常见场景。但是,在Pinpoint中,我们努力避免这种冲突,有以下两个选择
- 选择1:id的数据量小但是冲突的可能性很高
- 选择2:id的数据量大但是冲突的可能性低
- Pinpoint选择的是2【也就是说pinpoint为了使TransactionId的冲突减少,TransactionId的数据会大点】
可能有更好的方法来解决这个问题,我们提出几个想法,比如通过一个中心key服务器来产生key【这里指的应该就是前面的TransactionId】,但是由于性能和网络问题没有实现。目前我们仍然在考虑批量产生key作为替代解决方案。因此在将来,可能会开发出这样的方法,但就目前而言,我们采用了一种简单的方法。在Pinpoint中,TransactionId是可变的数据。
2.3、字节码注入,无需修改代码
前面我们解释了分布式事务追踪。一种方法是让开发者修改自己的代码,当发生RPC调用的时候允许开发者增加标记信息,然后修改代码负担比较重。
Twitter的Zipkin使用修改后的类库以及容器(Finagle)来提供分布式追踪的功能,然后它同样需要开发者修改代码,我们想要的是不修改代码就能实现追踪功能,同时提供代码级别的可见性,Pinpoint使用了字节码注入技术(bytecode instrumentation),Pinpoint的Agent干预(拦截)调用RPC的代码以便自动处理标记信息。
2.3.1、克服字节码注入的缺点
有两种方式实现分布式追踪。字节码注入是一种自动的方式
2.3.1.1、手动方式
开发者使用Pinpoint提供的api在关键位置记录数据
2.3.1.2、自动方式
开发者无需修改代码,Pinpoint决定那些代码需要干预和增强
2.3.2、两种方式的优缺点
尽管字节码注入需要很多的开发资源,但是部署应用的时候几乎不需要(即开发难,部署运行简单)
2.4、字节码注入的价值
我们使用字节码注入来实现,除了上面提到的,还有下面一些理由
2.4.1、隐藏API
如果API是暴露给开发者使用,作为API的提供者,想根据我们的需要修改API时往往会受到限制,这种限制会给我们很多压力
我们可能会修改API来纠正错误或者添加新功能,然而如果修改受到限制的话,我们很难完善API,解决此类问题的最佳答案是可扩展的系统设计,这并不是每个人都知道的简单选择。 创建完美的API设计几乎是不可能的,因为我们无法预测未来。
通过字节码注入技术,我们不用担心暴露追踪API导致的这些问题,也可以持续完善设计而不用考虑依赖关系。
想要使用Pinpoint开发应用的开发者需要注意API可能会修改,因为我们的第一优先级是提高性能和设计。
2.4.2、容易开始或者关闭
使用字节码注入的缺点是当分析的类库或者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)>
如果因为Pinpoint导致什么问题,只需要删除启动参数里的配置
2.5、字节码注入的原理
在Pinpoint中,我们通过抽象出拦截器提高开发效率和可访问性,Pinpoint在类加载时干预应用程序代码,注入必要的代码来跟踪分布式请求以及性能信息,由于跟踪代码直接注入应用程序代码,因此这提高了性能。
在Pinpoint中,API拦截部分和数据记录部分是分开的。 拦截器被注入到我们想要跟踪的方法中,并调用before()和after()方法来处理数据记录。 通过字节码检测,Pinpoint Agent只从必要的方法记录数据,这使得分析数据的大小变得紧凑。
2.6、如何优化Pinpoint agent的性能
2.6.1、使用二进制格式(Thrift)
你可以使用二进制格式(Thrift)加快编码速度,尽管它很难使用和调试,但是可以提高网络利用率以及减小生成的数据大小
2.6.2、使用可变长度编码和格式优化记录数据
如果你把一个长整型转化成定长的字符串,数据大小是8字节,但是,如果你使用可变长度编码,跟你长整型数字的大小,转化后的数据长度可能是1到10字节。为了减少数据大小,Pinpoint通过Compact Protocol of Thrift将数据编码为可变长度字符串,并记录要针对编码格式进行优化的数据。
Pinpoint Agent通过将基于根方法的剩余时间转换为矢量值来减少数据大小。
2.6.2.1、定长编码和可变长度编码的比较
- 有3个不同的方法开始调用和完成调用,你需要在这6处记录时间
- 其中A方法包含以下2个方法
- B方法
- C方法
2.6.2.2、采用定长编码
- 需要48字节(6处*8字节)
- 1处方法调用需要8字节
2.6.2.3、使用可变长度编码
根据其对应的格式记录数据,同时以根方法的起始时间为基准,计算其他方法处与基准的差异(矢量值)来计算时间,由于向量值是一个小数字,它消耗少量字节,因此只消耗13个字节而不是48字节
A方法 开始调用处:8个字节
B方法 开始调用处:1个字节
B方法 完成调用处:1个字节
C方法 开始调用处:1个字节
C方法 完成调用处:1个字节
A方法 完成调用处:1个字节
2.6.2.4、总结
如果执行方法需要更多时间,即使使用可变长度编码,也会增加字节数。 但是,它仍然比固定长度编码更有效。
2.6.3、使用常量表替换重复的API信息,SQL语句以及字符串
我们希望pinpoint能够实现代码级别的追踪,然而会造成数据变大的问题,每次高精度的数据发送到服务端,数据变大会增加带宽开销。
为了解决这种问题,我们采用了一种策略,通过在远程的HBase服务中创建一张常量表,既然每次发送"方法A"的数据到Pinpoint的Collector都有负载,Pinpoint Agent把"方法A"的数据转化成一个ID并在HBase的常量表中存储方法A的信息,并且使用这个ID继续后面的追踪,当用户在网站上检索跟踪数据时,Pinpoint Web会在常量表中搜索相应ID的方法信息并重新组织它们。
使用同样的方法用于减少SQL或常用字符串中的数据大小
2.6.4、处理批量请求的采样
Naver提供的在线门户服务请求非常庞大。 单个服务每天处理超过200亿个请求。 跟踪此类请求的一种简单方法是根据需要扩展网络基础架构和服务器以满足请求的数量。 但是,这不是处理这种情况的经济有效的方法。
在Pinpoint中,您可以只收集采样数据,而不是跟踪每个请求。 在请求很少的开发环境中,每个数据都会被收集。 在请求很大的生产环境中,只收集整个数据的1~5%,这足以分析整个应用程序的状态。 通过采样,您可以最大限度地减少应用程序中的网络开销,并降低网络和服务器等基础设施的花费。
Pinpoint中的采样方法
Pinpoint支持一个计数采样器(Counting Sampler),如果设置为10,那么只会收集十分之一的请求。
2.6.5、使用异步数据传输最小化被终止的应用线程
Pinpoint不会干挠应用程序线程,因为数据编码和远程消息传输是通过异步线程实现的
2.6.6、通过UDP传输数据
与Google的Dapper不同,Pinpoint通过网络传输数据以确保数据速度。 当数据流量突然爆发时,与你的服务共享网络可能是一个问题。
在这种情况下,Pinpoint Agent开始使用UDP协议,这样你的服务便有网络连接优先级
注意
数据传输的API可以被替换,因为他是独立的接口,你可以替换成不同的实现,比如通过本地文件
2.7、Pinpoint应用样例
2.7.1、场景
- 有2个应用,且都集成了Pinpoint
- TomcatA
- TomcatB
- TomcatA 有个test()方法,test()方法 会通过htto 访问 TomcatB的hello方法
2.7.2、Pinpoint在每个方法中做的事情
-
当请求到达TomcatA时,Pinpoint Agent产生了一个TraceId
- TX_ID: TomcatA^ TIME^1
- SpanId: 10
- ParentSpanId: -1(根)
-
从Spring MVC controllers中记录数据
-
干预 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”)
-
将打了标记的请求传输到TomcatB
- TomcatB检查传输过来的请求头部
- HttpServletRequest.getHeader(PINPOINT_TX_ID)–TomcatB成了子节点,因为它识别到了header中的TraceId
- TX_ID: TOMCATA^ TIME^1
- SPAN_ID: 20
- PARENT_SPAN_ID: 10
- 注意:到子节点的SPAN_ID其实是由父节点创建好的
-
从Spring MVC controllers中记录数据并完成请求
1. 当TomcatB中的请求完成时
1. Pinpoint agent将追踪数据发送到Pinpoint Collector
2. Pinpoint Collector把数据存储到HBase
2. 来自TomcatB的HTTP调用终止后,TomcatA的请求完成
1. Pinpoint agent将追踪数据发送到Pinpoint Collector
2. Pinpoint Collector把数据存储到HBase
3. UI从HBase读取跟踪数据,并通过对树进行排序来创建调用堆栈
3、总结
- Pinpoint是另一个与你的应用程序一起运行的应用程序。
- 使用字节码检测使Pinpoint看起来像是不需要修改代码。
- 通常,字节码检测技术使应用程序容易受到风险的影响; 如果Pinpoint出现问题,它也会影响你的应用程序。 但就目前而言,我们并没有摆脱这些威胁,而是专注于提高Pinpoint的性能和设计。 因为我们认为这使得Pinpoint更有价值。 因此,是否使用Pinpoint是由你决定的。