开头我先交代一下背景。我们团队做工业物联网平台,设备侧每秒产生几千条监测数据,量大、乱序、带时间戳。早期原型阶段图快,直接用内存Map存,数据一多就崩,后来换MySQL,写是能写进去,但查一个小时内所有点位变化曲线要扫几百万行,慢到没法用。被迫认真做时序数据库选型,最后选了金仓时序数据库——就是大家在社区里常说的"松果时序数据库",金仓团队推出的时序存储引擎。从原型验证到产线稳定运行,整整三个多月,踩了不少坑,也积累了一套可复用的方法。这篇手记不讲官方案例,只讲我们实际选型、部署、迁移、调优的过程,给正在评估时序库选型的朋友一个真实参考。
1. 为什么我们放弃自研存储,选择金仓时序数据库
1.1 原型阶段的存储窘境
先说说我们最初是怎么撑过来的。原型系统第一版,采集端把设备数据直接塞进ConcurrentHashMap,写个定时任务每五秒扫一次,拼成最近五分钟的曲线。单机演示没问题,一接入二十台真实设备就现原形。内存占用涨到1.4GB,GC频繁,曲线基本都是断的。后来第二版换MySQL,表结构按"设备ID+采集时间+字段值"设计,一天就多了三百万行。查询某台设备一个小时的曲线,SQL要按时间字段扫索引,虽然能出结果,但响应时间在两秒到五秒之间来回跳。更头疼的是存储膨胀,double类型存六个监测字段加时间戳和标签,单行带索引开销接近200字节,一个月下来磁盘吃掉了80GB。
这个阶段我们意识到,问题不在于数据库本身,而在于我们用错了模型。时序数据的特点是按照时间递增写入,基本不做更新删除,查询高度依赖时间范围过滤,而且需要对原始采样点做聚合降采样。通用关系型数据库的索引和存储结构并不是为这种写入模式优化的。与其继续在MySQL上做分区、做缓存、写一堆聚合逻辑,不如直接找一款真时序数据库。
1.2 选型评估维度与候选对比
我们拉了一张选型清单,按六个维度打分:写入吞吐、压缩率、查询能力、生态兼容、运维复杂度、国产化合规。前三个是硬指标,后面三个关系到我们这五六个人的小团队能不能长期维护得动。
当时进入初筛的有一款开源时序库、一款国内知名时序库,还有金仓时序数据库。开源那款功能很强,社区资料也多,但我们评估时发现它在大规模部署下的集群版是闭源付费的,开源版本单机扩容有上限。国内那款在物联网场景的口碑很好,压缩率做得漂亮,但它的查询语法偏私有化,我们团队几名后端只熟悉SQL和PostgreSQL生态,迁移成本会比较高。金仓时序数据库当时比较打动我们的一点,是它复用了SQL语法,兼容PostgreSQL网络协议,原有的一些BI查询工具几乎不用重新学,这对产线排期很关键。另外金仓本身在国产化落地方面成熟度较高,项目验收阶段资质这块稳一点。
1.3 金仓时序数据库入场
确定候选范围后,我们花了一周做详细调研。金仓时序数据库的核心卖点在我看来有三条:第一,基于列式存储加时间分区,压缩率高,数据膨胀控制得比关系型好一个量级;第二,时序模型明确,标签、字段、时间戳分开管理,正好匹配我们设备数据的特点;第三,兼容PostgreSQL协议,意味着Grafana、JDBC驱动、SQLAlchemy这些现成生态可以直接接,不用自己写Client。社区里有人叫它"松果时序数据库",据说是内部项目代号传出来的称呼,官方文档名称是金仓时序数据库,我们在下文就统一这么叫。
这里补一个经验:选型过程不要只看性能值,一定要带上自己真实的设备数据去做验证。我们当时把采集端积攒的十来个小时真实数据导出,分别灌进几套候选库里跑同一组查询,出来的对比结果和官方PPT里的测试数据差距不小。后面原型验证章节会详细展开。
2. 金仓时序数据库的核心机制与部署踩坑
2.1 时序模型的三元组与分区策略
金仓时序数据库在建模逻辑上是标准的时间序列模型,可以简单理解成"标签(tags)+字段(fields)+时间戳(timestamp)"三元组。标签用作维度过滤,比如设备ID、产线编号、设备类型;字段就是测点的具体值,比如温度、振动幅度、电流;时间戳标记采样发生的时间。建表时用CREATE STABLE或CREATE TABLE语句定义标签和字段,这点和InfluxDB的measurement概念很像,但它是纯SQL写法,我们的后端几乎没有学习成本。
分区策略是性能的基石。金仓时序数据库默认按时间自动分区,我们建表时指定了每一小时一个分区。为什么选小时而不是天?从写入频率倒推的,我们的设备每两秒上报一次聚合数据,一台设备一天产生43200条记录,五十台设备是216万条,天级分区在这个量级下查询时裁剪粒度太粗,小时级分区能大幅缩小扫描范围。当然分区过细也会带来元数据膨胀,小时级在我们当前量级下是平衡点。
写入方式上支持批量INSERT和参数绑定。我们在原型阶段用单条插入测了一下,单线程每秒只有几百条,后来改成批量insert,每次拼两百条记录一次提交,写入吞吐直接拉到每秒两万条以上。这个差距特别明显,生产环境必须走批量写入。
2.2 单机部署与集群形态的选择逻辑
部署形态上,我们最开始定的方案是单机主备,原因很直接:产线数据量在可预见的两年内单机可以扛住,没必要引入分布式存储模块的运维复杂度。金仓时序数据库的单机版和集群版安装包是分开的,集群版强依赖额外的协调组件和数据节点角色管理。我们评估后决定先单机,等数据量翻一个量级或需要跨机房容灾再扩集群。
安装过程本身不复杂,解压安装包,执行安装脚本,配置文件里指定数据目录、端口号。需要提醒的是,安装时务必想清楚数据目录的挂载位置,最好放在独立的数据盘上,不要跟系统盘抢I/O。我们第一次部署就放在根分区下,结果压测时CPU接受度还算正常,但磁盘I/O wait飙到30%,后来迁移到单独挂载的SSD卷后才稳定下来。
2.3 "permission should be u=rwx"完整排查链路
这里有本篇文章第一个要重点记录的坑。我们第一次在测试服务器上用service脚本启动金仓时序数据库时,启动日志里直接出现一行错误:permission should be u=rwx。当时第一反应是权限不够,顺手chmod 777,结果错误还是报。后来才搞明白,这句话不是"权限不足",而是数据库在启动时主动检查数据目录的权限模式,明确要求所属用户权限必须是rwx,也就是属主需要有完整的读写执行权限,但其他用户不应该有越权访问。我们chmod 777反而让目录成为任何人可读写,数据库认为自己不安全,拒绝启动。
排查链路是这样的:
- 第一步,看启动脚本对应的系统用户是谁。我们是root执行的service命令,但脚本内部用user参数切换到了独立的时序数据库用户。
- 第二步,用
ls -l看数据目录的当前属主和权限位,发现目录属主是root,权限是755,时序数据库用户对该目录没有写权限,启动脚本尝试写入元数据文件失败。 - 第三步,用
chown -R tsdbuser:tsdbgroup /data/tsdb把属主改掉,再用chmod 750设置权限,保证属主rwx、属组rx、其他人无权限。 - 第四步,重新执行启动,错误消失。
顺带说一个关联问题:如果用Docker部署,挂载数据卷时同样容易踩这个坑。宿主机上创建的挂载目录默认属主是root,容器内运行的数据库用户UID通常不是0,一启动就会碰到相同的permission should be u=rwx。解决方式是创建目录后先chown到容器内用户对应的UID,再启动容器,而不是在容器内改权限。我们后来在产线环境全部改成hostPath卷加初始化InitContainer做属主修正,彻底避免了手工操作的遗漏。
3. 原型验证:用真实设备数据打透读写与查询
3.1 写入压测与参数调优
原型验证阶段,我们没有用现成benchmark工具,直接把采集端的一个订阅管道对接到金仓时序数据库,连续灌了三个小时真实数据。数据大概是这样的形态:每台设备每两秒上报一条记录,包含六个整型字段和两个浮点字段,标签包括设备ID、产线编号、型号,总设备数五十台。
先测默认配置,批量写入每批100条,双线程并行,入库速率在每秒九千条左右。看监控发现写入瓶颈不在CPU,而是单批次太小导致提交频繁,日志刷盘量也大。优化手段有三个:
- 调大批量大小到500条,减少网络往返和Preparestatement重复解析。
- 写线程从2个加到4个,合理利用CPU多核。
- 连接池最大连接数从5提到20,注意金仓时序数据库的并发写入在50个连接内基本是线性提升,再多反而出现锁竞争。
调完之后,写入稳定在三万五千条每秒,吞吐提升接近四倍。对五十台设备每两秒一条的上报量来说,这个容量余量相当充足。这里给一个实操建议:上线前一定要确认客户端机器的网络小包转发能力,我们后来发现压测上不去,一部分原因是虚机网卡多队列没开,和数据数据库没什么关系。
3.2 聚合、降采样与插值查询实测
读写都通之后,我们重点测了查询能力。生产业务里最高频的查询是:给定某一台设备、某个时间段,返回该时间段内所有测点的曲线,前端绘图;以及给定时间段内按五分钟粒度降采样的均值曲线。
第一类查询金仓时序数据库走的是标签索引加时间分区裁剪,比如查最近一小时某台设备的数据,SQL写成普通select配合WHERE device_id和time范围,实测响应时间在100毫秒以内。第二类查询要用到连续查询或查询时下采样。我们的做法是写一个标准的GROUP BY time_bucket,把原始数据按五分钟窗口聚合,每天凌晨跑定时任务把前一天的历史降采样结果固化到单独的表,前端读小表,原始表用于深挖。这个组合策略在性能上很稳,Grafana画一天曲线基本感觉不到延迟,原始数据直接聚合全扫的话大概要2.8秒到3.5秒,做大盘监控可以接受,但交互式分析还是会钝。
插值场景我们遇到得少,因为设备数据本身就带时间戳,上游网络抖动会导致时间戳不是等间隔的。金仓时序数据库的时间序列函数库里提供了插值能力,语法类似interpolate(metric, '5s'),实测在毫秒级间隙补点和断点补线效果都可以。有个坑要注意:插值前必须先做排序去重,否则同一时间戳的重复数据会让插值结果出现尖刺,我们就是在一次压测里发现曲线出现毛刺,排查半天,定位到源头是采集端重连后重复发送了同一批数据。
3.3 和同类时序数据库的对比结果
同样的真实数据、同样的查询语句,我们拿金仓时序数据库和前面提到的两款时序库做了横向对比,结果放在一张表里更直观。
| 对比项 | 金仓时序数据库 | 开源时序库 | 国内同类时序库 |
|---|---|---|---|
| 10亿点数据占用 | 约38GB | 约45GB | 约29GB |
| 写入吞吐(批量) | 3.5万条/秒 | 3.1万条/秒 | 5.2万条/秒 |
| 一小时曲线查询 | 95ms | 120ms | 70ms |
| 按小时聚合降采样 | 1.1秒 | 1.6秒 | 0.8秒 |
| SQL兼容性 | 高,PostgreSQL协议 | 中等,需学习私有语法 | 中等,类InfluxQL |
| 客户端生态 | 成熟 | 成熟 | 一般 |
这个数据是我们自己的测试环境跑出来的,不代表官方基准。我的感受是,金仓时序数据库的绝对性能不是最顶尖的,但它在SQL兼容性、运维成熟度上的综合分最高。对我们这种要长期维护的团队来说,找到能快速上手的数据库,比单一性能多30%重要得多。
4. 从原型到产线:数据建模与访问生态的衔接
4.1 数据模型的规范化重构
原型阶段建表比较随意,标签字段类型全靠直觉,比如device_id用了varchar,产线编号用了int,数据没问题但查询性能没有调优。进入产线前,我们对数据模型做了一次系统重构。
第一件事是把标签全部统一为varchar或bigint类型,金仓时序数据库的标签索引在等值查询场景对整型和短字符串支持最好,尽量避免长文本标签。第二件事是压缩字段数量,原型的监测字段有十几个,其中两三个字段从上线起就一直是同一个值,这种数据应该放到标签里而不是字段里,否则白白浪费存储和索引空间。第三件事是明确时间精度,我们统一使用毫秒时间戳,避免不同设备一个用秒一个用微秒导致查询时间范围错乱。
还有一个容易被忽视的点:表名和字段命名规范。产线环境会有多张表对应不同设备类型,我们统一成meter_{type}的命名方式,字段一律小写加下划线。这个规范在以后的Grafana变量查询和SQL拼接自动化中帮了大忙。
4.2 双写迁移方案与历史数据回流
从原型到产线最怕的是切换窗口期丢数据。我们采用新旧双写的策略:切换的第一周,采集端同时写入临时关系库和金仓时序数据库,跑一个数据比对程序定时核对两边最新时间戳和记录条数,确认一致后关闭老库写入口。这个方案最大的好处是可以随时回退,业务侧不需要停服。
历史数据回流是用一个Java编写的迁移任务完成的,逻辑很简单:按设备ID分片,每片按小时分段从MySQL读取,组装成与金仓时序数据库表结构一致的批量insert语句,每500条一批提交。五十台设备约一个月的MySQL历史数据,一个晚上全部迁完。这里有个性能心得:迁移任务和查询任务共用数据库连接池,料想之外的超时很多,后来改成迁移任务使用独立的连接池,配置更大的batch和更长的socketTimeout,再也没出现半途断掉的状况。
4.3 客户端访问生态盘点(什么工具能连上)
产线环境上线后,我们团队内部陆续有人问"什么软件能访问松果时序数据库"。这个问题我们在选型时调研过,实际用下来感受很明确:大部分习惯用PostgreSQL生态的人几乎可以无缝切换。
可以直接用的工具包括:
- 官方自带命令行客户端,用来日常巡检和手工执行SQL,功能和psql类似。
- 标准JDBC驱动,Java后端通过HikariCP连接池接入,把连接串从关系库改成金仓时序数据库的jdbc地址即可。
- Grafana数据源插件,因为支持PostgreSQL协议,选择PostgreSQL数据源再改驱动就行,我们直接用它搭了产线实时看板。
- Python生态可以用SQLAlchemy加psycopg2驱动,pandas的read_sql方法能直接拉时序数据做离线分析。
- 一些BI工具比如Superset也支持PostgreSQL数据源,基本都能访问。
需要单独说明的是,如果项目里习惯用InfluxQL语法,金仓时序数据库并不提供完全兼容。我们团队有成员刚开始把InfluxDB的语法拿过来跑,报错后改用SQL内置的时间序列函数,很快就习惯了。这是一个学习曲线问题,不算长期成本。
5. 上线半年后的复盘与调优心得
5.1 监控告警与慢查询定位
产线运行半年,最深刻的体会是:时序数据库一旦稳定跑起来,最需要关注的不再是读写性能,而是慢查询和管理成本。我们上线初期完全没有监控,直到一次Grafana看板无响应才发现有客户端在跑一个大范围未带时间过滤条件的聚合查询,把CPU打满。
后来接入开源Prometheus体系,用exporter采集金仓时序数据库的关键指标,包括活跃连接数、查询执行时间分布、并发写入数、磁盘空间剩余等。同时把慢查询日志打开,设置阈值1秒,定期分析SQL模式。这半年定位到的慢查询差不多稳定在两类:一类是标签基数特别大的group by查询,另一类是跨多个分区但时间过滤条件写得不精确的查询。前者需要拆分组合查询,后者只要在SQL写法上规范一点,把时间范围缩小到业务真正需要的区间,性能提升往往立竿见影。
5.2 存储占用与生命周期管理
时序数据的存储膨胀问题,半年跑下来体会特别深。原始数据保留全量确实必要,但全量保留14个月显然没有必要。我们把生命周期分成三档:原始数据保留30天,按小时聚合的中间表保留6个月,按天聚合的统计表保留24个月。这个策略让存储占用稳定在每天新增约1.8GB,其中原始数据占大头。金仓时序数据库支持表级数据过期策略和清理命令,配置好之后不需要手工删数据。
压缩率这块我们也有一个数据供参考:原始数据写入后,因为列式存储加字典压缩,整型和浮点字段的实际落盘占用大约是关系型存储的四分之一到三分之一。存储成本直接变成选型时的加分项。
5.3 团队开发规范沉淀
最后聊制度层面。我们从这次项目中沉淀出三条团队规范,适合类似规模的小团队参考:
- 所有查询必须带显式时间范围,禁止不带time条件的全表扫描型聚合。
- 批量写入统一走公共封装,禁止业务代码里出现循环单条insert。
- 新增标签、字段必须走评审,避免出现同一含义不同命名的标签混乱。
这三条规范花了一个小时写好,却在后面半年里替我们挡住了大量低级事故。开发人员流动时,新同事半个小时就能通过规范文档和示例代码上手接数据。
后半年的运行数据是:日均新增记录超过800万条,查询平均响应时间稳定在200毫秒以内,存储总占用可控,Migrate和备份任务未出现一次中断。以上就是我们这次选型和落地的完整过程。最后分享一个我个人的小体会:真正决定时序数据库成不成功的,往往不在benchmark数字,而在团队能不能长期正确地使用它。选型时多花一天做真实数据验证,规范里多写一条查询限制,比项目上线后熬夜救火划算得多。