主流微服务全链路监控系统之战
共 11331字,需浏览 23分钟
·
2021-01-29 15:45
来源:https://www.jianshu.com/p/92a12de11f18
问题背景
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。
互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。
因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题。
全链路监控组件就在这样的问题背景下产生了。最出名的是谷歌公开的论文提到的 Google Dapper。
想要在这个上下文中理解分布式系统的行为,就需要监控那些横跨了不同的应用、不同的服务器之间的关联动作。
所以,在复杂的微服务架构系统中,几乎每一个前端请求都会形成一个复杂的分布式服务调用链路。一个请求完整调用链可能如下图所示:
![](https://filescdn.proginn.com/99eac19f44dcef4bb141792ac6de050b/59376c55504013beae360b3eefa30cda.webp)
如何快速发现问题? 如何判断故障影响范围? 如何梳理服务依赖以及依赖的合理性? 如何分析链路性能问题以及实时容量规划?
吞吐量,根据拓扑可计算相应组件、平台、物理设备的实时吞吐量。
响应时间,包括整体调用的响应时间和各个服务的响应时间等。
错误记录,根据服务返回统计单位时间异常次数。
请求链路追踪,故障快速定位:可以通过调用链结合业务日志快速定位错误信息。 可视化:各个阶段耗时,进行性能分析。 依赖优化:各个调用环节的可用性、梳理服务依赖关系以及优化。 数据分析,优化链路:可以得到用户的行为路径,汇总分析应用在很多业务场景。
1 目标要求
1、探针的性能消耗
APM 组件服务的影响应该做到足够小。服务调用埋点本身会带来性能损耗,这就需要调用跟踪的低损耗,实际中还会通过配置采样率的方式,选择一部分请求去分析请求路径。在一些高度优化过的服务,即使一点点损耗也会很容易察觉到,而且有可能迫使在线服务的部署团队不得不将跟踪系统关停。
2、代码的侵入性
即也作为业务组件,应当尽可能少入侵或者无入侵其他业务系统,对于使用方透明,减少开发人员的负担。
对于应用的程序员来说,是不需要知道有跟踪系统这回事的。如果一个跟踪系统想生效,就必须需要依赖应用的开发者主动配合,那么这个跟踪系统也太脆弱了,往往由于跟踪系统在应用中植入代码的 bug 或疏忽导致应用出问题,这样才是无法满足对跟踪系统“无所不在的部署”这个需求。
3、可扩展性
4、数据的分析
2 功能模块
一般的全链路监控系统,大致可分为四大功能模块:
1.埋点与生成日志
埋点即系统在当前节点的上下文信息,可以分为 客户端埋点、服务端埋点,以及客户端和服务端双向型埋点。埋点日志通常要包含以下内容traceId、spanId、调用的开始时间,协议类型、调用方ip和端口,请求的服务名、调用耗时,调用结果,异常信息等,同时预留可扩展字段,为下一步扩展做准备;
不能造成性能负担:一个价值未被验证,却会影响性能的东西,是很难在公司推广的! 因为要写 log,业务 QPS 越高,性能影响越重。通过采样和异步log解决。
2.收集和存储日志
每个机器上有一个 deamon 做日志收集,业务进程把自己的 Trace 发到 daemon,daemon 把收集 Trace 往上一级发送;
多级的 collector,类似 pub/sub 架构,可以负载均衡;
对聚合的数据进行 实时分析和离线存储;
离线分析 需要将同一条调用链的日志汇总在一起;
3.分析和统计调用链路数据,以及时效性
调用链跟踪分析:把同一 TraceID 的 Span 收集起来,按时间排序就是timeline。把ParentID 串起来就是调用栈。
抛异常或者超时,在日志里打印 TraceID。利用 TraceID 查询调用链情况,定位问题。
强依赖:调用失败会直接中断主流程
高度依赖:一次链路中调用某个依赖的几率高
频繁依赖:一次链路调用同一个依赖的次数多
离线分析:按TraceID汇总,通过Span的ID和ParentID还原调用关系,分析链路形态。
实时分析:对单条日志直接分析,不做汇总,重组。得到当前QPS,延迟。
4.展现以及决策支持
3 Google Dapper
3.1 Span
基本工作单元,一次链路调用(可以是RPC,DB等没有特定的限制)创建一个span,通过一个64位ID标识它,uuid较为方便,span中还有其他的数据,例如描述信息,时间戳,key-value对的(Annotation)tag信息,parent_id等,其中parent-id可以表示span调用链路来源。
![](https://filescdn.proginn.com/ea298c193f6c9341210c9b0201fddec7/302efbc7533352be119b2b93b183f348.webp)
上图说明了 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就由你的一次请求组成。
![](https://filescdn.proginn.com/c66abe76c39e8f1a9ca106bea355383d/38233e221ca087cee9b1cf74ae08643d.webp)
每种颜色的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 调用示例
1. 请求调用示例
当用户发起一个请求时,首先到达前端A服务,然后分别对B服务和C服务进行RPC调用;
B服务处理完给A做出响应,但是C服务还需要和后端的D服务和E服务交互之后再返还给A服务,最后由A服务来响应用户的请求;
![](https://filescdn.proginn.com/51f2c04cc676a9df210e831eb011360b/c352a6d9eb548d08acd09c4851b86281.webp)
2. 调用过程追踪
请求到来生成一个全局 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组织起整个调用父子关系。
![](https://filescdn.proginn.com/b62612753c4b3af8d1ffb0a3d9eaebe8/74c196372abd10e814a98333d67afd49.webp)
3. 调用链核心工作
调用链数据生成,对整个调用过程的所有应用进行埋点并输出日志。
调用链数据采集,对各个应用中的日志数据进行采集。
调用链数据存储及查询,对采集到的数据进行存储,由于日志数据量一般都很大,不仅要能对其存储,还需要能提供快速查询。
指标运算、存储及查询,对采集到的日志数据进行各种指标运算,将运算结果保存起来。
告警功能,提供各种阀值警告功能。
4. 整体部署架构
![](https://filescdn.proginn.com/d6ab2577fd87cceb47b4e4e4e6268a88/002aff4f678fbc74ef4423bc8d4ddaeb.webp)
5. AGENT无侵入部署
通过AGENT代理无侵入式部署,将性能测量与业务逻辑完全分离,可以测量任意类的任意方法的执行时间,这种方式大大提高了采集效率,并且减少运维成本。根据服务跨度主要分为两大类AGENT:
服务内AGENT,这种方式是通过 Java 的agent机制,对服务内部的方法调用层次信息进行数据收集,如方法调用耗时、入参、出参等信息。
跨服务AGENT,这种情况需要对主流RPC框架以插件形式提供无缝支持。并通过提供标准数据规范以适应自定义RPC框架:
(1)Dubbo支持;
(2)Rest支持;
(3)自定义RPC支持;
6. 调用链监控好处
准确掌握生产一线应用部署情况;
从调用链全流程性能角度,识别对关键调用链,并进行优化;
提供可追溯的性能数据,量化 IT 运维部门业务价值;
快速定位代码性能问题,协助开发人员持续性的优化代码;
协助开发人员进行白盒测试,缩短系统上线稳定期;
4 方案比较
Zipkin:由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。 Pinpoint:一款对Java编写的大规模分布式系统的APM工具,由韩国人开源的分布式跟踪组件。 Skywalking:国产的优秀APM组件,是一个对JAVA分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统。
1.探针的性能
主要是agent对服务的吞吐量、CPU和内存的影响。微服务的规模和动态性使得数据收集的成本大幅度提高。
2.collector的可扩展性
能够水平扩展以便支持大规模服务器集群。
3.全面的调用链路数据分析
提供代码级别的可见性以便轻松定位失败点和瓶颈。
4.对于开发透明,容易开关
添加新功能而无需修改代码,容易启用或者禁用。
5.完整的调用链应用拓扑
自动检测应用拓扑,帮助你搞清楚应用的架构
4.1 探针的性能
![](https://filescdn.proginn.com/e172ba517c6cb50ec9976dfb0c7da492/ea945e95007f4edd3337ffa1aa770a07.webp)
从上表可以看出,在三种链路监控组件中,skywalking 的探针对吞吐量的影响最小,zipkin的吞吐量居中。pinpoint 的探针对吞吐量的影响较为明显,在500并发用户时,测试服务的吞吐量从1385降低到774,影响很大。然后再看下CPU和memory的影响,在内部服务器进行的压测,对CPU和memory的影响都差不多在10%之内。
4.2 collector的可扩展性
1. zipkin
开发 zipkin-Server(其实就是提供的开箱即用包),zipkin-agent 与 zipkin-Server 通过 http 或者 mq 进行通信,http 通信会对正常的访问造成影响,所以还是推荐基于mq异步方式通信,zipkin-Server 通过订阅具体的 topic 进行消费。这个当然是可以扩展的,多个 zipkin-Server 实例进行异步消费 mq 中的监控信息。
![](https://filescdn.proginn.com/1bcc1e25de155d2c3339c03a9ec86637/ad2f610b8009e8e2507362691a3791fe.webp)
4.3 全面的调用链路数据分析
全面的调用链路数据分析,提供代码级别的可见性以便轻松定位失败点和瓶颈。
![](https://filescdn.proginn.com/9206c04688224fad1b5008f290c457c5/dba9b1dc015235e1d986a6daf1b90289.webp)
zipkin链路调用分析
![](https://filescdn.proginn.com/3d248ad3da80df937b6bbea44b5aceef/a8d2e260efdcb15f64bfbb92e19661f5.webp)
skywalking链路调用分析
![](https://filescdn.proginn.com/798cc66faab8e5fdfa2fd32d281fd5d0/cce8f1c633b57bd9bba16e76433d9abb.webp)
pinpoint链路调用分析
4.4 对于开发透明,容易开关
对于开发透明,容易开关,添加新功能而无需修改代码,容易启用或者禁用。我们期望功能可以不修改代码就工作并希望得到代码级别的可见性。
对于这一点,Zipkin 使用修改过的类库和它自己的容器(Finagle)来提供分布式事务跟踪的功能。但是,它要求在需要时修改代码。skywalking和pinpoint都是基于字节码增强的方式,开发人员不需要修改代码,并且可以收集到更多精确的数据因为有字节码中的更多信息。
4.5 完整的调用链应用拓扑
自动检测应用拓扑,帮助你搞清楚应用的架构。
![](https://filescdn.proginn.com/c9aff0b4e12e43346ee0bb9587b26c50/04cbf4aa3f466c8d1b5b47fad0ac70c8.webp)
![](https://filescdn.proginn.com/17d36d097688436f7e6297e97a41ac80/88192bfa1e672afa34dabd5958474c67.webp)
![](https://filescdn.proginn.com/dfdef6bfb7c858e42a55d18269dbc880/e8701a45a39e42569cf8da4b2462ad5a.webp)
上面三幅图,分别展示了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 也可以跟踪到方法调用级别,只是具体实现并没有这样做而已。
4.6.3 字节码注入 vs API 调用
Pinpoint 实现了基于字节码注入的 Java Agent 探针,而 Zipkin 的 Brave 框架仅仅提供了应用层面的 API,但是细想问题远不那么简单。字节码注入是一种简单粗暴的解决方案,理论上来说无论任何方法调用,都可以通过注入代码的方式实现拦截,也就是说没有实现不了的,只有不会实现的。但 Brave 则不同,其提供的应用层面的 API 还需要框架底层驱动的支持,才能实现拦截。
比如,MySQL 的 JDBC 驱动,就提供有注入 interceptor 的方法,因此只需要实现 StatementInterceptor 接口,并在 Connection String 中进行配置,就可以很简单的实现相关拦截;而与此相对的,低版本的 MongoDB 的驱动或者是 Spring Data MongoDB 的实现就没有如此接口,想要实现拦截查询语句的功能,就比较困难。
因此在这一点上,Brave 是硬伤,无论使用字节码注入多么困难,但至少也是可以实现的,但是 Brave 却有无从下手的可能,而且是否可以注入,能够多大程度上注入,更多的取决于框架的 API 而不是自身的能力。
4.6.4 难度及成本
经过简单阅读 Pinpoint 和 Brave 插件的代码,可以发现两者的实现难度有天壤之别。在都没有任何开发文档支撑的前提下,Brave 比 Pinpoint 更容易上手。Brave 的代码量很少,核心功能都集中在 brave-core 这个模块下,一个中等水平的开发人员,可以在一天之内读懂其内容,并且能对 API 的结构有非常清晰的认识。
Pinpoint 的代码封装也是非常好的,尤其是针对字节码注入的上层 API 的封装非常出色,但是这依然要求阅读人员对字节码注入多少有一些了解,虽然其用于注入代码的核心 API 并不多,但要想了解透彻,恐怕还得深入 Agent 的相关代码,比如很难一目了然的理解 addInterceptor 和 addScopedInterceptor 的区别,而这两个方法就是位于 Agent 的有关类型中。
因为 Brave 的注入需要依赖底层框架提供相关接口,因此并不需要对框架有一个全面的了解,只需要知道能在什么地方注入,能够在注入的时候取得什么数据就可以了。就像上面的例子,我们根本不需要知道 MySQL 的 JDBC Driver 是如何实现的也可以做到拦截 SQL 的能力。但是 Pinpoint 就不然,因为 Pinpoint 几乎可以在任何地方注入任何代码,这需要开发人员对所需注入的库的代码实现有非常深入的了解,通过查看其 MySQL 和 Http Client 插件的实现就可以洞察这一点,当然这也从另外一个层面说明 Pinpoint 的能力确实可以非常强大,而且其默认实现的很多插件已经做到了非常细粒度的拦截。
针对底层框架没有公开 API 的时候,其实 Brave 也并不完全无计可施,我们可以采取 AOP 的方式,一样能够将相关拦截注入到指定的代码中,而且显然 AOP 的应用要比字节码注入简单很多。
以上这些直接关系到实现一个监控的成本,在 Pinpoint 的官方技术文档中,给出了一个参考数据。如果对一个系统集成的话,那么用于开发 Pinpoint 插件的成本是 100,将此插件集成入系统的成本是 0;但对于 Brave,插件开发的成本只有 20,而集成成本是 10。从这一点上可以看出官方给出的成本参考数据是 5:1。但是官方又强调了,如果有 10 个系统需要集成的话,那么总成本就是 10 * 10 + 20 = 120,就超出了 Pinpoint 的开发成本 100,而且需要集成的服务越多,这个差距就越大。
4.6.5 通用性和扩展性
很显然,这一点上 Pinpoint 完全处于劣势,从社区所开发出来的集成接口就可见一斑。
Pinpoint 的数据接口缺乏文档,而且也不太标准(参考论坛讨论帖),需要阅读很多代码才可能实现一个自己的探针(比如 Node 的或者 PHP 的)。而且团队为了性能考虑使用了 Thrift 作为数据传输协议标准,比起 HTTP 和 JSON 而言难度增加了不少。
4.6.6 社区支持
这一点也不必多说,Zipkin 由 Twitter 开发,可以算得上是明星团队,而 Naver 的团队只是一个默默无闻的小团队(从 #1759 的讨论中可以看出)。虽然说这个项目在短期内不太可能消失或停止更新,但毕竟不如前者用起来更加放心。而且没有更多社区开发出来的插件,让 Pinpoint 只依靠团队自身的力量完成诸多框架的集成实属困难,而且他们目前的工作重点依然是在提升性能和稳定性上。
4.6.7 其他
4.6.8 总结
5 Tracing 和 Monitor 区别
- END -
推荐阅读 Prometheus 监控服务端口、网站状态等(黑盒监测) kubevirt在360的探索之路(K8s接管虚拟化),涨姿势~ 超详细!Kubernetes 学习笔记总结 基于 Nginx+lua+Memcache 实现灰度发布 Kubernetes生产环境最佳实践 一文搞懂蓝绿发布、灰度发布和滚动发布 高性能 Nginx HTTPS 调优 - 如何为 HTTPS 提速 30% 记一次 Linux服务器被入侵后的排查思路
点亮,服务器三年不宕机