TDengine 时间序列数据库乱序插入导致的存储压缩问题及解决方案
结论与原因分析
结论先行
- 在相同量级数据下,有序写入的磁盘占用 仅为乱序写入的约 1/5。
- 当 APP 运行记录以时间乱序方式写入 TDengine 时,列式压缩和时间索引优势难以发挥,压缩比从约 5% 恶化到 30%+。
- 在极端乱序场景下,时间索引和块管理的开销甚至可能超过业务数据本身,导致“压缩比大于 100%”这类严重浪费存储的情况。
测试服务器三天连续写入结果
经在测试服务器连续运行约 6 天的压测验证,得到如下对比结果:
| 插入方式 | 数据量 | 占用空间 | 压缩比 |
|---|---|---|---|
| 有序插入 | 123306953 条 | 约 3.39GB | 4.86% |
| 乱序插入 | 124087779 条 | 约 26.04GB | 37.09% |
在数据量相近的前提下,乱序写入导致磁盘占用放大到约 7.68 倍,压缩比从理想的 5% 左右上升到 30%+,说明时间乱序对 TDengine 的压缩机制影响极大。
生产环境对比数据
在生产环境中,真实业务数据进一步验证了这一现象。我们统计了单日数据量级约在 300 万 - 500 万条的 APP 运行记录:
| 环境场景 | 时间范围 | 写入方式 | 占用空间 | 压缩比 | 备注 |
|---|---|---|---|---|---|
| 生产-乱序 | 3 个月 | 持续乱序写入 | 237.02GB | ≈ 120.06% | 存在严重的空间膨胀 |
| 生产-有序 | 5 个月 | 顺序重写入同类数据 | 14GB | ≈ 5.10% | 恢复了正常的列式压缩水平 |
也就是说,同一类数据,如果写入顺序从“严重乱序”调整为“基本有序”,磁盘占用可以从数百 GB 量级降到十几 GB。
实际生产环境归档方案
为了解决上述存储膨胀问题,我们针对 APP 运行记录、设备运行时长等 "隔天汇总上报" 类数据,设计了一套 "热数据乱序写入 -> 定期 ETL 清洗 -> 归档数据有序重写" 的归档方案。
1. 归档架构设计
整体流程由 XXL-JOB 调度中心发起,通过 SSH 远程调用部署在 TDengine 节点的 Shell 和 Python 脚本,完成数据的导出、压缩与重写入。
2. 核心归档逻辑
步骤一:按天导出与压缩 (Backup)
使用 app_runtime_backup.sh 脚本,利用 TDengine 的 SELECT ... >> file.csv 功能,按 day 字段(时间分片)将热数据导出。
- 按天隔离:每次只导出一个自然日的数据,确保数据在时间维度上的纯净性。
- 即时压缩:导出完成后立即调用
zip压缩,大幅减少中间文件的磁盘占用(压缩比通常极高)。
<div style={{display: "none"}}>**核心概念**: 测试管理 | 伪代码示例 | 概览</div>
taos -s "SELECT ... FROM flow_app_runtime WHERE day = 20251001 >> app_runtime_20251001.csv"
zip app_runtime_20251001.zip app_runtime_20251001.csv
rm app_runtime_20251001.csv
步骤二:有序批量重写 (Restore)
使用 app_runtime_restore.py 脚本将备份数据写入归档库。
- 多线程并发:开启多个线程(如 10 个)并行处理不同日期的数据。
- 批量写入:读取 CSV 后,每 400 条记录拼接为一个大
INSERT语句批量写入,提高吞吐量。 - 重获有序性:由于 CSV 本身是按天导出的,且在插入时是批量追加,数据在物理存储上重新获得了“时间有序性”,从而激活了 TDengine 的列式压缩优势。
步骤三:调度策略 (XXL-JOB)
通过配置 XXL-JOB 参数灵活控制归档行为:
{
"backup": true,
"write": true,
"forceArchiveTwoMonthsAgo": true,
"archiveDayConfigs": [
{
"archiveDaysAgo": 45, // 归档 45 天前的数据
"retryLookbackDays": 10 // 自动重试过去 10 天内失败的归档
}
]
}
- 冷热分离:保留最近 45 天的热数据在线查询,45 天前的数据进入归档库。
- 自动容错:自动扫描并重试最近 10 天内状态为“失败”的归档任务。
其他类型数据归档建议
基于上述实践,对于不同类型的时序数据,建议采取差异化的归档策略:
| 数据类型 | 特征 | 归档建议 |
|---|---|---|
| APP/设备运行记录 | 隔天上报、乱序严重、查询频次低 | 全量重写归档。采用上述 ETL 方案,定期将乱序热数据导出并有序重写到冷库,追求极致压缩比。 |
| 设备实时监控/日志 | 实时写入、基本有序、数据量巨大 | TTL 自动过期 + 关键数据抽样。利用 TDengine 的 TTL 机制自动清理过期数据,仅将报警或关键事件转存到 MySQL 或 ES。 |
| 高频传感器数据 | 频率极高(秒级/毫秒级)、有序 | 降采样归档。使用 TDengine 的 downsampling 功能,将秒级数据聚合成分钟/小时级数据存入归档库,原始数据过期删除。 |
业务场景小结
- APP 运行记录、设备运行记录(开机时长等)属于“隔天上报”型数据,天然容易产生时间乱序;
- 设备离线、无网、关机等场景会导致数据延迟多天才上报;
- 设备本地时间与服务器时间存在偏差时,还会叠加“时间漂移型乱序”;
- 这些特性共同放大了 TDengine 在乱序写入场景下的存储压力。
原因分析(结合 TDengine 特性)
TDengine 能在有序写入场景下实现极高压缩率,核心依赖于以下机制:
- 时间有序的列式存储块:同一列数据按时间顺序写入到连续的数据块中,相邻值变化平滑,利于差分编码和重复值压缩;
- 基于时间的索引与分块:底层以时间为主键构建索引和时间分片,大量写入总是追加到“当前时间窗口”的少量块中;
- 针对时间序列优化的压缩算法:利用时间戳单调递增、数值变化缓慢等特征进行高效编码。
当大量数据按时间乱序写入时,上述特性会被逐步削弱甚至反转:
- 新写入数据频繁落在历史时间窗口,迫使系统不断打开和改写旧块,而不是顺序追加到尾部;
- 原本连续的时间序列被打碎,同一时间段的数据被拆散到多个块中,差值压缩效果大幅降低;
- 为了维护乱序写入后的时间索引,需要额外的索引记录和元数据,管理开销显著增加;
- 旧块被多次打散与重写,产生更多小块和碎片,最终表现为“压缩比升高、磁盘占用暴涨”。
这也是为什么在极端乱序的生产环境中,会出现“压缩比大于 100%”的情况:系统在存储同一批业务数据的同时,还为乱序写入支付了额外的索引与块管理成本。
测试背景
TDengine 作为高性能时间序列数据库,其内部优化机制对“写入顺序”高度敏感:
- 当数据按时间顺序写入时,TDengine 可以将数据连续落在相同时间窗口的块中,压缩算法充分利用时间序列的平滑性;
- 当大量数据乱序写入时,同一时间段的数据被切分到大量碎片块中,块数量、索引条目数都会迅速膨胀。
为了量化这种影响,编写了一个 Spring Boot 定时任务应用,持续向 TDengine 写入 APP 运行记录数据,对比有序写入与乱序写入下的压缩效率。
测试架构与数据流向(Mermaid)
整体数据流向可以抽象为:
在压测程序中,上述真实链路被浓缩为一个定时任务,持续向 TDengine 的热表和归档表写入模拟数据。