11|链路追踪:如何定制一个分布式链路跟踪系统 ?
下载APP
关闭
渠道合作
推荐作者
11|链路追踪:如何定制一个分布式链路跟踪系统 ?
2022-11-16 徐长龙 来自北京
《高并发系统实战课》
课程介绍
讲述:徐长龙
时长20:28大小18.69M
你好,我是徐长龙,这节课我们讲一讲如何实现分布式链路跟踪。
分布式链路跟踪服务属于写多读少的服务,是我们线上排查问题的重要支撑。我经历过的一个系统,同时支持着多条业务线,实际用上的服务器有两百台左右,这种量级的系统想排查故障,难度可想而知。
因此,我结合 ELK 特性设计了一套十分简单的全量日志分布式链路跟踪,把日志串了起来,大大降低了系统排查难度。
目前市面上开源提供的分布式链路跟踪都很抽象,当业务复杂到一定程度的时候,为核心系统定制一个符合自己业务需要的链路跟踪,还是很有必要的。
事实上,实现一个分布式链路跟踪并不难,而是难在埋点、数据传输、存储、分析上,如果你的团队拥有这些能力,也可以很快制作出一个链路跟踪系统。所以下面我们一起看看,如何实现一个简单的定制化分布式链路跟踪。
监控行业发展现状
在学习如何制作一个简单的分布式链路跟踪之前,为了更好了解这个链路跟踪的设计特点,我们先简单了解一下监控行业的现状。
最近监控行业有一次大革新,现代的链路跟踪标准已经不拘泥于请求的链路跟踪,目前已经开始进行融合,新的标准和我们定制化的分布式链路跟踪的设计思路很相似,即 Trace、Metrics、日志合并成一套系统进行建设。
三种监控各有千秋
在此之前,常见监控系统主要有三种类型:Metrics、Tracing 和 Logging。
Granfana官网展示的 Metrics
常见的开源 Metrics 有 Zabbix、Nagios、Prometheus、InfluxDb、OpenFalcon,主要做各种量化指标汇总统计,比如监控系统的容量剩余、每秒请求量、平均响应速度、某个时段请求量多少。
常见的开源链路跟踪有 Jaeger、Zipkin、Pinpoint、Skywalking,主要是通过分析每次请求链路监控分析的系统,我么可以通过 TraceID 查找一次请求的依赖及调用链路,分析故障点和传导过程的耗时。
Skywalking官方trace界面
kibana(ELK)官网,日志查找
而常见的开源 Logging 有 ELK、Loki、Loggly,主要是对文本日志的收集归类整理,可以对错误日志进行汇总、警告,并分析系统错误异常等情况。
这三种监控系统可以说是大服务集群监控的主要支柱,它们各有优点,但一直是分别建设的。这让我们的系统监控存在一些割裂和功能重复,而且每一个标准都需要独立建设一个系统,然后在不同界面对同一个故障进行分析,排查问题时十分不便。
随着行业发展,三位一体的标准应运而生,这就是 OpenTelemetry 标准(集成了 OpenCensus、OpenTracing 标准)。这个标准将 Metrics+Tracing+Logging 集成一体,这样我们监控系统的时候就可以通过三个维度综合观测系统运转情况。
常见 OpenTelemetry 开源项目中的 Prometheus、Jaeger 正在遵循这个标准逐步改进实现 OpenTelemetry 实现的结构如下图所示:
OpenTelemetry标准架构
事实上,分布式链路跟踪系统及监控主要提供了以下支撑服务:
监控日志标准
埋点 SDK(AOP 或侵入式)
日志收集
分布式日志传输
分布式日志存储
分布式检索计算
分布式实时分析
个性化定制指标盘
系统警告
我建议使用 ELK 提供的功能去实现分布式链路跟踪系统,因为它已经完整提供了如下功能:
日志收集(Filebeat)
日志传输(Kafka+Logstash)
日志存储(Elasticsearch)
检索计算(Elasticsearch + Kibana)
实时分析(Kibana)
个性定制表格查询(Kibana)
这样一来,我只需要制定日志格式、埋点 SDK,即可实现一个具有分布式链路跟踪、Metrics、日志分析系统。
事实上,Log、Metrics、trace 三种监控体系最大的区别就是日志格式标准,底层实现其实是很相似的。既然 ELK 已提供我们需要的分布式相关服务,下面我简单讲讲日志格式和 SDK 埋点,通过这两个点我们就可以窥见分布式链路跟踪的全貌。
TraceID 单次请求标识
可以说,要想构建一个简单的 Trace 系统,我们首先要做的就是生成并传递 TraceID。
TraceID在各个服务中的传递
分布式链路跟踪的原理其实很简单,就是在请求发起方发送请求时或服务被请求时生成一个 UUID,被请求期间的业务产生的任何日志(Warning、Info、Debug、Error)、任何依赖资源请求(MySQL、Kafka、Redis)、任何内部接口调用(Restful、Http、RPC)都会带上这个 UUID。
这样,当我们把所有拥有同样 UUID 的日志收集起来时,就可以根据时间(有误差)、RPCID(后续会介绍 RPCID)或 SpanID,将它们按依赖请求顺序串起来。
只要日志足够详细,我们就能监控到系统大部分的工作状态,比如用户请求一个服务会调用多少个接口,每个数据查询的 SQL 以及具体耗时调用的内网请求参数是什么、调用的内网请求返回是什么、内网被请求的接口又做了哪些操作、产生了哪些异常信息等等。
同时,我们可以通过对这些日志做归类分析,分析项目之间的调用关系、项目整体健康程度、对链路深挖自动识别出故障点等,帮助我们主动、快速地查找问题。
“RPCID” VS “SpanID 链路标识”
那么如何将汇总起来的日志串联起来呢?有两种方式:span(链式记录依赖)和 RPCID(层级计数器)。我们在记录日志带上 UUID 的同时,也带上 RPCID 这个信息,通过它帮我们把日志关联关系串联起来,那么这两种方式有什么区别呢?
我们先看看 span 实现,具体如下图:
span图
结合上图,我们分析一下 span 的链式依赖记录方式。对于代码来说,写的很多功能会被封装成功能模块(Service、Model),我们通过组合不同的模块实现业务功能,并且记录这两个模块、两个服务间或是资源的调用依赖关系。
span 这个设计会通过记录自己上游依赖服务的 SpanID 实现上下游关系关联(放在 Parent ID 中),通过整理 span 之间的依赖关系就能组合成一个调用链路树。
那 RPCID 方式是什么样的呢?RPCID 也叫层级计数器,我在微博和好未来时都用过,为了方便理解,我们来看下面这张图:
RPCID层级依赖计数器
你看,RPCID 的层级计数器实现很简单,第一个接口生成 RPCID 为 1.1 ,RPCID 的前缀是 1,计数器是 1(日志记录为 1.1)。
当所在接口请求其他接口或数据服务(MySQL、Redis、API、Kafka)时,计数器+1,并在请求当中带上 1.2 这个数值(因为当前的前缀 + “.” + 计数器值 = 1.2),等到返回结果后,继续请求下一个资源时继续 +1,期间产生的任何日志都会记录当前 前缀+“.”+计数器值。
每一层收到了前缀后,都在后面加了一个累加的计数器,实际效果如下图所示:
累加计数器
而被请求的接口收到请求时,如果请求传递了 TraceID,那么被请求的服务会继续使用传递过来的 TraceID,如果请求没有 TraceID 则自己生成一个。同样地,如果传递了 RPCID,那么被请求的服务会将传递来的 RPCID 当作前缀,计数器从 1 开始计数。
相对于 span,通过这个层级计数器做出来的 RPCID 有两个优点。
第一个优点是我们可以记录请求方日志,如果被请求方没有记录日志,那么还可以通过请求方日志观测分析被调用方性能(MySQL、Redis)。
另一个优点是哪怕日志收集得不全,丢失了一些,我们还可以通过前缀有几个分隔符,判断出日志所在层级进行渲染。举个例子,假设我们不知道上图的 1.5.1 是谁调用的,但是根据它的 UUID 和层级 1.5.1 这些信息,渲染的时候,我们仍旧可以渲染它大概的链路位置。
除此之外,我们可以利用 AOP 顺便将各个模块做一个 Metrics 性能统计分析,分析各个模块的耗时、调用次数做周期统计。
同时,通过这个维度采样统计数据,能够帮助我们分析这个模块的性能和错误率。由于 Metrics 这个方式产生的日志量很小,有些统计是每 10 秒才会产生一条 Metrics 统计日志,统计的数值很方便对比,很有参考价值。
但是你要注意,对于一个模块内有多个分支逻辑时,Metrics 很多时候取的是平均数,偶发的超时在平均数上看不出来,所以我们需要另外记录一下最大最小的延迟,才可以更好地展现。同时,这种统计只是让我们知道这个模块是否有性能问题,但是无法帮助我们分析具体的原因。
回到之前的话题,我们前面提到,请求和被请求方通过传递 TraceID 和 RPCID(或 SpanID)来实现链路的跟踪,我列举几个常见的方式供你参考:
HTTP 协议放在 Header;
RPC 协议放在 meta 中传递;
队列可以放在消息体的 Header 中,或直接在消息体中传递;
其他特殊情况下可以通过网址请求参数传递。
那么应用内多线程和多协程之间如何传递 TraceID 呢?一般来说,我们会通过复制一份 Context 传递进入线程或协程,并且如果它们之前是并行关系,我们复制之后需要对下发之前的 RPCID 计数器加 1,并把前缀和计数器合并成新的前缀,以此区分并行的链路。
除此之外,我们还做了一些特殊设计,当我们的请求中带一个特殊的密语,并且设置类似 X-DEBUG Header 等于 1 时,我们可以开启在线 debug 模式,在被调用接口及所有依赖的服务都会输出 debug 级别的日志,这样我们临时排查线上问题会更方便。
日志类型定义
可以说,只要让日志输出当前的 TraceId 和 RPCID(SpanID),并在请求所有依赖资源时把计数传递给它们,就完成了大部分的分布式链路跟踪。下面是我定制的一些日志类型和日志格式,供你参考:
你会发现,所有对依赖资源的请求都有相关日志,这样可以帮助我们分析所有依赖资源的耗时及返回内容。此外,我们的分级日志也在 trace 跟踪范围内,通过日志信息可以更好地分析问题。而且,如果我们监控的是静态语言,还可以像之前说的那样,对一些模块做 Metrics,定期产生日志。
日志格式样例
日志建议使用 JSON 格式,所有字段除了标注为 string 的都建议保存为字符串类型,每个字段必须是固定数据类型,选填内容如果没有内容就直接不输出。
这样设计其实是为了适配 Elasticsearch+Kibana,Kibana 提供了日志的聚合、检索、条件检索和数值聚合,但是对字段格式很敏感,不是数值类型就无法聚合对比。
下面我给你举一个例子用于链路跟踪和监控,你主要关注它的类型和字段用途。
这个日志不仅可以用在服务端,还可以用在客户端。客户端每次被点击或被触发时,都可以自行生成一个新的 TraceID,在请求服务端时就会带上它。通过这个日志,我们可以分析不同地域访问服务的性能,也可以用作用户行为日志,仅仅需添加我们的日志类型即可。
上面的日志例子基本把我们依赖的资源情况描述得很清楚了。另外,我补充一个技巧,性能记录日志可以将被请求的接口也记录成一个日志,记录自己的耗时等信息,方便之后跟请求方的请求日志对照,这样可分析出两者之间是否有网络延迟等问题。
除此之外,这个设计还有一个核心要点:研发并不一定完全遵守如上字段规则生成日志,业务只要保证项目范围内输出的日志输出所有必填项目(TraceID,RPCID/SpanID,TimeStamp),同时保证数值型字段功能及类型稳定,即可实现 trace。
我们完全可以汇总日志后,再对不同的日志字段做自行解释,定制出不同业务所需的统计分析,这正是 ELK 最强大的地方。
为什么大部分设计都是记录依赖资源的日志呢?原因在于在没有 IO 的情况下,程序大部分都是可控的(侧重计算的服务除外)。只有 IO 类操作容易出现不稳定因素,并且日志记录过多也会影响系统性能,通过记录对数据源的操作能帮助我们排查业务逻辑的错误。
我们刚才提到日志如果过多会影响接口性能,那如何提高日志的写吞吐能力呢?这里我为你归纳了几个注意事项和技巧:
1. 提高写线程的个数,一个线程写一个日志,也可以每个日志文件单独放一个磁盘,但是你要注意控制系统的 IOPS 不要超过 100;
2. 当写入日志长度超过 1kb 时,不要使用多个线程高并发写同一个文件。原因参考 append is not Atomic,简单来说就是文件的 append 操作对于写入长度超过缓冲区长度的操作不是原子性的,多线程并发写长内容到同一个文件,会导致日志乱序;
3. 日志可以通过内存暂存,汇总达到一定数据量或缓存超过 2 秒后再落盘,这样可以减少过小日志写磁盘系统的调用次数,但是代价是被强杀时会丢日志;
4. 日志缓存要提前 malloc 使用固定长度缓存,不要频繁分配回收,否则会导致系统整体缓慢;
5. 服务被 kill 时,记得拦截信号,快速 fsync 内存中日志到磁盘,以此减少日志丢失的可能。
“侵入式埋点 SDK”VS“AOP 方式埋点”
最后,我们再说说 SDK。事实上,使用“ELK+ 自定义的标准”基本上已经能实现大多数的分布式链路跟踪系统,使用 Kibana 可以很快速地对各种日志进行聚合分析统计。
虽然行业中出现过很多链路跟踪系统服务公司,做了很多 APM 等类似产品,但是能真正推广开的服务实际占少数,究其原因,我认为是以下几点:
分布式链路跟踪的日志吞吐很大,需要耗费大量的资源,成本高昂;
通用分布式链路跟踪服务很难做贴近业务的个性化,不能定制的第三方服务不如用开源;
分布式链路跟踪的埋点库对代码的侵入性大,需要研发手动植入到业务代码里,操作很麻烦,而且不够灵活。
另外,这种做法对语言也有相关的限制,因为目前只有 Java 通过动态启动注入 agent,才实现了静态语言 AOP 注入。我之前推广时,也是统一了内网项目的开源框架,才实现了统一的链路跟踪。
那么如果底层代码不能更新,如何简单暴力地实现链路跟踪呢?
这时候我们可以改造分级日志,让它每次在落地的时候都把 TraceId 和 RPCID(或 SpanID)带上,就会有很好的效果。如果数据底层做了良好的封装,我们可以在发起请求部分中写一些符合标准性能的日志,在框架的统一异常处理中也注入我们的标准跟踪,即可实现关键点的监控。
当然如果条件允许,我们最好提供一个标准的 SDK,让业务研发伙伴按需调用,这能帮助我们统一日志结构。毕竟手写很容易格式错乱,需要人工梳理,不过即使混乱,也仍旧有规律可言,这是 ELK 架构的强大之处,它的全文检索功能其实不在乎你的输入格式,但是数据统计类却需要我们确保各个字段用途固定。
最后再讲点其他日志的注意事项,可能你已经注意到了,这个设计日志是全量的。很多链路跟踪其实都是做的采样方式,比如 Jaeger 在应用本地会部署一个 Agent,对数据暂存汇总,统计出每个接口的平均响应时间,对具有同样特征的请求进行归类汇总,这样可以大大降低服务端压力。
但这么做也有缺点,当我们有一些小概率的业务逻辑错误,在采样中会被遗漏。所以很多核心系统会记录全量日志,周边业务记录采样日志。
由于我们日志结构很简单,如有需要可以自行实现一个类似 Agent 的功能,降低我们存储计算压力。甚至我们可以在服务端本地保存原始日志 7 天,当我们查找某个 Trace 日志的时候,直接请求所有服务器在本地查找。事实上,在写多读少的情况下,为了追一个 Trace 详细过程而去请求 200 个服务器,这时候即使等十秒钟都是可以接受的。
总结
系统监控一直是服务端重点关注的功能,我们常常会根据链路跟踪和过程日志,去分析排查线上问题。也就是说,监控越是贴近业务、越定制化,我们对线上业务运转情况的了解就越直观。
不过,实现一个更符合业务的监控系统并不容易,因为基础运维监控只会监控线上请求流量、响应速度、系统报错、系统资源等基础监控指标,当我们要监控业务时,还需要人工在业务系统中嵌入大量代码。而且,因为这些服务属于开源,还要求我们必须对监控有较深的了解,投入大量精力才可以。
好在技术逐渐成熟,通用的简单日志传输索引统计服务开始流行,其中最强的组合就是 ELK。通过这类分布式日志技术,能让我们轻松实现个性化监控需求。日志格式很杂乱也没关系,只要将 TraceID 和 RPCID(或 SpanID)在请求依赖资源时传递下去,并将沿途的日志都记录对应的字段即可。也正因如此,ELK 流行起来,很多公司的核心业务,都会依托 ELK 自定义一套自己的监控系统。
不过这么做,只能让我们建立起一个粗旷的跟踪系统,后续分析的难度和投入成本依然很大,因为 ELK 需要投入大量硬件资源来帮我们处理海量数据,相关知识我们后续章节再探讨,
思考题
请你思考一下,既然我们通过 ELK 实现 Trace 那么简单,为什么会在当年那么难实现?
欢迎你在评论区与我交流讨论,我们下节课见!
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 4
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
10|稀疏索引:为什么高并发写不推荐关系数据库?
下一篇
12|引擎分片:Elasticsearch如何实现大数据检索?
精选留言(1)
- John2022-11-20 来自北京老师,我有个疑问想请教下,未来的趋势是使用open telemetry 规范来使用Metric / Tracing /Logging? 那么ELK 遵循了open telemetry 规范?两者之间是什么关系呢
作者回复: 你好,John,很高兴收到你的提问,open telemetry目前是个新标准计划对三者进行统一,目前各个开源正在积极的在往这个标准靠拢合并,目前还在建设阶段。ELK是个支撑服务包含了数据存储、传输、计算、统计的功能,不过他需要我们人工做一些统计工作来实现数据分析,可以说他是一个日志平台,我们可以根据需要对存储的数据进行加工整理,目前ot的标准和他关系不大