4、数据存储选型:对象存储、列式存储与数据湖

数据存储选型这件事,说实话,是MLOps数据底座里最容易被低估的一环。很多人一上来就想着搭模型、调参数,结果数据量一上来,存储先崩了。我见过不止一个团队,因为选错了存储,最后整个pipeline重写——那滋味,真不好受。

今天咱们就把这块掰开揉碎聊清楚。我会从三个维度来讲:对象存储、列式存储和数据湖。它们不是互斥的,而是互补的。你想想看,一个完整的MLOps平台,这三样基本都得用上。

核心观点:没有万能的存储方案,只有适合你场景的组合拳。对象存储管原始数据,列式存储管在线查询,数据湖管分析治理——各司其职。

MLOps 数据存储选型全景图 对象存储 MinIO / S3 原始数据、模型文件 列式存储 HBase / Cassandra 在线特征、实时查询 数据湖 Delta Lake / Iceberg 分析、治理、版本管理 💡 典型数据流向 原始数据 → 对象存储(MinIO/S3)→ 列式存储(HBase)提供在线特征 → 数据湖(Delta Lake)做分析和训练集管理 MLOps 统一存储管理层

4.1 对象存储:MinIO 与 S3

对象存储,说白了就是把数据当对象来管。每个对象有个唯一的ID,存在扁平化的命名空间里。跟传统的文件系统不一样,它没有目录树的概念——虽然你看着像有目录,其实那只是前缀。

我个人习惯把对象存储叫做「数据沼泽」——什么东西都能往里扔。图片、日志、模型权重、训练数据,统统可以。而且它天生就是分布式、高可用的。

4.1.1 S3 兼容协议

AWS S3 是对象存储的事实标准。现在几乎所有对象存储都宣称兼容 S3 API。MinIO 就是其中最典型的开源实现。

特性 S3(AWS) MinIO
部署方式 托管服务 自建 / 私有云
API 兼容 原生 100% S3 兼容
性能 受限于网络 本地部署,低延迟
成本 按量付费 硬件成本 + 运维
适用场景 云原生、弹性伸缩 私有化、边缘、合规

我的经验:在MLOps场景里,我建议把MinIO作为本地缓存层,S3作为远程归档层。训练时从MinIO读数据,模型归档时推到S3。这样既快又省钱。

4.1.2 在 MLOps 中的典型用法

  • 原始数据存储:日志、传感器数据、图片、视频,直接以对象形式存入 bucket
  • 模型版本管理:每个训练产出的模型权重文件,按版本号命名存入
  • 中间结果缓存:特征工程产出的 parquet 文件,临时存放
  • 实验日志:MLflow 等工具的后端存储,直接对接 S3
# 使用 boto3 操作 S3 兼容存储
import boto3

client = boto3.client(
    's3',
    endpoint_url='http://minio:9000',  # MinIO 地址
    aws_access_key_id='minioadmin',
    aws_secret_access_key='minioadmin'
)

# 上传模型文件
client.upload_file(
    'model_v1.pt',
    'ml-models',
    'experiments/exp_001/model_v1.pt'
)

# 列出所有模型版本
response = client.list_objects_v2(
    Bucket='ml-models',
    Prefix='experiments/exp_001/'
)
for obj in response.get('Contents', []):
    print(obj['Key'])

避坑指南:我曾经遇到过团队把 MinIO 当数据库用——频繁地小文件读写。对象存储对小文件的性能很差,因为每次操作都有 HTTP 开销。记住:对象存储适合大文件、批量读写,不适合高频小 IO。

4.2 列式存储:HBase 与 Cassandra

列式存储跟行式存储最大的区别,就是它按列族来组织数据。你想想看,在ML场景里,我们经常只需要读取某几个特征列,而不是整行数据。这时候列式存储的优势就出来了。

HBase 和 Cassandra 都是 NoSQL 数据库,但设计哲学完全不同。我简单对比一下:

维度 HBase Cassandra
一致性模型 强一致性(CP) 最终一致性(AP)
架构 主从架构(HMaster + RegionServer) 去中心化(Gossip 协议)
查询语言 API 操作(Scan/Get/Put) CQL(类 SQL)
适用场景 在线特征存储、时序数据 高可用写入、跨数据中心
运维复杂度 较高(依赖 HDFS、ZooKeeper) 中等(无单点故障)

4.2.1 HBase 在特征存储中的应用

在MLOps里,HBase 最常见的角色就是在线特征存储。模型推理时,需要根据实体ID快速获取特征向量。HBase 的随机读写能力正好满足这个需求。

// HBase 特征存储表设计示例
Table: feature_store
RowKey: user_id + timestamp
Column Family: features
  - age
  - gender
  - click_count_7d
  - purchase_amount_30d
Column Family: metadata
  - update_time
  - data_version

// Java API 示例
Get get = new Get(Bytes.toBytes("user_12345_20240101"));
get.addFamily(Bytes.toBytes("features"));
Result result = table.get(get);
byte[] age = result.getValue(
    Bytes.toBytes("features"),
    Bytes.toBytes("age")
);

关键设计原则:RowKey 的设计决定了查询性能。我习惯把高频查询的字段放在 RowKey 前缀,比如 user_id。避免使用单调递增的 RowKey,否则会导致 Region 热点。

4.2.2 Cassandra 的高可用写入

Cassandra 的优势在于写入性能。如果你有海量的实时特征数据需要写入,而且对一致性要求不那么苛刻,Cassandra 是更好的选择。

我记得有个项目,需要每秒写入10万条用户行为数据用于实时特征计算。HBase 扛不住这么高的写入压力,换成 Cassandra 后,问题就解决了。

-- Cassandra 表结构
CREATE TABLE ml_features.user_features (
    user_id text,
    feature_name text,
    feature_value double,
    update_time timestamp,
    PRIMARY KEY (user_id, feature_name)
) WITH compaction = {
    'class': 'LeveledCompactionStrategy'
};

-- 写入示例
INSERT INTO ml_features.user_features (
    user_id, feature_name, feature_value, update_time
) VALUES (
    'user_12345', 'click_count_7d', 42.0, toTimestamp(now())
);

避坑指南:我曾经踩过一个坑——把 Cassandra 当关系型数据库用,做了很多跨分区的范围查询。Cassandra 的查询必须带分区键,否则性能极差。设计表结构时,一定要先想好查询模式。

4.3 数据湖:Delta Lake 与 Iceberg

数据湖这个概念,说白了就是「把数据都放在一个湖里,想怎么分析就怎么分析」。但早期的数据湖有个大问题——没有事务支持,数据质量没法保证。

Delta Lake 和 Apache Iceberg 就是来解决这个问题的。它们给对象存储加上了事务层,让数据湖变得可靠。

4.3.1 Delta Lake

Delta Lake 是 Databricks 开源的,跟 Spark 深度集成。它的核心是事务日志——每次数据变更都会记录在日志里,支持 ACID 事务。

  • 时间旅行:可以查询任意历史版本的数据
  • Schema 演进:支持自动添加、修改列
  • 数据版本管理:每个操作都有版本号
  • 统一批流:批处理和流处理共用同一份数据
# Delta Lake 基本操作
from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("DeltaLakeDemo") \
    .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
    .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") \
    .getOrCreate()

# 写入数据
df = spark.range(1000)
df.write.format("delta").save("/data/delta/numbers")

# 时间旅行:查询历史版本
df_v1 = spark.read.format("delta") \
    .option("versionAsOf", 1) \
    .load("/data/delta/numbers")

# Schema 演进
df_new = df.withColumn("new_col", df.id * 2)
df_new.write.format("delta") \
    .option("mergeSchema", "true") \
    .mode("append") \
    .save("/data/delta/numbers")

4.3.2 Apache Iceberg

Iceberg 跟 Delta Lake 类似,但更强调开放性和兼容性。它不绑定任何计算引擎,Spark、Flink、Trino 都能用。

特性 Delta Lake Apache Iceberg
事务实现 事务日志(JSON) Manifest 文件 + Snapshot
引擎绑定 深度绑定 Spark 引擎无关
分区演进 不支持 支持动态分区
隐藏分区 不支持 支持(自动分区裁剪)
社区生态 Databricks 主导 Apache 基金会

我的建议:如果你的技术栈以 Spark 为主,选 Delta Lake 会更顺手。如果有多引擎需求,或者希望更开放,Iceberg 是更好的选择。我个人在混合架构里更倾向 Iceberg,因为它不绑架你用什么引擎。

4.3.3 数据湖在 MLOps 中的定位

数据湖在MLOps里主要干三件事:

  1. 训练数据管理:保证训练集和测试集的可追溯性,每次训练用的数据版本都能回溯
  2. 特征存储的底层存储:离线特征计算的结果写入数据湖,再同步到 HBase 等在线存储
  3. 实验数据归档:每次实验的输入数据、中间结果、最终模型,都按版本归档
# 使用 Iceberg 管理训练数据
CREATE TABLE ml_training.user_features_iceberg (
    user_id STRING,
    feature_name STRING,
    feature_value DOUBLE,
    event_time TIMESTAMP,
    data_version STRING
) USING iceberg
PARTITIONED BY (days(event_time));

-- 插入训练数据
INSERT INTO ml_training.user_features_iceberg
SELECT 
    user_id,
    feature_name,
    feature_value,
    event_time,
    'v20240101' as data_version
FROM raw_features
WHERE event_time >= '2024-01-01';

-- 查询特定版本的数据
SELECT * 
FROM ml_training.user_features_iceberg
WHERE data_version = 'v20240101'
  AND user_id = 'user_12345';

核心原则:数据湖不是用来替代对象存储或列式存储的,而是在它们之上加了一层「治理层」。对象存储管原始数据,列式存储管在线查询,数据湖管版本和事务。三者配合,才是完整的MLOps数据底座。

4.4 选型决策矩阵

说了这么多,到底怎么选?我整理了一个决策矩阵,你可以根据自己场景对号入座:

场景 推荐方案 理由
原始数据存储(图片、日志) MinIO / S3 对象存储天然适合大文件、低成本
在线特征查询(毫秒级) HBase 强一致性,随机读写性能好
高并发特征写入 Cassandra 写入性能极佳,去中心化
训练数据版本管理 Delta Lake / Iceberg 支持时间旅行和事务
多引擎分析查询 Iceberg 引擎无关,兼容性好
私有化部署、合规要求 MinIO + HBase + Iceberg 全栈自建,数据不出域

最后提醒一句:不要为了用新技术而用新技术。我见过有人在小团队里硬上 Cassandra,结果运维成本比开发成本还高。选型之前,先想清楚你的数据量、查询模式、团队能力。合适的才是最好的。