问题背景
在微服务架构系统中,一个应用往往被拆分多个微服务,因此一次请求就需要调用多个微服务。这些微服务可能是由不同的开发团队维护、使用不同的编程语言实现、部署在不同的机房、每个微服务部署的服务器数量差异很大(几十台到几千台)、横跨多个数据中心等,归结成一点,就是部署情况和调用情况复杂。
因此,需要一些可以帮助理解系统行为、进行性能分析的工具,以便在发生故障的时候,能够快速定位和解决问题。全链路监控就是为解决上面这些问题产生的,可参考谷歌论文(Google Dapper,https://bigbully.github.io/Dapper-translation/)。
在微服务系统架构中,几乎每个前端请求都需要由一个复杂的分布式服务调用链处理,如下图。要想理解这类系统的行为,就需要监控那些横跨在不同微服务、不同机器间的关联动作。
在业务规模不断增大、微服务不断增多及频繁变更的情况下,复杂的调用链带来一系列问题:
- 如何快速发现问题
- 如何确定故障影响范围
- 如何梳理服务依赖以及依赖的合理性
- 如何分析链路性能以及实时容量规划
另外,我们还需要关注每个微服务调用的性能指标,例如:
- 吞吐量,组件、平台、物理设备的吞吐量(TPS)。
- 响应时间,整体调用的响应时间和各个微服务的响应时间。
- 错误记录,单位时间内的异常次数。
全链路性能监控能从整体到局部展示各项指标,将跨应用的所有调用链上的性能信息集中展现,方便度量整体性能和局部性能,方便找到故障的来源,缩短故障排查时间。有了全链路监控工具,我们就能够
- 请求链路追踪,快速定位故障。可以通过调用链结合业务日志快速定位错误信息。
- 可视化:对各个调用阶段的进行性能分析。
- 依赖优化:各个调用环节的可用性、梳理服务依赖关系以及优化。
- 数据分析,优化链路:可以得到用户的行为路径,汇总分析业务场景。
目标要求
选择全链路监控组件有哪些要求呢,总结如下:
1)探针的性能消耗
对原应用的影响应该足够小。服务调用本身会带来性能损耗,这就需要调用跟踪的低损耗,实际中还会配置采样率,选择一部分请求去分析请求路径。在一些高度优化过的服务中,即使一点点损耗也很容易察觉,可能迫使在线服务的部署团队将跟踪系统关停。
2)低代码侵入性
应当尽可能少入侵或者无入侵业务系统,对使用方透明,减少开发人员的负担。对于应用的程序员来说,不需要知道跟踪系统。如果一个跟踪系统想生效,就必须需要依赖应用的开发者主动配合,那么这个跟踪系统也太脆弱了。
3) 可扩展性
一个优秀的调用跟踪系统必须支持分布式部署,具备良好的扩展性,提供便捷的插件开发API。对于一些没有监控到的组件,应用开发者也可以自行扩展。
4)满足数据分析需求
数据的分析要快,分析的维度尽可能多。跟踪系统能提供足够快的信息反馈,就可以对生产环境下的异常状况快速反应。
功能模块
一般的全链路监控系统,大致分四大模块:
1)埋点日志模块
埋点分为客户端埋点、服务端埋点,以及客户端和服务端双向埋点。埋点日志通常包含traceId、spanId、调用开始时间、协议类型、调用方ip和端口、请求的服务名、调用耗时、调用结果,异常信息等,并且预留扩展字段。如前所述,记录埋点日志不能造成性能负担,所以通过采样和异步log实现。
2)收集和存储模块
主要支持分布式日志采集,同时增加MQ缓冲,具体如下:
- 在每台机器上部署一个 daemon 做日志收集,业务进程把自己的Trace(调用栈)发到daemon,daemon把Trace发送至上级;
- 多级collector,它类似pub/sub架构,可以支持负载均衡;
- 对聚合数据进行实时分析和离线存储;
- 离线分析将同一调用链的埋点日志汇总在一起;
3)分析和统计模块
调用链跟踪分析就是把同一个traceId的span收集起来,按时间排序(timeline)。把parentId串起来就是调用栈。如果抛异常或超时,在日志里打印traceId。可以利用traceId查询调用链情况,从而定位问题。
依赖程度分析:
- 强依赖:调用失败会直接中断主流程
- 高度依赖:一次链路中调用某个依赖的几率高
- 频繁依赖:一次链路调用同一个依赖的次数多
离线分析:按traceId汇总,通过spanId和parentId还原调用关系,分析链路形态。
实时分析:对单条日志直接进行分析,不做汇总、重组,得到当前QPS和延迟等。
4)展现及决策支持模块
将各项性能指标以图表形式展现出来。
Google Dapper全链路监控方案
1)Span
针对每个调用链路(可以是RPC或访问DB,没有特定限制)创建一个span,可以用一个64位ID标识,用uuid作为spanId比较方便。span中还有其他数据,例如描述信息、时间戳、key-value格式的(Annotation)tag信息,parentId等。parentId可以标识调用来源。
上图说明了span在一次总体调用链中是什么样的。Dapper记录了span的名称以及每个span的ID和父ID,以重建在一次追踪过程中不同span之间的关系。如果一个span没有父ID,就被称为root span。所有span都在一个特定的跟踪链上,共用一个跟踪id。
2)Trace
Trace是一个类似于树结构的Span集合,表示一次完整的跟踪,从请求到服务器开始,服务器返回响应结束,跟踪每次RPC调用的耗时,存在唯一标识traceId。
在上图中,每种颜色的note标注了一个span,一条链路通过一个traceId唯一标识。Span标识前端发起的请求信息。树节点是整个架构的基本单元,而每一个节点又是对span的引用。节点之间的连线表示span和它的父span之间的直接关系。虽然span在日志文件中只是简单代表span的开始和结束时间,它们在整个树结构中是相对独立的。
3)Annotation
Annotation用来记录请求特定事件的相关信息(例如时间),一个span中会有多个annotation注解,通常包含四个注解:
- (1) cs:Client Start,表示客户端发起请求。
- (2) sr:Server Receive,表示服务端收到请求。
- (3) ss:Server Send,表示服务端完成处理,并将结果发送给客户端。
- (4) cr:Client Received,表示客户端获取到服务端返回信息。
4)调用示例
假如调用过程如下:
- 用户发起一个请求,首先到达A服务,然后A服务分别对B服务和C服务进行RPC调用;
- B服务处理完给A做出响应,但是C服务还需要和后端的D服务和E服务交互之后再返还给A服务,最后由A服务来响应用户请求;
调用过程追踪
- 1)请求到来后,生成一个全局traceId,通过traceId可以串联起整个调用链,一个traceId代表一次请求。
- 2)除了traceId外,还需要生成spanId用于记录调用关系。每个服务会记录下parentId和spanId,通过他们可以构造出一个完整的调用链。
- 3)一个没有parentId的span成为root span,是调用链入口。
- 4)整个调用过程中的每个请求都要透传traceId和spanId。
- 5)每个服务将该次请求附带的traceId和附spanId作为parentId记录下来,并且将自己生成的spanId也记录下来。
- 6)要查看某次完整的调用链只要根据traceId查出所有调用记录,然后通过parentId和spanId构造出整个调用关系。
调用链监控做了哪些工作
- 调用链数据生成,对整个调用过程的所有应用进行埋点并输出日志。
- 调用链数据采集,对各应用中的日志数据进行采集。
- 调用链数据存储及查询,对采集到的数据进行存储,由于日志数据量很大,不仅要能对其存储,还需要能快速查询。
- 性能指标计算,对采集到的日志数据进行各种运算,并且将运算结果保存。
- 报警功能,提供各种阀值报警功能。
整体部署架构
系统架构采用ELK架构,如下图所示。
其中,Agent负责生成调用链日志,logstash负责采集日志,kafka负责提供数据给下游,storm计算汇聚结果并写入es。Storm负责抽取trace数据并落地到es,这是为了提供复杂查询。比如通过时间维度查询调用链,可以很快查询出所有符合的traceId,根据这些traceId到 中去查数据就快了。下方的logstash负责将kafka原始数据存储到hbase。
Agent无侵入部署
通过agent代理实现无侵入式部署,可以把性能测量与业务逻辑完全分离,测量任意类的任意方法的执行时间。这种方式大大提高了采集效率,减少了运维成本。Agent根据服务跨度可以分为两大类:
- 服务内Agent,这种方式是通过Java Agent机制,对服务内部的方法调用信息进行数据收集,如方法调用耗时、入参、出参等信息。
- 跨服务Agent,这种情况需要对主流RPC框架以插件形式提供无缝支持。并通过提供标准数据规范以适应自定义RPC框架。
对整个调用链路进行监控有以下优势
- 可以准确的掌握应用的部署情况。
- 从调用链全流程分析性能,识别关键调用链并进行优化。
- 提供可追溯的性能数据,量化业务的价值。
- 快速定位性能问题,协助代码优化。
- 协助开发人员进行白盒测试。
总结
目前市面上的全链路监控系统大多都是借鉴自Google Dapper,并且Google Dapper也是开源的,感兴趣可以去学习一下它的源码。除了Google Dapper之外,还有Zipkin、Pinpoint和Skywalking等类似的全链路监控组件可以选用。