0 问题背景
随着微服务架构的流行, 服务按照不同的维度进行拆分 ,一次请求往往需要涉及到多个服务。 互联网应用构建在不同的软件模块集上 ,这些软件模块, 有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心 。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题。
全链路监控组件就在这样的问题背景下产生了。最出名的是谷歌公开的论文提到的 Google Dapper 。 想要在这个上下文中理解分布式系统的行为,就需要监控那些横跨了不同的应用、不同的服务器之间的关联动作 。
所以, 在复杂的微服务架构系统中,几乎每一个前端请求都会形成一个复杂的分布式服务调用链路 。一个请求完整调用链可能如下图所示:
一个请求完整调用链
那么在业务规模不断增大、服务不断增多以及频繁变更的情况下,面对复杂的调用链路就带来一系列问题:
如何快速发现问题?
如何判断故障影响范围?
如何梳理服务依赖以及依赖的合理性?
如何分析链路性能问题以及实时容量规划?
同时我们会关注在请求处理期间各个调用的各项性能指标 ,比如:吞吐量(TPS)、响应时间及错误记录等。
吞吐量 ,根据拓扑可计算相应组件、平台、物理设备的实时吞吐量。
响应时间 ,包括整体调用的响应时间和各个服务的响应时间等。
错误记录 ,根据服务返回统计单位时间异常次数。
全链路性能监控 从整体维度到局部维度展示各项指标 ,将跨应用的所有调用链性能信息集中展现,可方便度量整体和局部性能,并且方便找到故障产生的源头,生产上可极大缩短故障排除时间。
有了全链路监控工具,我们能够达到:
请求链路追踪,故障快速定位 :可以通过调用链结合业务日志快速定位错误信息。
可视化 : 各个阶段耗时,进行性能分析。
依赖优化 :各个调用环节的可用性、梳理服务依赖关系以及优化。
数据分析,优化链路 :可以得到用户的行为路径,汇总分析应用在很多业务场景。
1 目标要求
如上所述,那么我们选择全链路监控组件有哪些目标要求呢?Google Dapper中也提到了,总结如下:
-
探针的性能消耗
-
代码的侵入性
-
可扩展性
-
数据的分析
2 功能模块
一般的全链路监控系统,大致可分为四大功能模块:
-
埋点与生成日志
-
收集和存储日志
-
分析和统计调用链路数据,以及时效性
-
展现以及决策支持
3 Google Dapper
3.1 Span
基本工作单元 ,一次链路调用(可以是RPC,DB等没有特定的限制)创建一个span,通过一个64位ID标识它,uuid较为方便,span中还有其他的数据,例如描述信息,时间戳,key-value对的(Annotation)tag信息,parent_id等,其中parent-id可以表示span调用链路来源。
上图说明了span在一次大的跟踪过程中是什么样的。 Dapper记录了span名称,以及每个span的ID和父ID,以重建在一次追踪过程中不同span之间的关系 。如果一个span没有父ID被称为root span。 所有span都挂在一个特定的跟踪上,也共用一个跟踪id 。
Span数据结构 :
type Span struct {
TraceID int64 // 用于标示一次完整的请求id
Name string
ID int64 // 当前这次调用span_id
ParentID int64 // 上层服务的调用span_id 最上层服务parent_id为null
Annotation []Annotation // 用于标记的时间戳
Debug bool
}
复制代码
3.2 Trace
类似于 树结构的Span集合 ,表示一次完整的跟踪,从请求到服务器开始,服务器返回response结束,跟踪每次rpc调用的耗时,存在唯一标识trace_id。比如:你运行的分布式大数据存储一次Trace就由你的一次请求组成。
每种颜色的note标注了一个span,一条链路通过TraceId唯一标识,Span标识发起的请求信息。 树节点是整个架构的基本单元,而每一个节点又是对span的引用 。节点之间的连线表示的span和它的父span直接的关系。虽然span在日志文件中只是简单的代表span的开始和结束时间,他们在整个树形结构中却是相对独立的。
3.3 Annotation
注解,用来记录请求特定事件相关信息(例如时间),一个span中会有多个annotation注解描述 。通常包含四个注解信息:
(1) cs :Client Start,表示客户端发起请求
(2) sr :Server Receive,表示服务端收到请求
(3) ss :Server Send,表示服务端完成处理,并将结果发送给客户端
(4) cr :Client Received,表示客户端获取到服务端返回信息
Annotation数据结构 :
type Annotation struct {
Timestamp int64
Value string
Host Endpoint
Duration int32
}
复制代码
3.4 调用示例
- 请求调用示例
当用户发起一个请求时,首先到达前端A服务,然后分别对B服务和C服务进行RPC调用;
B服务处理完给A做出响应,但是C服务还需要和后端的D服务和E服务交互之后再返还给A服务,最后由A服务来响应用户的请求;
- 调用过程追踪
请求到来生成一个全局TraceID ,通过TraceID可以串联起整个调用链,一个TraceID代表一次请求。
除了TraceID外, 还需要SpanID用于记录调用父子关系 。每个服务会记录下parent id和span id, 通过他们可以组织一次完整调用链的父子关系 。
一个没有parent id的span成为root span,可以看成调用链入口。
所有这些ID可用全局唯一的64位整数表示;
整个调用过程中每个请求都要透传TraceID和SpanID 。
每个服务将该次请求附带的TraceID和附带的SpanID作为parent id记录下,并且将自己生成的SpanID也记录下。
要查看某次完整的调用则 只要根据TraceID查出所有调用记录,然后通过parent id和span id组织起整个调用父子关系 。
- 调用链核心工作
调用链数据生成 ,对整个调用过程的所有应用进行埋点并输出日志。
调用链数据采集 ,对各个应用中的日志数据进行采集。
调用链数据存储及查询 ,对采集到的数据进行存储,由于日志数据量一般都很大,不仅要能对其存储,还需要能提供快速查询。
指标运算、存储及查询 ,对采集到的日志数据进行各种指标运算,将运算结果保存起来。
告警功能 ,提供各种阀值警告功能。
- 整体部署架构
通过AGENT生成调用链日志。
通过logstash采集日志到kafka。
kafka负责提供数据给下游消费。
storm计算汇聚指标结果并落到es。
storm抽取trace数据并落到es,这是为了提供比较复杂的查询 。比如通过时间维度查询调用链,可以很快查询出所有符合的traceID, 根据这些traceID再去 Hbase 查数据就快了 。
logstash将kafka原始数据拉取到hbase中。 hbase的rowkey为traceID,根据traceID查询是很快的 。
- AGENT无侵入部署
通过AGENT代理无侵入式部署,将性能测量与业务逻辑完全分离,可以测量任意类的任意方法的执行时间,这种方式大大提高了采集效率,并且减少运维成本。 根据服务跨度主要分为两大类AGENT :
务内AGENT ,这种方式是通过 Java 的agent机制,对服务内部的方法调用层次信息进行数据收集,如方法调用耗时、入参、出参等信息。
跨服务AGENT ,这种情况需要对主流RPC框架以插件形式提供无缝支持。并通过提供标准数据规范以适应自定义RPC框架:
(1)Dubbo支持; (2)Rest支持; (3)自定义RPC支持;
- 调用链监控好处
准确掌握生产一线应用部署情况 ;
从调用链全流程性能角度, 识别对关键调用链,并进行优化 ;
提供可追溯的性能数据 ,量化 IT 运维部门业务价值;
快速定位代码性能问题 ,协助开发人员持续性的优化代码;
协助开发人员进行白盒测试 ,缩短系统上线稳定期;
4 方案比较
市面上的全链路监控理论模型大多都是借鉴Google Dapper论文,本文重点关注以下三种APM组件:
Zipkin :由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。
Pinpoint :一款对Java编写的大规模分布式系统的APM工具,由韩国人开源的分布式跟踪组件。
Skywalking :国产的优秀APM组件,是一个对JAVA分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统。
以上三种全链路监控方案需要对比的项提炼出来 :
-
探针的性能
-
collector的可扩展性
-
全面的调用链路数据分析
-
对于开发透明,容易开关
-
完整的调用链应用拓扑
4.1 探针的性能
比较关注探针的性能,毕竟APM定位还是工具,如果启用了链路监控组建后,直接导致吞吐量降低过半,那也是不能接受的。对skywalking、zipkin、pinpoint进行了压测,并与基线(未使用探针)的情况进行了对比。
选用了一个常见的基于Spring的应用程序,他包含Spring Boot, Spring MVC,redis客户端,mysql。 监控这个应用程序,每个trace,探针会抓取5个span(1 Tomcat, 1 SpringMVC, 2 Jedis, 1 Mysql)。这边基本和 skywalkingtest 的测试应用差不多。
模拟了三种并发用户:500,750,1000。使用jmeter测试,每个线程发送30个请求,设置思考时间为10ms。使用的采样率为1,即100%,这边与生产可能有差别。pinpoint默认的采样率为20,即50%,通过设置agent的配置文件改为100%。zipkin默认也是1。组合起来,一共有12种。下面看下汇总表:
探针性能对比
从上表可以看出,在三种链路监控组件中, skywalking的探针对吞吐量的影响最小,zipkin的吞吐量居中。pinpoint的探针对吞吐量的影响较为明显 ,在500并发用户时,测试服务的吞吐量从1385降低到774,影响很大。然后再看下CPU和memory的影响,在内部服务器进行的压测,对CPU和memory的影响都差不多在10%之内。
4.2 collector的可扩展性
collector的可扩展性,使得能够水平扩展以便支持大规模服务器集群。
- zipkin
2. skywalking
skywalking的collector支持两种部署方式: 单机和集群模式。collector与agent之间的通信使用了gRPC 。
3. pinpoint
同样,pinpoint也是支持集群和单机部署的。 pinpoint agent通过thrift通信框架,发送链路信息到collector 。
4.3 全面的调用链路数据分析
全面的调用链路数据分析,提供代码级别的可见性以便轻松定位失败点和瓶颈。
- zipkin
zipkin的链路监控粒度相对没有那么细 ,从上图可以看到调用链中具体到接口级别,再进一步的调用信息并未涉及。
2. skywalking
skywalking 还支持20+的中间件、框架、类库 ,比如:主流的dubbo、Okhttp,还有DB和消息中间件。上图skywalking链路调用分析截取的比较简单,网关调用user服务, 由于支持众多的中间件,所以skywalking链路调用分析比zipkin完备些 。
3. pinpoint
pinpoint应该是这三种APM组件中, 数据分析最为完备的组件 。提供代码级别的可见性以便轻松定位失败点和瓶颈,上图可以看到对于执行的sql语句,都进行了记录。还可以配置报警规则等,设置每个应用对应的负责人,根据配置的规则报警,支持的中间件和框架也比较完备。
4.4 对于开发透明,容易开关
对于开发透明,容易开关,添加新功能而无需修改代码,容易启用或者禁用。我们期望功能可以不修改代码就工作并希望得到代码级别的可见性。
对于这一点, Zipkin 使用修改过的类库和它自己的容器(Finagle)来提供分布式事务跟踪的功能 。但是,它要求在需要时修改代码。 skywalking和pinpoint都是基于字节码增强的方式,开发人员不需要修改代码,并且可以收集到更多精确的数据因为有字节码中的更多信息 。
4.5 完整的调用链应用拓扑
自动检测应用拓扑,帮助你搞清楚应用的架构。
pinpoint链路拓扑
skywalking链路拓扑
zipkin链路拓扑
上面三幅图,分别展示了APM组件各自的调用拓扑,都能实现完整的调用链应用拓扑。相对来说, pinpoint界面显示的更加丰富,具体到调用的DB名,zipkin的拓扑局限于服务于服务之间 。
4.6 Pinpoint与Zipkin细化比较
4.6.1 Pinpoint与Zipkin差异性
-
Pinpoint 是一个完整的性能监控解决方案 :有从探针、收集器、存储到 Web 界面等全套体系; 而 Zipkin 只侧重收集器和存储服务 ,虽然也有用户界面,但其功能与 Pinpoint 不可同日而语。 反而 Zipkin 提供有 Query 接口 ,更强大的用户界面和系统集成能力,可以基于该接口二次开发实现。
-
Zipkin 官方提供有基于 Finagle 框架(Scala 语言)的接口 ,而其他框架的接口由社区贡献,目前可以支持 Java、Scala、Node、Go、Python、Ruby 和 C# 等主流开发语言和框架;但是 Pinpoint 目前只有官方提供的 Java Agent 探针 ,其他的都在请求社区支援中(请参见 #1759 和 #1760 )。
-
Pinpoint 提供有 Java Agent 探针,通过字节码注入的方式实现调用拦截和数据收集, 可以做到真正的代码无侵入,只需要在启动服务器的时候添加一些参数,就可以完成探针的部署 ;而 Zipkin 的 Java 接口实现 Brave ,只提供了基本的操作 API,如果需要与框架或者项目集成的话, 就需要手动添加配置文件或增加代码 。
-
Pinpoint 的后端存储基于 HBase,而 Zipkin 基于 Cassandra 。
4.6.2 Pinpoint与Zipkin相似性
Pinpoint 与 Zipkin 都是基于 Google Dapper 的那篇论文,因此理论基础大致相同。 两者都是将服务调用拆分成若干有级联关系的 Span,通过 SpanId 和 ParentSpanId 来进行调用关系的级联;最后再将整个调用链流经的所有的 Span 汇聚成一个 Trace,报告给服务端的 collector 进行收集和存储 。
即便在这一点上,Pinpoint 所采用的概念也不完全与那篇论文一致。比如他采用 TransactionId 来取代 TraceId,而真正的 TraceId 是一个结构,里面包含了 TransactionId, SpanId 和 ParentSpanId 。而且 Pinpoint 在 Span 下面又增加了一个 SpanEvent 结构,用来记录一个 Span 内部的调用细节(比如具体的方法调用等等),因此 Pinpoint 默认会比 Zipkin 记录更多的跟踪数据 。但是理论上并没有限定 Span 的粒度大小,所以一个服务调用可以是一个 Span,那么每个服务中的方法调用也可以是个 Span,这样的话, 其实 Brave 也可以跟踪到方法调用级别,只是具体实现并没有这样做而已 。