去年我们团队接了一条产线数字化转型的项目,最核心的部分就是把现场几百台设备、几万个测点的时序数据完整地存下来,再交给报表和告警去用。一开始大家想也不用想,直接上开源时序数据库。可真正折腾完原型、做完负载测试、再过了合规评审之后,最后落在生产环境里的却是金仓时序数据库。这篇手记不做产品评测,只把我们从一个原型Demo到产线运行的过程中,选型怎么变的、坑是怎么踩的、配置是怎么调的都记录下来,给那些正在做类似选型或者即将上时序库的团队一个参考。
如果你也在评估时序数据库,特别是团队以Java为主、运维人手不多、项目又有明确的采购与合规要求,这篇文章应该能帮你少走不少弯路。我们后面出现的所有性能数据都来自测试环境,不能代表金仓时序数据库的极限指标,但整个过程的方法是通用的。
1. 项目背景与需求拆解
1.1 我们面对的真实场景:每分钟数十万测点
这个项目的业务场景很典型。工厂改造之后,产线上的PLC、传感器、质检仪器都会通过边缘网关把实时数据往上传。网关这边跑的是MQTT和Modbus采集程序,数据先汇聚到Kafka,再向下游的数据平台分发。我们要负责的部分,是把Kafka里这些原始点位数据高吞吐地写入时序数据库,并且支撑两个核心应用:
- 实时监控大屏,每5秒刷新一次关键设备的温度、压力、转速曲线;
- 数据分析平台,业务需要按1分钟、5分钟、1小时做均值、最大值、分位数统计,还要能对比不同班次的数据。
先算一下数据量。现场共8条产线,每条约120台设备,每台设备平均上报20个测点,单测点上报频率在1秒到5秒不等。折算下来,峰值写入大约是38万点/秒,日增数据量在13亿条到24亿条之间。我们要求原始数据至少保留90天,超过90天可以自动清理,但经过聚合后的指标数据要保留1年。
这个体量放在这里,用内存数据库或者普通关系库都不现实。时序数据库的好处在于它把“时间戳+标签+数值”这种模型做了专门优化:写入按时间序追加、数据按时间分区、过期自然淘汰。可具体选哪家,没有想得那么简单。
1.2 从原型开始的功能性需求确认
因为不同团队关注点不一样,我们一开始就把需求按优先级列成了清单,避免后面选型被某个花哨特性带偏。我们当时整理的需求大致如下:
- 高并发写入:必须支持50万点/秒以上的批量写入能力,且写入失败不能丢数据;
- 实时查询能力:最近1小时的数据查询,P95响应时间要在200毫秒以内;
- 时间聚合能力:支持按任意时间粒度做降采样,像avg、max、min、percentile这类算子;
- 数据生命周期:自动分区分片,可以配置保留策略,过期分区自动删除;
- 生态对接:应用用Java,需要提供标准JDBC驱动,能够接SpringBoot;
- 权限与审计:多用户、多角色、操作审计,这是项目验收的硬性指标;
- 交付要求:采购清单里有基础软件合规项,必须支持国产CPU与国产操作系统的适配。
前面两项任何一款时序数据库都能做到,但后面几项,尤其是标准SQL能力、细粒度权限和信创清单,直接把不少开源时序库挡在了门外。这也是我们后来在原型验证阶段重点测试的对象。
2. 选型对比:为什么最终是金仓
2.1 我们对比过的几个时序数据库
团队花了两周时间,把市面上常见的时序方案都过了一遍。每家我们都用一个小Demo验证过,不是只看官网文档。最终候选名单里有InfluxDB、TDengine、TimescaleDB、IoTDB,还有金仓时序数据库。对比信息整理成了一张表:
| 对比项 | InfluxDB | TDengine | TimescaleDB | IoTDB | 金仓时序数据库 |
|---|---|---|---|---|---|
| 数据模型 | 标签+字段 | 标签+列 | 关系模型+超表 | 设备+测点 | 标签+字段/关系兼容 |
| SQL能力 | Flux/InfluxQL | 部分SQL | 完整SQL | 部分SQL | 完整SQL,含时序窗口函数 |
| 高可用方案 | 集群版需商业授权 | 开源有集群 | 依赖PostgreSQL生态 | 可配置副本 | 内置高可用 |
| 运维门槛 | 中 | 中低 | 中 | 中高 | 中低 |
| 权限体系 | 较简单 | 简单 | 强 | 中 | 强,支持审计 |
| 国产环境适配 | 一般 | 部分 | 需自行适配 | 一般 | 原生适配 |
| 企业支持 | 商业支持贵 | 有商业版 | 商业支持 | 有商业版 | 国内原厂支持 |
开源方案里,InfluxDB的性能和生态确实是标杆,但它的Flux查询语言对团队里的SQL熟手不太友好,企业版集群的成本也很高。TDengine写入性能很猛,但我们的报表场景涉及多个表JOIN,它在这块支持还偏弱。TimescaleDB本质上是一个PostgreSQL插件,功能完整,可团队没有专职DBA,碰到集群和高可用问题只能自己扛,而且当时对国产芯片的适配工作还不太透明。IoTDB在工业物联网里很对口,但我们业务里还有大量关系型报表,把它当唯一数据底座有点别扭。
2.2 选型背后的三个决定性因素
表面上看,每款产品都有自己的侧重点,但最终让天平倒向金仓时序数据库的原因有三个。
第一是合规与企业服务。这个项目验收时会检查软件采购清单,核心数据存储必须能用国内厂商提供的企业版产品,并且能拿出针对国产操作系统的适配认证。金仓的完整交付体系让这条链路很顺,原厂工程师能直接到现场支持,比我们自己折腾开源社区要省心很多。
第二是技术栈匹配度。团队都是写Java和SQL出身的,金仓时序数据库的SQL方言和我们熟悉的PostgreSQL风格非常接近,JDBC驱动可以直接用在SpringBoot工程里。这就意味着我们不用单独引入一套Flux或类SQL方言的客户端,团队成员上手成本极低。当时我拿一个原有项目的Mapper文件改了一下表名和SQL,直接就能跑通,这给了后续开发很大的信心。
第三是业务查询的复杂度。我们需要做跨设备、跨指标的分组聚合,甚至要把时序数据和基础档案表做关联,很多开源时序库在这种场景下要么得把数据导出来处理,要么得写一堆UDF。金仓时序数据库因为保留了完整的关系模型能力,能做到在同一个SQL里完成时间窗口函数和表关联,这正好卡中了我们的痛点。
原型还没做完,我心里其实已经有答案了。但选型这个事情不能拍脑袋,我们用真实数据跑完了写入、查询、保留策略和权限测试,才最终定了方案。
3. 原型验证:从Demo到可用的关键一步
3.1 快速搭建原型环境
选型进入验证阶段,我们找一台16核32G的测试服务器,装好CentOS兼容环境,用金仓时序数据库的安装包解压部署。整个过程大概半小时,没有任何坑。这里提醒一句,安装完成后目录权限和普通关系库不一样,默认安装路径下数据目录的所有者必须保持为启动用户,否则服务会直接拒绝启动报错。网上有不少权限问题的求助帖,基本就是这一步没有做对。
进入数据库后,我们按照业务模型建了一张测试表:
CREATE TIMESERIES TABLE sensor_data ( device_id VARCHAR(32) NOT NULL, metric_id VARCHAR(32) NOT NULL, ts TIMESTAMP NOT NULL, value DOUBLE PRECISION, quality SMALLINT, PRIMARY KEY (device_id, metric_id, ts) );这里需要注意,时序表的组成部分和普通表不一样。device_id和metric_id会被当作标签索引列,ts是时间主键,value和quality是数据值列。建表时最好把常用的过滤字段都放进主键里,因为时序数据查询几乎都是“先过滤标签,再做时间范围扫描”,主键顺序直接影响存储和查询性能。
3.2 原型中写入和查询的“初体验”
环境起来后,我们先写了一个Python脚本模拟数据写入,用JDBC的批量提交来压测。测试目标是客户端每秒写入10万条记录。最开始只用最基本的批量提交,写入性能只有不到5000条/秒。后来增大batchSize到500,开启rewriteBatchedInserts,并设置autocommit=false,每批5000条提交一次,性能直接提升到8万条/秒。这说明一个道理:时序数据库的写入瓶颈,很多时候不在数据库本身,而在客户端是不是真正利用了批量机制。
查询也做了个简单验证。我们需要算最近1小时每台设备5分钟的平均温度,SQL可以直接这样写:
SELECT time_bucket('5 minutes', ts) AS window_start, avg(value) AS avg_value FROM sensor_data WHERE device_id = 'PLC-01' AND metric_id = 'temp' AND ts >= now() - INTERVAL '1 hour' GROUP BY window_start ORDER BY window_start;这个SQL在原型环境上跑,1亿条数据量的表里,1小时数据量约50万条,扫描加聚合返回只要不到100毫秒。当时我们特意EXPLAIN看了一下执行计划,确认时间条件下推到了分区级别,没有全表扫描。这也是我们后来在产线上设计查询规范时反复强调的一点:条件里必须带上时间范围,否则再好的索引也白搭。
原型验证大概用了5天,把写入、查询、聚合、轮询删除、权限管理都跑了一遍。整体结论是:功能上能满足,性能还有进一步调优空间,可以进入产线建设。
4. 产线实战:集成、部署与调优
4.1 与SpringBoot应用的集成实践
我们生产数据接入服务是基于SpringBoot 2.7开发的,核心任务是从Kafka消费数据,再成批写入金仓时序数据库。代码层面没有引入任何特殊ORM,用的就是Spring的JdbcTemplate加原生的PreparedStatement批量操作。
先在pom.xml里引入JDBC依赖:
<dependency> <groupId>com.kingbase</groupId> <artifactId>kingbase8-tsdb-jdbc</artifactId> <version>8.6.0</version> </dependency>然后在application.yaml里配置数据源:
spring: datasource: tsdb: jdbc-url: jdbc:kingbase8://10.10.10.20:54321/factory_ts?reWriteBatchedInserts=true¤tSchema=public username: ts_user password: ts_password driver-class-name: com.kingbase8.Driver maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000这里有两个关键参数。一个是reWriteBatchedInserts=true,它让JDBC驱动在底层改写批量插入SQL,把一条条insert拼成多值insert,性能提升非常明显。另一个是maximum-pool-size,连接池不是越大越好,生产上20个连接足够了,配合批量写入,每秒几万条的吞吐完全没问题。连接池过大会反而增加数据库端的线程切换开销。
4.2 写入链路设计与数据生命周期管理
生产环境写入链路是:边缘网关 -> Kafka -> 数据接入服务 -> 金仓时序数据库。中间放Kafka不是多此一举,是为了削峰填谷。设备上报有非常明显的时段性,早晨开机和交接班时数据量是平时的3倍。如果让应用直接写数据库,峰值时连接池和磁盘都会被打满,而Kafka能把这部分压力摊平,也方便后续重放数据。
接入服务的核心代码简化成下面这个逻辑:
public void consumeToTsdb(List<MetricPoint> records) { if (records.size() < 500) { return; } String sql = "INSERT INTO sensor_data(device_id, metric_id, ts, value, quality) VALUES (?, ?, ?, ?, ?)"; tsdbJdbcTemplate.batchUpdate(sql, records, 500, (ps, record) -> { ps.setString(1, record.getDeviceId()); ps.setString(2, record.getMetricId()); ps.setObject(3, record.getTimestamp()); ps.setDouble(4, record.getValue()); ps.setShort(5, record.getQuality()); }); }这里有几条血泪经验。批量插入的批次大小不能太小,也不能太大。我们测下来500到1000一批是性能折中,如果单批次超过2000,数据库端内存分配和锁冲突都会增加,吞吐反而下降。还有一定要设置自动提交关闭,并且定期明确提交事务。默认开启自动提交时,每次batchUpdate都会隐式提交,等于把批量退化成单条写。
数据生命周期管理用的是数据库的自动分区能力。生产环境的表建成了按天分区,保留策略设为90天。DDL如下:
ALTER TABLE sensor_data SET (ttl_duration = '90 days'); ALTER TABLE sensor_data SET (partition_interval = '1 day');设置完成后,系统会每天自动创建新的分区,并清理超过90天的旧分区。这个机制非常省心,但我们后来发现,过期清理任务的默认执行时间在凌晨2点,如果一天的数据量特别大,清理时会对IO造成一定压力。我们就把执行时间挪到了业务低峰期,同时监控一下清理任务是否卡住。超过90天的原始数据删除后,聚合表数据仍然保留,所以报表不受影响。
4.3 查询性能优化与预聚合设计
生产环境的报表查询只靠直接扫描原始表肯定不行。我们上线第一周就被一个“查询某天全班次对比”的页面卡住了,后来靠预聚合解决了。
金仓时序数据库支持连续查询和物化视图,可以在数据写入的同时维护分钟级和小时级的预聚合结果。我们建了两张预聚合表:
CREATE TABLE agg_5min ( device_id VARCHAR(32), metric_id VARCHAR(32), window_start TIMESTAMP, avg_value DOUBLE PRECISION, max_value DOUBLE PRECISION, min_value DOUBLE PRECISION, count_value BIGINT, PRIMARY KEY (device_id, metric_id, window_start) );然后创建连续查询任务,每5分钟自动把最近5分钟的原始数据聚合写入这张表:
CREATE CONTINUOUS VIEW agg_5min_view AS SELECT device_id, metric_id, time_bucket('5 minutes', ts) AS window_start, avg(value) AS avg_value, max(value) AS max_value, min(value) AS min_value, count(value) AS count_value FROM sensor_data GROUP BY device_id, metric_id, time_bucket('5 minutes', ts);生产上我们保留原始数据90天,预聚合结果保留一年。报表查询优先查预聚合表,只有在需要看原始曲线时才查明细。这样大屏的5秒刷新和报表的分钟级统计都不成问题。
4.4 高可用与监控告警
生产环境我们采用了两台数据库节点加主从同步的架构。金仓时序数据库在这个模式下提供了自动故障切换能力,但前提是底层时钟要同步好,否则主从复制容易出现时间偏差,时序数据的顺序就会乱。我们是严格配置了NTP服务,所有节点统一从内网时间服务器同步。
监控上我们没有额外造轮子,直接用Prometheus抓取了金仓时序数据库暴露的指标,重点看几个:写入请求数、写入延迟、活跃连接数、分区数量、慢查询数量。Grafana面板做好之后,数据库有没有异常一眼就能看出来。这里最容易被忽略的监控项其实是分区数量。如果你发现分区数量不涨了,就说明分区的自动创建任务挂了,后面所有按新时间范围写入的数据都会掉进默认分区,性能和存储都会受到很大影响。
5. 常见问题与排查技巧实录
5.1 写入性能上不去的真实案例分析
项目刚联调时,接入服务压测只能跑到每秒3万条,和原型验证时的8万条差得很远。我们一度怀疑是生产库的磁盘比测试环境差,后来逐个排查才发现问题出在连接池的配置上。
当时HikariCP的maximum-pool-size设了50,看起来更充裕,但数据库端每次写入都要分配事务资源,连接太多导致上下文切换严重。把连接池下调到20,并且确保每批500条、每事务20批提交一次之后,吞吐反而拉升到了15万条/秒。
另外一个低级错误是接入服务的批量插入方法返回后,我们没有检查int[]的实际更新行数。有一天Kafka部分数据源重放了数据,导致同一时间戳同一设备同一测点的数据重复,写入出现主键冲突。虽然金仓时序数据库有upsert语义,但默认配置下冲突会返回异常。后来我们在插入语句里加了ON CONFLICT DO UPDATE,确保了幂等写入:
INSERT INTO sensor_data (device_id, metric_id, ts, value, quality) VALUES (?, ?, ?, ?, ?) ON CONFLICT (device_id, metric_id, ts) DO UPDATE SET value = EXCLUDED.value, quality = EXCLUDED.quality;这个调整在原型阶段没有暴露出来,因为原型数据都是顺序生成的。生产环境一旦有重放、补数任务,幂等写入就是必不可少的设计。
5.2 查询慢的排查思路
有一段时间监控大屏偶尔会卡一下,查下来是前端把时间参数传成了字符串,到了SQL里变成了对时间列做隐式转换,导致查询计划里时间范围没有下推到分区裁剪。解决方法是所有时间条件都用占位符传时间戳类型,不要用字符串拼接。
另外,报表要按“设备名称+指标中文名”查询,而表中只有device_id和metric_id。我们一开始用JOIN关联基础档案表,数据量一大,JOIN开销就成了主要瓶颈。后来把设备名称和指标名称冗余进了宽表,用空间换时间,查询响应立刻降了下来。时序库里尽量减少复杂JOIN,这个经验值得记下来。
5.3 部署安装时最常出现的权限错误
搜索金仓问题的时候总能看到“permission should be u=rwx”这个报错,我们首次安装时也中招了。这个报错的核心原因是数据目录或安装目录的权限过于严格,进程用户没有足够的执行和读写权限。解决办法很直接,把安装目录和数据目录的所有者改为启动用户,并赋予相应的rwx权限即可。
还有一个小坑是防火墙。金仓时序数据库默认端口是54321,不是常见的5432。安装完成后要确认端口放行,否则客户端连接会一直超时。当时我们排查了半小时,最后发现是安全组漏放了这个端口。
5.4 分区过期清理手动触发
前面提到自动清理任务默认在凌晨执行,但测试时我们等不到凌晨,所以手动触发了一下清理。命令大致如下:
CALL ts_cleanup_expired_partitions();执行后能很直观地看到哪些分区被删除了。生产环境我们还会在每天晚上低峰期手动执行一次这个存储过程,作为自动任务的补充。这么做的好处是既能保证过期数据及时删除,又能提前发现分区状态异常。清理任务本身不会阻塞正常写入,但还是建议在业务低峰期做。
6. 事后复盘与个人体会
6.1 选型时的核心判断,现在回头看依然成立
项目上线两个多月,最让我们庆幸的其实是选型阶段那个“把需求写清楚”的步骤。如果当时只看官方Benchmark选产品,后续开发和运维一定会走很多弯路。金仓时序数据库给我们带来的不只是写入和查询能力,更重要的是团队能用最熟悉的方式解决最复杂的数据问题:原厂支持能随时到现场处理问题,SQL兼容性让新同学也能快速接手,而完整的关系模型让时序数据和业务档案可以放在同一套体系里管理,省掉了ETL的麻烦。
当然它也有需要适应的地方,比如部分时序专用函数和PostgreSQL原生函数命名有差异,文档需要仔细核对。但整体上,对于一个以Java技术栈为主、处于从原型到产线阶段的工业项目来说,这是一条相当可靠的路径。
6.2 如果团队要复刻这套方案,先做这几件事
如果你所在的团队也准备用金仓时序数据库落地类似项目,我给三个建议。第一,原型阶段不要用造数工具生成完全均匀的数据,最好从现场导一部分真实数据出来测试,才能暴露主键冲突、时间乱序、峰值流量这些真实问题。第二,Java接入层一定从第一天就写好批量、幂等、异步三个模板,后面对接任何系统都能直接复用。第三,数据库侧把分区状态、过期清理、慢查询监控提前部署好,不要等功能上线后再补。很多排障工具在出问题那一刻才建,是来不及的。
最后再分享一个小技巧:Kafka消费程序里对写入金仓的结果要做失败重试,但重试不能无限次,否则消费者会一直卡在同一个批次上,我们实践下来的方案是重试3次后把失败批次写到一张本地重发表,使用定时任务每10分钟重新投递一次。这样既保证数据不丢,又不会阻塞实时链路。这套方案不是最复杂的,但一定是最可靠的。