ARTICLE DETAIL

资讯详情

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

物联网数据量暴涨?从MySQL迁移到金仓时序数据库的实战记录

物联网数据量暴涨?从MySQL迁移到金仓时序数据库的实战记录 我们最开始引以为傲的“全链路 MySQL”架构在 IoT 数据量冲到千万级后轰然作响——查询一个设备一周的曲线图要 5 秒凌晨批量同步直接锁表存储占用比业务数据本身还大。痛定思痛后团队在两个月内完成了时序数据库选型、原型验证、生产切换最终落地金仓时序数据库。这篇手记不讲虚的只记录从需求计算、选型对比、环境搭建、表结构设计到权限报错、连接池、时间戳精度这些实战坑的完整过程希望能帮正在做时序选型的团队少走几条弯路。1. 选型之前先把业务需求算清楚很多团队选型一上来就横向对比各家数据库的性能压测报告这其实走反了。选时序数据库之前第一步应该是把业务需求翻译成可量化的技术指标否则后面所有对比维度都是空的。1.1 数据规模到底有多大我始终建议先用最笨的方法估算数据量设备数量 × 测点数量 × 采集频率 × 存储时长。我们当时接了一个产线设备监测场景初期规划是 100 台设备每台设备有 4 类信号温度、振动、电流、功率每个信号按 1Hz 采集。一天的数据量就是100 台 × 4 个测点 × 86400 秒 3456 万条/天一个月就是 10 亿条左右做好每天 3000 万行的写入量级索引膨胀率通常会在 2~3 倍以上如果你还需要快速查询某个时间范围的曲线。MySQL 在这种场景下不是不能跑但每查询一次就是一次全表扫描加随机 IO索引占用的磁盘空间还会让日常备份变得非常痛苦。把需求算出来后团队才真正理解为什么需要一个专用存储。1.2 时序场景的三个核心诉求顺着业务反推我们发现当时对数据库的诉求非常清晰不是要一个“更快的关系库”而是要具备三件事高吞吐写入设备数据持续产生写入必须是追加式的不能因为索引、事务约束而频繁阻塞高效压缩温度、振动这类信号重复度高压缩比好的数据库能把存储成本降一个量级降采样查询用户看一年的趋势图不可能把一亿行原始数据都查出来必须能在分钟级/小时级切片上做聚合。这三个诉求恰好是时序数据库与传统关系的分水岭。我们后来评判所有候选方案都是拿这三条准则去套而不是盯着某一条官方压测数据。1.3 选型必须先建评分卡结合团队现状我们在选型前就拉了一张评分卡权重提前确定防止评审时被某家厂商的演示带偏评估维度权重考察内容写入性能25%批量写入吞吐、单行写入延迟查询性能20%时间范围聚合查询、最新值查询压缩与存储15%压缩比、存储占用、保留策略生态兼容性20%SQL 熟练度、JDBC 集成、周边工具运维成本10%部署复杂度、监控、备份恢复商业支持10%文档完善度、原厂响应速度没有这张卡后面很容易陷入“A 查询快、B 压缩好、C 运维省心”的纠结里。有了权重决策才有依据。2. 金仓时序数据库凭什么能入选候选方案圈了一圈后进入复赛的有 OpenTSDB、InfluxDB、TimescaleDB、金仓时序数据库。这里必须说每个产品都有自己的拥趸没有绝对的好坏只看是否匹配你的团队基因。2.1 主流时序数据库的横向速览先说说落选选手的理由这部分对我们后续坚定选择很重要。OpenTSDB 基于 HBase写入扩展性确实好但 HBase 那套依赖 HDFS、ZooKeeper 的运维体系太重了团队只有两个 DBA平时还要管业务库根本没精力再伺候一个大数据集群。InfluxDB 的查询语言是自家的 Flux/InfluxQL团队日常用 SQL 写报表迁移成本不小。另外 1.x 版本的集群版是商业功能考量性价比后放弃了。TimescaleDB 本身很优秀基于 PostgreSQL 的分区能力做时序增强SQL 兼容度也高。但当时我们评估过它的写入并发和压缩策略在单机场景下表现不错不过我们采购流程上需要一个能提供本地化技术支持的产品这成了关键变量。金仓时序数据库最终入选核心原因有三点底层基于 PostgreSQL 内核团队已有的 SQL 经验能直接复用提供标准 JDBC 驱动和 Spring Boot、MyBatis 等 Java 生态无缝集成有专职的原厂支持部署出现问题有人帮你趟坑。2.2 数据模型与我们的贴合度金仓时序数据库的数据模型让我印象深刻的点在于它对“测点模型”和“标签模型”的融合处理。实际建表时一张时序表可以包含时间戳、标签列、指标列把设备 ID、信号类型等维度用标签存把温度、振动幅度用指标存查询时按标签过滤再对指标做聚合。举个我们实际建表的例子CREATE TABLE device_signal ( ts TIMESTAMPTZ NOT NULL, device_id VARCHAR(32) NOT NULL, signal_type VARCHAR(16) NOT NULL, temperature DOUBLE PRECISION, vibration DOUBLE PRECISION, current_value DOUBLE PRECISION, power_value DOUBLE PRECISION );这种模型和我们的业务逻辑基本一一对应不需要像某些 TSDB 那样二次抽象数据模型。对于从关系库迁移过来的团队上手成本非常友好。2.3 决策逻辑不是最强而是最合适最终拍板时我们内部其实没有激烈争论。金仓时序单机写入能力虽然不一定是阵营里最强的但它提供了我们最看重的组合SQL 兼容性好、代码改动小、运维熟悉度高、原厂支持到位。这就回到选型的基本逻辑——除非你有千万级设备、百万级并发写入这种极端规模否则团队落地能力和生态适配能力往往比理论峰值性能更决定项目成败。3. 从原型到产线逐步实操记录选型结论出来后我们马上进入原型验证阶段。这里的经验是原型不要追求和生产一致但要覆盖全部关键链路。我们只用一台 4C8G 的虚拟机就完成了从建表、写入、查询到连续聚合的闭环验证整个原型周期大概用了两周。3.1 原型环境的快速搭建金仓时序数据库有官方的 Docker 镜像原型阶段直接用容器拉起最省事。我们用的编排文件长这样version: 3.3 services: kingbase-ts: image: kdb/kingbase-ts:V8 container_name: kingbase-ts-test ports: - 54321:54321 environment: - KDB_DATA_DIR/home/kingbase/data volumes: - ./kingbase-data:/home/kingbase/data restart: unless-stopped这里有个细节容器内默认数据目录和挂载卷权限一定要对齐否则后面会踩到一个特别低频的权限报错这个我在第 4 节详细说。启动成功后用命令行工具确认版本信息然后就可以开始建表造数据了。提示原型阶段别急着配置主从和备份先把写入链路的代码验证跑通更重要。容器的文件目录挂载到宿主机方便后面直接映射到生产环境。3.2 写入链路从 JDBC 到批量提交我们的原型验证用的 Java Spring Boot驱动直接引入了官方 JDBC配置上跟连 PostgreSQL 几乎一样spring.datasource.urljdbc:kingbase8://192.168.1.10:54321/iot_tsdb spring.datasource.usernamekdb_ts spring.datasource.passwordChangeMe spring.datasource.driver-class-namecom.kingbase8.Driver spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.minimum-idle5写入逻辑里我们很快就发现逐条 insert 完全不可行。3000 万行一天的规模逐条插入即使网络开销都能拖垮应用。最终全部改成 PreparedStatement 批量提交使用 addBatch 和每 500 条或 1 秒 flush 一次的策略。下面这段代码是我们所有设备信号写入的通用入口public void batchInsertSignals(ListDeviceSignal signals) { String sql INSERT INTO device_signal(ts, device_id, signal_type, temperature, vibration, current_value, power_value) VALUES(?, ?, ?, ?, ?, ?, ?); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { int count 0; for (DeviceSignal s : signals) { ps.setTimestamp(1, Timestamp.from(s.getTs())); ps.setString(2, s.getDeviceId()); ps.setString(3, s.getSignalType()); ps.setDouble(4, s.getTemperature()); ps.setDouble(5, s.getVibration()); ps.setDouble(6, s.getCurrentValue()); ps.setDouble(7, s.getPowerValue()); ps.addBatch(); if (count % 500 0) { ps.executeBatch(); } } ps.executeBatch(); } catch (SQLException e) { throw new RuntimeException(批量写入设备信号失败, e); } }实测下来4C8G 的机器上单线程批量写入能稳定跑到每秒 2 万行左右完全覆盖我们当时每秒约 400 行的写入需求。原型阶段这里就过了。3.3 查询优化连续聚合是核心武器时序场景里最常被问的问题是原始数据已经上亿条按分钟去查平均值怎么才能快金仓时序数据库的连续聚合Continuous Aggregate在这里体现出了真正的价值。我们创建了一个物化视图实现了每 5 分钟的温度、振动平均值预计算CREATE MATERIALIZED VIEW device_signal_5min WITH (timescaledb.continuous) AS SELECT time_bucket(5 minutes, ts) AS bucket, device_id, signal_type, avg(temperature) AS avg_temperature, avg(vibration) AS avg_vibration FROM device_signal GROUP BY bucket, device_id, signal_type;然后通过刷新策略让视图每隔一分钟自动刷新一次SELECT add_continuous_aggregate_policy(device_signal_5min, start_offset INTERVAL 1 hour, end_offset INTERVAL 10 minutes, schedule_interval INTERVAL 1 minute);查询端秒级响应就是从这来的。应用层直接查物化视图原始数据只在需要明细时才会碰。给团队的建议是凡是趋势图、报表类需求一律走预聚合视图不允许直接扫原始表。3.4 生产级部署的几个硬调整原型验证通过后我们花了大概一周时间从容器部署切换到了生产级裸机部署。这里有几个调整是必须做的独立数据目录与普通用户运行不要用 root 跑数据库进程单独建kingbase系统用户数据目录权限设为 700连接数规划生产环境把连接池最大连接数和数据库max_connections放到一起评估应用侧的 HikariCP 上限建议设为数据库上限的 70% 左右留出运维连接余量磁盘与备份策略时序数据文件增长快建议数据目录单独挂数据盘SSD定时备份采用物理备份 WAL 归档结合的方式避免每天全量备份引起的 IO 峰值监控接入将数据库的活跃连接数、表空间占用、写入延迟三个指标接入团队的 Prometheus用 Grafana 出面板每天盯一眼心里有底。4. 常见问题与排查技巧实录这一节记录的全部是我们实际碰到的坑。网上文档不一定写但生产环境大概率会遇到。4.1 启动报错permission should be urwx这是我们在容器部署转向物理部署时遇到的一个高频问题。当时一切配置看起来正常结果启动数据库服务直接报错FATAL: permission should be urwx排查过程花了点时间最后定位到根因切换运行用户时数据目录的属主和权限没有跟着变。金仓时序数据库对数据目录权限检查很严格要求属主为当前运行用户且权限必须包含属主读写执行rwx、组和其他用户无写权限。修复命令很简单chown -R kingbase:kingbase /data/kingbase chmod 700 /data/kingbase chmod -R 600 /data/kingbase/data/*遇到过几次后来干脆写进了部署脚本。这里还有一个衍生小提醒备份目录、归档目录权限同样要核查否则会在执行备份任务时二次触发权限问题。注意金仓系列数据库对权限校验非常严格新增数据目录后要把目录属主和权限检查放在“启动数据库”之前不要等到报错了再倒排查。4.2 连接池爆掉后的僵尸连接上线第三天监控面板上的活跃连接数突然冲到 200数据库负载瞬间升高。排查应用日志发现业务代码里有几个定时任务每隔 30 秒开一个连接查状态然后忘了关闭连接。因为 HikariCP 默认的 keepalive 时间为 30 秒这些没关闭的连接不仅没被回收反而在池里攒成了“僵尸连接”。排障后的整改包含两个方面业务代码全面排查统一改用 try-with-resources 确保连接释放数据源配置增加连接有效性检查spring.datasource.hikari.connection-test-querySELECT 1 spring.datasource.hikari.validation-timeout3000 spring.datasource.hikari.max-lifetime1800000这里想强调一个经验连接池的参数设置不是越高越好maximum-pool-size设成 200 不代表数据库能扛 200 并发反而可能因为连接数过多导致锁竞争严重。连接池大小应该按业务并发量推算我们的场景最终稳定在 30已经绰绰有余。4.3 时间戳精度和时区的坑时序数据库对时间最敏感建议所有设备端上报统一使用毫秒级时间戳数据库表字段统一用TIMESTAMPTZ并且确保应用JVM和数据库会话都使用 UTC 时区。我们曾因为服务器时区是 CST、数据库时区是 UTC导致查询结果和实际时间相差 8 小时排查了很久才发现。另外在写 Java 代码时不要把LocalDateTime直接 setTimestamp它会丢失时区信息。正确做法是先转成ZonedDateTime再转TimestampTimestamp.from(Instant.parse(2024-06-01T00:00:00Z))4.4 瞬时写入峰值怎么处理产线启动时会有大量设备同时上线瞬时写入量可能是平时的 50 倍。如果应用层不做限流数据库会直接扛不住。我们的方案是引入一个简单的内存队列做削峰每次批量写入封装成带缓冲的 writer超过队列容量时丢弃最老的数据打告警优先保证数据库不被打崩。生产环境需要的是“回弹性”而不是“硬扛”。5. 再分享几个稳定性的额外加固点上线稳定运行三个月后我们又做了两轮优化这两件事对生产系统的长期稳定性帮助很大也一并记录下来。5.1 批量写入失败后的幂等重试批量写入在极端情况下会失败失败往往伴随着网络超时或数据库主备切换此时到底有没有写入成功应用层很难判断。如果直接重发可能产生重复数据。我们的处理方式是在时序表里增加一个应用侧生成的唯一批次号利用数据库的唯一索引保证幂等CREATE UNIQUE INDEX idx_signal_batch ON device_signal(ts, device_id, signal_type, batch_id);重试前先按唯一键删除再插入或者直接使用 insert on conflict 的 upsert 语义保证重试不产生脏数据。这个改动不复杂但为后续宕机演练省下了很多对账精力。5.2 磁盘容量告警要提前量时序数据库的磁盘占用增长是线性的而且随着保留策略调整可能突然翻倍。我们的告警阈值设的是 70%但实测发现数据盘经常一夜之间从 60% 涨到 85%。建议除了容量阈值告警还要加一个“每日增量”指标用今天的增量推算剩余可用天数低于 30 天就提前告警避免半夜翻车。峰时流量是大家对 IoT 项目最常见的高难度场景但从我们的角度看一个维护健康、告警及时的时序系统背后还藏着许多细致入微的工作。回到最初的话题——团队做时序选型需要的是与自身技术栈、运维文化高度匹配的数据库而金仓时序数据库刚好补齐了这块拼图的最低缺口我们少写了几万行转换代码少踩了几个跨生态的坑多了几个能随时上门排查问题的工程师。手里有项目在推进类似的架构先把数据量算清楚再画个评分卡你会很快发现真正适配的往往不是参数表上最亮眼的那一个。希望这篇手记能成为你别踩我们当初那些坑的一张小地图。
返回列表