去哪儿旅行海量指标数据采集与存储
作者介绍
肖双,2018年加入去哪儿网,运维开发专家,目前负责去哪儿网 CI/CD、监控平台和云原生相关建设。期间负责落地了去哪儿网容器化平台建设,协助业务线大规模应用迁移至容器平台,完成监控系统 Watcher 2.0 的改造升级。对监控告警、CI/CD、DevOps 有深入的理解和实践经验。
一、背景
监控系统对于大家来说已经不陌生了,而我接触的监控系统也比较多,从早期的 cacti、nagios/nrpe、zabbix 到后来的 statsd、graphite 和近些年流行的 openFalcon、prometheus 等。可以看到大家对监控系统定位有些变化,从早期的侧重于基础设施监控到现在同时也重视应用的内部状态监控。而应用内部状态监控通常是通过代码埋点,指标上报/暴露的方式完成。
一般来说一个基本的监控系统至少需要具备三方面的能力:
数据采集
从各个数据源上抓取数据并存储到时序 DB 中,或者由数据源自己讲数据 Push 到收集器中,然后存储到时序 DB
数据展示
以不同类型的图或组织方式,将收集到的数据展示给用户,并提供通用的聚合能力,用户查看和分析数据
异常告警
对上报的数据做静态阈值或动态阈值之类的异常检测,将异常信息或指标通知给相关用户
再往深做,会有一些偏智能化的探索和实践,比如:
根因推荐
产生告警后根据应用拓扑、日志、事件等信息,综合分析,尝试分析出当前告警的原因,并生成报告推荐给用户,帮助用户快速定位问题。
故障自愈
产生故障或告警后,提取故障或告警特征,匹配对应的应急预案,执行预案,恢复故障。
另外当公司内部告警量过多时就会产生一些告警治理的需求,比如告警抑制、降噪和收敛,甄别出真正有用的告警精准的通知,对于一些频繁报警又没有人处理的进行降噪处理,告警量大时针对某一些业务维度进行聚合收敛等。另外告警需要进行链路追踪,一个告警事件出来后,需要知道都在什么时候通知给了什么人,有没有处理。
Watcher 是去哪儿网内部的一站式监控平台,由于开发较早当时 Graphite 的流行程度和扩展性都比较好,因此后端存储的选型是用的 graphite+whisper 做的二次开发,前端控制面则基于 grafana 做的二次开发。目前每分钟收集存储的指标总量在上亿级,检测和处理的报警量是百万级的。因为考虑到数据量比较大,所以我们的设计思路跟 openFalcon 是有些相似的,我们要求监控体系中任何组件都能够很好的水平扩展。
二、海量指标采集存储遇到的问题和解决方案
2.1 数据采集量大
数据采集这块是我们自研的 Qmonitor 系统,它类似于 Prometheus 的 scrape 模块,Prometheus 是一个 all in one 的系统,它将数据采集、存储、查询、异常检测这些全都放在一个系统中,这样做的好处是部署和运维比较简单,但是 Watcher 是将这些组件拆分开的,保证每个组件的独立性和扩展性。用户引用 QmonitorClient 包在应用中埋点,然后暴露出一个 http 接口的 url,Server 端每隔一段时间抓取数据分析聚合数据然后 Push 到真正的存储集群。
一开始遇到的便是采集指标数据的问题,主要的问题集中在以下 3 点:
要采集数据多,目前去哪儿网每分钟需要采集的应用指标在上亿级,同时根据这上亿级指标会聚合出来大概 4 千万左右的汇总指标
海量的聚合计算,上面提到过每一个应用实例中暴露出的每一个指标都需要跟应用的节点数(实例数)做聚合计算,根据上亿的单机指标,会聚合出对应的汇总指标。
为什么需要汇总指标,这里举一个例子,比如你有一个应用,这个应用部署了 100 台主机因此就有 100 个实例在运行,每个实例都会记录自己的接口 foo 的访问次数,比如指标名叫 foo.access.qps,那么通常我们在看 foo.access.qps 的时候是希望以应用为维度的,我们通常更关心当前整个应用的 foo 接口的 qps 而不是某一个实例的。
要求时效性高,必须在 1 分钟内完成所有抓取、计算和推送任务。
如上图所示,Master 节点定时从 DB 中获取所有的任务,然后通过 MQ 分发,Worker 端消费并且处理数据,而 Worker 采用 Python 的多进程多线程模型,将处理后的数据推送到后端存储集群上。这本质上就是一个典型的 Producer/Consumer 模型,这种模型扩展性好而且开发简单,因此我们第一版的数据采集应用便是基于此开发的。
但是随着任务量和指标量的增长,慢慢的出现了几个瓶颈:
任务量增多了之后,从 DB 中获取全量任务,并且通过 MQ 派发时所消耗的时长也越来越长了,时效性不能保证
Python 应用做大量聚合计算时,CPU 消耗比较高,需要更多的机器资源才能满足当前采集任务。
上图是 Worker 内部的逻辑结构图,Worker 大概的一个工作流程是这样的,每个 Worker 内部根据自己的定时器触发任务执行,一旦任务触发 scraper 便会从自己缓存的 Task 中拿到要采集的 url 信息,并发采集,采集回数据后根据不同的客户端类型放入到对应的解析器中,因为去哪儿网早期有多套埋点方式,因此需要兼容不同的客户端埋点格式,解析后的数据会生成统一的 SingleMetric 对象,SingleMetric 对象描述了指标的名称、数据、Tag、所属主机等等信息,生成的SingleMetric 对象会被放在缓存池中,当某个 AppCode 所有的 SingleMetric 采集完毕,聚合器从缓存池中取出这一组 SingleMetric 进行聚合计算,计算后生成 Metric 对象放入发送队列,Sender 从发送队列中取出 Metric Push 到后端存储集群。
新应用我们采用 Go+Gorotouine 的方式开发,对于资源消耗更低,吞吐量更大。整体改造重构后我们用现在的 15 台 worker 机替换了原来的 50+ 的 worker 主机,便能满足目前的数据采集需求,整体任务执行时延也控制在 1 分钟之内。
2.2 存储集群压力大
上面提到过我们后端的存储集群使用的是 Graphite+Whisper 做的二次开发,它自带了数据接收器(cabron-relay)+聚合器(carbon-aggregator)+存储器(carbon-cache)+时序 DB(whisper)这样的一套组件,是基于 Twisted 异步框架开发。而其中原生的 carbon-aggregator 组件在实现上有一些性能问题,在数据量大的时候内存和 CPU 的消耗都是巨大的,还有内存泄漏的风险。
目前我们的业务埋点指标每分钟更新的聚合指标量在 4 千万左右,单机指标则是上亿,每分钟磁盘写入总量 10GB,每天 14TB 左右的数据。由于数据量的增长,存储集群的 CPU、内存和磁盘 IO 的使用率都比较高,因此我们针对性的做出了下面几种优化方案:
1. 存储集群的完全分布式
Graphite 本身的扩展性是比较好的,它的每个组件都是比较容易扩展的,但是扩展起来运维成本是比较高的。因此我们将这一套组件进行单元化部署,配合 Salt 管理工具可以一键快速的部署出多个集群,将不同BU的数据分散到各个 carbon 集群中,这样做的好处一个是水平扩展可以降低单集群压力,另外各个 BU 的数据在底层是完全隔离的(虽然上层无须关心),不会因为一个部门或应用的数据突然暴涨,造成影响面太广。2. 优化 carbon-aggregator
carbon-aggregator 是 graphite 自带的聚合组件,能够允许用户通过配置的方式,来自定义聚合规则,比如指标 foo.access.qps 在每秒钟都可能上报几百甚至上千次数据,我们需要将单位时间内(假定 1 分钟)的数据聚合成一个总数存储到 tsdb 中,这个总数才是我们需要的数据,那么我们就可以在 aggregation-rules.conf 配置文件中添加一行配置规则 foo.access.qps (60) = sum foo.access.qps,这行配置表示对指标 foo.access.qps 每分钟的数据做 sum 计算。
原生的 aggregator 对于每一个要聚合的规则内部都会生成一个 map 用于缓存被处理过的指标,而且默认没有清理规则,会造成内存泄漏,随着运行时间越长这个内存占用就越来越大。而原生自带的 TTLCache 和 LRUCache 又性能太差,因此我们需要自己开发一种清理机制。
另外 aggregator 默认行为是即便匹配到了一个 rule,仍然会继续往下匹配,这不是我们需要的,我们只需要匹配到的第一条聚合规则生效即可,后面的都是无效动作。
基于此我们的优化思路分为下面几点:
用一个全局 cache 来替代每个 Rule 内部的 cache,避免同一个指标被多次 cache 造成内存浪费。这类似于倒排一样,原本是 rule → cache 这样一个顺序,现在倒过来变成 cache → rule,这样后期在 match_rule 的过程中也能避免掉循环各个 rule 进行匹配。
添加异步清理机制,对于每一个打数的指标会设置一个自增的 TTL,TTL 最大是 3,如果指标一段时间没打数,则TTL递减,一旦递减为 0,则此指标从缓存中清除掉,也就是说连续 3 个 Flush 区间这个指标都没有打数,那么它会被从缓存中清除。异步清理机制也解耦了指标更新逻辑,不用在每次更新时实时去检测是否有要清理的指标。
一旦匹配到规则,立即返回,不继续向下匹配
2.3 磁盘 IO 高
Whisper 是 Graphite 自带的时序 DB,它是类似 RRDTool 一样的轮转时序 DB。目前我们的聚合指标主要是用的 Whisper 存储(单机指标采用的 Clickhouse,如果想更换 DB,比如想使用 InfluxDB,那么只需要修改 carbon-cache 组件继承 TimeSeriresDatabase 类,重写相关 write/create ... 等方法即可)。Whisper 的每一个指标都会单独存储成一个特殊的文件,文件在生成时便按照相应的storage schema划分好一个个的 datapoint 的数据槽,用来存放数据。比如一个指标设置 1 分钟存一个数据点,一共存一天的数据,那么这个文件就会生成 1440 个数据槽,之所以叫轮转 DB,是因为过了这一天后,当存第 1441 个数据时会自动覆盖第一个槽的 datapoint。
因为这个特点,出现了一个问题便是,有的客户端在短时间内对同一个指标反复推送数据,比如一分钟内推送几十次甚至上千次,那么也只会是最后一次推送的数据生效,因为单位时间内只会落在同一个数据槽里,前面的无论多少次写都是无效写,是对资源无谓的消耗。因此我们做了相关优化如下图:# cache all metrics
metric.<name> (60) = last metric.<<name>>
最后在配置文件中,开启令牌桶限流和 whisper 异步写策略,来控制磁盘写入频率,更多的利用内存的性能。
三、总结
指标数据的采集和存储是建设监控系统的第一步也是最重要的一步,后期所有的功能都会基于此来构建。本文重点描述了去哪儿网监控系统在大量指标数据场景下的收集和存储时遇到的问题和解决方案,可以看到我们现在使用的方案有优点也有缺点,比如使用 whisper 作为时序 DB,优点是简单、支持回写而且经过这些年在去哪儿网内部已经沉淀了一套非常成熟的运维方案,但是缺点也非常明显,因为多存储策略会有级联写问题,IO 使用率依然较高尽管我们做了一些优化,因此我们开始尝试其他的 DB,比如 Clickhouse。也可以看到近些年许多新兴的监控系统也都采用 Go 语言来写,的确 Go 语言在监控领域中有天然的优势,并发高,cpu 消耗低。因此未来 Watcher 系统也会慢慢的向 Go 技术栈转换,而我们的一部分系统已经开始用 Go 重新设计开发了,如果有对监控和 Go 感兴趣的同学,欢迎共同交流。
END