4. 数据存储选型:对象存储、列式存储与时序数据库
数据存哪,怎么存,这问题看着基础,但坑特别多。我见过不少团队,模型迭代到一半发现数据读不出来,或者训练效率被 IO 卡死,回头一查,全是存储选型没想清楚。
这一章,咱们就把对象存储、列式存储和时序数据库这三类东西掰开揉碎。不聊虚的,直接说选型逻辑和实战经验。
4.1 对象存储:数据湖的基石
对象存储,说白了就是 OSS 或者 S3 这类服务。它存的是“对象”——文件加上元数据,扁平结构,没有目录树。你想想看,一个桶里几亿个文件,传统文件系统早崩了,但对象存储扛得住。
核心定位:数据湖的底座,存原始数据、模型文件、训练日志、Checkpoint。
我个人习惯把对象存储当作“冷热分离”的冷端。热数据放本地 SSD,冷数据全扔 OSS。为什么?便宜,扩容简单,而且 S3 协议是事实标准,几乎所有框架都支持。
4.1.1 选型要点
- 兼容性:必须支持 S3 兼容 API。别搞私有协议,后面对接 Spark、Flink、PyTorch 会痛不欲生。
- 吞吐能力:单桶带宽上限是多少?并发请求数?我遇到过单桶 QPS 打满导致训练中断的情况,后来加了前缀分区才解决。
- 数据一致性:大部分对象存储是最终一致性。读后写能立刻读到吗?有些场景需要强一致,比如模型注册。
我的经验:训练 Checkpoint 直接写 OSS 时,一定要用分段上传(Multipart Upload)。否则大文件失败一次就得重传,浪费几个小时。
4.2 列式存储:分析查询的加速器
Parquet 和 ORC 是列式存储的代表。行存适合 OLTP,列存适合 OLAP。为什么?因为分析查询通常只读少数列,列存可以跳过无关数据,压缩率也高。
我记得有一次优化特征存储,把 CSV 换成 Parquet,查询速度提升了 10 倍,存储空间降了 60%。嗯,这差距就是这么夸张。
4.2.1 Parquet vs ORC
| 维度 | Parquet | ORC |
|---|---|---|
| 压缩率 | 高,支持多种编码 | 更高,自带轻量索引 |
| 生态兼容 | Spark、Hive、Presto 原生支持 | Hive 生态更好,Spark 稍弱 |
| 谓词下推 | 支持,但不如 ORC 精细 | 支持,带统计信息更高效 |
| 写入性能 | 中等,适合批量 | 略优,适合流式写入 |
怎么选?如果你的技术栈是 Spark 为主,Parquet 更顺手。如果 Hive 用得重,ORC 更合适。我个人偏向 Parquet,因为生态更广,而且 PyArrow 读写方便。
4.2.2 实战避坑
- 小文件问题:列式存储最怕小文件。每个文件都有元数据,文件多了元数据膨胀,查询变慢。我建议每个文件 256MB 到 1GB。
- Schema 演化:模型迭代中特征会变。Parquet 支持添加列,但删除列要小心。我习惯用嵌套结构(Struct/List)预留扩展位。
- 分区策略:按时间分区是常规操作。但分区粒度过细(比如按小时)会导致大量小目录,反而降低性能。
曾经踩过的坑:有一次我把特征表按天分区,每天 1000 个文件。跑了半年后,Hive 元数据服务直接 OOM。后来改成按周分区,文件合并到 200 个以内,问题解决。
4.3 时序数据库:监控与指标的首选
模型训练过程中,GPU 利用率、Loss 曲线、数据延迟这些指标,都是时序数据。用关系数据库存?可以,但查询慢,存储膨胀快。时序数据库(如 InfluxDB、TimescaleDB、Prometheus)专门为这类场景设计。
你想想看,每秒几万条指标写入,还要支持按时间窗口聚合、降采样、实时报警。普通数据库扛不住。
4.3.1 选型对比
| 特性 | InfluxDB | TimescaleDB | Prometheus |
|---|---|---|---|
| 存储引擎 | 自研 TSM | 基于 PostgreSQL | 自研 TSDB |
| 写入性能 | 极高,单机百万点/秒 | 高,依赖 PG 调优 | 高,但单机有限 |
| 查询语言 | Flux / InfluxQL | SQL | PromQL |
| 集群能力 | 企业版才支持 | 原生 PG 集群 | Thanos / Cortex 扩展 |
| 适用场景 | IoT、APM、监控 | 金融、DevOps | K8s 监控、告警 |
我个人习惯:训练指标用 InfluxDB,因为写入简单,查询灵活。基础设施监控用 Prometheus,跟 K8s 集成好。如果团队 SQL 能力强,TimescaleDB 也不错。
4.3.2 数据建模要点
- Tag 和 Field 区分:Tag 是索引字段(如 GPU ID、模型名),Field 是指标值(如 loss、accuracy)。Tag 不要太多,否则索引膨胀。
- 降采样策略:原始数据保留 7 天,之后自动聚合到分钟级。我一般用连续查询(CQ)或者 Retention Policy 实现。
- 避免高基数:Tag 的取值数量不要超过百万级。比如把用户 ID 当 Tag,会导致性能雪崩。
我的习惯:训练时每 10 秒记录一次指标,用 InfluxDB 的批量写入接口。查询时用窗口函数做平滑,比在应用层算快得多。
4.4 整体选型框架
三种存储不是互斥的,而是协同的。我画了一张图,帮你理清关系。
从图里能看出来,对象存储是中心,所有原始数据先进去。然后根据使用场景,转成列式存储做分析,或者写入时序数据库做监控。其他存储(MySQL、Redis)负责元数据和缓存。
4.5 选型决策清单
最后,我整理了一个清单,帮你快速决策:
- 存原始文件? → 对象存储(OSS/S3)
- 做大规模分析查询? → 列式存储(Parquet/ORC)
- 存监控指标和时序数据? → 时序数据库(InfluxDB/Prometheus)
- 需要事务和强一致? → 关系数据库(MySQL/PostgreSQL)
- 需要低延迟缓存? → Redis/Memcached
重要提醒:不要试图用一种存储解决所有问题。我见过有人用 HBase 存时序数据,结果查询慢、运维复杂。选型要匹配场景,而不是追新。
嗯,这一章的内容就这些。存储选型没有银弹,但理解了每种存储的优缺点,你就能在模型迭代中少走弯路。下次遇到数据读写的性能问题,先回头看看存储层选对了没有。