系统设计

系统设计

PG 扩展

透明自动分区特性

在时序数据的应用场景下,其记录数往往是非常庞大的,很容易就达到 数以 billion 计。而对于 PG 来说,由于大量的还是使用 B+tree 索引,所以当数据量到达一定量级后其写入性能就会出现明显的下降(这通常是由于索引本身变得非常庞大且复杂)。这样的性能下降对于时序数据的应用场景而言是不能忍受的,而 TimescaleDB 最核心的自动分区特性需要解决就是这个问题。这个特性希望达到的目标如下:

  • 随着数据写入的不断增加,将时序数据表的数据分区存放,保证每一个分区的索引维持在一个较小规模,从而维持住写入性能

  • 基于时序数据的查询场景,自动分区时以时序数据的时间戳为分区键,从而确保查询时可以快速定位到所需的数据分区,保证查询性能

  • 分区过程对用户透明,从而达到 Auto-Scalability 的效果

TimescaleDB 对于自动分区的实现,主要是基于 PG 的表继承机制进行的实现。TimescaleDB 的自动分区机制概要可参见下图:

在这个机制下, 用户创建了一张普通的时序表后,通过 TimescaleDB 的接口进行了 hyper table 注册后,后续的数据写入和查询操作事实上就由 TimescaleDB 接手了。上图中,用户创建的原始表一般被称为“主表”(main table), 而由 TimescaleDB 创建出的隐藏的子表一般被称为“chunk”

需要注意的是,chunk 是伴随着数据写入而自动创建的,每次创建新的 chunk 时会计算这个 chunk 预计覆盖的时间戳范围(默认是 一周)。且为了考虑到不同应用场景下,时序数据写入速度及密度都不相同,对于创建新分区时,新分区的时间戳范围会经过一个自适应算法进行计算,以便逐渐计算出某个应用场景下最适合的时间戳范围。与 PG 10.0

自适应算法的详细实现位于 TimescaleDB 的 chunk_adaptive.c 的 ts_calculate_chunk_interval(),其基本思路就是基于历史 chunk 的 时间戳填充因子以及文件尺寸填充因子 进行合理推算下一个 chunk 应该按什么时间戳范围来进行界定。

借助透明化自动分区的特性,根据官方的测试结果,在同样的数据量级下,TimescaleDB 的写入性能与 PG 的 传统单表 写入场景相比,即使随着数量级的不断增大,性能也能维持在一个比较稳定的状态。

面向时序场景的定制功能

TimescaleDB 的对外接口就是 SQL,它 100%地继承了 PG 所支持的全部 SQL 特性。除此之外,面向时序数据库的使用场景,它也定制了一些接口供用户在应用中使用,而这些接口都是通过 SQL 函数(标准名称为 User-defined Function)予以呈现的。以下列举了一些这类接口的例子。

time_bucket()函数

该函数用于 降采样 查询时使用,通过该函数指定一个时间间隔,从而将时序数据按指定的间隔降采样,并辅以所需的聚合函数从而实现降采样查询。一个示例语句如下:

SELECT time_bucket('5 minutes', time)

AS five_min, avg(cpu)

FROM metrics

GROUP BY five_min

ORDER BY five_min DESC LIMIT 10;

将数据点按 5 分钟为单位做降采样求均值。

新增的聚合函数

为了提供对时序数据进行多样性地分析查询,TimescaleDB 提供了下述新的聚合函数。

  • first() 求被聚合的一组数据中的第一个值

  • last() 求被聚合的一组数据中的最后一个值

  • histogram() 求被聚合的一组数据中值分布的直方图

  • drop_chunks() 删除指定时间点之前/之后的数据 chunk. 比如删除三个月时间前的所有 chunk 等等。这个接口可以用来类比 InfluxDB 的 Retention Policies 特性,但是目前 TimescaleDB 尚未实现自动执行的 chunk 删除。若需要完整的 Retention Policies 特性,需要使用系统级的定时任务(如 crontab)加上 drop_chunks()语句来实现。

drop_chunks()的示例语句如下。含义是删除 conditions 表中所有距今三个月之前以及四个月之后 的数据分区:

SELECT drop_chunks(older_than => interval '3 months', newer_than => interval '4 months', table_name => 'conditions');

除此之外,TimescaleDB 定制的一些接口基本都是方便数据库管理员对元数据进行管理的相关接口,在此就不赘述。包括以上接口在内的定义和示例可参见官方的 API 文档。

TimescaleDB 的存储机制

TimescaleDB 对 PG 的存储引擎未做任何变更,因此其索引数据和表数据的存储都是沿用的 PG 的存储。而且,TimescaleDB 给 chunk 上索引时,都是使用的默认的 B+tree 索引,因此每一个 chunk 中数据的存储机制可以参见下图:

Chunk 存储机制

关于这套存储机制本身不用过多解释,毕竟 TimescaleDB 对其没有改动。不过考虑到时序数据库的使用场景,可以发现 TimescaleDB 的 Chunk 采用这套机制是比较合适的:

  • PG 存储的特征是 只增不改,即无论是数据的插入还是变更。体现在 Heap Tuple 中都是 Tuple 的 Append 操作。因此这个存储引擎在用于 OLTP 场景下的普通数据表时,会存在表膨胀问题;而在时序数据的应用场景中,时序数据正常情况下不会被更新或删除,因此可以避免表膨胀问题(当然,由于时序数据本身写入量很大,所以也可以认为海量数据被写入的情况下单表实际上仍然出现了膨胀,但这不是此处讨论的问题)。

  • 在原生的 PG 中,为了解决表膨胀问题,所以 PG 内存在 AUTOVACUUM 机制,即自动清理表中因更新/删除操作而产生的“Dead Tuple”,但是这将会引入一个新的问题,即 AUTOVACUUM 执行时会对表加共享锁从而对写入性能的影响。但是在时序数据的应用场景中,由于没有更新/删除的场景,也就不会存在“Dead Tuple“,因此这样的 Chunk 表就不会成为 AUTOVACUUM 的对象,因此 INSERT 性能便不会受到来自这方面的影响。

至于对海量数据插入后表和索引增大的问题,这正好通过上述的 自动分区 特性进行了规避。此外,由于 TimescaleDB 完全基于 PG 的存储引擎,对于 WAL 也未做任何修改。因此 TimescaleDB 的高可用集群方案也可基于 PG 的流复制技术进行搭建。TimescaleDB 官方也介绍了一些基于开源组件的 HA 方案