一种低成本的分布式调用追踪系统方案

5.00 avg. rating (99% score) - 5 votes
转载请注明: 吹水小镇 | reetsee.com
原文链接地址: http://blog.reetsee.com/archives/598

RPC_new

如何低成本搭建一套全量日志检索系统,来搜索以logid为索引的调用链,以及任意关键字呢?这篇文章给出我亲身实践的一些经验。

本文首发公众号 – 吹水小镇


由于做图比较费时间,为了避免消磨写文章的耐性,下面只在必要处添加示意图。

在微服务、虚拟化大行其道的今天,分布式调用追踪越来越重要,国内最早做出并打响了名声的应该是阿里的鹰眼系统。而在今年,阿里关于鹰眼系统做了进一步的分享,其中不少细节读来实在是赏心悦目,大家感兴趣的可以在微信搜索《阿里微服务之殇及分布式链路追踪技术原理》。


分布式调用追踪(或分布式链路追踪),我个人有过一段时间的投入,并且在一家体量较大的公司成功实践过一种低成本的方案,能够做到:

  1. 每一次请求的调用链都可以检索到,并且几乎无延迟,只要请求发生了,就能搜到;
  2. 在合理时间内,可以搜索链路中某一个模块的日志中的任一关键字,做到不丢不漏;
  3. 完成这套系统搭建的额外机器成本极低,初期1台普通的机器加1个数据库即可完成

整个系统从方案产出阶段,到最终投入实际使用,我都全程参与过,并且在亿级DAU的应用中成功实践过。而这个系统的缺点,在后文中我会详述。

基本原理

实际调用链要获取的最关键信息只有参与调用链的节点(模块),以及调用关系(RPC来源或去向),唯一标识请求的logid,只要知道这三个,就能构建出最基本的调用链。

在搭建系统前,要满足以下要求:

  1. 每个模块的日志必须有自己的固定路径,并且按一个相对合理的时间段进行日志切分(例如按小时切分);
  2. 每个模块必须打印RPC日志。即模块发生RPC调用时,要将调用的下游模块名、IP地址、端口、错误码等信息打到一个单独的日志中;
  3. RPC请求中需要携带logid,并在整个调用链透传。logid的生成规则是:第一跳时间 + 第一跳机器的IP + 随机数;
  4. 可以根据IP反查到这个IP对应的机器(容器)上部署了什么模块;
  5. 要有一台机器,它有权限登录到内网所有的机器,执行特定的命令。

当你的生产环境满足了上述需求,就基本满足了搭建一个低成本分布式调用追踪系统的能力。给定一个logid,查询这个logid关联请求调用链的方法很简单,思考一下如果我们没有这样的系统,研发人员会怎么画出整个调用拓扑?

首先,研发人员会拿到一个logid,从logid,研发人员知道了请求进入内网的时间是2017年11月6号18:00:00,并且知道了整个调用链中根节点的IP地址是10.0.0.1。

在10.0.0.1上,部署了模块 A1, A2, A3,然后从 A1, A2, A3的日志中,快速寻找18:00:00左右,有没有带有logid的日志出现,假如在 A1 中找到了日志,那么就知道调用链的根节点就是A1模块。

然后在A1模块的RPC日志中,查找18:00:00左右带有logid的日志,发现有2条关联的,一条调用了IP1:PORT1的B1模块,一条调用了IP2:PORT2的B2模块,这样我们再去IP1以及IP2机器,用同样的方法找到对应的日志(注意,在IP1和IP2上我们直接就知道要找的模块分别是B1, B2,不再需要遍历这台机器上的模块)。

不断通过上述的方法,我们可以人肉画出一个logid关联的调用拓扑图。所以用系统去实现是一样的原理。

一个低成本的分布式调用追踪系统,它只要能解析logid,知道第一跳发生的时间、IP,以及后续的RPC去向,就能完全构建出调用链,只要掌握树结构的层次遍历,都不能实现出对应的代码。

具体优势

不存在水平扩容的问题。完全不需要因为日志量变大而对这个分布式调用追踪系统进行任何水平扩容,因为所有的日志都保存在线上机器,他们本身就充当了日志容器。

几乎没有计算成本。日志的排序、分区都是线上机器在打日志过程中就完成的,每一份日志都做好了切分,且日志内记录时间严格有序,搜索过程中只使用线上机器一小部分CPU和IO。

天然的索引。这里面涉及到的索引其实包含这么几个:IP到模块列表的索引,模块到日志路径的索引,时间到日志文件的索引(例如按小时切分、按天切分,那么给定20171107 18点就知道日志在哪个文件中),日志文件内时间对日志行的索引(日志都是严格有序的,所以具体一个时间点大概在日志中的位置是可以确定的,下面会解释)。

系统结构简单。从系统结构来看,其实只有搜索任务管理后台,搜索结果存储,以及线上机器三个部分,涉及的中间环节极少,如下图:

overallarch_new

同时解决了搜调用链和搜关键字的问题。在这个系统中搜调用链实际是搜关键字的递归版本。在搜关键字时,实际就是给出时间区间、日志路径,然后遍历区间内日志,拉取命中关键字的部分,搜调用链就是在业务日志及RPC日志中,在logid的附近时间区间(几秒内)搜索logid本身,并拉取日志,然后针对日志内容获得下一轮搜索的信息。

难点1:对日志文件进行二分查找

而这里面有一个技术难点,就是我们如何在日志文件中快速定位到日志时间在18:00:00前后区间的日志,例如说17:59:55 ~ 18:00:05的日志?

可以用二分查找的方式,因为落地到本机上的日志文件,全部都是按时间严格有序的,只要日志里面每一行都带了打印时间,就能进行二分查找,对日志文件进行二分查找的程序我已经写好了,可以在 https://github.com/fanfank/timecat 获取,而关于这个日志文件二分查找工具的介绍,我也写过一篇文章介绍,具体可以参见地址:http://blog.reetsee.com/archives/502 。

如果谁有现成的日志想试一下效果,也可以把部分日志粘贴到我做的这个timecat演示页面中:http://aap.reetsee.com/page/timecat ,选好日志时间的起始、结束区间点一下搜索即可(注意,timecat虽然支持识别多种时间日期格式,例如nginx默认格式、时间戳等,但建议大家在日志中使用如2017-11-07 18:00:00的标准格式,里面带时区也可以):

aap_new

难点2:搜索行为不影响线上服务

即便读时间的读取区间小,但相应区间内的日志可能会很多(以前我们日志量最大的模块,一天可以打300多GB的日志,单机,这个其实就很极端了),另外一个是即便日志量小,如果并发到一台线上机器任务太多,也有可能在磁盘IO上占用过多。

解决的思路比较加单,一个是对任务数量做限制,并发到单台机器的任务数不能超过阈值;另外一个是对任务IO本身做限制。

在前期平台使用规模不大时(例如一天共计提交任务量在几百次),其实可以只限制单个任务的IO,到后面任务量起来后,可以单纯限制平台内同时执行的任务数,例如平台最大并发执行3个任务,后面的任务要等待前面的任务执行完成才能开始。

难点3:虚拟化下实例IP变化的问题

不少团队已经把微服务运行在虚拟化环境下,这个时候每次上线都可能造成实例IP发生改变。

如果使用了诸如Docker等虚拟化技术,那么RPC日志中需要记录的是可访问IP,避免实际查询时IP已经消失从而造成日志查询不到的问题,如果日志中记录的是虚IP,那么可以将其替换成物理IP来解决这个问题,或者建立一个能根据虚IP、时间查询对应的物理IP的接口。

难点4:虚拟化下日志路径问题

如果使用了虚拟化,那么要注意容器内日志路径及实际映射的物理机日志路径问题,否则可能查询不到日志。

难点5:扩容、缩容、迁移时日志文件名的问题

不少团队喜欢把日志文件的命名方式弄成这样:

  1. 当前的日志文件使用 xxx.log 命名;
  2. 过了切分时间的日志文件(例如1个小时前的)会从 xxx.log 重命名为 xxx.log.2017110813 等格式的文件。

这里面可能会有一种case,就是如果一个模块在10点从IP1迁移走了,导致IP1上的日志文件 xxx.log 保存的都是10点的日志,而12点又迁移回IP1,这个时候模块会把12点的日志也打到 xxx.log,等发生切分的时候就会导致 xxx.log.2017110812 的文件中既包含10点的日志,又包含12点的日志。

因此无论是否使用了虚拟化环境,都建议架构团队把框架日志的命名方式设置如下:

  1. 总是把日志打到带时间后缀的日志中,例如 xxx.log.2017xxxxx;
  2. 为当前打入日志的文件创建一个不带时间后缀的软链 xxx.log。

可能会有人疑问为何一定要留有 xxx.log 呢?主要是为了研发同学到线上机器查看日志时方便,直接 tail -F *.log 就可以了,同时也方便了部分实时日志查看工具的,这些工具总是查看 xxx.log 的日志就可以,而不需要根据时间日期做特殊判断。

难点6:兼容日志文件切分不准确的问题

有些日志文件的切分方式我们没法精细控制,例如 Nginx 日志是用 crontab 等定时任务定期发送一个信号给 Nginx,然后 Nginx对日志文件进行 rename,或者用 logrotate 等工具实现,这种切分方式就不是精细的,可能 前一天、前一个小时的日志中包含后一天、下一小时开头的几条日志。

这时我们查询日志时,就要多对几个日志文件做检查,查询11点的日志,可能要把10点的日志文件也看下,因为有二分查找,所以这种多查看几个文件的 overhead 是非常、非常低的,基本可以忽略。

难点7:线上机器会删日志,要查看较长时间前的日志

这个系统原本有一个问题就是如果线上机器把日志删除了,那么相应的关键字、调用链就搜索不到了。

解决办法是把日志进行收集,把日志文件收集到存储集群中,记录好原日志路径与收集路径的映射关系即可。这里要注意的是必须整个文件进行拷贝,因为要保证日志里每一行的有序性,否则无法二分查找。

有了存储集群后,整个搜索逻辑和原本几乎没有改变,几乎没有需要改造的地方。

这一块其实有非常多的细节,例如当天的数据依然到线上机器搜索,超过一天的日志就到存储集群搜索,这样连IO和并发任务数都不用限制了,因为不会影响线上机器。

而且收集系统是天然无限水平扩展的,所有的文件路径映射关系都保存在数据库中,当存储集群容量不足时加入新机器即可,新机器的可使用容量比较大,那么日志传输时就把日志往大容量机器传输,整个集群不同机器的占用空间很快就会平衡。计算传输时的目标机器可以使用堆排序,容量最大的机器总是在堆顶。

这一部分不是这个系统的原生功能,所以初期搭建时根本不需要考虑这个问题。

下面顺便给出加入日志收集后整个系统的大致架构:

detail_overallarch_new

缺点

在描述上述难点时,相信大家已经隐约感觉到了,为什么有那么多要注意的地方?

这就是因为,这个系统对整个框架的实现、基础架构的实现是强依赖的,要完成对应的功能,必须要框架、基础架构配合,日志规范、打印方式等在一开始就要定得比较好,每个服务的边界也必须足够清晰。在引入虚拟化后问题要解决的 corner case 会更多。

而且对日志严格有序的要求既是优点又是缺点,优点是有序的日志无论认为查看还是机器处理都更容易,缺点是当涉及远程收集时,如果要使用同一套系统,那么也必须要求日志是严格有序的。

而传统的先收集、后分组、再计算、最后生成的方式最大的好处就是灵活,与线上机器的耦合基本就是部署一个agent去采集日志,后面所有的处理与框架、架构没有任何关系,不需要三条腿走路,而代价就是每台机器要额外部署的agent,agent收集日志后中间临时落地的集群无论存储还是计算量都非常大,而且是与线上机器数量成正比的,线上机器越多,中间用于存储、计算的机器也越多,成本非常高昂。

何时用哪套方案

如果你的团队对 Hbase 维护得比较好,而且日志量相对没那么大,Hbase资源也相对充足,那么可以快速地用 Hbase 搭建一个调用链查询系统,把对logid的索引加到 rowkey 中,根据 rowkey 扫出日志后再还原调用链就行。

如果给到你的资源本身就不多,并且你有一台能访问线上所有机器的机器,就可以采用我上述所说的方案,其实搭建起来非常简单,先完成关键字搜索功能:到某批机器对指定日志文件(们)的某个时间区间搜索关键字。再完成调用链搜索。这个方案随着公司发展以及服务上云,会遇到进一步的问题,到时候解决就要考虑很多细碎问题,并且搜索逻辑中要加入不少晦涩的兼容代码,那个时候就不建议继续使用了。

如果你一开始就有充足的资源,或者你们团队专门就是做问题定位、分析、日志抽取等,那么建议一开始就走agent采集日志,中间落地后处理的方案。要注意的是,查询调用链、关键字毕竟是少数,再多,一个服务一天也不可能人工查询上万次(除非绑定了某些自动化任务),而为这极少数的需求采集全量的日志是否值得?我认为是不值得的,虽然存储不值钱,但采集量比查询量何止多了千万倍?因此这种走集中收集的系统不应该以定位具体某几行日志为最终目标,而应该是以发现某个特征问题,并给出大致定位方向为最终目标,如果再这个过程中,能给出有问题的示例日志,那就是最好不过了,并且这种系统后续更有价值的是它在采集过程中挖掘到的服务稳定性、耗时、流量等数据。

最后

从写这篇文章一开始,我就知道没法把整个系统的细节都说细,所以打算简单写下,没想到还是超出了这么多篇幅。

希望这篇文章能给大家一些帮助。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注