Milvus StorageV2 写入流程分析
概述
StorageV2 是 Milvus 的新一代存储格式,使用 Apache Arrow + Parquet 技术栈,相比 StorageV1(Binlog 格式)具有更高的压缩率和查询性能。本文档详细分析 StorageV2 的完整写入流程。
一、整体架构
1.1 数据流向
1 | 用户 Insert/Delete 请求 |
1.2 关键组件
| 组件 | 职责 | 位置 |
|---|---|---|
| WriteBuffer | 内存缓冲区,缓存写入数据 | internal/flushcommon/writebuffer/ |
| SyncTask | 同步任务,负责将数据写入存储 | internal/flushcommon/syncmgr/task.go |
| BulkPackWriterV2 | StorageV2 写入器 | internal/flushcommon/syncmgr/pack_writer_v2.go |
| PackedRecordWriter | Arrow Record → Parquet 转换器 | internal/storage/record_writer.go |
| PackedWriter | 底层 Parquet 写入器(C++ FFI) | internal/storagev2/packed/ |
二、详细流程分析
2.1 数据准备阶段(WriteBuffer → SyncPack)
文件: internal/flushcommon/writebuffer/write_buffer.go
1 | func (wb *writeBufferBase) getSyncTask(ctx context.Context, segmentID int64) (syncmgr.Task, error) { |
关键数据结构:
1 | // InsertData 包含所有字段的数据 |
2.2 SyncTask 执行阶段
文件: internal/flushcommon/syncmgr/task.go
1 | func (t *SyncTask) Run(ctx context.Context) (err error) { |
2.3 BulkPackWriterV2 写入阶段
文件: internal/flushcommon/syncmgr/pack_writer_v2.go
2.3.1 Write 方法主流程
1 | func (bw *BulkPackWriterV2) Write(ctx context.Context, pack *SyncPack) ( |
2.3.2 serializeBinlog:InsertData → Arrow Record
关键步骤:将内存中的 InsertData 转换为 Arrow Record
1 | func (bw *BulkPackWriterV2) serializeBinlog(_ context.Context, pack *SyncPack) (storage.Record, error) { |
BuildRecord 详细过程(internal/storage/serde.go):
1 | func BuildRecord(b *array.RecordBuilder, data *InsertData, schema *schemapb.CollectionSchema) error { |
类型映射示例:
| Milvus 类型 | Arrow 类型 | 说明 |
|---|---|---|
DataType_Int64 |
arrow.Int64Type |
64 位整数 |
DataType_FloatVector |
arrow.FixedSizeBinaryType{ByteWidth: dim * 4} |
固定大小二进制(向量) |
DataType_Timestamp |
arrow.Int64Type |
Timestamp 作为 Int64 存储 |
DataType_JSON |
arrow.BinaryType |
JSON 作为二进制存储 |
2.3.3 writeInserts:Arrow Record → Parquet 文件
核心流程:
1 | func (bw *BulkPackWriterV2) writeInserts(ctx context.Context, pack *SyncPack) ( |
2.4 PackedRecordWriter:Arrow → Parquet 转换
文件: internal/storage/record_writer.go
2.4.1 Write 方法
1 | func (pw *packedRecordWriter) Write(r Record) error { |
2.4.2 PackedWriter(C++ FFI)
文件: internal/storagev2/packed/packed_writer.go
1 | func (pw *PackedWriter) WriteRecordBatch(recordBatch arrow.Record) error { |
底层 C++ 实现(internal/core/src/segcore/packed_writer_c.cpp):
1 | // C++ 端接收 Arrow RecordBatch,写入 Parquet 文件 |
三、列组(Column Group)机制
3.1 列组概念
列组是 StorageV2 的重要优化机制,将相关字段组织在一起,减少文件数量,提高查询效率。
示例:
1 | // 列组配置示例 |
3.2 列组的作用
- 减少文件数量:多个字段可以写入同一个 Parquet 文件
- 优化查询:相关字段在一起,减少 I/O
- 支持列剪枝:查询时只读取需要的列组
3.3 文件路径结构
普通模式:
1 | {rootPath}/{collectionID}/{partitionID}/{segmentID}/ |
Manifest 模式:
1 | {rootPath}/{collectionID}/{partitionID}/{segmentID}/ |
四、关键优化点
4.1 零拷贝转换
- Arrow Record 在内存中直接使用,无需序列化
- Go → C++ 通过 FFI 传递 Arrow C Data Interface,零拷贝
- Arrow → Parquet 使用 Arrow 的 Parquet 写入器,高效转换
4.2 批量写入
- 使用
RecordBatch批量写入,减少 I/O 次数 - 支持缓冲写入,提高吞吐量
4.3 压缩优化
- Parquet 使用列式压缩,压缩率高
- 支持多种压缩算法(Zstd, Snappy, Gzip 等)
- 在 Milvus 中默认使用 Zstd 压缩
4.4 元数据管理
- Timestamp 范围:在写入时计算并记录
- 统计信息:记录压缩前后大小、行数等
- Manifest:统一管理文件元数据(Manifest 模式)
五、与 StorageV1 的对比
| 特性 | StorageV1 (Binlog) | StorageV2 (Parquet) |
|---|---|---|
| 文件格式 | Protobuf + 自定义编码 | Apache Parquet |
| 文件组织 | 每个字段一个文件 | 按列组组织,多个字段一个文件 |
| 压缩率 | 中等 | 高(3-10倍) |
| 查询性能 | 需要读取多个文件 | 列式访问,更高效 |
| 元数据 | 每个文件独立元数据 | 统一 Manifest 管理 |
| Timestamp 存储 | 独立 binlog 文件 | Arrow Record 的一列 |
| 类型支持 | 基础类型 | 支持复杂类型(JSON、嵌套结构等) |
六、总结
6.1 核心流程
- 数据准备:WriteBuffer → SyncPack
- 数据转换:InsertData → Arrow Record(
serializeBinlog) - 数据写入:Arrow Record → Parquet 文件(
writeInserts) - 底层存储:通过 C++ FFI 调用 Parquet 写入器
6.2 关键技术
- ✅ Apache Arrow:内存中的列式数据格式
- ✅ Apache Parquet:持久化的列式存储格式
- ✅ 列组机制:优化文件组织和查询性能
- ✅ 零拷贝转换:Arrow → Parquet 高效转换
- ✅ FFI 集成:Go 和 C++ 无缝协作
6.3 优势
- 高压缩率:节省 50-80% 存储空间
- 高性能:列式访问减少 60-90% I/O
- 灵活性:支持复杂数据类型和 Schema Evolution
- 兼容性:与其他大数据工具无缝集成
StorageV2 通过 Arrow + Parquet 技术栈,为 Milvus 提供了高效、灵活、可扩展的存储解决方案。