分布式链路追踪(Distributed Tracing),也叫 分布式链路跟踪,分布式跟踪,分布式追踪 等等。
本文使用分布式Trace来简称分布式链路追踪。
本篇文章只是从大致的角度来阐述什么是分布式Trace,以及一个分布式Trace系统具备哪些要点和特征。
场景
先从几个场景来看为什么需要分布式Trace
场景1
开发A编写了一段代码,代码依赖了很多的接口。一个调用下去没出结果,或者超时了,Debug之后发现是接口M
挂了,然后找到这个接口M
的负责人B,告知B接口挂了。B拉起自己的调用和Debug环境,按照之前传过来的调用方式重新Debug了一遍自己的接口,发现NND是自己依赖的接口N
挂了,然后找到接口N
负责人C。C同样Debug了自己的接口(此处省略一万个‘怎么可能呢,你调用参数不对吧’),最终发现是某个空判断错误,修复bug,转告给B说我们bug修复了,B再转告给A说,是C那个傻x弄挂了,现在Ok了,你试一下。
就这样,一个上午就没了,看着手头的需求越堆越高,内心是这样
场景2
哪一天系统完成了开发,需要进行性能测试,发现哪些地方调用比较慢,影响了全局。A工程师拉起自己的系统,调用一遍,就汇报给老板,时间没啥问题。B工程师拉起自己的系统,调用了一遍,也没啥问题,同时将结果汇报了给老板。C工程师这时候发现自己的系统比较慢,debug发现原来是自己依赖的接口慢了,于是找到接口负责人。。balabala,和场景1一样,弄好了。老板一一把这些都记录下来,满满的一本子。哪天改了个需求,又重新来一遍,劳民伤财。
解决方案
这两种场景只是缩影,假设这时候有这样一种系统,
它记录了所有系统的调用和依赖,以及这些依赖之间的关系和性能。打个比方,一个网页访问了应用M,应用M又分别访问了A,B,C,D四个应用,如下面这样的结构
那么在这个系统中就能够看到,一个网页Request了一个应用M,花费了多少时间,请求的IP是多少,请求的网络开销是多少。应用M执行时间是多久,是否执行成功,访问A,B,C,D分别花了多少时间,是否成功,返回了什么内容,测试是否通过。 然后到下一步,A,B,C,D四个应用本次执行的时间是多久,有没有超时,调用了多少次DB,每次调用花费了多少时间。
作为示例,给出一个阿里鹰眼的trace图:
trace就犹如一张大的json表,同一层级的数据代表同一层级的应用,越往下代表是对下层某个应用的依赖。从图中可以很方便的看到每一个应用调用的名称,调用花费的时间,以及是否成功。
下面这张图是我们使用微软的application insights生成的tracing图
有些敏感数据打上了马赛克,尽请谅解。不过还是可以清晰的看到应用之间的依赖关系,有处标红来代表此次调用出现了问题。
有了这个系统,场景1和场景2中的需求就能解决吗?如果有了分布式trace,这些场景中的问题又是怎么解决呢?
对于场景1中的case,开发A发现自己的接口挂了或者比较慢,而且Debug发现并不是自己代码的错误,这时候他找到自己的这一次trace,图中就会列出来这一次trace的所有依赖和调用,以及各调用之间的关系。A发现,自己调用的链路到N
接口那里就断了,并且调用N
接口返回500错误,于是A直接和N
接口的负责人C联系,C立马修复了错误。
在A调用出错的时候,系统自动检测出在N
接口出错,系统立马生成一份错误报告发到A和C的邮箱,A拿到报告的时候就直接能够知道那个环节出错了,而C拿到报告的时候发现,A在调用我的接口,并且我的接口出错了。这就是出错的主动通知。
对于场景2,项目开发完成了,或者有新的pull request merge到主分支了,触发了自动化测试。测试下来同样生成一张链路分析图,不管是开发,测试,DBA,还是老板,很容易从里面看到哪些应用的响应速度慢了,读取DB的时间慢了,接口挂了这些参数。再也不用一个一个搜集评测报告了。加快了持续集成和持续迭代。
分布式Trace关乎到的不仅仅是开发,运维,还有测试,DBA,以及你老板的工作量。
上面的例子只是一个缩影,如果一个公司内部存在成千上万个接口调用,到时候接口负责人都找不到的时候,时间成本和沟通成本无法想象。
标准
现有的分布式Trace基本都是采用了google 的Dapper标准。
Dapper的思想很简单,就是在每一次调用栈中,使用同一个TraceId将不同的server联系起来。
我们使用几张Dapper的图来简单说明下
依赖
首先来一张应用依赖图
就是这样一个调用链,一个用户请求了应用A,应用A需要请求应用B和应用C,而应用C需要请求应用D和应用E。
span
Dapper首先要做的就是规定Trace的结构和基本要素,如下图:
一次单独的调用链也可以称为一个span,dapper记录的是span的名称,以及每个span的ID和父ID,以重建在一次追踪过程中不同span之间的关系,上图中一个矩形框就是一个span,前端从发出请求到收到回复就是一个span。
再细化到一个span的内部,如下图:
对于一个特定的span,记录从Start到End,首先经历了客户端发送数据,然后server接收数据,然后server执行内部逻辑,这中间可能去访问另一个应用。执行完了server将数据返回,然后客户端接收到数据。
一个span的内容就能构成Trace上面的一个基本元素,可以在这个span中埋点打上各种各样的Trace类型,比如,一般将客户端发送记录成依赖(dependency),服务端接收客户端以及回复给客户端这两个时间统一记录成请求(request),如果打上这两种,那么在运行完这个span之后,日志库中就会多出两条日志,一条是dependency的日志,一条是request的日志。
现在的Trace SDK,都可以进行配置去自动记录一些事件,比如数据库调用依赖,http调用依赖,记录上游的请求等等,也可以自己手动埋点,在需要打上记录点的地方写上记录的代码即可。
结构
Dapper中给出的是一张这样的图
首先各个日志收集点按照一定的采样率将日志写进数据文件,然后通过管道将这些日志文件按照一定的traceId排定输出到BigTable中去。
如果一个系统完成了上面阐述的架构,基本可以构成一个简单的Trace系统。
traceId和parentId的生成
在整个过程中,TraceId和ParentId的生成至关重要。首先解释下TraceId
和ParentId
。TraceId
是标识这个调用链的Id,整个调用链,从浏览器开始放完,到A到B到C,一直到调用结束,所有应用在这次调用中拥有同一个TraceId
,所以才能把这次调用链在一起。
既然知道了这次调用链的整个Id,那么每次查找问题的时候,只要知道某一个调用的TraceId
,就能把所有这个Id的调用全部查找出来,能够清楚的知道本地调用链经过了哪些应用,产生了哪些调用。但是还缺一点,那就是链。
在java中有种数据结构叫LinkedList
,还有种数据结构叫Tree,即通过父节点就能够知道子节点,或者通过子节点能够知道父节点是谁(双向链表),那么我想知道应用A调用了哪些应用,而又有哪些应用调用了应用A,单纯从TraceId
里面根本看不出来,必须要指定自己的父节点才行,这就是ParentId
的作用。
先来看一张常规的调用图
调用从一个浏览器发起,然后进入到微服务框架中,每一个服务都是一个独立的应用,应用之间通过RPC进行调用。
分布式trace有两个要求,1 是所有的一次调用链都采用一个traceId,2是能够记录这次调用时从哪里来的。
在这点上不同的产品有不同的实现方式。
可以想象,最简单的,就是在一开始浏览器请求的时候,定义两个字段(约定好的),比如一个叫TraceId
,一个叫ParentId
,放到http的header中,传递给应用A,应用A解析传递过来的字段,就知道了TraceId
和ParentId
,即知道了本次调用链的Id,以及上一个应用的本次节点Id,然后就打上日志:某某时间应用A收到了一条请求,TraceId
是XXX,它的ParentId
是XX。
这样以后在查找问题的时候,先找到这次调用链的TraceId,发现有两个应用记录了这个Id,一个是前端的浏览器端记录过,一个是应用A记录过,并且链接关系是前端访问的应用A。
传递两个参数的方式简洁易懂,这有个不好的地方,就是每次需要传递两个参数,那么有没有一种方案能够将两个Id合并为一个Id呢?可以的。不同的产品实现是不一样的。
用上面的图作一定解析,(上面的图是阿里的鹰眼使用的Trace架构),首先为第一个发起这个请求的request分配一个根id,即TraceId,就是上面图中的0,这个0就是整个Trace中的TraceId
,然后应用A拿到了这个号,再在这个0后面添加上0.1和0.2分配给A所请求的应用B和C,B跟C拿到0.1和0.2之后,便可以把这个Id作为ParentId
,那么应用B怎么获取TraceId
呢,很简单,只要把string split一下,取第一个值就行,这里取出来就是0. 所以在设计Id组成的时候,不要把分隔符设计进去,不然就不搞混的。
业内实现
- 开源的 Open Tracing
openTracing是为了解决不同系统之间的兼容性设计的,现在也成为了各个第三方Trace系统的依赖的规范。
Twitter的 Zipin
阿里 鹰眼
大众点评 (Cat)[https://github.com/dianping/cat]
这是开源的产品
- Microsoft Application insights
写在后面
这篇文章只是从最简单的方面去阐述分布式Trace,甚至连分布式都没有涉及。真正搭建一个分布式Trace,不仅仅需要定义结构,还需要保证日志的高可用,支持高并发和高性能。
比如阿里的鹰眼架构:
使用Storm集收集和分类日志数据,然后将简单分析完的数据一方面写进Hbase供实时查询,一方面将全量的日志写进HDFS,使用hadoop集群对这些数据进行统计计算,经过鹰眼的服务器把数据渲染展示出来。
可以看到,使用高可用的鹰眼中间件,即需要保证日志的写入不会丢失,又不会对现有的业务产生性能影响,选择合适的消息采样率至关重要。日志文件收集的数据库,需要保证在大并发的条件下依然能够顺利写入和读取。还需要保证不同的server之间的数据关联和事务保证。
这一系列的加起来,就是个庞大的系统了。