时序数据库的难题不只在写入速度
@toc
刚接触时序数据库的时候,我其实也跟很多人一样,把目光死死盯在一个问题上。那就是每秒到底能写进去多少条数据。设备啊、车辆啊、传感器啊,这些东西一直在不停地往上报数据。那写入的吞吐量当然很重要了。但是在真正的生产环境里面,数据写进去,这仅仅只是个起点而已。接下来呢,系统还得去查最近几分钟有没有异常,还得去回放过去一年的趋势,还得按设备和区域去做聚合。甚至还要去关联工单、关联空间位置。而且在设备数量一直不停地往上涨的时候,它还得保持稳定。
这也是我后来重新去理解时序数据库的一个开始。时序数据库面对的,其实不是那种一次性导完就拉倒的任务。它面对的是一条会一直变长的数据流。写入、查询、压缩、扩展,还有日常运维,这些事会长期同时存在。金仓时序数据库反反复复在提的超表(Hypertable)、Chunk,还有时间与空间分区。这些东西要解决的,正是这种要长期跑下去的工程问题。
这张图里面最要紧的一块,其实就是逻辑超表这一层。为什么呢?因为应用端只需要对着这张逻辑表写代码就行了。底下的数据到底切成多少块,路由怎么走,这些全交给数据库自己去维护。这个架构到底好不好用,最后还是得落回三个能测出来的指标上。第一个,写入能不能一直稳住。第二个,你查某个时间窗口的时候,它能不能精准命中间合理的范围。第三个,你扩容加了机器之后,那些热点是不是被均匀地分摊开了。
一、为什么普通分区表会逐渐吃力
很多项目刚开始搞的时候,会用普通的关系表去存时间数据。接着呢,再按月份或者日期去做分区。这种方案吧,比较好理解,早期的需求确实也能满足。但是问题也就跟着来了。随着你的设备数变多了,采样频率变高了,留存的时间也变长了。这时候系统的压力,它就不是一条直线上去了。
首先就是写入的压力。工业设备上面的温度啊、压力啊、电流啊、振动值还有状态值,这些东西是源源不断产生的。采集点从几千个涨到几十万个甚至更多之后,写入这事儿就不再是你以为的那么简单的一个插入动作了。它还会扯上日志啊、索引啊、磁盘 I/O 还有并发事务。其次就是查询的压力。用户不光要查某一台设备的曲线,他还要查某一个时间窗口里面全部设备的断面情况,要查异常点,还要查聚合出来的结果。再次呢,就是索引和分区的管理。分区数量变多了,索引规模变大了,冷热数据混在一起,这些东西到后面会变得根本没法靠人去维护。
普通的分区表,确实能解决一部分按时间范围去裁剪的问题。但是在企业层级里的真实应用中,最后一定会碰到几个绕不开的实际问题。比如说,分区怎么自己建出来?分区的间隔怎么跟着数据量去调?老数据怎么压缩?冷热数据怎么管?设备这个维度怎么把写入压力分散开?扩容的时候怎么把数据挪到更多节点上去?如果这些活儿全都靠写脚本、靠人工去弄。那么数据库跑的时间越长,就越离不开那么一两个懂内部细节的人了。这是一个很危险的情况。 
二、超表把物理分片藏在逻辑表后面
超表这个东西,它最大的价值在哪里呢?就是给应用端只露一张逻辑表出来。同时呢,底下那些物理切分的事儿,系统自己按时间和可选的空间维度去管了。写应用的人,他面对的还是那张熟悉的表,还是用标准的 SQL 写代码。他不需要在每一次查数据的时候,自己去手动拼上几十个物理分区的名字。更不需要把底下 Chunk 的名字写进业务代码里面去。
Chunk 是什么呢?其实就是数据库里头,用来管时间序列数据的一块一块连续的数据。系统会根据时间的范围,把数据塞进不同的块里面。当你去查某个时间窗口的时候,它只去碰那些可能沾边的块就行了。那些老的块呢,你还可以按策略去压缩它,归档它,或者直接删掉。对于应用来说,底下这些动作它是不用管的,也就是透明的。应用还是只管对着业务表读写就行。 
不过话说回来,虽然它透明,但并不代表你就不需要去设计它了。超表的时间粒度,你得结合你的写入量、你平时查的时间窗口,还有你打算怎么压缩,这几块一起来看。如果粒度搞得太大,单个块里面的数据量就会特别高,查询和维护的成本一下就上去了。那如果粒度搞得特别小呢,元数据和管理上的开销又会变很大。资料里的使用手册给了一个默认的块间隔,还说了能用 chunk_time_interval 去调。但在真实项目里,你还是得自己跑压测,看看数据怎么涨的,然后再把参数定下来。
三、从普通表开始建立一张超表
下面仅仅是为了说明结构用的。具体到函数啊、参数啊、还有版本要求,你得去看你自己部署的那个版本的金仓数据库文档。
sql
CREATE TABLE conditions (
time TIMESTAMPTZ NOT NULL,
device_id INTEGER NOT NULL,
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION
);
SELECT create_hypertable(
'conditions',
'time',
chunk_time_interval => INTERVAL '1 day'
);
SELECT time_bucket('1 hour', time) AS hour,
device_id,
avg(temperature) AS avg_temperature
FROM conditions
WHERE time >= now() - INTERVAL '7 days'
GROUP BY hour, device_id
ORDER BY hour, device_id;
你把这个过程拆开看,其实里面包含了三个挺重要的思路。第一个呢,就是先按普通表的样子去把业务字段定义好。开发的人不用一上来就去管底下怎么分片。第二个,就是通过超表转换去指定一个时间列。这之后,系统就会按时间自己去建 Chunk、去管 Chunk 了。第三个,就是你查的时候,还是用你习惯的标准 SQL 去写。那个时间桶的聚合呢,也能跟其他的业务字段随便结合着用。
块间隔不是越小越好
你可以先跑下面这个查询去瞅瞅,看看这些块的时间范围和大小到底是多少。看完之后你再决定要不要去改它。不过要提醒一句,函数和视图的名字,还是得按你目标版本文档来。
sql
SELECT chunk_schema,
chunk_name,
range_start,
range_end,
chunk_creation_time
FROM timescaledb_information.chunks
WHERE hypertable_name = 'conditions'
ORDER BY range_start DESC;
SELECT set_chunk_time_interval(
'conditions',
INTERVAL '24 hours'
);
改完之后呢,这个新的间隔,它往往仅仅只是对你后面新创建的那些数据块管用。已经生成了的那些老块,它不会自己又重新切一刀。这个细节特别容易被人漏掉。也就是说,如果你的目标是把历史数据的物理组织给改掉,那你光跑一次参数调整是不够的。你还得重新去规划一下数据怎么迁移,或者怎么重建。
在真实做项目的时候吧,如果说同一个时间窗口里,你老是去查某个特定设备或者特定区域。那这时候你可以考虑再加一个空间维度进去。这里说的"空间",不一定非得是地图上的地理位置。它也可以是设备 ID 啊,站点啊,产线啊这些维度。只要是能把写入压力给分散开的就行。金仓时序数据库提到的二维分区,其实就是把时间轴和设备这种空间轴给捏在一块儿了。时间维度管着数据块什么时候生、什么时候死。空间维度呢,就通过哈希之类的方式,把那些高并发的写入给打散到不同的数据节点上去。
四、自动分区让运维从人工排班变成规则管理
咱们要是纯靠手去建时间分区,最大的问题是什么呢?就是你把未来的数据范围,提前给写死在脚本里了。系统跑着跑着,过了几个月,可能就出现新时间段没有对应的分区了。又或者是你那个建分区的脚本执行失败了,临时去补分区还影响了业务。超表按时间自动去建 Chunk 之后呢,新插进去的数据到底落在哪个范围里,这就归系统管了。应用那边根本不用去操心底下物理结构长什么样。
另外呢,自动分区这东西,它还能跟数据的生命周期绑在一块儿。最近的数据,你是拿来做实时告警和在线查的,那得保持比较好的访问性能。稍微旧一点的数据,拿来看趋势、做故障追溯的,你就可以把它压缩存起来。那些过了业务留存期限的数据,直接按策略删掉或者归档就行了。资料里的手册给了 show_chunks、compress_chunk、decompress_chunk、drop_chunks 这几个管理函数。这也说明了一件事,管时序数据,绝对不是"把数据存进去"就完事了。你还得把数据从生出来、到活跃、到压缩、再到清理,这完整的一圈给设计好。
说到压缩,这个策略你也得结合你的查询模式来看。设备数据经常是按设备和时间去读的,那你排序列和分段列就得尽量去配合这种访问方式。如果说压缩完之后,你还是老是去改那些老的数据块。那压缩和解压花掉的力气,就把压缩省下来的空间给抵消掉了。正确的做法是什么呢?不是一拍脑袋就去开压缩。而是先分清楚哪些是热数据,哪些是冷数据。然后再看你的写入会不会迟到,会不会补录,还有查询窗口是多大,把这些都看完了再去定策略。 我在做项目的时候呢,一般会先给每一类数据定一张生命周期表。接着再根据这个去配策略:

弄这张表干嘛呢?其实就是把自动管理变成一条条能去审计的规则。如果没有这个生命周期,那压缩就仅仅只是一个孤零零的开关。到最后你还是得靠人去清理。
五、分布式超表解决的是持续增长问题
当你这数据量,大到一台机器实在扛不住的时候。那你肯定得往水平方向去扩展了,也就是加机器。分布式超表就是把那张逻辑表,给拉长到了好几个数据节点上去。应用这边呢,看着还是那张统一的表。但是系统在底下,会根据时间和空间分区,把数据指派到合适的节点上去。这么干的好处,不光是把磁盘空间给变大了。更重要的是,它把写入的压力、存储的压力还有查询的压力,给分摊到了更多的资源上。
不过到了分布式场景里面,数据怎么分布你就得特别留神了。如果你只搞了时间分区,没有合理地搞空间分区。那可能某几个特别热的设备,就把压力全砸到少数几个节点上了。那如果你的空间维度设计得不合理呢,又可能出现查一次数据要跑遍所有节点的情况。金仓时序数据库资料里强调的二维分区、元数据路由,还有本地聚合,它想干的事儿是什么呢?就是让数据尽可能在它自己待的那个节点上,就把排序啊、聚合啊、分组啊给做完了。别让那些明细数据在节点之间来回跑。
这个对真实业务来说,其实非常关键。用户通常来说,并不是想把过去一个月每一条采样值全拉到中心节点来看。他想知道的,往往是每小时的平均温度是多少,异常的设备有几台,某条线路的能耗趋势是什么样。先在数据所在的地方算一下局部的结果,然后再把结果汇总起来。这样既能省掉很多网络传输,也能让中心节点少算点东西。
如果说你查来查去,最后还是把所有的明细数据全拉回中心节点来算。那分布式超表的好处,其实就被你给弄没了。所以你在设计查询的时候,得优先去想怎么把条件推到下面去,怎么把时间窗口裁剪好,怎么在本地做聚合。而不是光知道加数据节点的数量。
六、时序数据要进入融合分析,而不是停在监控曲线
光看一条温度曲线,其实你很少能直接从上面得出什么业务结论来的。你得去关联设备型号啊,看看装在哪个位置啊,查查维修记录啊,还有以前出过什么故障。一段车的轨迹数据,你得结合 GIS 的区域和道路信息来看。一组电网负荷的数据,你还得结合用户情况、区域情况和设备状态。如果时序数据库只能单独把曲线存起来,那它的价值也就一直停在监控那一层了。
金仓数据库它把时序这块能力,直接塞进了一个融合数据库的架构里面去。它强调什么呢?强调跟关系型数据、文档数据、向量数据还有 GIS 数据放在一起处理。这么搞最直接的好处,就是你不用为了做关联分析,反反复复地去搞数据同步了。对于企业来说,底座统一了,也就意味着你管权限啊、做备份啊、查审计啊、分配资源啊,这些事都更容易统一去办了。当然啦,统一也不代表说所有的业务代码都得重写一遍。到底要不要在库里面把关联做了,你还是得看数据量有多大,还有平时是怎么访问的。
北京那个轨道交通应急指挥调度平台的例子,其实也印证了这一点。我在相关资料中看到,那个平台用了一主多从的读写分离架构。主节点专门负责高频写入,从节点呢,就顶着实时监控、业务查询还有后台分析。在数据量和查询压力都很大的情况下,它写入的性能跟旧系统比,提了十倍还不止。有些历史分析,以前要花几分钟的,现在几秒钟就出来了。这个例子重点不是在吹一个孤立的峰值有多高。而是说,它的写入、查询、存储、可用性还有分析,是被塞进同一套工程方案里面去了。
七、我对时序数据库选型的四个判断
第一个嘛,你得看它能不能持续写。别光看它一次压测的时候峰值能到多少。因为你的设备数量和采样频率以后肯定是会涨的,系统得在长期跑的时候稳得住。
第二个,你得看它处理复杂查询的能力。别光看它查单条曲线行不行。真实业务里面,往往是时间、设备、区域、状态还有聚合条件,这些东西全混在一起用的。
第三个,看数据的生命周期。别光盯着它能存多少容量。热数据、温数据、冷数据,它们需要的不一样,访问的方式不一样,你付出的成本也不一样。
第四个,看融合的能力。别光看它这个专用库跑得有多快。时序数据这东西,只有跟业务的档案啊、空间信息啊、还有知识数据关联起来,它才比较容易进到预测维护和综合研判的流程里面去。

我现在再去评估一个时序数据库,其实已经不会只盯着问它每秒能写多少条了。我更想问的问题是:数据越来越多以后,分区还能不能控得住;查询越来越复杂以后,还能不能查得准、查得快;节点一个个加进去之后,应用是不是还是只对着一张逻辑表;业务从监控往分析和 AI 走的时候,时间数据能不能跟其他的模型在同一个底座里面配合着用。
超表这个东西的意义在哪呢?其实也就是把这些长期存在的麻烦事,给拆成了一块一块你能管得住的机制。Chunk 让物理组织对应用透明了,自动分区让数据长的时候不用人去补表了,时间和空间组合在一起让分布式扩展有了方向,压缩和生命周期策略让长期留存不再只是一个纯花钱的负担。所以你要去理解金仓时序数据库的价值,也得把它放在这套工程体系里面去看。写入速度快,这只是一个基础。能不能持续稳住,能不能用标准 SQL,能不能水平扩展,还有能不能做多模融合。这些才是决定时序数据到底能不能真正进到你们核心业务里的关键。