分布式链路跟踪技术

一、背景

随着互联网架构的扩张,分布式系统变得日趋复杂,越来越多的组件开始走向分布式化,如微服务、消息收发、分布式数据库、分布式缓存、分布式对象存储、跨域调用,这些组件共同构成了繁杂的分布式网络,很容易出现以下问题:

  • 一个请求经过了这些服务后其中出现了一个调用失败的问题,只知道有异常,但具体的异常在哪个服务引起的就需要进入每一个服务里面看日志,这样的处理效率是非常低的。
  • 一个请求经过一系类服务之后最终返回结果或者超时,各服务都正常运行,但不知道是哪个服务响应时间过长,需要查看每个服务的日志文件,但无法确定服务调用的时间和时长

二、具备的功能

故障快速定位

通过调用链跟踪,一次请求的逻辑轨迹可以用完整清晰的展示出来。开发中可以在业务日志中添加调用链ID,可以通过调用链结合业务日志快速定位错误信息。

各个调用环节的性能分析

在调用链的各个环节分别添加调用时延,可以分析系统的性能瓶颈,进行针对性的优化。通过分析各个环节的平均时延,QPS等信息,可以找到系统的薄弱环节,对一些模块做调整,如数据冗余等。

数据分析等

调用链绑定业务后查看具体每条业务数据对应的链路问题,可以得到用户的行为路径,经过了哪些服务器上的哪个服务,汇总分析应用在很多业务场景。

生成服务调用拓扑图

通过可视化分布式系统的模块和他们之间的相互联系来理解系统拓扑。点击某个节点会展示这个模块的详情,比如它当前的状态和请求数量。

三、分布式调用跟踪系统的设计

分布式调用跟踪系统的设计目标

  • 低侵入性,应用透明:作为非业务组件,应当尽可能少侵入或者无侵入其他业务系统,对于使用方透明,减少开发人员的负担
  • 低损耗:服务调用埋点本身会带来性能损耗,这就需要调用跟踪的低损耗,实际中还会通过配置采样率的方式,选择一部分请求去分析请求路径
  • 大范围部署,扩展性:作为分布式系统的组件之一,一个优秀的调用跟踪系统必须支持分布式部署,具备良好的可扩展性

埋点和生成日志

埋点即系统在当前节点的上下文信息,可以分为客户端埋点、服务端埋点,以及客户端和服务端双向型埋点。埋点日志通常要包含以下内容:

TraceId、RPCId、调用的开始时间,调用类型,协议类型,调用方ip和端口,请求的服务名等信息;

调用耗时,调用结果,异常信息,消息报文等;

预留可扩展字段,为下一步扩展做准备;

抓取和存储日志

日志的采集和存储有许多开源的工具可以选择,一般来说,会使用离线+实时的方式去存储日志,主要是分布式日志采集的方式。典型的解决方案如Flume结合Kafka等MQ。

分析和统计调用链数据

一条调用链的日志散落在调用经过的各个服务器上,首先需要按 TraceId 汇总日志,然后按照RpcId 对调用链进行顺序整理。用链数据不要求百分之百准确,可以允许中间的部分日志丢失。

扫描二维码关注公众号,回复: 5128801 查看本文章

计算和展示

汇总得到各个应用节点的调用链日志后,可以针对性的对各个业务线进行分析。需要对具体日志进行整理,进一步储存在HBase或者关系型数据库中,可以进行可视化的查询。

四、数据采集方式

探针埋点

Agent也就是常说的探针埋点,Agent可以理解为虚拟机级别的AOP,可以利用Agent实现热部署等功能

  • 代码侵入式埋点

    代码侵入式埋点是指提供应用开发的SDK,或者提供集成埋点代码的框架供应用开发者调用。像Google这类具备框架研发能力的企业将植入点选在开发框架或通信框架中,确保基于统一框架开发或通信的应用天然具备埋点能力,开发团队除框架外无需关注埋点实现和调用方式。这种埋点方式优势在于使用框架后无需额外关注埋点能力,变相降低了埋点的成本,Twitter的Zipkin、淘宝的鹰眼、大众点评的CAT等都属于这类埋点方式。

    代码侵入式埋点具有较好的扩展性,方便用户自定义采集的数据类型与层次。但无论提供框架埋点还是提供装备库、SDK的方式都需要侵入代码,在应用开发及框架升级等场景下,应用需要重新修改代码。同时,对于应用开发人员来说,精准地识别埋点位置也有难度,且基于代码侵入的埋点跟踪级别较低,无法获取足够详细的运行动态信息。

  • 字节码增强式埋点

    字节码增强式埋点方式无需修改代码,不同的编程语言通过不同的技术在语言运行环境或基础库上植入。利用字节码增强技术,Java应用在启动JVM时通过不同的埋点插件覆盖不同的通信协议、中间件和开发框架,对Java基础调用代码进行函数级埋点。**这种埋点方式的优势在于能够采集到堆栈级的调用信息及其他更多运行态信息,帮助使用者无需日志等辅助手段即可快速完成问题定位。**代表性的开源产品PinPoint及大部分商业化产品都采用这类方式。

    使用字节码增强技术进行APM数据采集时,通过在应用启动时配置Java Agent探针的方式主动干预应用代码行为,应用开发者无需进行代码修改,由APM产品来决定在哪些API进行数据埋点。理论上来说字节码增强技术能够在任意位置进行埋点。

数据互联

互联数据(Wire Data)是指连接客户端和服务器之间的网络光纤所承载的全量、双向的通讯数据。它是被处理过的、高价值的业务可用数据源,是实时、直观了解系统业务运行状况最全面客观的参考数据资源。

通过实时地将网络中传输的海量数据重组而成的结构化数据,可以帮助IT运维人员创建行为基线、检测异常行为,进行实时地性能故障定位和排除等;同时,让运维团队贴近业务,可以用数据来监测业务的交易量、成功率、失败率等,为业务和运营团队提供数据支撑,让IT基础设施更好地提供澎湃动力,为科技引领业务创新创造可能。

五、实现方案

Google Drapper(闭源)

Drapper实现所用名词

  • Trace:一次完整的分布式调用跟踪链路。一次完整的分布式调用跟踪链路。
  • Span:追踪服务调基本结构,表示跨服务的一次调用,多span形成树形结构,组合成一次Trace追踪记录
  • Annotation:在span中的标注点,记录整个span时间段内发生的事件
  • BinaryAnnotation:可以认为是特殊的Annotation,用户自定义事件
  • Annotation类型:保留类型
  • Cs CLIENT_SEND:客户端发起请求
  • Cr CLIENT_RECIEVE:客户端收到响应
  • Sr SERVER_RECIEVE:服务端收到请求
  • Ss SERVER_SEND:服务端发送结果

Drapper设计目标

  • 低消耗

    dapper本质是用来发现性能消耗问题,如果dapper本身很消耗性能,没人愿意使用,因此低消耗是必须的,dapper使用一系列创新方法确保低消耗,比如使用采样方法。

  • 应用级透明

    应用级透明的意思是程序员可以不需要在自己的代码中嵌入dapper相关的代码就能达到分布式追踪日志记录的目的。每一个工程师都希望自己的代码是纯粹的,如果需要嵌入dapper相关代码,那么既影响代码维护,又影响bug定位。

  • 扩展性好

    对于一个快速发展的互联网公司而言,用户规模快速增长导致着服务以及机器数量越来越多,因此dapper需要适应相应的发展,扩展性要好。

Drapper实现的关键点

  • dapper日志记录的格式

    dapper用span来表示一个服务调用开始和结束的时间,也就是时间区间(图2对应着图1b的调用图)。dapper记录了span的名称以及每个span的ID和父ID,如果一个span没有父ID被称之为root span。所有的span都挂在一个特定得追踪上,共用一个跟踪ID,这些ID用全局64位整数标示,也就是下图的traceID。
    在这里插入图片描述

  • 如何实现应用级透明?

    在google的环境中,所有的应用程序使用相同的线程模型、控制流和RPC系统,既然不能让工程师写代码记录日志,那么就只能让这些线程模型、控制流和RPC系统来自动帮助工程师记录日志了。

    举个例子,几乎所有的google进程间通信是建立在一个用C++和JAVA开发的RPC框架上,dapper把跟踪植入这个框架,span的ID和跟踪的ID会从客户端发送到服务端,这样工程师也就不需要关心。

  • dapper跟踪收集的流程

    在这里插入图片描述

    1. 各个服务将span数据写到本机日志上

    2. dapper守护进程进行拉取,将数据读到dapper收集器里

    3. dapper收集器将结果写到bigtable中,一次跟踪被记录为一行

  • 如何尽可能降低开销?

    作为一个分布式追踪系统,dapper希望尽可能降低性能开销。如果对每一次的请求都进行追踪收集,开销还是有点大的。一个比较好的方式是通过统计采样的方法,抽样追踪一些请求,从而达到性能开销与精度的折中。

    dapper的第一个版本设置了一个统一的采样率1/1024,也就是1024个请求才追踪一次。后来发现对一些高吞吐的服务来说是可以的,比如每秒几十万的请求,但是对一些低吞吐量的服务,比如每秒几十个请求的服务,如果采样率设置为1/1024,很多性能问题可能不会被追踪到。因此在第二版本dapper提供了自适应的采样率,在低吞吐量时候提高采样率,在高吞吐量时降低采样率。

    上面的采样是在第一个阶段,此外在收集器将span数据写到bigtable时,还可以使用第二次采样,即不一定都将数据写入到bigtable中。

Dapper的不足

  • 某些时候缓冲一些请求,然后一次性操作会比较高效,比如I/O请求等。各个请求都有traceID,但是聚集之后只有一个请求,因此只能选择一个TraceID用于传递到聚集请求,这时追踪会中断。
  • Dapper可能找出某个环节慢了,但不一定能找出根源。比如一个请求慢可能不是它自身慢,而可能它在消息队列中比较靠后。

CAT(开源)

CAT设计目标

  • 实时处理:信息的价值会随时间锐减,尤其是事故处理过程中。
  • 全量数据:最开始的设计目标就是全量采集,全量的好处有很多。
  • 高可用:所有应用都倒下了,需要监控还站着,并告诉工程师发生了什么,做到故障还原和问题定位。
  • 故障容忍:CAT本身故障不应该影响业务正常运转,CAT挂了,应用不该受影响,只是监控能力暂时减弱。
  • 高吞吐:要想还原真相,需要全方位地监控和度量,必须要有超强的处理吞吐能力。
  • 可扩展:支持分布式、跨IDC部署,横向扩展的监控系统。
  • 不保证可靠:允许消息丢失,这是一个很重要的trade-off,目前CAT服务端可以做到4个9的可靠性,可靠系统和不可靠性系统的设计差别非常大。

整体设计

在整个CAT从开发至今,一直秉承着简单的架构就是最好的架构原则,整个CAT主要分为三个模块,cat-client,cat-consumer,cat-home。

  • cat-client 提供给业务以及中间层埋点的底层sdk。
  • cat-consumer 用于实时分析从客户端的提供的数据。
  • cat-home 作为用户提供给用户的展示的控制端。

在实际开发和部署中,cat-consumer和cat-home是部署在一个jvm内部,每个CAT服务端都可以作为consumer也可以作为home,这样既能减少整个CAT层级结构,也可以增加整个系统稳定性。
在这里插入图片描述
上图是CAT目前多机房的整体结构图:

  • 路由中心是根据应用所在机房信息来决定客户端上报的CAT服务端地址
  • 每个机房内部都有的独立的原始信息存储集群HDFS
  • cat-home可以部署在一个机房也可以部署在多个机房,在做报表展示的时候,cat-home会从cat-consumer中进行跨机房的调用,将所有的数据合并展示给用户
  • 实际过程中,cat-consumer、cat-home以及路由中心都是部署在一起,每个服务端节点都可以充当任何一个角色

模型设计

监控模型

CAT主要支持以下四种监控模型:

  • Transaction 适合记录跨越系统边界的程序访问行为,比如远程调用,数据库调用,也适合执行时间较长的业务逻辑监控,Transaction用来记录一段代码的执行时间和次数
  • Event 用来记录一件事发生的次数,比如记录系统异常,它和transaction相比缺少了时间的统计,开销比transaction要小
  • Heartbeat 表示程序内定期产生的统计信息, 如CPU利用率, 内存利用率, 连接池状态, 系统负载等
  • Metric 用于记录业务指标、指标可能包含对一个指标记录次数、记录平均值、记录总和,业务指标最低统计粒度为1分钟
消息树

CAT监控系统将每次URL、Service的请求内部执行情况都封装为一个完整的消息树、消息树可能包括Transaction、Event、Heartbeat、Metric等信息。

客户端设计

在这里插入图片描述
CAT客户端是java实现的,客户端在收集端数据方面使用ThreadLocal,是线程本地变量,也可以称之为线程本地存储。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

在监控场景下,为用户提供服务都是web容器,web容器比如Tomcat或者Jetty,后端的rpc服务端比如dubbo或者点评自研的服务框架pigeon,也都是基于线程池来实现的。业务方在处理业务逻辑时基本都是在一个线程内部调用后端服务,数据库,缓存等,将这些数据拿回来在进行业务逻辑逻辑封装,最后将结果展示给用户。所以将所有的监控请求作为一个监控上下文存入于线程变量就非常合适。

上图理解起来不太直观,可以看一下cat-client源码进行理解,就是创建一个ThreadLocal,在使用ThreadLocal中会有内存泄漏问题

API设计

当设计者对监控以及性能分析有足够深度的理解下,才能定义好监控的API,监控和性能分析所针对的场景有如下几种

  • 一段代码的执行时间,一段代码可以是URL执行耗时,也可以是SQL的执行耗时
  • 一段代码的执行次数,比如程序抛出异常记录次数,或者一段逻辑的执行次数
  • 定期执行某段代码,比如定期上报一些核心指标,jvm内存、gc等指标
  • 关键的业务监控指标,比如监控订单数、交易额、支付成功率等

在如上的领域模型的基础上,CAT设计自己核心的几个监控对象 Transaction、Event、Heartbeat、Metric

序列化和通信

序列化和通信是整个客户端包括服务端性能里面很关键的一个环节

  • CAT序列化协议是自定义序列化协议,自定义序列化协议相比通用序列化协议要高效很多,这个在大规模数据实时处理场景下还是非常有必要的。
  • CAT通信是基于Netty来实现的NIO的数据传输,Netty是一个非常好的NIO开发框架,在这边就不详细介绍。
客户端埋点

日志埋点是监控活动的最重要环节之一,日志质量决定着监控质量和效率。CAT的埋点目标是以问题为中心,像程序抛出exception就是典型问题。我个人对问题的定义是:不符合预期的就可以算问题。比如请求未完成,响应时间快了慢了,请求TPS多了少了,时间分布不均匀等等。

在互联网环境中,典型的突出的容易出问题的场景,包括跨模块调用,跨公司调用等。比如

  • HTTP/REST、RPC/SOA、MQ、Job、Cache、DAL;
  • 搜索/查询引擎、业务应用、外包系统、遗留系统;
  • 第三方网关/银行, 合作伙伴/供应商之间;
  • 各类业务指标,如用户登录、订单数、支付状态、销售额。

服务端设计

服务端单机cat-consumer的整体架构如下:
在这里插入图片描述
如上图,CAT服务端在整个实时处理中,基本上实现了全异步化处理。

  • 消息接收是基于Netty的NIO实现
  • 消息接收到服务端就存放内存队列,然后程序开启一个线程会消费这个消息做消息分发
  • 每个消息都会有一批线程并发消费各自队列的数据,以做到消息处理的隔离
  • 消息存储是先存入本地磁盘,然后异步上传到hdfs文件,这也避免了强依赖hdfs

当某个报表处理器处理来不及时候,比如Transaction报表处理比较慢,可以通过配置支持开启多个Transaction处理线程,并发消费消息。
在这里插入图片描述

实时分析

CAT服务端实时报表分析是整个监控系统的核心,CAT重客户端采集的是是原始的Logview,目前一天大约有3000亿的消息,所以需要在这些消息基础上实现丰富报表,以支持业务问题以及性能分析的需要。

CAT根据日志消息的特点(比如只读特性)和问题场景,量身定做的。CAT将所有的报表按消息的创建时间,一小时为单位分片,那么每小时就产生一个报表。当前小时报表的所有计算都是基于内存的,用户每次请求即时报表得到的都是最新的实时结果。对于历史报表,因为它是不变的,所以就实时不实时也就无所谓了。

CAT基本上所有的报表模型都可以增量计算,它可以分为:计数、计时和关系处理三种。计数又可以分为两类:算术计数和集合计数。典型的算术计数如:总个数(count),总和(sum),均值(avg),最大/最小(max/min),吞吐(tps)和标准差(std)等,其他都比较直观,标准差稍微复杂一点,大家自己可以推演一下怎么做增量计算。那集合运算,比如95线(表示95%请求的完成时间),999线(表示99.9%请求的完成时间),则稍微复杂一些,系统开销也更大一点。

报表建模

CAT每个报表往往有多个维度,以transaction报表为例,它有5个维度,分别是应用、机器、Type、Name和分钟级分布情况。如果全维度建模,虽然灵活,但开销将会非常之大。CAT选择固定维度建模,可以理解成将这5个维度组织成深度为5的树,访问时总是从根开始,逐层往下进行。

CAT服务端为每个报表单独分配一个线程,所以不会有锁的问题,所有报表模型都是非线程安全的,其数据是可变的。这样带来的好处是简单且低开销。

CAT报表建模是使用自研的maven plugin自动生成的。所有报表是可合并和裁剪的,可以轻易地将2个或多个报表合并成一个报表。在报表处理代码中,CAT大量使用访问者模式(visitor pattern)。

性能分析报表

在这里插入图片描述

故障发现报表
  • 实时业务指标监控 :核心业务都会定义自己的业务指标,这不需要太多,主要用于24小时值班监控,实时发现业务指标问题,图中一个是当前的实际值,一个是基准值,基准值是根据历史趋势计算的预测值。如下图就是当时出故障,直观看到支付业务出问题的故障。
    在这里插入图片描述
  • 系统报错大盘
  • 实时数据库大盘、服务大盘、缓存大盘等
存储设计

CAT系统的存储主要有两块

  • CAT的报表的存储
  • CAT原始logview的存储

报表是根据logview实时运算出来的给业务分析用的报表,默认报表有小时模式,天模式,周模式以及月模式。CAT实时处理报表都是产生小时级别统计,小时级报表中会带有最低分钟级别粒度的统计。天、周、月等报表都是在小时级别报表合并的结果报表。

原始logview存储一天大约300TB的数据量,因为数据量比较大所以存储必须要要压缩,原始logview需要根据messageId读取。在这样的情况下,存储整体要求就是批量压缩以及随机读。在当时场景下,并没有特别合适成熟的系统以支持这样的特性,所以我们开发了一种基于文件的存储以支持CAT的场景,在存储上一直是最难的问题,我们一直在这块持续的改进和优化。

消息ID的设计

CAT每个消息都有一个唯一的ID,这个ID在客户端生成,后续CAT都通过这个ID在进行消息内容的查找。比如在分布式调用里面,RPC消息需要串起来,比如A调用B的时候,在A这端生成一个MessageId,在A调用B的过程中,将MessageId作为调用传递到B端,在B执行过程中,B用context传递的MessageId作为当前监控消息的MessageId。

CAT消息的MessageId格式ShopWeb-0a010680-375030-2,CAT消息一共分为四段

  • 第一段是应用名shop-web
  • 第二段是当前这台机器的ip的16进制格式,01010680表示10.1.6.108
  • 第三段的375030,是系统当前时间除以小时得到的整点数
  • 第四段的2,是表示当前这个客户端在当前小时的顺序递增号
存储数据的设计

消息存储是CAT最有挑战的部分。关键问题是消息数量多且大,目前美团点评每天处理消息3000亿左右,大小大约300TB,单物理机每秒要处理200MB左右的流量。CAT服务端基于此流量做实时计算,还需要将这些数据压缩后写入磁盘。

整体存储结构如下图
在这里插入图片描述
CAT数据文件分为两种,一类是index文件,一类是Data文件

  • data文件是分段GZIP压缩,每个分段大小小于64K,这样可以用16bits可以表示一个最大分段地址
  • 一个MessageId都用需要48bits的空间大小来存索引,索引根据MessageId的第四段来确定索引的位置,比如消息MessageId为ShopWeb-0a010680-375030-2,这条消息ID对应的索引位置为2*48bits的位置
  • 48bits前面32bits存数据文件的块偏移地址,后面16bits存数据文件解压之后的块内地址偏移
  • CAT读取消息的时候,首先根据MessageId的前面三段确定唯一的索引文件,在根据MessageId第四段确定此MessageId索引位置,根据索引文件的48bits读取数据文件的内容,然后将数据文件进行GZIP解压,在根据块内偏移地址读取出真正的消息内容,
服务端设计总结

CAT在分布式实时方面,主要归结于以下几点因素:

  • 去中心化,数据分区处理
  • 基于日志只读特性,以一个小时为时间窗口,实时报表基于内存建模和分析,历史报表通过聚合完成
  • 基于内存队列,全面异步化,单线程化,无锁设计
  • 全局消息ID,数据本地化生产,集中式存储
  • 组件化、服务化理念

Pinpoint

Pinpoint设计目标

  • 分布式事务跟踪,跟踪跨分布式应用的消息
  • 自动检测应用拓扑,帮助你搞清楚应用的架构
  • 水平扩展以便支持大规模服务器集群
  • 提供代码级别的可见性以便轻松定位失败点和瓶颈
  • 使用字节码增强技术,添加新功能而无需修改代码

整体设计

功能模块
  • Pinpoint-Collector:收集各种性能数据
  • Pinpoint-Agent:和自己运行的应用关联起来的探针
  • Pinpoint-Web:将收集到的数据显示成WEB网页形式
  • HBase Storage:收集到的数据存到HBase中
    在这里插入图片描述
分布式事务追踪

在Google Dapper中分布式事务追踪是如何工作的
在这里插入图片描述
当一个消息从Node1发送到Node2时,分布式追踪系统的核心是在分布式系统中识别在Node1中处理的消息和在Node2中处理的消息之间的关系。

问题在于无法在消息之间识别关系。例如,我们无法识别从Node1发送的第N个消息和Node2接收到的N’消息之间的关系。换句话说,当Node1发送完第X个消息时,是无法在Node2接收到的N的消息里面识别出第X个消息的。有一种方式试图在TCP或者操作系统层面追踪消息。但是,实现很复杂而且性能低下,而且需要为每个协议单独实现。另外,很难精确追踪消息。

不过,Google dapper实现了一个简单的解决方案来解决这个问题。这个解决方案通过在发送消息时添加应用级别的标签作为消息之间的关联。例如,在HTTP请求中的HTTP header中为消息添加一个标签信息并使用这个标签跟踪消息。

Pinpoint中的数据结构

Pinpoint中,核心数据结构由Span, Trace, 和 TraceId组成。

  • Span: RPC (远程过程调用/remote procedure call)跟踪的基本单元; 当一个RPC调用到达时指示工作已经处理完成并包含跟踪数据。为了确保代码级别的可见性,Span拥有带SpanEvent标签的子结构作为数据结构。每个Span包含一个TraceId。
  • Trace: 多个Span的集合; 由关联的RPC (Spans)组成. 在同一个trace中的span共享相同的TransactionId。Trace通过SpanId和ParentSpanId整理为继承树结构.
  • TraceId: 由 TransactionId, SpanId, 和 ParentSpanId 组成的key的集合. TransactionId 指明消息ID,而SpanId 和 ParentSpanId 表示RPC的父-子关系。
    • TransactionId (TxId): 在分布式系统间单个事务发送/接收的消息的ID; 必须跨整个服务器集群做到全局唯一.
    • SpanId: 当收到RPC消息时处理的工作的ID; 在RPC请求到达节点时生成。
    • ParentSpanId (pSpanId): 发起RPC调用的父span的SpanId. 如果节点是事务的起点,这里将没有父span - 对于这种情况, 使用值-1来表示这个span是事务的根span。
TraceId如何工作

下图描述TraceId的行为,在4个节点之间执行了3次的RPC调用:
在这里插入图片描述
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 (java虚拟机)启动时间, 和 SequenceNumbers/序列号组成.

  • AgentId: 当Jvm启动时用户创建的ID; 必须在pinpoinit安装的全部服务器集群中全局唯一. 最简单的让它保持唯一的方法是使用hostname($HOSTNAME),因为hostname一般不会重复. 如果需要在服务器集群中运行多个JVM,请在hostname前面增加一个前缀来避免重复。
  • JVM 启动时间: 需要用来保证从0开始的SequenceNumber的唯一性. 当用户错误的创建了重复的AgentId时这个值可以用来预防ID冲突。
  • SequenceNumber: Pinpoint agent 生成的ID, 从0开始连续自增;为每个消息生成一个.

Dapper 和 Zipkin, Twitter的一个分布式系统跟踪平台, 生成随机TraceIds (Pinpoint是TransactionIds) 并将冲突情况视为正常。然而, 在Pinpiont中我们想避免冲突的可能,因此实现了上面描述的系统。有两种选择:一是数据量小但是冲突的可能性高,二是数据量大但是冲突的可能性低。我们选择了第二种。

可能有更好的方式来处理transaction。我们起先有一个想法,通过中央key服务器来生成key。如果实现这个模式,可能导致性能问题和网络错误。因此,大量生成key被考虑作为备选。后面这个方法可能被开发。现在采用简单方法。在pinpoint中,TransactionId被当成可变数据来对待。

字节码增强,无需代码修改

前面我们解释了分布式事务跟踪。实现的方法之一是开发人员自己修改代码。当发生RPC调用时容许开发人员添加标签信息。但是,修改代码会成为包袱,即使这样的功能对开发人员非常有用。

Twitter的 Zipkin 使用修改过的类库和它自己的容器(Finagle)来提供分布式事务跟踪的功能。但是,它要求在需要时修改代码。我们期望功能可以不修改代码就工作并希望得到代码级别的可见性。为了解决这个问题,pinpoint中使用了字节码增强技术。Pinpoint agent干预发起RPC的代码以此来自动处理标签信息。

克服字节码增强的缺点

字节码增强在手工方法和自动方法两者之间属于自动方法。

  • 手工方法: 开发人员使用ponpoint提供的API在关键点开发记录数据的代码
  • 自动方法: 开发人员不需要代码改动,因为pinpoint决定了哪些API要调节和开发

下面是每个方法的优点和缺点:

优点 缺点
手工跟踪 1. 要求更少开发资源 2. API可以更简单并最终减少bug的数量 1. 开发人员必须修改代码 2. 跟踪级别低
自动跟踪 1. 开发人员不需要修改代码 2. 可以收集到更多精确的数据因为有字节码中的更多信息 1. 在开发pinpoint时,和实现一个手工方法相比,需要10倍开销来实现一个自动方法 2. 需要更高能力的开发人员,可以立即识别需要跟踪的类库代码并决定跟踪点 3. 增加bug发生的可能性,因为使用了如字节码增强这样的高级开发技巧

字节码增强的价值

我们选择字节码增强的理由,除了前面描述的那些外,还有下面的强有力的观点:

隐藏API

一旦API被暴露给开发人员使用,我们作为API的提供者,就不能随意的修改API。这样的限制会给我们增加压力。

我们可能修改API来纠正错误设计或者添加新的功能。但是,如果做这些受到限制,对我们来说很难改进API。解决这个问题的最好的答案是一个可升级的系统设计,而每个人都知道这不是一个容易的选择。如果我们不能掌控未来,就不可能创建完美的API设计。

而使用字节码增强技术,我们就不必担心暴露跟踪API而可以持续改进设计,不用考虑依赖关系。对于那些计划使用pinpoint开发应用的人,换一句话说,这代表对于pinpoint开发人员,API是可变的。现在,我们将保留隐藏API的想法,因为改进性能和设计是我们的第一优先级。

容易启用或者禁用

使用字节码增强的缺点是当Pinpoint自身类库的采样代码出现问题时可能影响应用。不过,可以通过启用或者禁用pinpoint来解决问题,很简单,因为不需要修改代码。

通过增加下面三行到JVM启动脚本中就可以轻易的为应用启用pinpoint:

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

如果因为pinpoint发生问题,只需要在JVM启动脚本中删除这些配置数据。

字节码如何工作

由于字节码增强技术处理java字节码, 有增加开发风险的趋势,同时会降低效率。另外,开发人员更容易犯错。在pinpoint,我们通过抽象出拦截器(interceptor)来改进效率和可达性(accessibility)。pinpoint在类装载时通过介入应用代码为分布式事务和性能信息注入必要的跟踪代码。这会提升性能,因为代码注入是在应用代码中直接实施的。
在这里插入图片描述
在pinpoint中,拦截器API在性能数据被记录的地方分开(separated)。为了跟踪,我们添加拦截器到目标方法使得before()方法和after()方法被调用,并在before()方法和after()方法中实现了部分性能数据的记录。使用字节码增强,pinpoint agent可以记录需要方法的数据,只有这样采样数据的大小才能变小。

Pinpoint agent的性能优化

使用二进制格式(thrift)

通过使用二进制格式(thrift)可以提高编码速度,虽然它使用和调试要难一些。也有利于减少网络使用,因为生成的数据比较小。

使用变长编码和格式优化数据记录

如果将一个长整型转换为固定长度的字符串, 数据大小一般是8个字节。然而,如果你用变长编码,数据大小可以是从1到10个字符,取决于给定数字的大小。为了减小数据大小,pinpoint使用thrift的CompactProtocol协议(压缩协议)来编码数据,因为变长字符串和记录数据可以为编码格式做优化。pinpoint agent通过基于跟踪的根方法的时间开始来转换其他的时间来减少数据大小。
在这里插入图片描述
为了得到关于三个不同方法被调用时间的数据,不得不在6个不同的点上测量时间,用固定长度编码这需要48个字节(6 * 8)。

以此同时,pinpoint agent 使用可变长度编码并根据对应的格式记录数据。然后在其他时间点通过和参考点比较来计算时间值(在vector中),根方法的起点被确认为参考点。这只需要占用少量的字节,因为vector使用小数字。图中消耗了13个字节。

如果执行方法花费了更多时间,即使使用可变长度编码也会增加字节数量。但是,依然比固定长度编码更有效率。

用常量表替换重复的API信息,SQL语句和字符串
处理大量请求的采样
使用异步数据传输来最小化应用线程中止
使用UDP传输数据

比较

cat pinpoint
依赖 Java 6,7,8
Maven3.2.3+
mysql5.6+
Linxu内核2.6+
Java 6,7,8
maven3+
Hbase0.94+
实现方式 代码埋点(拦截器,注解,过滤器等) java探针,字节码增强
存储选择 mysql , hdfs HBase
通信方式 - thrift
MQ监控 不支持 不支持
全局调用统计 支持 支持
trace查询 不支持 不支持
报警 支持 支持
JVM监控 不支持 支持
star数 4.5K 5.6K
优点 功能完善。 完全无侵入, 仅需修改启动方式,界面完善,功能细致。
缺点 代码侵入性较强,需要埋点
文档比较混乱,文档与发布版本的符合性较低,需要依赖点评私服 (或者需要把他私服上的jar手动下载下来,然后上传到我们的私服上去)。
不支持查询单个调用链, 对外表现的是整个应用的调用生态。
二次开发难度较高
文档 网上资料较少,仅官网提供的文档,比较乱 文档完善
开发者 大众点评 naver
使用公司 大众点评, 携程, 陆金所,同程旅游,猎聘网 naver

未完待续……

猜你喜欢

转载自blog.csdn.net/lijunpeng71/article/details/83822236