LanceDB: 二进制数据湖的崛起

数据湖曾承诺可以存储任意格式的数据,但一直未能兑现——直到 AI 大模型时代对海量图像、视频、文本的需求涌现。本文以 LanceDB 为例,探讨如何通过列式存储、向量索引等技术,构建一套的二进制数据湖系统。

背景:一个迟到十年的 Use Case

2010 年前后,数据湖(Data Lake)概念横空出世。它的核心卖点之一就是:不像数仓只能存结构化数据,数据湖可以原样存储任意格式——包括图片、视频、音频

2016年的维基百科对“Data Lake”的描述

然而事实上,这不过是一句空话。数据湖的主流用户是数据分析师和数据工程师,他们的世界里只有 CSV、JSON、Parquet。图片和视频成了 PPT 中的一句空谈。御三家 Iceberg、Delta Lake、Hudi 以及 Spark、Hive 等查询引擎都专注于结构化数据,本质上仍是在取代数据仓库的位置。

直到 AI 大模型的席卷而来。训练 GPT、Stable Diffusion 这样的模型,需要海量的图片、视频、音频、文本作为训练数据。经典做法是将每个文件单独存成 S3 上的一个 object,然后在数据库或 Parquet 里维护一张元数据表,通过文件路径去关联。但是,是否有专用的、更革命性的方式呢?

数据湖宣传了十年的那个 use case,终于真的来了,可是它却没有准备好。

解决方案:以 LanceDB 为例

设想一下,你要为这些新场景开发下一代二进制数据湖,要解决哪些核心问题?LanceDB 是当今最具代表性的二进制数据湖,它底层是 Lance 的列式存储格式(用 Rust 实现),专门针对多模态场景设计。下面我们以它为例,逐一分析这些挑战与解决方案。

Lance 主要组件(LanceDB/Lance Namespace/Format)与经典数据湖技术栈的类比

存储效率

"每个文件一个 Object"的方式下,哪怕一张图片只有几十 KB,它也会在我们的元数据 DB 以及 S3 上产生一条元数据记录,独占一次 HTTP 请求。当你有 10 亿张图片时,元数据开销和 API 调用成本将变得不可忽视。理想的做法是将大量小文件合并打包成少数几个大文件,并在文件内部建立高效的随机访问索引——既降低 IO 次数,又减少 metadata 开销。

Lance 对此给出的方案是:将二进制 Blob 数据(图片、视频、音频字节)与结构化元数据(标签、时间戳、embedding 向量)存储在同一张表里,并且允许直接用 offset+size 的方式读取。相比于经典方案,这样也不再需要维护元数据和对象存储这两套系统之间的一致性。

Lance 将 Blob 数据平铺地合并成一个列,并通过 offset+size 进行随机读取

对于大型二进制字段,Lance 推荐将 Blob 数据存储在独立的文件中(称为 Packed External Blobs),而 Parquet 列只保存指向它的偏移量。读取时可以 lazy,只有当 reader 确实需要文件内容时再去发起 IO。

检索能力

把数据存起来只是第一步,能查才有价值。

按 ID 查询是最基本的需求——给定一个或一批 row ID,取出对应的图片和标注。这类点查需要 O(1) 的时间复杂度,不能全表扫描。LanceDB 的论文 对此给出了针对性的格式设计,兼顾随机访问和批处理。

Lance Format 的设计

Lance format 试图在随机访问和批处理之间取得平衡:对于大字段(向量、图片等 ≥128 字节的字段),采用"全 zip 编码"将所有 buffer 按行交错排列,使得任意行的随机读取最多只需 2 次 IOPS,与数据嵌套深度无关;对于小型数据,则使用类 Parquet 的 miniblock 分块方案。

注意这套设计的收益主要体现在 NVMe 本地缓存层(可达数十万 IOPS),在直接访问 S3 等对象存储时,由于 IOPS 上限本身就只有数万,优化空间相对有限。论文的评估也基于 NVMe 介质进行。

LanceDB 格式对比 Parquet/Arrow 格式,来自论文 [1]

二进制数据也带来了一种全新的检索范式:语义检索,比如“找出所有与这张图片语义相似的图片”。估计你已经猜到了,这是通过近似向量搜索(ANN)完成的。Lance 格式内置了向量索引

Lance 的向量索引是一个全局索引(i.e. 不按 Fragment 分区),它分为3个独立层:

  1. 聚类 Clustering:通过全局 K-means 选出中心点,可以看作是向量索引自己的“分区”
  2. 子索引 Sub-index: 分区后的向量如何索引,可选 Flat(平铺,不索引)或者 HNSW 索引
  3. 量化层:可选通过量化算法压缩长度、加速计算,可选 PQ/SQ/RabitQ 等量化算法

写入时,也采用了类似 LSM-Tree 的增量 compaction 方案。详细设计可以看 Claude 老师的总结

最后,LanceDB 将多种检索范式统一在一个查询接口里,允许混合查询:

1
2
3
4
5
6
7
8
results = (
table.search("a cat sitting on a windowsill", query_type="hybrid")
.where("date > '2024-01-01' AND resolution = '4K'")
.reranker("cross_encoder")
.select(["id", "label", "embedding"])
.limit(20)
.to_pandas()
)

生态集成

最后一个问题往往被低估:数据存好了,怎么使用?

数据科学家和 ML 工程师的主要工具链是 Python。他们习惯用 Pandas DataFrame 操作数据,用 PyTorch DataLoader 加载训练数据,用 Ray Data 做分布式数据预处理。

如果只提供 SQL 接口和 Java SDK,工程师需要自己写胶水代码才能接进 PyTorch,那用户体验将是灾难性的。一套好用的 Python API 以及与 PyTorch、Ray 等软件的原生集成,是二进制数据湖的必选项。

这里还藏着一个容易被忽视的难题:现有的 Python 乃至非 Python 生态库,在设计时压根不知道什么是"数据湖"——它们统统假设数据以普通文件的形式存在于文件系统中。无论是 OpenCV 读图片、av 解码视频,还是 HuggingFace 的 Dataset,它们的 API 接受的都是一个 File (文件描述符)或文件路径。把 blob 数据从列式存储里读出来之后,如何让它看起来像一个文件,是这条路上绕不过去的兼容性挑战。

为了对接大量假设“数据就是文件”的既有库,Lance 提供了 BlobFile 对象——一个实现了 Python io.RawIOBase 接口的 Wrapper,可以懒加载列式存储中的 blob 数据,让下游库(如 av、OpenCV)误以为自己在读普通文件:

1
2
3
4
5
6
ds = lance.dataset("./videos.lance")
blobs = ds.take_blobs("video", ids=[0])
# blobs[0] 是一个 file-like 对象,可以直接传给 av、OpenCV 等库
with av.open(blobs[0]) as container:
for frame in container.decode(video=0):
process(frame)

当然,BlobFile 仅仅是 hack——通过实现 read()seek()tell() 等方法来"伪装"成文件,而非真正的文件系统路径。一旦某个库在内部绕过标准接口(比如直接传文件路径给 C 扩展、调用 os.fstat()、或使用 mmap),这层包装就会失效。

更进一步,每个框架还有自己更高层的封装。举例来说,Lance 官方为了支持 PyTorch,提供了原生的 PyTorch DataLoader:

1
2
3
4
5
6
# 直接作为 PyTorch DataLoader 使用
for batch in DataLoader(
table.where("video_height >= 720").shuffle()
):
images, labels = batch["image"], batch["label"]
outputs = model(images)

换句话说,只有经过官方验证过的框架和库才能放心使用。在 AI 领域框架更新迭代极快的今天,追着各种新框架逐一适配,可能是一件耗时费力的持续性工程投入。Paimon 的 blob-as-descriptor 方案也面临同样的困境。

传统数据湖的跟进

JVM 生态的传统数据湖格式也开始意识到多模态数据的重要性,不过各家的进展和侧重点差异显著。

Apache Paimon:最积极的跟进者

Apache Paimon 是阿里巴巴主导的流批一体数据湖格式,2024 年正式从 Apache 孵化器毕业成为顶级项目。在目前几个主流格式里,Paimon 在二进制数据存储方面的工程投入是最为系统的。

Paimon 引入了专门的 Blob 存储机制 PIP-35: Introduce Blob to store multimodal data,核心设计思路与 LanceDB 有几分相似:

  • 独立 Blob 文件:Blob 列不再嵌入 Parquet 文件,而是存储在独立的 blob 文件中,Parquet 只保存偏移量引用。
  • 新 BlobType:在 Java API 中引入 BlobType,Flink/Spark SQL 中通过 blob-field option 声明,支持延迟流式读取。
  • 解耦 Compaction:blob 列的 compaction 策略与结构化列分离,互不影响。

此外,Paimon 还引入了 Object Table,把对象存储中的非结构化数据(图片、视频目录)直接映射为 Paimon 表,方便用户用熟悉的 API 进行操作。

Apache Iceberg / Delta Lake:暂无动作

Iceberg v3(2025 年)和 Delta Lake 4.0(2025 年 9 月)不约而同地引入了跨生态统一的 Variant 类型,解决了半结构化 JSON 数据的存储问题,这是两个项目在"半结构化"方向最大的动作。

然而对于非结构化数据(图片、视频字节),两者目前仍然建议:不在格式内处理,建议将文件存对象存储、路径引用存表。Iceberg 社区早在 2020 年就有 unstructured module 的提案,至今仍停留在概念阶段。这是 Parquet 列式 Row Group 机制的架构决定的——混入大体积 blob 极易触发 OOM,短期内难以根本性改变。

Apache Hudi:已制定 Roadmap

Hudi 1.x 路线图明确将非结构化 Blob 的原生支持(索引、更新、变更捕获)列为重点,并在引入向量索引能力,朝“AI 原生 Lakehouse”方向演进。相关进展包括:RFC-100: Unstructured Data Storage in Hudi, #14290 Add Support for Vector Indexing in Apache Hudi 等。社区贡献者也在探索将 LanceDB 集成进 Hudi 生态,用 Hudi 管理 ACID 语义、用 LanceDB 提供向量检索。不过完整的生产级实现尚未到来,目前仍是 Roadmap。

但是,你真的需要吗?

技术选型是 trade-off 的艺术。对象存储的高延迟是不可避免的,无论格式多先进都无法改变。即使是 Lance 声称的“100x 快于 Parquet 的随机访问”,其绝对延迟仍在毫秒到数十毫秒量级——与具备 buffer pool 和本地盘的数据库、数据仓库相比,仍有数量级的差距。

那么,二进制数据湖适合哪些场景呢?

  1. 对延迟不敏感的批处理任务,尤其是模型训练。 大模型训练的 DataLoader 可以在 GPU 计算的同时预取下一批数据,只要数据吞吐量够高,几十毫秒的单次读取延迟完全可以被流水线掩盖。
  2. PB 级数据量,降低存储成本成为最高优先级。 当数据规模达到 PB 级,S3 存储成本(相对于 SSD)的优势就会变得极其显著——可能相差 10 到 100 倍。此时勉强够用的延迟 + 极低的存储成本往往是最优解,没得选。

而像在线推理的实时特征获取、以及任何 P99 延迟要求在 50ms 以内的服务。这类场景应该选择向量数据库+Object等低延迟方案。

References

  1. Lance: Efficient Random Access in Columnar Storage through Adaptive Structural Encoding
  2. Blob API - Lance
  3. Apache Paimon多模态数据湖实践:从结构化到非结构化的技术演进
  4. 阿里ALake数据湖:多模态数据存储处理方案
  5. From BI to AI: A Modern Lakehouse Stack with Lance and Iceberg