我要讲的内容是偏工程的:怎么在万亿交易量下实现足够实时的秒级监控?
先介绍一下监控系统 Sunfire,它是阿里集团的业务监控系统,前身是蚂蚁的 xflush, 支持应用标准化监控,如操作系统,JVM,中间件等。除此之外还有更强大的日志监控能力,大多数业务的监控指标都从应用的日志中抽取。目前覆盖了集团几乎所有 BU 和绝大多数业务,每分钟处理 TB 级日志。
下面将从以下四个方面进行讲解:
架构规模与挑战
技术选择
方向
一、架构
每分钟处理这么大的TB级日志量,我们是怎么设计架构去实现它的呢?
1.1、传统日志监控
上图是传统的日志监控,现在大多数监控平台采用的一个方案。Agnet 检测日志变化增量推送,经过消息中间件如 kafka,流式计算引擎如 Jstorm/flink 去消费 kafka 产生出来的数据,中间的流式计算可能有多步的处理,最后流向 DB,很传统的架构。
这种架构会有一个问题就是:某一分钟的数据,何时可以发报警?
1.2、流式计算的问题
Process Time 超过 Event Time Window
我们最早尝试了上面传统的架构,但是,会有一个问题,我到底什么时候这个数据才能发报警呢?因为这个架构最麻烦的是我不知道什么时候数据已经全部到齐了。如果机器很多,agent 返回数据的时间并不确定, 要保证所有机器日志采齐了数据才准确,这在流式计算里很难处理。
这是个经典的问题, 有两篇文章很详细的讲解了流式计算中如何解决这种问题:
https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-101
https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102
但是数据丢了就是丢了, 无论怎么样就是不准了,也很难拍出一个 delay 的时间确保数据可以用来发报警, 那么当数据不准时, 我们能不能知道不准了呢? 为了解决这个问题我们走了另一条路线: 让主动权留在服务端。
1.3、Sunfire 功能结构
这是 Sunfire 的功能结构,比较重要是 Sunfire-lika 模块,它用来支撑整个计算框架的,就是线程模型、消息调度处理、故障自愈恢复都是通过这个模块实现的。
1.4、Sunfire 架构
这是 Sunfire 架构图。这个架构图是怎么工作的呢?首先有三个角色 Brain、Reduce 和 Map。这三个角色我们统称为计算模块。
ConfigDB 里面配置了监控项。监控项会定义配置需要从哪个应用、哪个路径采集日志、采集回来的日志应该做哪些的处理、根据什么样的规则进行计算。
Brain 会按照周期从 ConflgDB 里读取配置,生成拓扑。然后安装到 Reduce 上面,Reduce 把拓扑再分解成它的子任务,再安装到 Map 上面,最后 map 去拉日志。
这里画了两个租户,租户 A 和共享租户,其实就是资源是独享的还是共用的。因为我们有一些核心的交易监控,也有一些不太重要的,还有很多边缘业务。如果是很重要的用户,比如说交易,我们就单独给它一个租户,它的所有计算资源都是它自己独享的。对于一些边缘的业务是可以共用服务器的。我们现在有80多个租户,基本上一个租户对应一个大的业务。
1.5、时序图
用时序图的视角看一下上面的任务。这个拓扑包括了配置,也包括这个拓扑任务从多少个服务器,到底从哪些服务器上去采集日志,都是在这个拓扑里面完成的。有了这个拓扑,才有了节点故障时候,恢复它的前提条件。因为拓扑里面包含了所有信息,无论是哪个节点挂掉了,上游都能用它来恢复下游节点。
把这个拓扑安装到一台 Reduce 上面去,然后 Reduce 会把它分解掉。假如我有1台 Reduce,有100个 map,Reduce 会把这个任务分节成100个 Map。如果这时候有1000个 Agent,有可能每个 Map 会采集10个 Agent 的日志,最终 Map 去 Agent 拉取日志,然后再一步步往回走,Map 做初步的计算, Reduce 再做进一步的聚合存入到 HBase,然后最终返回给 Brain,告诉它这个任务完成了。
这里面存在很多可能会出问题的点,因为集群非常庞大,跑着跑着机器可能就挂掉了,这对我们来说是很正常的,一天挂掉十几台机器也是常有的事。下面说一下怎么解决可靠性的问题。
1.6、关键点
上面架构有两个关键点:
- 一个是 Preload,就是任务是提前注册的。它不是在需要的时候才生成任务。
我们把任务提前下发下去了,有什么好处呢?假如集群有一些坏掉的机器可能网络很慢也可能连不上,在这个阶段就可以提前发现这些机器屏蔽掉,在后面真正去做任务的时候,延迟就会相应的降低很多,因为不需要再去等去重试了。
同时,Preload 是输入共享的前提,因为不同的人会配同样的日志,并且规则可能也是类似的,我们在这里会做输入共享,去共享日志的采集来减少带宽和 CPU 的消耗,也会共享中间一部分计算的结果。
- 另一个是 Pull,主动权控制在服务端,就是服务端发现数据拉不上来,想要放弃还是重试,可以由自己做出决定了。最终服务端会决定多长的时间内,一定把这些全部都处理完,而不会过了很长一段时间还有数据突然推上来的问题了。
还有就是 push 时, 有可能遇到网络抖动, 导致失败, 重试也不成功, 但在 pull 模式下, 相当于把 agent 作为 hadoop 中的 hdfs 节点, 只要日志还在, 我们就有补数据的机会
另外,降低用户开销对我们来说也是比较重要的,像双十一场景,交易的应用开销非常大,我们一定要尽量降低它们的开销。比如占了10%的 CPU,交易的用户就受不了这个开销。
因此,所有的计算都是在服务端完成,也使得我们的集群规模非常大。
二、规模与挑战
2.1、挑战
挑战主要来自于这四个方面,都是因为规模而引起的挑战。
2.2、规模
现在有80多个租户,租户基本上一个租户对应一个大的业务,比如交易是一个租户,阿里妈妈是一个租户,高德是一个租户。部署机器最多的时候有6000多台,上面的应用有8000多个,每分钟处理的日志量在3000GB 以上,这只是常态化的日志量并不是最高峰的日志量。这么大的日志量用一个消息中间件去承载也是很困难的, 这也是我们没用流式计算的原因之一.
2.3、场景挑战
- 某应用有上万台服务器,每分钟产生的日志量近1T,如何在秒级完成采集并输出准确的结果?
- 假如有很多人配置了基于该日志的监控项,如何降低开销?
- 假如过程中有服务器宕机了怎么办?
2.4、快速
我们怎么实现快速拉取呢?
在 server 端,其中核心的链路是异步的,所有的通信也是异步的,没有一个地方允许有锁。这两个是通过上面提到的 lika 框架来实现的,lika 框架没有什么特别神奇的地方,把 Akka 的一些核心理念拿出来做了一个简化的框架,更简单更容易维护。
在 Agent 端,最重要的是用了 Zero-copy,使得读日志不经过任何 CPU 的处理,直接通过 socket 发送出去。这样最大的好处是对用户极小开销,坏处就是不能压缩了。
RandomAccessfile 是配合动态二分法来使用的,配日志的时候没有让用户指定时间字段应该在哪个位置,时间是什么格式的,这些都是我们自己判断的。以及怎么知道用户的某个周期应该推上去的日志是哪些呢?就通过动态二分法来实现的。
Brain 生成拓扑的时候,是有时间戳的。agent 拿到以后,简单来说先看头和尾有没有,因为日志是不断打出来的,采集也是不断进行的,尾部拿到的概率特别大。如果不在就根据这个时间去找,把它做二分查找,最后找到时间。上面提到的唯一开销就来自这里,要去猜时间在哪,在极端情况下对用户的 CPU 也能控制在8%以下。
2.5、准确
准确性从这个系统一开始设计时贯穿始终的,也是我们为什么在一开始没有用流式计算的原因。
我们除了 pull 的机制来把控制权保持在服务端之外,还设计了齐全度,这对我们来说是非常重要的。传统的监控一个指标产生一个值就行了,我们每一个值还会对应一个相对应的齐全度。
这个齐全度代表什么意思呢?比如1000台机器里面有几台机器的网络不通或者机器挂掉了,因为机器多了什么问题都会有, 这很正常。
我们会在最后采集完成的时候,多打出来一个指标说1000台机器采集成功900台,失败100台,成功率是90%。这时候用户就有参考了,如果此时发现交易量下跌了,一看齐全度也下跌了,基本上可以认为是采集的问题导致的下跌,有可能并不是真正的业务下跌,可以来找我们看为什么采集缺失。
因此齐全度是我们特意设计出来,为了让用户直观感受到采集的完整度的一个概念。
有了上面的措施还是不能保证准确,还需要有各种各样的测试来验证这些设计是不是可靠的。所以在线上搭很多环境,测试同学造了各种各样的配置,如虚拟的应用大部分机器都是坏掉的,或者大部分机器没有产生日志。再配合上各种各样的日志计算规则,去实时校验。
准确性回归是我们每次发布之前都必须做的,也是自动触发的过程。只要我们每次打包都会触发一次准确性校验。自灰度就是找一些小白鼠,先发布他们,再发布重要的客户。
2.6、稳定
上面是我列举的一些影响系统稳定性的部分问题。最常见的像下发失败,这种好处理,直接重试就可以了。如果已经下发成功了,但是在做的过程中失败的,这就很麻烦了。所以我们 lika 框架很重要的一点,就是为这个服务的。比如 Brain 生成任务以后,它安装成功了一个 Reduce,Brain 就会去守护这个 Reduce。
我们有一套机制来保证 Reduce 执行成功,直到返回成功给 Brain,这个任务才结束。如果没返回,Brain 就会不断探测它,一旦探测到它失败了,比如这台机器连不上了,或者机器是好的但是任务中间出异常挂掉了,那么 Brain 会重试它,换一台机器继续做这个任务。像 Reduce 安装完 Map 后失败了,也是类似的逻辑。
拉日志也会带来一些不可控的事情,就是我不知道要拉的日志到底有多大。有可能我这边分配的计算机器数很少,但是用户日志量非常大,就有可能把我们打爆了。因此我们有一系列自我保护的逻辑,会计算每个监控项的开销,不能高于某一个值。如果高于这个值,说明监控项消耗资源太高了,可能配了一些极其复杂的策略,这时为了自保必须把它kill掉。
我们也实现内存的分配策略,就是每次拉日志的大小是计算出来的。经过一系列的因素计算出来这次能拉多少日志。如果内存不够,等一会儿再去拉。
同时,我们也做了一系列的自我监控。我们是拿自己搭的另外一套环境来监控自己。报警也是在这上面配的,来观察各个租户的状态是不是正常的。
以上这些措施构成了稳定性的保证。
2.7、稳定性验收
稳定性最终是需要验收的,不能说我们说稳定就稳定。上图是我们设计的一些场景。比如有多少机器宕机,看宕机的过程有没有数据丢失或者数据不准。还有网络丢包,Hbase 服务中断等等,再恢复看能不能恢复。再有像整个机房断网,让某个机房成为孤岛,来验证它的稳定性。
2.8、成本
在成本方面,集群机器的数量比较庞大,我们一直想努力降低成本。主要通过下面三个方面来做的。
租户间调度/输入共享
降低成本最重要的技术手段就是做了输入共享,输入共享在很多情况下能减少起码三倍或者五倍的日志拉取。因为在多数情况下一个日志会产出多个指标,不同的指标也可能会打到同一份日志里面。
怎么做呢?Brain 提前注册了 Reduce,Reduce 提前注册了 Map,Map 上有一个关系,就是这个 Map 要采集哪些机器上的哪些日志。最终可以构建出来一个关系来,就是监控项跟机器上的日志的对应关系。比如说第一个监控项要采集100台机器上的某个日志,第二个监控项还是要采集这批机器上的同样日志。这两个任务就合并掉了,最终所有的采集同一份日志的任务都会被合并掉,这是提前注册里面可以做的事情。
关系构建好了以后,就触发一个定时器来触发拉取。
清理僵尸配置
我们根据某个配置它最近一段时间被多少人访问,有没有报警,报警后有没有人处理,等等一系列指标计算出监控项的健康度。如果健康度太低,就会通知用户去清理它,减少我们配置量。
统计值优先
统计值优先也是现在不得不做的一个优化。因为以前很多应用打的都是流水的日志,以交易举例, 交易有很多环节, 每个环节至少有一行日志,最终有可能1亿笔交易对应100亿条日志。所以会要求大的业务方,把这些值改成统计值,至少是每秒或者每分钟聚合后的值打出来。
2.9、输入共享
多个配置一份日志只采集一次
三、技术选择
这是我们做监控的过程中做的一些技术选择。拉和推模式各有优缺点,为了准确性选择了拉的模式,不排除推的模式也能搞定准确性,还是会走到推的路线上来,因为架构总是不断迭代的。
计算应该在 Server 端还是 Agent 端执行呢?因为用户接受不了 CPU 使用率过高,会影响正常业务,因此我们最终选择所有的计算都在 Server 端完成。
对于使用开源框架还是自研框架,我们也希望用开源框架,但如果有的地方满足不了或者开源社区的方向跟我们期望的方向不太一致,我们可能就会基于这个框架的思想定制一个简易的。只有核心的设计,代码体量小维护也简单,其实我们计算框架做出来以后,几乎没有产生过什么 BUG,因外它只做了消息分发线程池管理和故障守护这几件事情。
在数据库选择上,当前我们是直接写 Hbase,正在和 HiTSDB 团队对接, 这是一个类 OpenTSDB 的存储, 在阿里云上也有提供。
对于监控来说,我们最终选择的自运维,我们几乎没有强依赖任何系统。为什么呢?因为我们有个理念,监控应该是最基础的设施,如果我们强依赖别人,我们监控不了它,所以我们做了一个自运维体系。
以上就是做的一些技术选择,经过了很多次迭代,最终走到了现在的路线。
四、方向
现在我们的方向是这四个:
标准化一体化
服务化
智能化
4.1、标准化: MQL
select avg(cpu.util),max(load.load1) from system where app='AppTest'
since 30mselect * from sunfire.1005_SM_13 since
30mselect * from spring filter class='classA' and method='methodB' where ip='192.168.1.1' since 1h
我们叫 MQL,我们的 MQL 希望让用户能够用一个通用的语法来查询所有的监控数据。甚至是其他监控系统的数据,这样用户不用管数据是哪个平台产生的。MQL 在使用上也比原来的 API 更直观一些,会是我们后面主推的提供给用户 API 的方式。
4.2、一体化
我们还做了很多一体化的事情,比如说发现交易下跌了,这时候交易的应用有没有做变更,有没有扩容、缩容重启的操作,这是用户关心的。我们统计出来有相当比例的故障是因为变更导致的,当业务异常的时候直观的看到有没有变更,可以为他省很多时间。虽然这个事情做起来很简单,但是作用是很大的。
我们还把宿主机和网络监控也关联起来了,现在用的都是容器,但有的问题可能是因为宿主机出问题了,或者上面负载太高了,用户可以做出直观的判断。
同时,还把报警集成在钉钉里面完成。钉钉有什么好处呢?它跟传统的短信、邮件报警不一样,它可以有很丰富的交互。用户可以点击进来看报警的详情,甚至可以有曲线、报警的历史,点进去还可以做一些重启机器的操作,或者觉得这是个误报我要关闭半个小时,都可以在这里一站式完成。这比以前用短信收报警的方式前进了一大步。
钉钉
钉钉一站式报警处理
4.3、智能化
智能基线让用户只要配一个规则就可以了。原先是一天内不同时间业务指标的范围可能都不一样,用户只能根据时间段配了一堆规则。上图是简化后的规则,有了智能基线以后只要配当前值和基线比超过百分之多少就报警,就这么简单。