
监控告警是网站可用性的第一道防线,为网站提供更加实时可靠高效的监控告警,对互联网企业具有非凡的意义。致力于这个目标,经过不断地改进,携程新一代监控告警平台 Hickwall 在存储效率、查询速度和告警可靠性方面都有了极大的改善。
本文将从存储、聚合、告警三个方面介绍 Hickwall 在核心架构方面的演进。
架构演进概述
为了更好地了解 Hickwall 在核心架构方面的设计,我们首先将 Hickwall 第一代的架构和现有架构进行比较。
Hickwall 最初的研发是在 2015-2016 年,当时我们调研了业界知名的开源监控系统。
比如 Graphite,拥有非常好的生态,但是集群配置复杂,每个指标都采用一个文件存储,导致小文件多,iowait 高,并且使用 python 实现,性能方面不太令人满意。
再比如 OpenTSDB,基于 HBase 天然就支持分布式,但是也受限于 HBase,多维查询的时候性能比较差。而其他的监控系统也并未非常成熟,最后我们决定使用 ElasticSearch 作为存储引擎。下图是第一代的核心架构图。

在这个架构中监控数据从 Proxy 进来,经过格式整理、数据补全、限流后发送到 Kafka。Donwsample 消费 Kafka 中的原始数据进行时间维度上的聚合,聚合成 5m、15m 等时间维度的数据点之后写入到 Kafka。Consumer 消费 Kafka 中的原始数据和聚合数据写入到 ES,通过 API-Server 提供统一的接口给看图和告警。
因为 ES 的查询性能无法满足 Trigger 高频率的拉取需求,我们另外增加了 Redis 用来缓存最近一段时间的数据用于告警。这套架构初步实现了监控系统的功能,但是在使用过程中我们也发现以下几个问题:
组件过多。运维架构追求的是至简至稳,过多的组件会增加部署和维护的难度。另外在团队人员变动的情况下,新成员进来无法快速上手。
数据堆积。Consumer 消费 Kafka 出现问题,容易导致 Kafka 中数据堆积,用户将无法看到线上系统的当前实时状态,直到将堆积的数据消费完。按照我们的实践经验,数据堆积的时间往往会有几十分钟,这对于互联网企业来讲是个非常大的问题。
数据链条过长。监控数据从 Proxy 进来到 Trigger 告警需要依次经过 6 个组件,任何一个组件出现问题,都可能导致告警漏告或误告。
为了解决这些问题,我们研发了 Hickwall 的第二代架构,使用自研的 Influxdb 集群取代了 ES 作为存储引擎,如下图。

在这个架构中监控数据从 Proxy 进来分三路转发,第一路发送给 Influxdb 集群,确保无论发生任何故障,只要 Hickwall 恢复正常,用户就能立即看到线上系统的当前状态。
第二路发送给 Kafka,由 Downsample 完成数据聚合后将聚合数据直接写入到 Influxdb 集群。第三路发送给流式告警,这三路数据互不影响,即使存储和聚合都出现问题,告警依然可以正常工作,确保了告警的可靠稳定。
Influxdb 集群设计
ES 用于时间序列存储存在不少问题,例如磁盘使用空间大,磁盘 IO 使用多,索引维护复杂,写入和查询速度慢等。
而 Influxdb 是排名第一的时间序列数据库,能针对时间范围进行高效的查询,支持自动删除过时数据,较低的使用和维护成本。只是早期的 Influxdb 不够稳定,bug 比较多,直到 2017 年底。我们经过测试确认 Influxdb 已经足够稳定可以交付生产,就萌生了用 Influxdb 替换 ES 的想法。当然 Influxdb 存在单点问题,在 0.12 版本以后,官方的集群方案还闭源了。
为了解决 Influxdb 的单点问题,我们研发了 Influxdb 的集群方案 Incluster,如下图。

Incluster 并没有对 Influxdb 进行代码侵入式的修改,而是在上层维护关于数据分布和查询的元数据,因此当 Influxdb 有重大发布的时候 Incluster 能够及时更新数据节点。
客户端通过 Incluster 节点写入数据,Incluster 按照数据分布策略将写入请求转发到相关的 Influxdb 节点上,查询的时候按照数据分布策略从各个节点上读取数据并合并查询结果。在元数据这一层 Incluster 采用 raft 保证元数据的一致性和分区容错性,在具体数据节点上使用一致性 hash 保证数据的可用性和分区容错性。
Incluster 提供了三种数据分布策略 Series、Measurement 和 Measurement+Tag。通过调整数据分布策略,Incluster 能够尽量做到减少数据热点并在查询时减少查询节点。在实践过程中,我们使用 Measurement 策略来存储系统指标,如 CPU;使用 Measurement+Appid 策略来存储请求量。
作为一个分布式存储,磁盘损坏不可避免,灾备是必须考虑的问题。我们按照数据分布策略通过读取 Influxdb 底层的 TSM 数据文件,来恢复损坏的节点上面的数据。实践经验表明 Incluster 能够做到半个小时恢复一个损坏的节点。

在用户使用方面,Incluster 提供了对 InfluxQL 的透明支持,也提供了类 Graphite 语法用于配图。类 Graphite 语法可以简化配图语法,提供 InfluxQL 无法实现的功能,例如查询最近一段时间变化最剧烈的指标,除此之外还可以屏蔽底层存储细节,以后如果想使用比 Influxdb 更优秀的时间序列存储引擎,可以减少用户迁移成本。

数据聚合的探索
Influxdb 在数据存储和简单查询方面表现出色,但是在数据聚合上就存在一些问题。
Influxdb 提供了 Continuous Query Language(CQL)用于数据聚合,但是经过测试发现 CQL 内存占用较大。Influxdb 原本需要的内存就不小,在我们使用过程中 128G 内存已经使用了一半,如果再加上 CQL 的内存,容易造成节点不稳定。
另外 CQL 无法从不同的节点获取数据进行聚合,在 Incluster 集群方案中存在资源浪费维护复杂的问题。因此我们将数据聚合功能独立出来,在外部进行数据聚合后再将聚合数据写入到 Incluster。
时间维度的聚合是有状态的计算,我们面临两个问题。一个是中间状态如何减少内存的使用,另外一个是节点重启的时候中间状态如何恢复。
我们通过指定每个节点需要消费的 Kafka Partition,使得每个节点需要处理的数据可控,避免 KafkaPartition Rebalance 导致内存不必要的使用,另外通过对 Measurement 和 Tag 这些字符串的去重可以减少内存使用。中间状态恢复方面我们并没有使用保存 CheckPoint 的方法,而是通过提前一段时间消费来恢复中间状态。这种方式避免了保存 CheckPoint 带来的资源损耗。
业务场景聚合主要的挑战在于一次聚合涉及到的指标数太多,聚合逻辑复杂。例如某个应用的某个接口的请求成功率,涉及到的指标数目上千,这种聚合查询 Influxdb 无法支持的。
我们的解决方案是使用 ClickHouse 进行预聚合。ClickHouse 是俄罗斯开源的面向 OLAP 的分布式列式数据库,拥有极高的读写性能,并提供了强大的 SQL 语言和丰富的数据处理函数,可以完成很多指标的处理,例如 P95。
流式告警的实现
告警最简单的实现就是定时从数据库中拉取数据,然后检查一下数据是否有异常。但是这种 Pull 的方式对存储存在一定的压力,尤其是告警规则告警对象众多的时候,对存储的可靠性和响应时间有极高的要求。
我们经过研究发现告警数据在所有监控数据中占比其实不大,以携程为例只占了 8%,而且需要的绝大部分都是最近几分钟的数据,如果我们能从数据流中直接获取所需要的数据,就能过滤掉大部分不必要的数据,避免对后台存储的依赖,让告警变得更加可靠实时。
实现流式告警最大的挑战是数据订阅。我们不可能让每一个告警规则都去消费一遍数据流,最好的方式是消费一遍数据流然后将告警数据准确的分发到告警上下文中。
在这里如何降低数据分发的时间复杂度和空间复杂度是最大的难点。
Hickwall 的实现思路是减治法,通过 Measurement 精确匹配减少下一步需要匹配的规则数量,通过 tagValue 的布隆过滤器判断是哪个 Trigger 节点需要的数据。Trigger 节点收到数据以后对数据点进行精确的匹配过滤,转发到具体的告警上下文中。
这个方案的优势在于时间复杂度不随规则数量告警对象而线性增长,空间复杂度不随 tagValue 的长度而增长。

Hickwall 使用 Akka 框架进行告警逻辑和告警数据的处理。Akka 是异步高并发的框架,提供了 Actor 编程模型,能够轻松实现并发地处理数据和执行告警逻辑。
生产系统是个时刻变化的系统,每时每刻都可能有机器上下线,每时每刻都可能有应用发布变更,随着这些变动告警系统需要随之增删告警对象和修改告警阈值。而 Actor 的创建删除是非常轻量的,为生产系统提供了非常友好的抽象,降低了开发成本。
Hickwall 使用了 RocksDB 来缓存告警数据,通过 JNI 直接嵌入到 Trigger 实例中。RocksDB 是 Facebook 开源的 KV 数据库,基于 Google 的 LevelDB 进行了二次开发,底层存储使用 LSM Tree,拥有极高的写入速率。
无停滞的处理数据在流式告警中是非常重要的,使用 RocksDB 能够减少 JVM 中的对象,减少内存的使用,进而减少了 JVM GC 的压力。

在用户使用方面,Hickwall 提供了基于 JS 语法的 DSL 语言,Init DSL 负责数据的订阅和接收到数据后的处理工作,提供了 groupBy、filter、exclude、summarize 等流式计算中常见的数据处理函数,Run DSL 负责具体的告警逻辑,判断是否有异常。
考虑到 DSL 书写有一定的难度,Hickwall 提供了语法检查、历史数据回测等功能,帮助用户书写出符合需求的告警逻辑。

作者简介
陈汉,携程网站运营中心研发工程师,从事 Hickwall 监控告警平台的研发工作。 经历了 Hickwall 项目的雏形到交付生产再到不断改进,通过整个开发过程,对监控领域有了深入的了解。喜欢探究系统的底层原理,对分布式有浓厚的兴趣。本文来自陈汉在“2018 携程技术峰会”上的分享,首发于“携程技术中心“。
评论