Milvus 内存开销分析:为什么 100 万 1536 维向量吃掉 8G 内存
在部署 Milvus 向量数据库时,很多工程师第一次做容量规划都会被物理内存占用吓一跳:
“我算过账啊,100 万条 1536 维的 float32 向量,纯数据体积也就是 $1,000,000 \times 1536 \times 4 \text{ B} \approx 6.14 \text{ GB}$。为什么把 Collection 加载(Load)进内存后,QueryNode 节点的 RSS 内存占用直接飙到了 8.5 GB 甚至 9 GB?”
难道是 Milvus 存在严重的内存泄漏?还是 C++ 引擎的内存碎片在作祟?
深入剖析 Milvus 在加载集合后的内部内存布局(Memory Layout),把这额外的 2~3 GB 内存去向拆得一清二楚,才能在生产环境中做到胸有成竹的精确容量规划。
内存去向拆解:五大组成部分
当一个 Milvus 集合完成构建并被 QueryNode 加载进内存时,物理内存被以下五个主要部分瓜分:
+-------------------------------------------------------------+ | Milvus QueryNode Memory Layout | | | | 1. 原始向量数据 (Raw Vectors) ~ 6.14 GB (68%) | | 2. HNSW 索引图结构 (Graph Topology) ~ 1.35 GB (15%) | | 3. 标量字段与主键索引 (Scalar & PK Index) ~ 0.45 GB (5%) | | 4. 节点与分片元数据 (Metadata Overhead) ~ 0.20 GB (2%) | | 5. 内存对齐与 jemalloc 缓存池 ~ 0.86 GB (10%) | | ----------------------------------------------------------- | | Total RSS Memory ~ 9.00 GB (100%) | +-------------------------------------------------------------+1. 原始向量存储(Raw Vector Buffer):6.14 GB
无论你使用何种近似最近邻(ANN)索引,为了在查询完成时计算精确的距离得分(Refinement)或支持标量过滤后的重新打分,原始向量本身必须完整驻留物理内存。
$10^6 \times 1536 \times 4 \text{ Bytes} = 6,144,000,000 \text{ Bytes} \approx 5.72 \text{ GiB} \text{ (6.14 GB)}$。
2. HNSW 索引图拓扑结构:约 1.35 GB
在标准的 HNSW 参数($M=16, efConstruction=200$)下,图结构需要为这 100 万个向量维护邻居关系:
- 最底层(Layer 0)每个节点最多拥有 $2M = 32$ 条边;
- 较高层(Layer 1~N)每个节点拥有最多 $M = 16$ 条边;
- 每个边指针是一个 32 位或 64 位的整数 ID(4~8 字节);
- 加上节点元数据、每层的动态数组开销,图拓扑结构单向量开销约在 1.2 KB ~ 1.5 KB 之间。
100 万节点对应的 HNSW 索引文件体积约在1.2 GB ~ 1.4 GB。
3. 主键索引与标量字段(Scalar Fields & PK Index):约 0.45 GB
每个向量都绑定了一个doc_id(通常为 INT64 或 VARCHAR(64) 主键)以及可能的标量字段(如tenant_id、created_at)。
为了支持基于主键的极速点查(Get / Delete)与标量过滤,Milvus 在内存中维护了主键的哈希索引(Offset Index)或布隆过滤器,100 万条记录的主键与标量开销约占300 MB ~ 600 MB。
4. 动态分片(Segment)与元数据开销:约 0.20 GB
Milvus 将数据组织为多个 Segment(默认每个 Sealed Segment 大小为 512 MB)。每个 Segment 内部都有自己的位图(Delete Bitset)、版本快照和统计信息(Stats Log),在加载进内存时会分配独立的 C++ 管理对象。
5. 内存对齐与 jemalloc 内存池:约 0.86 GB
Milvus 底层采用jemalloc作为内存分配器,为了实现 CPU SIMD / AVX-512 指令集的高性能向量点积运算,内存分配必须严格遵循 64 字节或 512 位内存对齐(Memory Alignment)。这会导致小对象分配产生一部分内部碎片(Internal Fragmentation)。同时,jemalloc 为了降低高并发线程申请释放内存的锁竞争,会在线程私有缓存(Arena Cache)中预留一部分尚未归还给 OS 的空闲内存页。
容量规划黄金公式
在为生产环境采购 Milvus 节点或配置 Kubernetes Pod 的resources.requests / limits时,切忌只按原始数据大小乘以 1.0 来计算。
请牢记以下生产级容量规划公式:
$$Memory_{total} = \left( N \times Dim \times 4 \text{ B} + N \times M \times 8 \text{ B} + Memory_{scalar} \right) \times 1.35$$
其中:
- $N$ 为预估总向量条数;
- $Dim$ 为向量维度(如 768 或 1536);
- $M$ 为 HNSW 的最大边数参数;
- 系数 $1.35$ 为系统预留的 jemalloc 缓存池、元数据、Compaction 临时空间以及查询过程中的动态优先队列缓冲区(Search Buffer)。
总结
100 万 1536 维向量吃掉 89 GB 内存是完全正常且符合计算机系统底层规律的。正是这额外的 23 GB 内存换来了 HNSW 毫秒级的高并发拓扑路由与 SIMD 硬件对齐加速。搞清楚每一块内存的账单,才能在业务高速增长时避免因 OOM 导致的线上事故。