链路追踪
设计目标
- 无处不在的部署
- 持续的监控
- 低消耗
- 应用级的透明
- 延展性
- 低延迟
理论-Dapper
参考 Google Dapper 论文实现,为每个请求都生成一个全局唯一的 traceid,端到端透传到上下游所有节点,每一层生成一个 spanid,通过traceid 将不同系统孤立的调用日志和异常信息串联一起,通过 spanid 和 level 表达节点的父子关系。
Dapper,大规模分布式系统的跟踪系统: https://bigbully.github.io/Dapper-translation/
核心概念:
- Tree
- Span
- Annotation
调用链
在跟踪树结构中,树节点是整个架构的基本单元,而每一个节点又是对 span 的引用。虽然 span 在日志文件中只是简单的代表 span 的开始和结束时间,他们在整个树形结构中却是相对独立的。
核心概念:
- TraceID
- SpanID
- ParentID
- Family & Title
追踪信息
追踪信息包含时间戳、事件、方法名(Family+Title)、注释(TAG/Comment)。
客户端和服务器上的时间戳来自不同的主机,我们必须考虑到时间偏差,RPC 客户端发送一个请求之后,服务器端才能接收到,对于响应也是一样的(服务器先响应,然后客户端才能接收到这个响应)。这样一来,服务器端的 RPC 就有一个时间戳的一个上限和下限。
植入点
Dapper 可以以对应用开发者近乎零浸入的成本对分布式控制路径进行跟踪,几乎完全依赖于基于少量通用组件库的改造。如下:
当一个线程在处理跟踪控制路径的过程中,Dapper 把这次跟踪的上下文的在 ThreadLocal中进行存储,在 Go 语言中,约定每个方法首参数为 context(上下文)
覆盖通用的中间件&通讯框架、不限于:redis、memcache、rpc、http、database、queue。
架构图
跟踪消耗
处理跟踪消耗:
- 正在被监控的系统在生成追踪和收集追踪数据的消耗导致系统性能下降,
- 需要使用一部分资源来存储和分析跟踪数据,是Dapper性能影响中最关键的部分:
- 因为收集和分析可以更容易在紧急情况下被关闭,ID生成耗时、创建Span等;
- 修改agent nice值,以防在一台高负载的服务器上发生cpu竞争;
采样:
如果一个显着的操作在系统中出现一次,他就会出现上千次,基于这个事情我们不全量收集数据。
跟踪采样
固定采样,1/1024:
这个简单的方案是对我们的高吞吐量的线上服务来说是非常有用,因为那些感兴趣的事件(在大吞吐量的情况下)仍然很有可能经常出现,并且通常足以被捕捉到。然而,在较低的采样率和较低的传输负载下可能会导致错过重要事件,而想用较高的采样率就需要能接受的性能损耗。对于这样的系统的解决方案就是覆盖默认的采样率,这需要手动干预的,这种情况是我们试图避免在 Dapper 中出现的。
应对积极采样:
我们理解为单位时间期望采集样本的条目,在高 QPS 下,采样率自然下降,在低 QPS 下,采样率自然增加;比如1s内某个接口采集1条。
二级采样:
容器节点数量多,即使使用积极采样仍然会导致采样样本非常多,所以需要控制写入中央仓库的数据的总规模,利用所有 span 都来自一个特定的跟踪并分享同一个 traceid 这个事实,虽然这些 span 有可能横跨了数千个主机。
对于在收集系统中的每一个 span,我们用hash算法把 traceid 转成一个标量Z ,这里0<=Z<=1,我们选择了运行期采样率,这样就可以优雅的去掉我们无法写入到仓库中的多余数据,我们还可以通过调节收集系统中的二级采样率系数来调整这个运行期采样率,最终我们通过后端存储压力把策略下发给 agent采集系统,实现精准的二级采样。
下游采样:
越被依赖多的服务,网关层使用积极采样以后,对于 downstream 的服务采样率仍然很高。
API
搜索:
按照 Family(服务名)、Title(接口)、时间、调用者等维度进行搜索
详情:
根据单个 traceid,查看整体链路信息,包含 span、level 统计,span 详情,依赖的服务、组件信息等;
全局依赖图:
由于服务之间的依赖是动态改变的,所以不可能仅从配置信息上推断出所有这些服务之间的依赖关系,能够推算出任务各自之间的依赖,以及任务和其他软件组件之间的依赖。
依赖搜索:
搜索单个服务的依赖情况,方便我们做“异地多活”时候来全局考虑资源的部署情况,以及区分服务是否属于多活范畴,也可以方便我们经常性的梳理依赖服务和层级来优化我们的整体架构可用性。
推断环依赖:
一个复杂的业务架构,很难避免全部是层级关系的调用,但是我们要尽可能保证一点:调用栈永远向下,即:不产生环依赖。
经验&优化
性能优化:
1、不必要的串行调用;2、缓存读放大;3、数据库写放大;4、服务接口聚合调用;
异常日志系统集成:
如果这些异常发生在 Dapper 跟踪采样的上下文中,那么相应的 traceid 和 spanid 也会作为元数据记录在异常日志中。异常监测服务的前端会提供一个链接,从特定的异常信息的报告直接导向到他们各自的分布式跟踪;
用户日志集成:
在请求的头中返回 traceid,当用户遇到故障或者上报客服我们可以根据 traceid 作为整个请求链路的关键字,再根据接口级的服务依赖接口所涉及的服务并行搜索 ES Index,聚合排序数据,就比较直观的诊断问题了;
容量预估:
根据入口网关服务,推断整体下游服务的调用扇出来精确预估流量再各个系统的占比;
网络热点&易故障点:
我们内部 RPC 框架还不够统一,以及基础库的组件部分还没解决拿到应用层协议大小,如果我们收集起来,可以很简单的实现流量热点、机房热点、异常流量等情况。同理容易失败的 span,很容易统计出来,方便我们辨识服务的易故障点;
opentraceing:
标准化的推广,上面几个特性,都依赖 span TAG 来进行计算,因此我们会逐步完成标准化协议,也更方便我们开源,而不是一个内部“特殊系统”;