一、背景
监控体系关于咱们来说已经不陌生了,而我接触的监控体系也比较多,从前期的 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-Worker 这样的架构。
如上图所示,Master 节点定时从 DB 中获取一切的使命,然后通过 MQ 分发,Worker 端消费并且处理数据,而 Worker 选用 Python 的多进程多线程模型,将处理后的数据推送到后端存储集群上。这本质上就是一个典型的 Producer/Consumer 模型,这种模型扩展性好并且开发简略,因而咱们第一版的数据搜集运用就是依据此开发的。
可是跟着使命量和目标量的增长,慢慢的出现了几个瓶颈:
- 使命量增多了之后,从 DB 中获取全量使命,并且通过 MQ 派发时所耗费的时长也越来越长了,时效性不能确保
- Python 运用做很多聚合核算时,CPU 耗费比较高,需求更多的机器资源才能满意当时搜集使命。
因而咱们对这种模型做了优化调整,如下图所示。 总体上仍然是 Master-Worker 这样的一个架构,可是咱们取消掉了 MQ 的使命分发,每个 Worker 一旦发动会将自己注册到 etcd,Master 发现有 Worker 改变就会触发 Rebalance 行为,Rebalance 后 Worker 节点便能拿到自己所属的 Task 并将这些 Task 缓存下来,Worker 节点变成了有状况的节点,这样的长处是每分钟不必分派 Task,Task 是一开始就分配好了的,节省了这个分配的时刻。Master 和 Worker 通过 etcd 来进行事件通讯,Master 监听到 DB 中有使命改变(新增/删去/修改等),会通过 etcd 告诉到对应的 worker,Worker 更新自己缓存的 Task。如果其中一个 Worker 挂掉了,那么他担任的 Task 会被 Master 重新分配调度到其他 Worker 上持续履行,而 Master 也通过主从选举的办法保障自己的高可用。
上图是 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 区间这个目标都没有打数,那么它会被从缓存中铲除。异步整理机制也解耦了目标更新逻辑,不必在每次更新时实时去检测是否有要整理的目标。
- 一旦匹配到规矩,立即回来,不持续向下匹配
下图是部分示例代码: 优化前的 aggregator,在单机百万目标的处理下,正常占 3G 左右,运转的时刻久了之后会发生内存走漏,经常单进程会占到 6G 乃至更多的内存。 而优化后的 aggregator,同样单机百万目标的情况下,单进程常态在 500M-1G 左右的内存空间,没有内存走漏问题。
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。
由于这个特点,出现了一个问题就是,有的客户端在短时刻内对同一个目标重复推送数据,比方一分钟内推送几十次乃至上千次,那么也只会是最终一次推送的数据收效,由于单位时刻内只会落在同一个数据槽里,前面的不管多少次写都是无效写,是对资源无谓的耗费。因而咱们做了相关优化如下图: 将单位时刻内单目标的屡次写兼并,增加 last 聚合办法,坚持跟 whisper 本来结果相同
一起对聚合器增加规矩,可以让一切目标都进入聚合器,聚合器每 60s 将自己 cache 的目标数据 write 到 tsdb 中(在去哪儿网默许的目标检测间隔是 60s),以此达到写兼并的效果。
# cache all metrics
metric.<name> (60) = last metric.<<name>>
最终在配置文件中,敞开令牌桶限流和 whisper 异步写策略,来操控磁盘写入频率,更多的利用内存的功能。
三、总结
目标数据的搜集和存储是建造监控体系的第一步也是最重要的一步,后期一切的功能都会依据此来构建。本文要点描绘了去哪儿网监控体系在很多目标数据场景下的搜集和存储时遇到的问题和解决计划,可以看到咱们现在运用的计划有长处也有缺点,比方运用 whisper 作为时序 DB,长处是简略、支持回写并且通过这些年在去哪儿网内部已经沉淀了一套十分成熟的运维计划,可是缺点也十分显着,由于多存储策略会有级联写问题,IO 运用率依然较高尽管咱们做了一些优化,因而咱们开始尝试其他的 DB,比方 Clickhouse。也可以看到近些年许多新式的监控体系也都选用 Go 言语来写,的确 Go 言语在监控领域中有天然的优势,并发高,cpu 耗费低。因而未来 Watcher 体系也会慢慢的向 Go 技能栈转化,而咱们的一部分体系已经开始用 Go 重新规划开发了,如果有对监控和 Go 感兴趣的同学,欢迎一起沟通。