选自《不一样的技术创新——阿里巴巴2016双11背后的技术》,全书目录:https://yq.aliyun.com/articles/68637
本文作者:郁松、章邯、程超、癫行
前言
2016财年,阿里巴巴电商交易额(GMV)突破3万亿元人民币,成为全球最大网上经济体,这背后是基础架构事业群构筑的坚强基石。
在2016年双11全球购物狂欢节中,天猫全天交易额1207亿元,前30分钟每秒交易峰值17.5万笔,每秒支付峰值12万笔。承载这些秒级数据背后的监控产品是如何实现的呢?接下来本文将从阿里监控体系、监控产品、监控技术架构及实现分别进行详细讲述。
1.阿里监控体系
阿里有众多监控产品,且各产品分工明确,百花齐放。
整个阿里监控体系如下图:
集团层面的监控,以平台为主,全部为阿里自主研发(除引入了第三方基调、博睿等外部检测系统,用于各地CDN用户体验监控),这些监控平台覆盖了阿里集团80%的监控需求。
此外,每个事业群均根据自身特性自主研发了多套监控系统,以满足自身特定业务场景的监控需求,如广告的GoldenEye、菜鸟的棱镜、阿里云的天基、蚂蚁的金融云(基于XFlush)、中间件的EagleEye等,这些监控系统均有各自的使用场景。
阿里的监控规模早已达到了千万量级的监控项,PB级的监控数据,亿级的报警通知,基于数据挖掘、机器学习等技术的智能化监控将会越来越重要。
阿里全球运行指挥中心(GOC)基于历史监控数据,通过异常检测、异常标注、挖掘训练、机器学习、故障模拟等方式,进行业务故障的自动化定位,并赋能监控中心7*24小时专业监控值班人员,使阿里集团具备第一时间发现业务指标异常,并快速进行应急响应、故障恢复的能力,将故障对线上业务的影响降到最低。
接下来将详细讲述本文的主角:承载阿里核心业务监控的SunFire监控平台。
2. 监控技术实现
2.1 监控产品简介
SunFire是一整套海量日志实时分析解决方案,以日志、REST 接口、Shell 脚本等作为数据采集来源,提供设备、应用、业务等各种视角的监控能力,从而帮您快速发现问题、定位问题、分析问题、解决问题,为线上系统可用率提供有效保障。
SunFire利用文件传输、流式计算、分布式文件存储、数据可视化、数据建模等技术,提供实时、智能、可定制、多视角、全方位的监控体系。其主要优势有:
- 全方位实时监控:提供设备、应用、业务等各种视角的监控能力,关键指标秒级、普通指标分钟级,高可靠、高时效、低延迟。
- 灵活的报警规则:可根据业务特征、时间段、重要程度等维度设置报警规则,实现不误报、不漏报。
- 管理简单:分钟级万台设备的监控部署能力,故障自动恢复,集群可伸缩
- 自定义便捷配置:丰富的自定义产品配置功能,便捷、高效的完成产品配置、报警配置。
- 可视化:丰富的可视化 Dashboard,帮助您定制个性化的监控大盘。
- 低资源占用:在完成大量监控数据可靠传输的同时,保证对宿主机的 CPU、内存等资源极低占用率。
2.2 监控架构
Sunfire技术架构如下:
2.3 监控组件介绍
针对架构图中的各个组件,其中最关键的为采集(Agent)、计算(Map、Reduce)组件,接下来将对这两个组件进行详细介绍。
2.3.1 采集
Agent负责所有监控数据的原始采集,它以 Agent 形式部署在应用系统上,负责原始日志的采集、系统命令的执行。
2.3.1.1 定位
日志原始数据的采集,按周期查询日志的服务,且日志查询要低耗、智能。Agent上不执行计算逻辑。
2.3.1.2 特性
2.3.1.2.1 低耗
采集日志,不可避免要考虑日志压缩的问题,通常做日志压缩则意味着它必须做两件事情:一是磁盘日志文件的内容要读到应用程序态;二是要执行压缩算法。
这两个过程就是CPU消耗的来源。但是它必须做压缩,因为日志需要从多个机房传输到集中的机房。跨机房传输占用的带宽不容小觑,必须压缩才能维持运转。所以低耗的第一个要素,就是避免跨机房传输。SunFire达到此目标的方式是运行时组件自包含在机房内部,需要全量数据时才从各机房查询合并。
网上搜索zero-copy,会知道文件传输其实是可以不经过用户态的,可以在linux的核心态用类似DMA的思想,实现极低CPU占用的文件传输。SunFire的Agent当然不能放过这个利好,对它的充分利用是Agent低耗的根本原因。以前这部分传输代码是用c语言编写的sendfile逻辑,集成到java工程里,后来被直接改造为了java实现。
最后,在下方的计算平台中会提到,要求Agent的日志查询服务具备“按周期查询日志”的能力。这是目前Agent工程里最大的难题,我们都用过RAF(RandomAccessFile),你给它一个游标,指定offset,再给它一个长度,指定读取的文件size,它可以很低耗的扒出文件里的这部分内容。然而问题在于:周期≠offset。从周期转换为offset是一个痛苦的过程。
在流式计算里一般不会遇到这个问题,因为在流式架构里,Agent是水龙头,主动权掌握在Agent手里,它可以从0开始push文件内容,push到哪里就做一个标记,下次从标记继续往后push,不断重复。这个标记就是offset,所以流式不会有任何问题。
而计算平台周期任务驱动架构里,pull的方式就无法提供offset,只能提供Term(周期,比如2015-11-11 00:00分)。Agent解决此问题的方式算是简单粗暴,那就是二分查找法。而且日志还有一个天然优势,它是连续性的。所以按照对二分查找法稍加优化,就能达到“越猜越准”的效果(因为区间在缩小,区间越小,它里面的日志分布就越平均)。
于是,Agent代码里的LogFinder组件撑起了这个职责,利用上述两个利好,实现了一个把CPU控制在5%以下的算法,目前能够维持运转。其中CPU的消耗不用多说,肯定是来自于猜的过程,因为每一次猜测,都意味着要从日志的某个offset拉出一小段内容来核实,会导致文件内容进入用户态并解析。这部分算法依然有很大的提升空间。
2.3.1.2.2 日志滚动
做过Agent的同学肯定都被日志滚动困扰过,各种各样的滚动姿势都需要支持。SunFire的pull方式当然也会遇到这个问题,于是我们简单粗暴的穷举出了某次pull可能会遇到的所有场景,比如
- 正常猜到了offset
- 整个日志都猜不到offset
- 上次猜到了offset,但是下次再来的时候发现不对劲(比如滚动了)
- 等等等等
这段逻辑代码穷举的分支之多,在一开始谁都没有想到。不过仔细分析了很多次,发现每个分支都必不可少。
2.3.1.2.3 查询接口
Agent提供的查询服务分为first query和ordinary query两种。做这个区分的原因是:一个周期的查询请求只有第一次需要猜offset,之后只需要顺序下移即可。而且计算组件里有大量的和Agent查询接口互相配合的逻辑,比如一个周期拉到什么位置上算是确定结束?一次ordinary query得到的日志里如果末尾是截断的(只有一半)该如何处理…… 这些逻辑虽然缜密,但十分繁琐,甚至让人望而却步。但现状如此,这些实现逻辑保障了SunFire的高一致性,不用担心数据不全、报警不准,随便怎么重启计算组件,随便怎么重启Agent。但这些优势的背后,是值得深思的代码复杂度。
2.3.1.2.4 路径扫描
为了让用户配置简单便捷,SunFire提供给用户选择日志的方式不是手写,而是像windows的文件夹一样可以浏览线上系统的日志目录和文件,让他双击一个心仪的文件来完成配置。但这种便捷带来的问题就是路径里若有变量就会出问题。所以Agent做了一个简单的dir扫描功能。Agent能从应用目录往下扫描,找到同深度文件夹下“合适”的目标日志。
2.3.2 计算
由Map、Reduce组成计算平台,负责所有采集内容的加工计算,具备故障自动恢复能力及弹性伸缩能力。
2.3.2.1 定位
计算平台一直以来都是发展最快、改造最多的领域,因为它是很多需求的直接生产者,也是性能压力的直接承担者。因此,在经过多年的反思后,最终走向了一条插件化、周期驱动、自协调、异步化的道路。
2.3.2.2 特性
2.3.2.2.1 纯异步
原来的SunFire计算系统里,线程池繁复,从一个线程池处理完还会丢到下一个线程池里;为了避免并发bug,加锁也很多。这其中最大的问题有两个:CPU密集型的逻辑和I/O密集型混合。
对于第一点,只要发生混合,无论你怎么调整线程池参数,都会导致各式各样的问题。线程调的多,会导致某些时刻多线程抢占CPU,load飙高;线程调的少,会导致某些时刻所有线程都进入阻塞等待,堆积如山的活儿没人干。
对于第二点,最典型的例子就是日志包合并。比如一台Map上的一个日志计算任务,它要收集10个Agent的日志,那肯定是并发去收集的,10个日志包陆续(同时)到达,到达之后各自解析,解析完了data要进行merge。这个merge过程如果涉及到互斥区(比如嵌套Map的填充),就必须加锁,否则bug满天飞。
但其实我们重新编排一下任务就能杜绝所有的锁。比如上面的例子,我们能否让这个日志计算任务的10个Agent的子任务,全部在同一个线程里做?这当然是可行的,只要回答两个问题就行:
1)如果串行,那10个I/O动作(拉日志包)怎么办?串行不就浪费cpu浪费时间吗?
2)把它们都放到一个线程里,那我怎么发挥多核机器的性能?
第一个问题,答案就是异步I/O。只要肯花时间,所有的I/O都可以用NIO来实现,无锁,事件监听,不会涉及阻塞等待。即使串行也不会浪费cpu。
第二个问题,就是一个大局观问题了。现实中我们面临的场景往往是用户配置了100个产品,每个产品都会拆解为子任务发送到每台Map,而一台Map只有4个核。所以,你让一个核负责25个任务已经足够榨干机器性能了,没必要追求更细粒度子任务并发。
因此,计算平台付出了很大的精力,做了协程框架。
我们用akka作为协程框架,有了协程框架后再也不用关注线程池调度等问题了,于是我们可以轻松的设计协程角色,实现CPU密集型和I/O密集型的分离、或者为了无锁而做任务编排。接下来,尽量用NIO覆盖所有的I/O场景,杜绝I/O密集型逻辑,让所有的协程都是纯跑CPU。按照这种方式,计算平台已经基本能够榨干机器的性能。
2.3.2.2.2 周期驱动
所谓周期驱动型任务调度,说白了就是Map/Reduce。Brain被选举出来之后,定时捞出用户的配置,转换为计算作业模型,生成一个周期(比如某分钟的)的任务, 我们称之为拓扑(Topology), 拓扑也很形象的表现出Map/Reduce多层计算结构的特征。所有任务所需的信息,都保存在topology对象中,包括计算插件、输入输出插件逻辑、Map有几个、每个Map负责哪些输入等等。这些信息虽然很多,但其实来源可以简单理解为两个:一是用户的配置;二是运维元数据。拓扑被安装到一台Reduce机器(A)上。A上的Reduce任务判断当前集群里有多少台Map机器,就产生多少个任务(每个任务被平均分配一批Agent),这些任务被安装到每台机器上Map。被安装的Map任务其实就是一个协程,它负责了一批Agent,于是它就将每个Agent的拉取任务再安装为一个协程。至此,安装过程结束。Agent拉取任务协程(也称之为input输入协程,因为它是数据输入源)在周期到点后,开始执行,拉取日志,解析日志,将结果交予Map协程;Map协程在得到了所有Agent的输入结果并merge完成后,将merge结果回报到Reduce协程(这是一个远程协程消息,跨机器);Reduce协程得到了所有Map协程的汇报结果后,数据到齐,写入到Hbase存储,结束。
上述过程非常简单,不高不大也不上,但经过多年大促的考验,其实非常的务实。能解决问题的架构,就是好的架构,能简单,为何要把事情做得复杂呢?
这种架构里,有一个非常重要的特性:任务是按周期隔离的。也就是说,同一个配置,它的2015-11-11 00:00分的任务和2015-11-11 00:01分的任务,是两个任务,没有任何关系,各自驱动,各自执行。理想情况下,我们可以做出结论:一旦某个周期的任务结束了,它得到的数据必然是准确的(只要每个Agent都正常响应了)。所以采用了这种架构之后,SunFire很少再遇到数据不准的问题,当出现业务故障的时候我们都可以断定监控数据是准确的,甚至秒级都可以断定是准确的,因为秒级也就是5秒为周期的任务,和分钟级没有本质区别,只是周期范围不同而已。能获得这个能力当然也要归功于Agent的“按周期查询日志”的能力。
2.3.2.2.3 任务重试
在上节描述的Brain->Reduce->Map的任务安装流程里,我们对每一个上游赋予一个职责:监督下游。当机器发生重启或宕机,会丢失一大批协程角色。每一种角色的丢失,都需要重试恢复。监督主要通过监听Terminated事件实现,Terminated事件会在下游挂掉(不论是该协程挂掉还是所在的机器挂掉或是断网等)的时候发送给上游。由于拓扑是提前生成好且具备完备的描述信息,因此每个角色都可以根据拓扑的信息来重新生成下游任务完成重试。
若Brain丢失,则Brain会再次选主, Brain读取最后生成到的任务周期, 再继续生成任务
若Reduce丢失,每个任务在Brain上都有一个TopologySupervisor角色, 来监听Reduce协程的Terminated事件来执行重试动作
若Map丢失,Reduce本身也监听了所有Map的Terminated事件来执行重试动作
为确保万无一失,若Reduce没有在规定时间内返回完成事件给Brain,Brain同样会根据一定规则重试这个任务。
过程依然非常简单,而且从理论上是可证的,无论怎么重启宕机,都可以确保数据不丢,只不过可能会稍有延迟(因为部分任务要重新做)。
2.3.2.2.4 输入共享
在用户实际使用SunFire的过程中,常常会有这样的情况:用户配了几个不同的配置,其计算逻辑可能是不同的,比如有的是单纯计算行数,有的计算平均值,有的需要正则匹配出日志中的内容,但这几个配置可能都用的同一份日志,那么一定希望几个配置共享同一份拉取的日志内容。否则重复拉取日志会造成极大的资源消耗。
那么我们就必须实现输入共享,输入共享的实现比较复杂,主要依赖两点:
其一是依赖安装流,因为拓扑是提前安装的,因此在安装到真正开始拉取日志的这段时间内,我们希望能够通过拓扑信息判断出需要共享的输入源,构建出输入源和对应Map的映射关系。
其二是依赖Map节点和Agent之间的一致性哈希,保证Brain在生成任务时,同一个机器上的日志,永远是分配相同的一个Map节点去拉取的(除非它对应的Map挂了)。
站在Map节点的视角来看:在各个任务的Reduce拿着拓扑来注册的时候,我拿出输入源(对日志监控来说通常可以用一个IP地址和一个日志路径来描述)和Map之间的关系,暂存下来,每当一个新的Reduce来注册Map,我都判断这个Map所需的输入源是否存在了,如果有,那就给这个输入源增加一个上游,等到这个输入源的周期到了,那就触发拉取,不再共享了。
2.3.3 其他组件
存储:负责所有计算结果的持久化存储,可以无限伸缩,且查询历史数据保持和查询实时数据相同的低延迟。Sunfire原始数据存储使用的是阿里集团的Hbase产品(HBase :Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统),用户配置存储使用的是MongoDb。
展示:负责提供用户交互,让用户通过简洁的建模过程来打造个性化的监控产品。基于插件化、组件化的构建方式,用户可以快速增加新类型的监控产品。
自我管控:即OPS-Agent、Ops-web组件,负责海量Agent的自动化安装监测,并且承担了整个监控平台各个角色的状态检测、一键安装、故障恢复、容量监测等职责。
2.4 秒级监控产品化
目前SunFire已将秒级监控能力产品化,将秒级监控能力赋能给研发运维,用户可自行根据实际业务监控需求进行配置,操作简单便捷,非常灵活。
效果图如下: