本文将通过对时序数据的基本概念、应用场景以及京东智联云时序数据库HoraeDB的介绍,为大家揭秘HoraeDB的核心技术架构和解决方案。
首先我们来了解下时序数据库的基本概念。时序数据库全称时间序列数据库,主要用于处理带时间标签的数据,带时间标签的数据也称为时序数据。
时序数据库是一种高性能、低成本、稳定可靠的在线时序时空数据库服务,提供高效读写、高压缩比存储、时序数据插值及聚合计算等服务,广泛应用于服务和服务器监控系统、物联网(IoT)设备监控系统、生产安全监控系统和电力检测系统等行业场景。此外,它还能提供时空场景的查询和分析能力。
时序数据库中存储的是时序数据,时序数据的结构特点和简单举例如下:
上图展示的是一个服务和时间点紧密关联的流量变化数据,数据如下:
- Metric:时序数据的指标名称;
- Tags:对时序数据指标进行补充描述的标签,描述清楚数据是什么、来源等,方便后期对数据进行筛选和聚合计算等;
- Dps:时序数据的数据点,是一系列随着时间变化的值,分为时间戳和具体的value两个部分。
- 证券交易:可以用时序数据库来保存随着时间波动的交易价格等数据;
- 气温变化情况:可以用时序数据库存储气温变化的数据,便于记录和分析某个地区的气温变化波动规律;
- 服务器监控:通过对大规模应用集群和机房设备的数据采集,存储到时序数据库中,就可以实时关注设备运行状态、资源利用率和业务趋势,实现数据化运营和自动化开发运维;
- 物联网传感器:物联网设备无时无刻不在产生海量的设备状态数据和业务消息数据,这些数据有助于进行设备监控、业务分析预测和故障诊断;
- 网站/服务监控数据:通过日志或者其他方式对原始指标数据进行采集和实时计算,最后将实时计算的结果数据存储到时序数据库,实现对网站和服务的监控和分析。
- 时序数据的写入:每秒需要有百万至千万量级的时序数据点写入,并且写入流量没有低峰期和高峰期的区分,所以时序数据库需要解决好如何7*24小时支持好百万甚至上千万级的时序数据写入问题;
- 时序数据的读取:如何低延迟解决好单个请求读取百万级数据点的查询以及几十万级别数据计算和聚合问题;
- 成本问题:时序数据数据量级较大,且在业务上可能至少需要存储一年以上的历史数据,以便于后期来对数据进行分析和处理,所以时序数据库需要解决好存储成本的问题。
HoraeDB是京东智联云自研的一款时序数据库,在数据的写入协议上完全兼容OpenTSDB,数据的查询上兼容OpenTSDB restful API和PromQL。
HoraeDB主要有以下特点:
- 高性能:支持数据批量异步写入,高并发查询以及强大的数据计算和聚合能力;
- 高可用:数据存储分布式架构,副本数和数据的一致性级别灵活可调;可以根据需要,对数据的写入做多AZ双写和HA查询等;
- 使用成本低:丰富的数据类型,REST接口、数据写入查询均使用json格式,并且接口和协议上完全兼容OpenTSDB和PromQL,历史服务前移学习成本较低;
- 兼容开源生态:兼容OpenTSDB和PromQL协议以及开源的Kibana组件,可以方便的使用开源生态中已存在的工具和组件对时序数据进行查询、分析和展示等。
HoraeDB在整体架构上从上往下进行分层,大概可以分为以下几层:
- Http-Server层:主要负责HTTP服务端口的监听、接收以及响应用户的请求;
- 协议处理层:这层是负责对接各种已有的开源组件的协议。主要是包括OpenTSDB-Parser、Prometheus-Adapter、Remote-Read、Remote-Write等几个组件:
◆OpenTSDB-Parser:负责对当前开源的OpenTSDB的请求协议进行解析和处理,包括查询请求和响应请求;
◆ Prometheus-Adapter:对PromQL查询语法进行解析,解析成HoraeDB内部标准的查询协议;
◆ Remote-Read:实现Prometheus的Remote-Read模式,可以使用Prometheus来查询HoraeDB中的数据,具体配置使用方式可以参考Prometheus配置说明;
◆ Remote-Writer:实现Prometheus的Remote-Write模式,可以在生产环境中将HoraeDB作为Prometheus的分布式集群存储解决方案,将Prometheus中采集到的数据写入到HoraeDB。具体配置使用方式可以参考Prometheus配置说明。 - HoraeDB协议层:包括HoraeDB-PutReq和HoraeDB-QueryReq,这一层主要是定义好HoraeDB自身写入的数据的结构和查询请求的结构;
- 处理引擎层:包括了write-engine和query-engine,这层是分别进行数据写入和查询的处理逻辑;
- 数据队列:这一层主要是针对数据写入而言的,数据要写入到底层存储,会先写到队列中,然后消费端根据配置文件的指定的后端,进行数据的处理;
- 存储Client层:适配各种各样的三方存储组件,负责数据的持久化存储处理,目前实现了ES-Client、Cassandra-Client、Cache-Client、Local-Storage:
◆ ES-Client:负责将时序数据中的Meta部分信息写入到ES进行存储;
◆ Cassandra-Client:负责将时序数据中的数据点写入到Cassandra中进行持久化存储;
◆ Cache-Client:将新写入的热点时序数据写入到高速缓存组件,对热点数据进行Cache。 - 数据存储层
◆ Cassandra:存储时序数据中的数据点;
◆ ES:存储时序数据中的Meta信息;
◆ TS-Cache:自研的时序数据Cache系统,内部使用delta-delta和XOR编码方式对时序数据的时间戳和值进行压缩,可以存储最近三小时的热点时序数据。
HoraeDB在底层数据存储将时序数据拆分成两部分进行存储,一部分是meta数据,这部分数据主要是对时序数据进行描述的,描述了一条时间序列是什么,来自于哪里;另外一部分是时序数据点,包括时间戳和具体的值,这部分数据是会随着时间变化,周期性上报的部分。例如下面一条数据:
- Meta部分包括了name、tags、additionTag等三个部分,对一条时间线进行了描述;
- 数据点部分包括了timeStamp和value,是具体的时序数据点。
它们各自的特点如下:
- Meta
◆ 写入后,基本不会有变更;
◆ 需要支持多维度,多种方式进行数据筛选,比如Tag精确匹配,前缀搜索、正则匹配等多种搜索;
◆ 数据量相对小。 - 时序数据点
◆ 周期性汇报;
◆ 和时间戳强关联;
◆ 需要支持按照时间范围进行数据筛选;
◆ 数据量大。
根据以上特点,我们在数据持久化存储中,针对Meta和时序数据点,分别选择了Elasticsearch和Cassandra来进行数据存储。
Elasticsearch
Elasticsearch是一个基于RESTful web接口并且构建在Apache Lucene之上的开源分布式搜索引擎。
同时ES还是一个分布式文档数据库,其中每个字段均可被索引,而且每个字段的数据均可被搜索,能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。
Cassandra
Cassandra是一套开源分布式NoSQL数据库系统。具有以下特点:
- 线性扩展,轻松应对速度、多样性和复杂性问题:Cassandra是线性扩展,可以根据前台数据流量轻松确定集群规模;
- 架构简单,运维成本低:Cassandra不依赖外部组件,所有必须的操作都集成在Cassandra内部了。同时,由于它是P2P对等架构,无主,环上的节点都是对等的,极度简化部署及后续运维工作,适合大规模部署;
- 高可用:Cassandra采用了许多容错机制。由于Cassandra是无主的,所以没有单点故障,可以做到不停服滚动升级。这是因为Cassandra可以支持多个节点的临时失效(取决于群集大小),对集群的整体性能影响可以忽略不计。Cassandra提供多地域容灾,允许您将数据复制到其他数据中心,并在多个地域保留多副本。
HoraeDB在Cassandra中的Schema定义:
- 行定位:uuid + partitionKey来唯一定位到一行数据,uuid是根据name + 排序后的Tag来计算出来的,唯一可以表示单个时间序列的ID, partitionKey是由程序来定义的划分行的字符串,比如按照天来划分行,那么我们就可以传入数据时间戳的当天日期;
- 列定位:每列由数据的时间戳来表示,每个时序点占一列,每列中包含value和createTime字段。value是经过编码后的数据,createTime是由时序数据点到来的时间计算出的timeUUID来表示,用于支持多版本的特性。
业务对于监控数据的使用需求多种多样,有查最新数据的异常告警,也有查看一整年指标数据的趋势图展示,数据量越大查询耗时就越久,如果放在浏览器端处理也要耗费大量的内存。这不但对系统造成了很大的压力,也给用户带来了难以忍受的查询体验。鉴于此,我们引入了多级的降采样机制来应对不同跨度的数据查询。
如上图所示,提前降低采样算法就是将连续不断流入的时序数据点来进行分桶,计算出这个桶内的均值、最大值、最小值、总和等,这样用户在查询数据的时候,直接返回合适的采样数据即可。
为了提供多种粒度的数据,HoraeDB支持对原始数据进行多级降采样,比如可以将原始数据降采样成10m和1h粒度的数据。
流式抽样的核心处理思想如上图所示:在HoraeDB的内存中保存了每个时间序列。每个序列上都维护了当前这个序列的cnt、sum、max、min、last等值,随着时间的推移,时序数据点源源不断的流入,相应序列上的cnt、sum、max、last都会进行累加或者值更新,当到达设定的时间窗口,比如10分钟,就会将计算后的结果存入到Cassandra中。
流式抽样中需要解决的难点主要有两个:数据迟到问题和时间窗口到达后对底层Cassandra的压力冲击。HoraeDB对这些问题的解决方案如下:
- 数据迟到问题:为了解决数据迟到问题,HoraeDB的数据抽样器对每个时间序列的抽样结果暂存一个周期,等待迟到的数据到来,然后再存入到底层Cassandra;
- 时间窗口到达,对底层尖峰流量:数据发送使用了令牌桶算法来进行数据的发送,控制数据发送频率。
在时序的场景下,我们经常会有一些大批量数据分析计算的需求。比如需要计算10万个服务器30分钟内的cpu变化趋势。这样的查询会很慢,同时也会导致我们的时序存储服务不稳定。
为了应该对这种业务场景,我们做了预聚合计算方案,预聚合计算分为流式聚合计算和批量计算。如下图所示:
- 流式聚合计算
对于大规模时序数据分析场景,我们采用流式聚合计算,如上图所示,数据上报到kafka。我们的Flink Job会根据用户提前配置好的聚合规则,对流入的数据进行聚合计算,计算完成后,写回到我们的存储中,流式聚合计算有以下的特点:
◆ 计算性能强,可横向扩展;
◆ 计算支持灵活性相对较弱;
◆ 需要部署单独的计算等组件。 - 批量聚合计算
批量聚合计算使用的是拉模式进行数据聚合计算,核心逻辑是HoraeDB根据用户创建的聚合规则,设置一些定时计算任务,计算任务会定时向底层存储层发起数据拉取和计算,计算结果写回到存储中,批量聚合计算有以下的特点:
◆ 计算灵活性强;
◆ 无需单独部署计算组件和消息队列;
◆ 支持到5w线左右,对底层存储开销高。
HoraeDB原有架构下,做一些大批量数据查询和计算时存在以下问题:
- 聚合性能低:原有引擎在执行聚合运算的时候,也和传统数据库所通常采用的iterative执行模式一样,迭代执行聚合运算。问题在于每次iteration执行,返回的是一个时间点。Iterative 执行每次返回一条时间点,但对HoraeDB查询有可能需要访问大量时间线数据,这样的执行方式效率上并不可取;
- 查询速度慢
◆ 从Cassandra查询数据是一个比较耗时的过程,会耗费比较长的时间在数据准备上;
◆ 整个计算过程需要等Cassandra中的数据全加载到内存中,才开始计算。
- 内存和CPU资源消耗高
◆ HoraeDB有可能在短时间内下发太多Cassandra读请求,一个查询涉及到的Cassandra读请求同时异步提交,有可能在很短时间内向Cassandra下发大量的读请求。这样,一个大查询就有可能把底层的Cassandra打爆;
◆ 同时拉取大量的数据点到内存中,会导致HoraeDB内存打爆。
新的查询引擎针对老的查询计算引擎进行了优化:
借鉴传统数据库执行模式,引入Pipeline的执行模式。Pipeline包含不同的执行计算算子(operator),一个查询被物理计划生成器解析分解成一个Query Plan, 由不同的执行算子组成,DAG上的root operator负责驱动查询的执行,并将查询结果返回调用者。在执行层面,采用的是top-down需求驱动的方式,从root operator驱动下面operator的执行。这样的执行引擎架构具有如下优点:
- 这种架构方式被很多数据库系统采用并证明是有效的;
- 接口定义清晰,不同的执行计算算子可以独立优化,而不影响其他算子;
- 易于扩展:通过增加新的计算算子,很容易实现扩展功能。比如目前查询协议里只定义了tag上的查询条件。如果要支持指标值上的查询条件(cpu.usage >= 70% and cpu.usage <=90%),可以通过增加一个新的ValueFilterOp来实现。
时序数据中,一般情况下,最近三个小时的数据是被查询比较多的热点数据,在HoraeDB中,为了加速这部分数据的访问,我们把最近三小时的数据放到自研的分布式缓存组件-TS-Cache中去。
TS-Cache是一个分布式的数据缓存系统,数据通过Hash算法均匀的分布在各个TS-Cache节点中。因为TS-Cache是个缓存组件,对数据的稳定性要求并没有那么高,所以我们的时序数据在TS-Cache中保存的是单副本。
数据在内存中的组织如上图所示,数据的顶层是一个SharedMap,它其实就是一个数组,目的是用来在内部对时序数据进行分片,降低gc对服务性能的影响,以及适当的减小锁的粒度,提升服务的读写性能。数据中的一个项就是SeriesMap,用来保存时序数据项和它的时序数据点的值,其内部的核心数据结构是一个跳表,跳表中的每一项对应的是一个时序数据中的一个项,在这边用SeriesData来进行表示。
SeriesData中有两个核心的数据结构,一个是cs,一个是blocks,cs是我们的一个流式压缩序列,这个流式压缩需要时根据上面所述的压缩算法来进行实现的,时序数据写入系统后,会直接写到这个压缩序列cs中,每隔一定时间,会把压缩序列中的数据导出,形成一个block数据块,写到blocks数组中。
为了用更少的内存来存储更多的数据,在TS-Cache中的数据是压缩成为block后进行存储,放到了上图的blocks数组中进行存储;数据压缩算法使用的是Gorilla这篇论文所提出的数据压缩算法,Gorilla引入了对timestamp和value的高压缩比算法,可大幅降低数据存储的大小。
Timestamp根据时间关联的条目进行差值计算、以及差值的差值计算得到占用字节数非常小的数值并进行保存。同样Value使用XOR算法进行计算得到占用存储更小的数值进行保存。
- 时间戳压缩:时序数据的产生大部分情况下都是有周期性的,可能是30s、1m等,所以在存储时间戳的时候,我们只需要存储时间戳直接的差值;
- 值压缩:通过对历史数据进行分析,发现大部分相邻时间的时序数据的值比较接近,而如果Value的值比较接近,则在浮点二进制表示的情况下,相邻数据的Value会有很多相同的位。整数型数据的相同位会更多。相同位比较多,意味着如果进行XOR运算的话会有很多位都为0,那么我们将当前值与前序值取XOR(异或)运算,保存XOR运算结果。
HoraeDB作为一个海量时序数据的存储系统,经常会遇到一些突发性的流量高峰或者一些不合理的大查询、慢查询,如何保障HoraeDB的稳定性便成了一个要攻破的技术难点。
为了保障HoraeDB的稳定性,我们做了以下几个方面的工作:
- 服务隔离;
- HoraeDB读写分离:读写分别部署不同实例,避免大查询慢查询影响数据写入;
- 计算处理层和存储层分离:数据写入和查询处理逻辑在HoraeDB层做,底层数据存储放到存储层进行;
- 机房隔离:HoraeDB部署上支持多写,底层存储可以配置不同机房,实现数据多写互备,查询支持多机房HA查询;
- 限流
◆ 在HTTP接口层支持接口调用频率进行限流;
◆ 针对写入,HoraeDB内部通过写入队列来感知当前写入负载,当队列满,即丢弃数据,保护HoraeDB正常工作;
◆ 查询上,基于查询时间跨度、时间线条数、数据点规模等进行了限制,避免大查询影响系统稳定性。 - 查询负载管理:因为在我们的时序数据查询场景中,80%以上的情况都是查询最近三小时内的数据,都是一些查询和计算量比较小的查询任务,所以在查询上我们根据查询请求进行计算量简单预估,分为慢查询和快查询,分别放入到对应的查询任务队列,避免慢查询影响快查询;
- 综合全面的服务监控指标,HoraeDB的问题可以快速被发现和定位。