滴滴夜莺Nightingale01-架构详解

文档地址:https://n9e.didiyun.com/zh/docs

在部署和配置出现问题的时候,反复看上面的文档和里面的视频

1.监控平台架构概述

在这里插入图片描述

  • collector即agent,可以采集机器常见指标,支持日志监控,支持插件机制,支持业务通过接口直接上报数据
  • transfer提供rpc接口接收collector上报的数据,然后通过一致性哈希,将数据转发给多台tsdb和多台judge
  • tsdb即原来的graph组件,用于存储历史数据,支持配置为双写模式提升系统容灾能力,tsdb会把监控数据转发一份给index
  • index是索引模块,替换原来的mysql方案,在内存里构建索引,便于后续数据检索,性能大幅提升
  • judge是告警引擎,从monapi(portal)同步监控策略,然后对接收到的数据做告警判断,如满足阈值,则生成告警事件推到redis
  • monapi(alarm)从redis读取judge生成的事件,进行二次处理,补充一些元信息,生成告警消息,重新推回redis
  • 各发送组件,比如mail-sender、sms-sender等,从redis读取告警消息,发送告警,抽出各类sender是为了后续定制方便
  • monapi集成了原来多个模块的功能,提供接口给js调用,api前缀为/api/portal,数据查询走transfer,干掉了原来的query组件,api前缀为/api/transfer,索引查询的api前缀/api/index,于是,前面搭建nginx,即可通过不同location将请求转发到不同后端
  • 数据库仍然使用mysql,主要存储的内容包括:用户信息、团队信息、树节点信息、告警策略、监控大盘、屏蔽策略、采集策略、部分组件心跳信息等

整体看来,夜莺的核心处理逻辑可以分成三大块:

  1. 绿色部分,是数据采集、传输、存储链路,同时对外暴露查询接口,用于查询索引信息和历史监控数据

  2. 红色部分,是告警引擎,依据告警策略,对监控数据做阈值判断,如果监控数据触发阈值,会生成告警事件,推给redis队列,由monapi(alarm)模块来消费处理

  3. 紫色部分,是用户交互部分,提供web页面给终端用户操作,进行数据查看、告警策略配置、告警采集配置、告警屏蔽配置、告警事件查看等等

2.数据采集、传输、存储链路

监控系统,要想工作,第一步要解决的就是监控数据采集问题,只有数据采集上来了,才能看图,才能做告警判断。

要监控的对象很多,硬件、OS、网络设备、中间件、业务进程…不同的对象有各自关注的监控指标,如何才能将这各类监控指标纳入一个统一的系统进行管理呢?首先,我们要定义一种通用的数据结构,来描述监控指标数据,只要大家遵照这个规范,就能够接入夜莺。

2.1数据结构

{
"endpoint": "10.4.5.6",
"metric": "disk.bytes.free.percent",
"tags": "mount=/home,fstype=xfs",
"counterType": "GAUGE",
"value": 45.3,
"timestamp": 1589205320,
"extra": ""
}

这个数据结构描述了监控数据的来源对象,即endpoint,指标名称,即metric,只有一个指标名称描述能力太过薄弱,所以加了维度信息,即tags,counterType是数据类型,支持GAUGE和COUNTER两种类型,value表示当前监控指标的值,timestamp是数据采集的时刻,extra是一个扩展字段,用来存放一些额外信息,比如traceid。

如上数据结构定义好之后,所有的目标对象,都可以将监控数据组织成这个格式,推给夜莺服务端,于是,理论上夜莺可以监控所有数值类型的时序数据。

2.2数据采集

绿色部分最左侧,是采集器,即collector,这是一个客户端常驻进程,部署到目标机器上,采集机器的一些常见指标,比如cpu、内存、磁盘相关的指标,默认支持linux、windows两个操作系统。

collector默认采集200个左右的常见指标,但是这些指标未必能够满足所有的场景需求,于是collector设计了一个插件机制,允许用户将监控采集脚本放到指定目录,collector就会将脚本自动识别为插件,周期性运行,截获插件的stdout(即标准输出,组织为上面的数据结构)作为监控数据上报。

collector还内置日志监控能力,这个并非是要把日志采集了发给服务端,在服务端处理,夜莺的日志监控,是在目标机器处理的,利用了目标机器的算力。在服务端配置正则表达式,下发到目标机器,collector就会去读取指定的日志文件,用正则去匹配相关的行,提取日志中的相关数据,进行计算,将最后的结果推给服务端。日志监控对于系统日志、业务进程日志,都是非常好用的利器,比如我们可以用日志监控这个能力,来做接口监控,统计某个服务的接口QPS、延迟、成功率等指标。

除了插件机制,collector也会直接开放http接口,用于接收直接的监控数据推送,比如业务进程内嵌SDK埋点,采集相关指标数据,然后周期性推给本机的collector,collector将数据实时转发给服务端。

另外,对于一些中间件监控,比如MySQL、Redis等,社区很多用户自行写了采集器,这些采集器通常也是将监控数据推给本机的collector,然后collector做预处理之后实时转发给服务端。

collector推送监控数据给服务端,实际是推给了服务端的transfer组件,使用TCP长连接,RPC调用的方式,协议采用msgpack,性能优秀。transfer为了容灾考虑,通常会部署多个,collector里就可以配置多个transfer的地址,调用的时候随机尝试,如果某个调用失败,会自动尝试其他的transfer组件,以此来提升容灾能力。

具体部署collector的时候需要用进程管理工具来管理collector进程,比如systemd、supervisor等,配置开机自启动,配置进程挂掉自动拉起,通过运维手段来提升可用性。

2.3数据转发

海量监控数据在存储的时候,势必无法用一台机器搞定,所以要做成集群,监控数据做分片,即,来了一条监控数据,我们要考虑这条数据应该交给哪个后端服务器来存储,不同的监控数据,落到不同的后端节点上,这样即使数据量再大,只要堆机器,就能扛得住。

某一条监控数据,与后端节点的对应关系,到底是如何计算或分配的呢?典型的处理方式有两类,一个是中心端KV记录,一个是通过算法计算。监控数据如果非常多,比如10亿,我们会倾向于使用算法来分片,因为KV的话就要搞定这10亿条记录,这个元信息本身的维护,是一个很麻烦的事情。

夜莺就是用算法来分片,具体算法是采用一致性哈希,即监控数据由collector推给transfer,transfer就会对数据做计算,求得一个唯一标识,然后计算这个唯一标识应该落在哈希环的哪个位置,即可知道该条监控数据应该交给哪个后端节点来处理,然后直接将数据转发给对应的后端存储即可。

所以transfer就类似一个数据路由网关,接收数据,计算下一个节点,转发,手工。transfer本身无状态,可以水平扩展,部署多个实例达到容灾的效果。transfer由于在内存里构建了哈希环,知道各监控数据对应到哪个后端tsdb,所以,transfer也具备数据查询接口,前面挂nginx,处理/api/transfer这个location的相关请求,返回前端要求的各类监控数据查询需求。

2.3.1数据存储

负责数据存储的组件是tsdb,transfer将监控数据转发给tsdb,tsdb会把监控数据存储起来。存储介质有两个,一个是硬盘,所有历史监控数据都会落盘,一个是内存,主要用来存储近期几个小时的数据,方便后续应对查询请求,毕竟内存I/O可比硬盘I/O快多了。

内存介质,容量较小,要想存放尽可能多的监控数据,就需要做压缩,这里我们参照facebook公开的gorilla压缩算法,对内存里的数据做压缩处理,平均一条监控指标只需大约1.37byte,压缩效果明显。

而落盘的数据,我们使用了rrdtool,rrdtool是一个环形数据库,在很多老牌的监控系统中都有采用,每一条监控指标,就表现为硬盘上的一个rrd文件。rrdtool内置了归档策略,老数据可以降采样,如果精度要求不高,可以存储非常久远的数据,比如几年,都不成问题。

上面讲到tsdb模块是通过一致性哈希的方式来做集群,如此的话,单个节点挂掉,就会影响1/N的数据,那如何做容灾?这里采用一个双写的机制。即部署两个tsdb集群,transfer转发数据的时候,同时转发到两个tsdb集群,查询的时候默认先从第一个集群查询,如果第一个集群查询失败,再尝试第二个,如此,便可大幅提升集群整体的可用可靠性。

2.3.2 索引处理

上面讲到的监控数据结构,里边包含多个字段,value和timestamp是存放在tsdb模块的,而endpoint、metric、tags这些信息,我们称为索引数据,索引数据是交给index模块处理的。

用户在页面上点击某个endpoint,查看endpoint下有哪些监控指标,就是从index读取的。索引数据的构建,是需要监控数据的,毕竟,endpoint、metric、tags字段都是来自监控数据,所以,tsdb模块在存储数据的同时,会把数据推一份给index,index来构建内存索引。

索引数据在index内存里,如何容灾?这里有几点设计:

1、index模块会周期性把内存数据dump到硬盘上持久化存储,这样下次重启的时候,就可以从硬盘读取索引数据,然后在内存里重新构建出来。

2、index可以部署多个实例,每个实例都向monapi发心跳包,如果某个index挂掉了,就没法发出心跳包了,所以monapi是知道哪些index活着,哪些已经挂掉了。tsdb发数据给index的时候,就要先询问monapi,到底哪些index还活着,就把数据发给对应的index,有几个活着就发几个,每个index都是存储的全量索引数据。

3、最后一个讨巧的设计,index模块部署多个,如果某个index挂掉了长时间没有启动,本地硬盘的索引数据会有滞后性,可能部分新数据的索引没能更新进来,此时index模块启动,再从本地硬盘load数据意义不大,此时index会自动尝试去读取(调其他index的API)其他index的索引数据,如果读到了就直接用,如果没读到,再退而求其次,读本地硬盘的数据。

注意,tsdb转发数据给index的时候,并非是无脑转发所有监控数据。因为对于一条时序数据,只要第一个点交给index即可完成索引构建,后面时间采集上来的数据,不需要再发给index,徒增压力而已。所以tsdb实际在内存里有一个缓存,记录哪些数据已经发给index了,如果已经发过的,就不会再发了,起到一个增量发送的效果。

假设有1000万时序指标,tsdb集群有10台,每台tsdb会固定的处理大约100万时序数据,所以tsdb内存里针对index的这个增量逻辑所需的缓存,就是100万条,如果我们让transfer直接转发数据给index,由于transfer可能会接收所有collector上报的数据,所以为了达到增量推送index的效果,transfer内存的缓存就会有1000万条,这就太大了,这也是为何index的数据是来自tsdb,而不是直接来自transfer的原因。

index模块前面是nginx,负责处理/api/index这个location的相关请求,如果某个index挂掉了,nginx会自动摘除,所以对于前端用户请求,不会有什么影响。

3. 监控数据告警链路

监控告警链路核心有两块功能:告警判断、事件处理。告警判断是通过judge模块,judge模块生成事件之后,推给redis队列,然后由monapi(alarm)模块消费redis队列,对事件进行处理。

3.1 告警判断

judge模块做告警判断,需要两个数据,一个是用户在页面上配置的告警策略,一个是监控数据。告警策略是存在MySQL的,judge会周期性同步到本地内存,便于处理。在大一些的公司,策略可能有几千条,为了容灾和扩展性考虑,一般会部署多个judge实例,不同的judge实例处理不同的告警策略,即,对告警策略做分片,这里用的分片算法,仍然是一致性哈希,跟这个算法耗上了 😃

judge模块会周期性发心跳请求给monapi,monapi就知道当前有哪些judge是活着的,及时踢掉故障的judge,由活着的judge来承担告警策略处理。monapi会在内存里构建judge的哈希环,策略id落到环上的哪个judge,就决定了对应关系。

策略同步频率,默认是9s,为啥是9s,因为考虑一般监控数据的采集频率不会小于10s,大都是10s、20s、30s、60s居多,所以9s同步一次策略,下次数据到来的时候能够应用到新策略即可。

judge所用到的监控数据,是transfer模块转发过来的,transfer是通过什么逻辑转发的呢?比如来了一条监控数据,transfer会判断,这条数据关联了哪些告警策略?如果关联了告警策略 #56 和 #89 ,那就看这两条策略分别是哪个judge在负责处理,就把数据转发给相关的judge。不同编号的策略和judge实例之间已经在monapi模块完成了哈希映射,transfer从monapi同步策略的时候,会把这个映射关系一并同步下来,便于处理。

从上面的逻辑可以看出来,如果某个监控数据没有关联任何告警策略,实际是不会发给任何一台judge的,减少不必要的开销。

如上所述,transfer已经判断过监控数据跟哪些策略有关联关系了,那judge就不需要再做判断了,transfer直接把这个对应关系发给judge即可,judge接收到数据的同时,也就知道了应该由哪些策略来处理,对应的做相关阈值比对即可。

如果监控数据触发了阈值,会生成告警事件,告警事件会推给redis队列,这里为了容灾考虑,会部署多个redis,judge会先尝试推第一个redis,如果发现它挂了,就尝试第二个redis。

3.2 事件处理

每个redis都一一对应一个monapi(alarm)模块来消费告警事件,由于judge推送事件的方式是优先第一个redis,所以正常情况下,只有一个monapi(alarm)会消费到告警事件,其他的monapi(alarm)都是空闲的。

monapi(alarm)的处理逻辑比较简单,但是很实用,我们在策略里看到的告警收敛、告警升级,其处理逻辑都是在monapi(alarm)里的。简单概括流程,就是从redis pop出来告警事件,然后入库存储,便于后续页面查看历史告警事件,然后给告警事件补充一些相关字段,比如用户的联系方式,如果事件配置了回调,就去回调,然后把最终要发送的告警消息重新推回redis新的队列,由各个sender模块来做后续发送处理,至于各个sender模块如何处理,就看各自公司的消息通道了, github.com/n9e 下面有一些常见sender可以直接拿来使用。

至此,告警处理链路的逻辑就完成了。

4.用户交互部分

这块很简单,总共有3个模块提供前端http接口,分别是:

  • transfer:负责查询历史监控数据
  • index:负责查询监控数据的索引
  • monapi:负责对配置信息做增删改查,比如用户团队相关、策略相关、大盘相关,负责历史告警事件的查询

这三个模块前面再通过一个nginx反向代理,来作为js请求的唯一入口,nginx本身还负责静态资源的serve,通常会在nginx前面再搞一个四层的负载均衡,这个就看各个公司已有的基础设施支持情况了。

最后简单说说MySQL,通过上面的描述可以知道,监控历史数据不是存在MySQL里的,这个和Zabbix不同,因为MySQL虽然可以做高可用,但没法做水平扩展,容量瓶颈明显。MySQL总共建了三个库,其作用分别为:

  • uic:用户团队相关的信息存储
  • hbs:心跳相关的表,夜莺心跳逻辑没有使用etcd、zookeeper等重量级选手,使用MySQL,也够用
  • mon:采集/告警/屏蔽策略的存储、监控大盘、历史告警事件等
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值