ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从选型到产线实践:金仓时序数据库在Java工业项目中的落地手记

从选型到产线实践:金仓时序数据库在Java工业项目中的落地手记 去年我们团队接了一条产线数字化转型的项目最核心的部分就是把现场几百台设备、几万个测点的时序数据完整地存下来再交给报表和告警去用。一开始大家想也不用想直接上开源时序数据库。可真正折腾完原型、做完负载测试、再过了合规评审之后最后落在生产环境里的却是金仓时序数据库。这篇手记不做产品评测只把我们从一个原型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还有金仓时序数据库。对比信息整理成了一张表对比项InfluxDBTDengineTimescaleDBIoTDB金仓时序数据库数据模型标签字段标签列关系模型超表设备测点标签字段/关系兼容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并设置autocommitfalse每批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 groupIdcom.kingbase/groupId artifactIdkingbase8-tsdb-jdbc/artifactId version8.6.0/version /dependency然后在application.yaml里配置数据源spring: datasource: tsdb: jdbc-url: jdbc:kingbase8://10.10.10.20:54321/factory_ts?reWriteBatchedInsertstruecurrentSchemapublic username: ts_user password: ts_password driver-class-name: com.kingbase8.Driver maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000这里有两个关键参数。一个是reWriteBatchedInsertstrue它让JDBC驱动在底层改写批量插入SQL把一条条insert拼成多值insert性能提升非常明显。另一个是maximum-pool-size连接池不是越大越好生产上20个连接足够了配合批量写入每秒几万条的吞吐完全没问题。连接池过大会反而增加数据库端的线程切换开销。4.2 写入链路设计与数据生命周期管理生产环境写入链路是边缘网关 - Kafka - 数据接入服务 - 金仓时序数据库。中间放Kafka不是多此一举是为了削峰填谷。设备上报有非常明显的时段性早晨开机和交接班时数据量是平时的3倍。如果让应用直接写数据库峰值时连接池和磁盘都会被打满而Kafka能把这部分压力摊平也方便后续重放数据。接入服务的核心代码简化成下面这个逻辑public void consumeToTsdb(ListMetricPoint 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 urwx”这个报错我们首次安装时也中招了。这个报错的核心原因是数据目录或安装目录的权限过于严格进程用户没有足够的执行和读写权限。解决办法很直接把安装目录和数据目录的所有者改为启动用户并赋予相应的rwx权限即可。还有一个小坑是防火墙。金仓时序数据库默认端口是54321不是常见的5432。安装完成后要确认端口放行否则客户端连接会一直超时。当时我们排查了半小时最后发现是安全组漏放了这个端口。5.4 分区过期清理手动触发前面提到自动清理任务默认在凌晨执行但测试时我们等不到凌晨所以手动触发了一下清理。命令大致如下CALL ts_cleanup_expired_partitions();执行后能很直观地看到哪些分区被删除了。生产环境我们还会在每天晚上低峰期手动执行一次这个存储过程作为自动任务的补充。这么做的好处是既能保证过期数据及时删除又能提前发现分区状态异常。清理任务本身不会阻塞正常写入但还是建议在业务低峰期做。6. 事后复盘与个人体会6.1 选型时的核心判断现在回头看依然成立项目上线两个多月最让我们庆幸的其实是选型阶段那个“把需求写清楚”的步骤。如果当时只看官方Benchmark选产品后续开发和运维一定会走很多弯路。金仓时序数据库给我们带来的不只是写入和查询能力更重要的是团队能用最熟悉的方式解决最复杂的数据问题原厂支持能随时到现场处理问题SQL兼容性让新同学也能快速接手而完整的关系模型让时序数据和业务档案可以放在同一套体系里管理省掉了ETL的麻烦。当然它也有需要适应的地方比如部分时序专用函数和PostgreSQL原生函数命名有差异文档需要仔细核对。但整体上对于一个以Java技术栈为主、处于从原型到产线阶段的工业项目来说这是一条相当可靠的路径。6.2 如果团队要复刻这套方案先做这几件事如果你所在的团队也准备用金仓时序数据库落地类似项目我给三个建议。第一原型阶段不要用造数工具生成完全均匀的数据最好从现场导一部分真实数据出来测试才能暴露主键冲突、时间乱序、峰值流量这些真实问题。第二Java接入层一定从第一天就写好批量、幂等、异步三个模板后面对接任何系统都能直接复用。第三数据库侧把分区状态、过期清理、慢查询监控提前部署好不要等功能上线后再补。很多排障工具在出问题那一刻才建是来不及的。最后再分享一个小技巧Kafka消费程序里对写入金仓的结果要做失败重试但重试不能无限次否则消费者会一直卡在同一个批次上我们实践下来的方案是重试3次后把失败批次写到一张本地重发表使用定时任务每10分钟重新投递一次。这样既保证数据不丢又不会阻塞实时链路。这套方案不是最复杂的但一定是最可靠的。
返回列表