上一篇 下一篇 分享链接 返回 返回顶部

向量检索边缘化:在香港服务器上落地 Milvus / pgvector 的冷热分层与 NVMe Pool + RDMA 直通

发布人:Minchunlin 发布时间:2025-09-24 19:24 阅读量:709

那天晚上 23:40,香港到东京的 RTT 稳定在 45ms 左右,但真实用户的向量召回链路一旦绕行到核心区的“统一大库”,尾延迟就像被人拽住脚踝。我们的召回服务在云上,embedding 在云上,向量索引也在云上,唯独用户在香港。A/B 面板给我看,一半流量在 95 分位卡到 350ms——这不是我能接受的数字。

我盯着机柜里两台新上的 NVMe 盒子发呆:一组满血的 U.2 盘,旁边是两张亮着蓝灯的 ConnectX 网卡。我脑子里只有一句话:把检索推到边缘,把热数据钉在 NVMe 上,用 RDMA 把吞吐和延迟都榨出来。于是,就有了下面这套方案与踩坑记录。


1. 目标与路线图

业务目标

  • P95 向量检索延迟 ≤ 80ms(端到端,香港用户,TopK=50,查询向量维度 768/1024)

  • 热数据 100% 命中边缘,冷数据回落不超过 300ms

  • 单集群吞吐 ≥ 8k QPS,峰值≥ 12k QPS(只读阶段)

  • 故障演练覆盖:单盘/单网卡/单节点/单交换机/单可用区失效

技术路线

  • 存储层:NVMe Pool 以 NVMe-oF (RDMA/RoCEv2) 暴露卷;本地/远端混合池化

  • 引擎层:Milvus(2.x)负责“热检索 + 冷分层至对象存储”,PostgreSQL + pgvector 负责元数据/回查与一部分通用相似度检索(以 SQL 能力补齐)

  • 冷热分层:Milvus 的 segment/索引放 热层 NVMe冷层 MinIO(S3 兼容);pgvector 通过 Tablespace 做表/索引的冷热 tablespace 迁移

  • 网络:25/100GbE RoCEv2 RDMA,PFC + ECN,Jumbo Frame;业务网与存储网物理/逻辑隔离

  • 编排:Kubernetes + Helm 部署 Milvus 组件;PostgreSQL 裸机+Patroni(或单机起步,后期再切)

  • 操作系统CentOS 7.9(内核升级至 ELRepo kernel-ml 以便更好的 RDMA/NVMe 驱动支持)


2. 机房与硬件清单(落地可复用)

注:以下为我最终上线时的 实际可行 配置,不是实验室“神机”。预算友好、可替换。

2.1 角色与节点规格

角色 数量 CPU 内存 本地存储 网络 备注
NVMe-oF Target(存储池) 2 2×Xeon Silver 4310 256GB 8×3.84TB U.2 NVMe(RAID0 by SPDK/VOL) 2×100GbE ConnectX-6(RoCEv2) 负责 RDMA 暴露卷
Milvus Query/Index Node 3 2×Xeon Silver 4310 256GB 2×1.92TB NVMe(本地缓存) 2×25GbE ConnectX-5 可选 1×A10 GPU(可选)
Milvus Meta / etcd / Pulsar / MinIO 3 1×Xeon Silver 4210 128GB 2×960GB SSD(RAID1) 2×25GbE 可合并/拆分
PostgreSQL + pgvector 2(主备) 1×Xeon Gold 6230 256GB 系统盘+2×3.84TB NVMe(Tablespace 热层) 2×25GbE Patroni/keepalived
汇聚/监控 2 任意 64GB 2×480GB SSD 2×10GbE Prom+Grafana+Loki

交换机

  • TOR:Mellanox SN2010(或等价 25/100GbE),启用 PFC/ECN

  • 核心:任意兼容 100GbE,上行 LAG

2.2 操作系统与固件

  • CentOS 7.9(注意 EOL,生产请上内核新版本):

     
    # 升级到 ELRepo 主线内核,提升 RDMA/NVMe 驱动可用性 yum install https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm -y yum --enablerepo=elrepo-kernel install kernel-ml -y grub2-set-default 0 && reboot
  • 固件:升级 NVMe 固件、MLX 网卡固件到供应商建议的稳定版本(机房窗口操作)


3. 网络拓扑与关键参数

3.1 逻辑拓扑(简化)

 
[Clients(HK)] │ 业务网(外部) [Edge Gateways/LBs] │ 业务网(内部 VLAN 110) ┌───▼───────────────────────────────────────────────────────────────┐ │ Kubernetes(Milvus 集群,VLAN 110/120) │ │ Proxy/Query/Index ────► NVMe-oF Initiators (RDMA VLAN 120) │ │ │ │ │ │ │S3/对象存储(MinIO,VLAN 130) │RDMA │ │ ▼ ▼ │ │ [MinIO] ◄────── RDMA ──────► [NVMe-oF Targets] │ └──────────────────────────────────────────────────────────────────┘ │ [PostgreSQL 主/备,VLAN 110/130]

3.2 网络参数(RoCEv2 推荐)

值/建议
MTU 9000(端到端一致:主机、交换机、容器 CNI)
RDMA RoCEv2(基于 UDP 的 L3 可路由)
流控 PFC(优先级 3)+ ECN(交换机开启,DiffServ 映射)
优先级 DSCP=26(示例),网卡/交换机/主机一致映射到 RoCE 队列
中断亲和 irqbalance 关闭,手动绑核(见后文)
NUMA QueryNode/IndexNode 绑到与 RDMA 网卡同一 NUMA 域
监控 mlx5 exporter / rdma 计数器、PFC pause 统计

主机侧 RDMA 简要配置

 
# MLX 网卡 RoCEv2 开启 mlxconfig -d mlx5_0 s ROCE_CC_MODE=ECN ROCE_CC_RP=1 ROCE_CC_NP=1 ethtool -A eth2 rx on tx on # PFC 由交换机 / DCB 配置保证 # Jumbo & DSCP 保持一致 ip link set dev eth2 mtu 9000 tc qdisc replace dev eth2 root fq # 关闭 irqbalance,手动中断绑核(示例) systemctl stop irqbalance for i in /proc/irq/*/mlx5_comp*/smp_affinity_list; do echo 8 > "$i"; done # 绑CPU核 3(示例)

4. NVMe Pool + RDMA 直通(NVMe-oF + SPDK)

4.1 NVMe-oF Target 部署

前置:Hugepage & DPDK

 
# Hugepage echo 8192 > /proc/sys/vm/nr_hugepages echo 'vm.nr_hugepages=8192' >> /etc/sysctl.conf # 绑定 NVMe-oF Target 使用的网卡到 DPDK(视 SPDK 版本而定) modprobe vfio-pci dpdk-devbind.py --bind=vfio-pci 0000:af:00.0 # 示例 PCI 地址

编译/安装 SPDK(精简示例)

 
yum groupinstall "Development Tools" -y yum install numactl-devel CUnit-devel libaio-devel openssl-devel rdma-core-devel -y git clone https://github.com/spdk/spdk.git cd spdk && git submodule update --init ./configure --with-rdma --enable-debug make -j && make install

SPDK NVMe-oF Target 配置(nvmf_target.json 片段)

 
{ "subsystems": [ { "subsystem": "nvmf", "config": [ { "method": "nvmf_set_config", "params": { "acceptor_poll_rate": 10000 } }, { "method": "nvmf_create_transport", "params": { "trtype": "RDMA", "max_queue_depth": 1024, "io_unit_size": 131072, "max_io_qpairs_per_ctrlr": 64, "in_capsule_data_size": 8192 } } ] }, { "subsystem": "bdev", "config": [ { "method": "bdev_nvme_attach_controller", "params": { "name": "Nvme0", "trtype": "PCIe", "traddr": "0000:b1:00.0" } }, { "method": "bdev_split_create", "params": { "name": "Nvme0n1", "split_count": 8, "split_size_mb": 512000 } } ] }, { "subsystem": "nvmf", "config": [ { "method": "nvmf_create_subsystem", "params": { "nqn": "nqn.2025-09.hk.edge:nvme0", "allow_any_host": true, "serial_number": "EDGE0001" }}, { "method": "nvmf_subsystem_add_ns", "params": { "nqn": "nqn.2025-09.hk.edge:nvme0", "namespace": "Nvme0n1p0" }}, { "method": "nvmf_subsystem_add_listener", "params": { "nqn": "nqn.2025-09.hk.edge:nvme0", "trtype": "RDMA", "traddr": "10.120.0.10", "trsvcid": "4420" }} ] } ] }

Target 运行

 
sudo -E /usr/local/bin/nvmf_tgt -m 0xF --json /etc/spdk/nvmf_target.json

4.2 Initiator(Milvus/PG 节点)挂载卷

 
# 发现目标 nvme discover -t rdma -a 10.120.0.10 -s 4420 # 连接 nvme connect -t rdma -n nqn.2025-09.hk.edge:nvme0 -a 10.120.0.10 -s 4420 # 磁盘将以 /dev/nvmeXnY 显示,创建文件系统 mkfs.xfs -f /dev/nvme1n1 mkdir -p /data/nvme_hot && echo "/dev/nvme1n1 /data/nvme_hot xfs noatime,nodiscard,allocsize=2M 0 0" >> /etc/fstab && mount -a

注意

  • NVMe-oF target 双机做 一致性与多路径:使用 nvme multipath -i on 并在业务低峰做 failover 演练。

  • 观察 nvme reconnect 行为,必要时设置 ctrl loss tmo


5. Milvus:热在 NVMe,冷在 MinIO(S3)

5.1 部署(Kubernetes + Helm,关键 values)

存储与分层核心参数(示例)

 
common: storageType: remote # 远端对象存储(MinIO/S3) # 分层:热段缓存与索引本地落盘 localStorage: enabled: true path: /data/nvme_hot/milvus cacheSizeGB: 512 # 本地段缓存,配合 QueryNode minio: enabled: true resources: { ... } mode: distributed persistence: size: 8Ti # 冷层容量(与生命周期策略联动) etcd: { ... } pulsar: { ... } queryNode: resources: limits: cpu: "20" memory: "120Gi" config: queryNode: cache: cacheSize: 120000 # 约 120GB,段/向量缓存(内存) maxParallelism: 32 persistence: mountPath: /data/nvme_hot/milvus/query indexNode: resources: limits: cpu: "24" memory: "160Gi" config: indexNode: indexSliceSize: 2147483648 # 2GB 切片 build: hnsw: M: 48 efConstruction: 320 ivf_pq: nlist:
&
目录结构
全文