由于时间较早,那时候业界还没有像样的TSDB产品,类似Prometheus,InfluxDB都是后起之秀,所以Dashboard选型主要使用了HBase来存储Metrics数据。并且基于HBase来实现了TSDB,解决了一些HBase热点问题,同时将部分查询聚合下放到HBase,目的是优化其查询性能,目前看来总体方案依赖HBase/HDFS还是有点重。
近些年,随着携程监控All-in-One产品的提出。对于内部的Metrics存储统一也提出了新的要求。由于Dashboard查询目前存在的诸多问题以及Metrics统一的目标,我们决定替换升级Dashboard现有的HBase存储方案,并且在Metrics场景提供统一的查询层API。
二、整体架构
Dashboard产品主要分了6个组件,包括dashboard-engine,dashboard-gateway,dashboard-writer,dashboard-HBase存储,dashboard-collector,dashboard-agent。目前实时写入数据行数6亿条/分钟,架构图如下:
- dashboard-engine是查询引擎。
- dashboard-gateway是提供给用户的查询界面。
- dashboard-writer是数据写入HBase的组件。
- dashboard-collector是基于Netty实现的Metrics数据收集的服务端。
- dashboard-agent是用户打点的客户端,支持sum,avg,max,min这几种聚合方式。
- dashboard-HBase是基于HBase实现的Metrics存储组件。
产品主要特性如下:
- 支持存储精确到分钟级的基于时间序列的数据。
- 单个指标数据可支持多个tag。
- 展现提供任意形式的视图同时可灵活基于tag进行分组。
三、目前的存在问题
基于HBase的Metrics存储方案虽然具有良好的扩展性,比较高的吞吐,但是随着时间发展,已经不是最优的TSDB方案了,可以归纳总结为如下几个痛点。
- 在TSDB场景查询慢,整体表现不如专业的TSDB。
- HBase热点问题,容易影响数据写入。
- HBase技术栈运维操作很重。
- 采用自研协议,不支持业界标准的Prometheus协议,无法和内部All-in-one监控产品较好的融合。
四、替换难点
- 系统写入数据量大,6亿条/分钟。
- Dashboard数据缺乏治理,很多不合理高维的metrics数据,日志型数据,经过统计,整体基数达上千亿,这对TSDB不友好,这部分需要写入程序做治理。如图2所示是top20基数统计,有很多Metric基数已经上亿。
- Dashboard系统存在时间久,内部有很多程序调用,替换需要做到对用户透明。
五、替换升级方案
从上面的架构来看,目前我们替换的主要是dashboard-writer和dashboard-HBase这两个最核心的组件。为了对用户的平滑迁移,其他组件稍作改动,在dashboard-engine组件上对接新的查询API即可替换升级成功。对于用户侧,查询的界面dashboard-gateway和打点的客户端dashboard-agent还是原有的模式不变,因此整个的替换方案对用户透明。具体如下:
1、dashboard-HBase升级为dashboard-vm
存储从HBase方案替换成VictoriaMetrics+ClickHouse混合存储方案:
- VictoriaMetrics是兼容主流Prometheus协议的TSDB,在TSDB场景下查询效果好,所以会接入绝大多数TSDB数据。
- 基于ClickHouse提供元数据服务,主要为界面的adhoc查询服务,原来这部分元数据是存储在HBase里面,新的方案采用ClickHouse来存储。元数据主要存储了measurement列表,measurement-tagKey列表,measurement-tagKey-tagValue列表这三种结构,目前在ClickHouse创建了一张表来存这些元数据。
本地表结构为:
CREATE TABLE hickwall.downsample_mtv
(`timestamp` DateTime,
`metricName` String,
`tagKey` String,
`tagValue` String,
`datasourceId` UInt8 DEFAULT 40)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/hickwall_cluster-{shard}/downsample_mtv', '{replica}')
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (timestamp, metricName, tagKey)
TTL timestamp + toIntervalDay(7)
SETTINGS index_granularity = 8192
分布式表结构为:
CREATE TABLE hickwall.downsample_mtv__dt
(`timestamp` DateTime,
`metricName` String,
`tagKey` String,
`tagValue` String,
`datasourceId` UInt8 DEFAULT 40)
ENGINE = Distributed(hickwall_cluster, hickwall, downsample_mtv, rand())
- ClickHouse存储少量日志型的数据
由于长期缺乏一些治理,Dashboard还存储了一些日志型数据,这类数据是一些基数很大但数据量少的数据,不适合存储在VictoriaMetrics。为了实现所有数据透明迁移,这部分数据经过评估,通过白名单配置的方式接入ClickHouse来存储,需要针对每一个接入的日志型指标来创建表和字段。目前的做法是按照BU维度来建表,并且针对指标tag来创建字段,考虑到接入的日志型指标数量少,所以表的字段数量会相对可控。用机票FLT的表结构举例如下图。
2、Dashboard-writer升级为Dashboard-vmwriter
Dashboard-collector会分流全量的数据到Kafka,Dashboard-vmwriter的工作流程大致是消费Kafka->数据处理->数据写入存储。Dashboard-vmwriter主要实现了以下几个核心的功能:
- Metrics元数据抽取功能,负责抽取出measurement,tagKey,tagValue写入ClickHouse的mtv本地表。这块元数据存储主要依赖了Redis(用于实时写入)和ClickHouse(用于查询)。
- 指标预聚合功能,用于加速查询。对接公司内部的配置中心来下发预聚合的配置,配置格式如下。
下面的配置会生成ClusterName和appid这两个维度组合的credis预聚合指标。
{
"metricName": "credis.java.latency",
"tagNames": [
"ClusterName",
"appid"
]
}
配置下发后,Dashboard-vmwriter会自动聚合一份预聚合指标存入VictoriaMetrics,指标命名规则为hi_agg.{measurement}_{tag1}_{tag2}_{聚合field}。同样的,查询层API会读取同样的预聚合配置来决定查询预聚合的指标还是原始的指标,默认为所有的measurement维度都开启了一份预聚合的配置,因为在TSDB实现中,查一个measurement的数据会扫描所有的timeseries,查询开销很大,所以这部分直接去查预聚合好的measurement比较合理。
- 数据治理:异常数据自动检测及封禁,目前主要涉及以下方面:
1)基于HyperLogLog的算法来统计measurement级别的基数,如果measurement的基数超级大,比如超过500万,那么就会丢弃一些tag维度。
2)基于Redis和内存cache来统计measurement-tagKey-tagValue的基数,如果某个tagValue增长过快,那么就丢弃这个tag的维度,并且记录下丢弃这种埋点。Redis主要使用了set集合,key的命名是{measurement}_{tagKey},成员是[tagValue1,tagValue2,… , tagValueN],主要是通过sismember来判断成员是否存在,sadd来添加成员,scard判断key的成员数量。
写入程序会先在本地内存Cache查找Key的成员是否存在,没有的话会去Redis查找,对Redis的qps是可控的,本地Cache是基于LRU的淘汰策略,本地内存可控。整个过程是在写入的时候实时进行的,也能保证数据的及时性和高性能,写入Redis的元数据也会实时增量同步到ClickHouse的mtv表,这样用户界面也能实时查询到元数据。
3)数据高性能写入,整个消费的线程模型大概是一个进程一个kafka消费线程n个数据处理线程m个数据写入线程。线程之间通过队列来通信,为了在同一个进程内方便数据做预聚合操作。假设配置了4个数据处理线程,那么就会按照measurement做hash,分到4个bucket里面处理,这样同一个measurement的数据会在一个bucket里面处理,也方便后续的指标预聚合处理。
private int computeMetricNameHash(byte[] metricName) {
int hash = Arrays.hashCode(metricName);
hash = (hash == Integer.MIN_VALUE ? 0 : hash);
return hash;
}
byte[] metricName = metricEvent.getName();
hash = computeMetricNameHash(metricName);
buckets[Math.abs(hash) % bucketCount].add(metricEvent);
经过程序埋点测算,正常情况下整体链路的数据写入延迟控制在1s内,大约在百毫秒级。
3、Metrics统一查询层
契约上,兼容了Dashboard原来的查询协议,也支持标准的prometheus协议。
实现上,封装了VictoriaMetics+ClickHouse的统一查询,支持元数据管理,预聚合管理,限流,rollup策略等。
查询层主要提供了以下四个核心接口。
- Data接口:根据measurement,tagKey,tagValue返回时序数据,数据源是VictoriaMetrics。
- Measurement接口:返回limit数量的measurement列表,数据源是ClickHouse。
- Measurement-tagKey接口:返回指定measurement的tagKey列表,数据源是ClickHouse。
- Measurement-tagKey-tagValue接口:返回指定measurement和tagkey的tagValue的列表,数据源是ClickHouse。
如下图第一张所示是新的存储架构,第二张是VictoriaMetrics自身的架构。
需要注意到,整个数据写入层是单机房写单机房的存储集群,是完全的单元化结构。最上层通过统一的数据查询层汇总多个机房的数据进行聚合输出。在可用性方面,任何单一机房的故障仅会影响单机房的数据。
六、替换前后效果对比
1)替换后的查询耗时从MAX,AVG,STD提升近4倍。查询耗时大多落在10-50ms之间。相比之前HBase经常查询超时,整体查询的稳定性也好了很多,见图6,7。
2)写入稳定性提升,彻底解决了因为HBase热点引发的数据积压。
3)替换后支持了更多的优秀的特性,可以基于promQL实现指标的逻辑计算,同比环比,模糊匹配等。
七、未来规划
1)统一查询层接入所有Metrics数据,除了Dashboard,目前内部还有HickWall,Cat有大量Metrics数据没有接入统一查询层,目前采用的是直连openrestry+VictoriaMetrics的方式,openrestry上面做了一些简单的查询逻辑,这块计划后续接入统一查询层,这样内部可以提供统一的元信息管理,预聚合策略等,达到Metrics架构统一。
2)提供统一写入层,总体Metrics目前是近亿级/秒,这块写入目前主要是基于Kafka消费进存储的方式,内部这块写入是有多个应用在处理,如果有统一的写入层那么就能做到写入逻辑统一,和查询层的查询策略也能做到联动,减少重复建设。
3)Metrics的存储统一层提供了较好的典范,内部的日志存储层统一也在如火如荼的进行中,也会往这样的一个方向发展。