本文将讲述Pinpoint的技术,如事务追踪和字节码增强。并介绍应用于Pinpoint Agent的优化方法,该方法修改字节码并记录性能数据。
仿照Google Dapper的分布式事务追踪
Pinpoint仿照Google Dapper追踪单个事务中的分布式请求。
在Google Dapper中分布式事务追踪如何工作
当一个消息从Node1发送到Node2时,分布式追踪系统的作用是:在分布式系统中识别Node1和Node2之间的关系。(图1)
问题是无法识别消息之间的关系。例如,我们无法识别从Node1发送的N个消息与Node2接收到的N'个消息之间的关系。换句话说,当第X个消息从节点1被发送时,这个消息不能在Node2接收到的N'个消息中被识别。
曾经试图在TCP或操作系统级别跟踪消息。然而,这样做实现复杂度高且性能低,因为它需要针对每个协议单独实现。除此之外,这样做很难准确地追踪消息。
然而,Google Dapper实现了一个简单的解决方案来解决这个问题。这个解决方案是:在发送一个消息时,添加应用程序级的标记,这些标记可以是消息之间的链接。例如,在HTTP请求中的HTTP Header中为一个消息添加标记信息,并使用这个标记追踪这个消息。
Pinpoint基于Google Dapper的追踪技术,但是已经修改为:在一次远程调用中追踪分布式事务,通过在调用的Header中添加应用程序级的标记数据。标记数据由一组Key组成,这些Key被定义为一个TraceId。
关于Google Dapper的更多信息,请看:"Dapper, a Large-Scale Distributed Systems Tracing Infrastructure"。
Pinpoint中的数据结构
在Pinpoint中,核心数据结构由Spans、Traces和TraceIds组成。
1.Span:RPC追踪的基本单元。当一个RPC到达时,Span标示工作已经处理完成并包含追踪数据。为了确保代码级别的可见性,一个Span具有将SpanEvent标记为一个数据结构的子节点。
2.Trace:一个Span的集合。它由有关联的RPCs(Spans)组成。在相同Trace中的Spans,共享相同的TransactionId。
3.TraceId:由TransactionId、SpanId和ParentSpanId组成的一个keys的集合。TransactionId指示消息的ID,而SpanId和ParentSpanId表示RPCs的父-子关系。
4.TransactionId(TxId):从单个事务跨分布式系统发送/接收的消息的ID。它必须跨整个服务器集群做到全局唯一。
5.SpanId:当收到RPC消息时一个工作被处理的ID。当一个RPC到达一个节点时生成SpanId。
6.ParentSpanId(pSpanId):发起RPC的父Span的SpanId。如果一个节点是一个事务的起点,将没有父span - 对于这种情况,使用值-1表示这个span是一个事务的根span。
Google Dapper和NAVER Pinpoint在术语上的不同
Pinpoint中的术语"TransactionId"和Google Dapper中的术语"TraceId"有相同的含义。Pinpoint中的术语“TraceId”指的是一个keys的集合。
TraceId如何工作
下图说明了一个TraceId在4个节点之间执行了3次RPC调用时的运转状态。
在图2中,一个TransactionId(TxId)表示三个不同的RPC作为单个事务彼此关联。然而,一个TransactionId本身不能精确描述PRC调用之间的关系。为了识别PRC调用之间的关系,需要SpanId和ParentSpanId(pSpanId)。假设一个节点是Tomcat,可以将一个SpanId想象成一个处理HTTP请求的线程。一个ParentSpanId指示发起这个RPC调用的父SpanId。
使用TransactionId,Pinpoint可以发现有关联的n个Span,并能够使用SpanId和ParentSpanId将这n个span排列为继承树结构。
一个SpanId和一个ParentSpanId是64位长度的整型。可能产生冲突,因为这个数字是任意生成的。但是考虑到值的范围可以从-9223372036854775808到9223372036854775807,因此不太可能会发生冲突。如果Key之间出现冲突,Pinpoint和Google Dapper会让开发人员知道发生了什么,而不是解决冲突。
一个TransactionId由AgentIds,JVM启动时间和SequenceNumbers组成。
1.AgentId:当JVM启动时用户创建的ID。AgentId必须在安装了Pinpoinit的全部服务器集群中全局唯一。最简单的让它保持唯一的方法是使用hostname($HOSTNAME),因为hostname一般不会重复。如果需要在服务器集群中运行多个JVM,请在hostname后面增加一个后缀来避免重复。
2.JVM 启动时间:需要用来保证从0开始的SequenceNumber的唯一性。当用户错误地创建了重复的AgentId时,这个值可以用来预防ID冲突。
3.SequenceNumber: Pinpoint Agent生成的ID,从0开始连续自增,为每个消息生成一个。
Dapper和Zipkin(Twitter的一个分布式系统追踪平台),生成随机TraceIds(Pinpoint中的TransactionIds)并将冲突情况视为正常。然而,在Pinpiont中我们想避免冲突的可能。有两种可用的方案:
第一种方法数据量小但是冲突的可能性高。第二种方法数据量大但是冲突的可能性低。我们选择了第二种。
可能有更好的方式来处理事务。我们提出了几个想法,例如通过中央key服务器发布key。但是,由于性能问题和网络错误,我们在系统中没有实现这一点。我们仍然把批量发布key作为一个替代解决方案。因此未来这个方法有可能被开发。但是现在,我们采用了一个简单的方法。在Pinpoint中,TransactionId被视为可变数据。
字节码增强,无需代码修改
前面我们解释了分布式事务追踪。实现的方法之一是开发人员自己修改代码。当发生RPC调用时,允许开发人员添加标记信息。但是,修改代码会成为包袱,即使这样的功能对开发人员非常有用。Twitter的Zipkin使用修改类库和它的容器(Finagle)的方式来提供分布式事务追踪的功能。但是,它要求在需要时修改代码。我们想要这个功能不修改代码就可以工作,并且希望确保代码级别的可见性。为了解决这个问题,Pinpoint中采用了字节码增强技术。Pinpoint Agent干预发起RPC调用的代码,以此来自动处理标记信息。
克服字节码增强的缺点
如下所示,实现分布式事务追踪有两种方法。字节码增强是一种自动化方法
1.手动方法: 开发人员开发代码时,在关键点使用Pinpoint提供的API记录数据。
2.自动方法: 开发人员不涉及代码修改,开发人员不涉及代码修改,因为Pinpoint决定要干预和开发哪个API。
下面是每种方法的优点和缺点:
字节码增强是一种高难度和高风险的技术。然而,使用这种技术有很多好处。虽然它需要大量的开发资源,但是应用到服务几乎不需要任何资源。例如,下面展示了使用字节码增强的自动方法和使用类库的手动方法的成本对比(在这里的上下文中,成本是为了澄清而假设的随机数)。
1.自动方法:总成本100
①Pinpoint开发成本:100
②服务应用成本:0
2.手动方法:总成本30
①Pinpoint开发成本:20
②服务应用成本:10
上面的数据告诉我们手动方法比自动方法有更划算。但是,由于NAVER拥有上千个服务,因此无法保证在NAVER有相同的结果。例如,如果我们有10个需要修改的服务,总成本将计算如下:
Pinpoint开发成本20+服务应用成本10×10个服务=120
正如你所看到的,自动方法对我们来说更加节省成本。
我们很幸运,在Pinpoint团队中拥有很多高能力并且专注于Java的开发人员。因此,我们相信克服Pinpoint开发中的技术难点只是个时间问题。
字节码增强的价值
我们选择字节码增强(自动方法)的原因,除了前面描述的那些外,还有下面的要点:
1、隐藏API
如果API被暴露给开发人员使用。我们作为API的提供者,根据需要修改API的行为就会被限制。这样的限制会给我们带来压力。
我们可能修改API来纠正错误设计或者添加新的功能。但是,如果做这些受到限制,对我们来说很难改进API。解决这个问题的最好的答案是有一个可升级的系统设计,而每个人都知道这不是一个容易的选择。创建完美的API设计几乎是不可能的,因为我们无法预测未来。
使用字节码增强,我们不必担心由于暴露追踪API而引起的问题,并且能够在不考虑依赖关系的情况下持续改进设计。对于那些计划使用Pinpoint开发应用程序的人来说,请注意,Pinpoint开发人员可以更改API,因为改善性能和设计是我们的首要目标。
2.容易启用或禁用
使用字节码增强的缺点是,当类库的分析部分或Pinpoint本身出现问题时,它可能会影响你的应用程序。但是,你可以简单地通过禁用Pinpoint来解决这个问题,而不必更改任何代码。
通过将下面三行(与Pinpoint Agent的配置相关联)添加到JVM启动脚本中,可以容易地为应用程序启用Pinpoint:
①-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
②-Dpinpoint.agentId=
③-Dpinpoint.applicationName=
如果由于Pinpoint而出现任何问题,你只需要JVM启动脚本中删除这些配置数据。
字节码增强如何工作
由于字节码增强技术必须处理Java字节码,有增加开发风险的趋势,同时会降低生产率。此外,开发人员容易出错。在Pinpoint中,我们通过拦截器的抽象提高了生产力和可访问性。Pinpoint通过在类加载时,干预应用程序代码并注入必要的代码,来追踪分布式事务和性能信息。这提高了性能,因为追踪代码直接注入到应用程序代码中。
在Pinpoint中,API拦截部分和数据记录部分是分开的。拦截器被注入到我们想要追踪的方法中,并调用before()和after()方法来处理数据记录。通过字节码增强,Pinpoint Agent能够只从必要的方法记录数据,这使得分析数据的大小变得紧凑。
Pinpoint Agent性能优化
最后,我们将介绍如何优化Pinpoint Agent的性能
使用二进制格式(Thrift)
通过使用二进制格式(Thrift),可以提高编码速度。虽然它难以使用和调试,但是通过减少生成的数据量可以提高网络使用效率。
使用可变长的编码和格式优化数据记录
如果将一个长整数转换为固定长度的字符串,则数据大小为8个字节。但是,如果使用可变长度编码,则数据大小可以在1到10个字节之间变化,具体取决于给定数字的大小。为了减少数据大小,Pinpoint通过Thrift的Compact Protocol将数据编码为可变长度字符串,并记录要针对编码格式进行优化的数据。Pinpoint Agent通过将基于根方法的剩余时间转换为向量值来减少数据大小。
For more information on the variable-length encoding, see “Base 128 Varints” in Google Developers.
如图4所示,为了得到3个不同的方法何时被调用并完成的信息,你需要测量6个不同点的时间。使用固定长度编码,这个过程需要48个字节(6个点×8个字节)。
同时,Pinpoint Agent使用可变长度编码,并根据相应的格式记录数据。基于根方法的起始时间,利用向量值的差值计算其他点的时间信息。因为向量值是一个小数,所以它消耗少量的字节,结果只消耗13个字节,而不是48个字节。
如果执行方法需要更多的时间,那么即使使用可变长度编码,也会增加字节数。然而,它仍然比固定长度编码更有效。
用常量表替换重复的API信息,SQL语句和字符串
我们希望Pinpoint能够做到代码级别的追踪。然而,它在增加数据大小方面存在问题。每次向服务器发送高精度的数据时,由于数据的大小,它增加了网络使用率。
为了解决这个问题,我们采用了在远程HBase服务器中创建常量表的策略。由于每次都向Pinpoint Collector发送“methodA”的数据将会是一个负担,因此Pinpoint Agent将“methodA”的数据转换一个为ID,并将这个信息作为HBase中的常量表存储,并使用这个ID继续追踪数据。当用户在网站上检索追踪数据时,Pinpoint Web在常量表中搜索相应ID的方法信息并重新组织它们。使用同样的方式来减少SQL语句的数据大小和频繁使用的字符串的数据大小。
处理批量请求的样本
Naver提供的在线门户服务请求量非常庞大。单个服务每天处理超过200亿个请求。跟踪此类请求的一种简单的方法是:根据需要,扩展网络基础架构和服务器以满足请求数量。但是,这不是处理这种情况的经济有效的方法。在Pinpoint,你可以只收集采样数据而不必追踪每一个请求。在请求很少的开发环境中,收集每个数据。而在请求较大的生产环境中,只收集到整个数据中的1~5%,足以分析整个应用程序的状态。通过采样,可以最小化应用程序中的网络开销,并降低网络和服务器等基础设施的成本。
Pinpoint中的采样方法
Pinpoint支持计数采样,如果设置为10,则它只收集10个请求中的1个请求的数据。我们计划增加新的采样器来更有效率的收集数据。
注:对应的配置项在Pinpoint Agent的pinpoint.config文件中,默认"profiler.sampling.rate=1"表示全部
使用异步数据传输来最小化应用程序线程中止
pinpoint不干预应用程序线程,因为编码数据或远程消息是由另一个线程异步传输的。
通过UDP传输数据
与Google Dapper不同,Pinpoint通过网络传输数据以确保数据速度。当数据流量爆发时,与你的服务共享网络可能是个问题。在这种情况下,Pinpoint Agent开始使用UDP协议为你的服务给出网络连接的优先级。
注意:数据传输API可以被替换,因为它被分离为接口。它可以更改为以不同的存储数据的实现方式,如本地文件。
Pinpoint应用示例
以下是如何从应用程序中获取数据的示例,以便你可以全面了解前面描述的内容。
图5显示了,当你在TomcatA和TomcatB中安装Pinpoint时可以看到的内容。你可以将单个节点的追踪数据视为单个事务,它表示分布式事务追踪的流。
下面描述了Pinpoint为每个方法都做了什么:
1.当请求到达TomcatA时, Pinpoint Agent生成一个TraceId
①TX_ID: TomcatA^TIME^1
②SpanId: 10
③ParentSpanId: -1(Root)
2.从Spring MVC的控制器中记录数据
3.干预HttpClient.execute()方法的调用,并在HttpGet中配置TraceId
①创建一个子TraceId
②TX_ID: TomcatA^TIME^1 -> TomcatA^TIME^1
③SPAN_ID: 10 -> 20
④PARENT_SPAN_ID: -1 -> 10 (parent SpanId) - Configures the child TraceId in the HTTP header.
⑤HttpGet.setHeader(PINPOINT_TX_ID, “TomcatA^TIME^1”)
⑥HttpGet.setHeader(PINPOINT_SPAN_ID, “20”)
⑦HttpGet.setHeader(PINPOINT_PARENT_SPAN_ID, “10”)
4.传输打好Tag的请求传输到TomcatB
①TomcatB 检查传输过来的请求的Header
②HttpServletRequest.getHeader(PINPOINT_TX_ID) - TomcatB在Header中标识TraceId时成为了一个子节点
③TX_ID: TomcatA^TIME^1
④SPAN_ID: 20
⑤PARENT_SPAN_ID: 10
5.记录来自Spring MVC控制器的数据并完成请求
①当来自TomcatB的请求完成时,Pinpoint Agent将追踪数据发送到Pinpoint Collector并将其存储在HBase中。
②来自TomcatB的HTTP调用结束以后,来自TomcatA的请求也就完成了。Pinpoint Agent将追踪数据发送到Pinpoint Collector并将其存储在HBase中。
③UI从HBase中读取追踪数据,并通过排序成树来创建调用堆栈。
结论
Pinpoint是另一个与应用程序一起运行的应用程序。使用字节码增强使得Pinpoint看起来不需要修改代码。通常,字节码增强技术使应用程序更容易受风险的影响。如果Pinpoint中发生了问题,也会影响应用程序。但是现在,我们并没有消除这些威胁,而是集中精力改进Pinpoint的性能和设计。因为我们认为这使得Pinpoint更有价值。因此,是否使用Pinpoint由你决定。我们还是有大量的工作需要完成来改进Pinpoint。尽管不完整,但Pinpoint还是作为开源项目发布了。我们将持续努力开发并改进Pinpoint以便满足你的期望。
Written by Woonduk Kang
2011年,我就这样写我自己——作为一名开发人员,我想制作一个用户愿意付费的软件程序,像Microsoft或者Oracle。随着Pinpoint作为一个开源项目推出,我的梦想似乎有些成真。 目前,我的愿望是使Pinpoint对用户更有价值和吸引力。