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

香港服务器 NVMe SSD + Linux 调优:提升 AI 向量数据库(Qdrant/Milvus)查询性能的教程

发布人:Minchunlin 发布时间:2025-09-09 10:22 阅读量:686


凌晨 2 点 17 分,我站在香港将军澳机房的冷通道里,风像刀子一样灌进袖口。23U 的那台托管机刚被夜班工程师从机柜里拉出来,U.2 背板的温度探头在手电光里跳个不停;监控屏上,向量检索的 p99 一撞上夜间导入高峰就变红。报警说“磁盘抖动”,可我更担心是 NVMe 的热降频叠加内核 I/O 路径的“好心”优化。于是我把 KVM 插上,先关掉 BIOS 的深省电,确认 PCIe 链路没降宽,再把数据盘与 WAL 从逻辑上“拎出来”。那一刻我心里有数了:要用一套盘位规划 + 文件系统 + 内核参数 + 索引策略的组合拳,把这台香港服务器榨到它该有的样子。接下来每一步,都是跪在机房地板上做出来的。

1. 目标与指标(先讲结果要长什么样)

业务目标: 百万到数亿向量(768 维,cosine/L2),查询 QPS 300~1000,p95 ≤ 20ms、p99 ≤ 40ms,导入期间依然可读。

系统目标:

  • NVMe 全盘随机读 p99 ≤ 2ms,穿透到应用后 p95 ≤ 15~20ms
  • 单机能抗 600 QPS(批量=1,top-k=10),水平扩展到 3 节点后线性接近 1500 QPS
  • 温度< 70℃,无热降频;掉电可靠,不关闭写入保护

2. 现场硬件与机房条件(别纸上谈兵)

2.1 服务器与盘位清单

型号/参数 数量 备注
CPU AMD EPYC 7443P(24C,PCIe 4.0) 1 单路,NUMA=1,延迟更稳定
内存 256GB DDR4-3200 1 4 通道起步,8 条更好
NVMe(数据) U.2 3.84TB(PCIe 4.0,写入耐久 1 DWPD) 2 组 mdraid0 做向量段/索引
NVMe(WAL/元数据) U.2 1.92TB(PCIe 4.0,3 DWPD) 1 单盘,专职写多的小文件
SATA SSD(系统盘) 960GB 2 RAID1 装系统
网卡 10GbE x2(LACP) 2 走向量服务流量与管理面
机房条件 N+1 冷通道、独立 PDU - 风道充足是 NVMe 不降频的生命线

注:你可能拿到的是 Intel 至强 + PCIe3 NVMe,没关系,方法论通用,只是绝对数值不同。

2.2 盘位与用途规划

盘符 用途 文件系统 关键挂载
/dev/md0(nvme0n1+nvme1n1) 向量段、索引数据 XFS noatime,inode64,logbufs=8,logbsize=256k
/dev/nvme2n1 WAL、元数据(DB 自身) ext4 noatime,commit=120(配合定时 fstrim)
/dev/md1(sata 系统盘) OS 与日志 XFS 默认

3. BIOS/固件与内核(决定你能不能跑满 NVMe)

BIOS 建议:

  • CPU Governor 固定 Performance;关 C-State 深度(保留 C1/C1E 即可)
  • PCIe ASPM 关;NVMe 的 APST 限制延迟
  • NUMA 单路天然有利;双路时将 NVMe 插在与应用进程同 NUMA 的根端口上

内核建议(CentOS 7 仍大量在线的现实):

CentOS 7 自带 3.10 内核对 blk-mq、io_uring 支持弱,我这边在香港机房统一用 ELRepo 的 kernel-ml(5.x/6.x):

# 安装新内核
yum install https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm -y
yum --enablerepo=elrepo-kernel install -y kernel-ml

# 设为默认
grub2-set-default 0
grub2-mkconfig -o /boot/grub2/grub.cfg   # BIOS
# 若是UEFI:
# grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg

# 开机参数(降低尾延迟)
sed -i 's/^\(GRUB_CMDLINE_LINUX=".*\)"/\1 nvme_core.default_ps_max_latency_us=0 pcie_aspm=off"/' /etc/default/grub

如果你是 Rocky/Alma 8/9,就简单很多,默认内核已经 OK。

4. NVMe 分区、RAID 与文件系统(决定吞吐与稳定性的“三件套”)

4.1 对齐分区与 RAID0

# 确认设备
lsblk -d -o NAME,MODEL,SIZE,ROTA
nvme list

# 用 gdisk 对齐到 1MiB,略
# 组 RAID0(注意 chunk)
mdadm --create /dev/md0 --level=0 --raid-devices=2 /dev/nvme0n1 /dev/nvme1n1 --chunk=1024
mdadm --detail /dev/md0
mdadm --examine --scan >> /etc/mdadm.conf

为什么用 RAID0?

读为主、写可控的向量检索,对带宽与 IOPS 都敏感,条带化能把两块盘的并发充分打满。我们把极端写入(WAL/小元数据)剥离到第三块 NVMe,避免 RAID0 的一致性开销。

4.2 文件系统:数据用 XFS,WAL 用 ext4

# 数据盘 XFS:提高并行度和日志缓冲
mkfs.xfs -f -m reflink=0 -d agcount=32 -l size=256m /dev/md0

# WAL 盘 ext4:稳健,小写放缓冲,配合定期 fstrim
mkfs.ext4 -F -E stride=1024,stripe_width=2048 /dev/nvme2n1

# 挂载
mkdir -p /data/vector /data/wal
echo '/dev/md0 /data/vector xfs defaults,noatime,inode64,logbufs=8,logbsize=256k 0 0' >> /etc/fstab
echo '/dev/nvme2n1 /data/wal ext4 defaults,noatime,commit=120 0 0' >> /etc/fstab
mount -a

不要在线 discard(discard 挂载),会抖尾延迟。改用系统级 fstrim:

systemctl enable fstrim.timer
systemctl start fstrim.timer

5. I/O 调度与系统参数(把“抖动”按在地上)

5.1 NVMe 调度器与电源

# 查看调度器
cat /sys/block/nvme0n1/queue/scheduler
# 通常为 [none] mq-deadline kyber

# 设 none(多队列下最省事)
echo none > /sys/block/nvme0n1/queue/scheduler
echo none > /sys/block/nvme1n1/queue/scheduler
echo none > /sys/block/nvme2n1/queue/scheduler

# 禁用深省电(已通过内核参数确保),再保险:
for d in /sys/class/nvme/nvme*/device/power/control; do echo on > $d; done

5.2 sysctl 与 ulimit

cat >> /etc/sysctl.d/99-vector.conf <<'EOF'
vm.swappiness=1
vm.dirty_background_ratio=5
vm.dirty_ratio=20
vm.max_map_count=262144
fs.aio-max-nr=1048576
fs.file-max=2097152

net.core.rmem_max=134217728
net.core.wmem_max=134217728
net.core.netdev_max_backlog=250000
net.ipv4.tcp_rmem=4096 87380 134217728
net.ipv4.tcp_wmem=4096 65536 134217728
EOF
sysctl --system

# 文件句柄
echo '* soft nofile 1048576' >> /etc/security/limits.conf
echo '* hard nofile 1048576' >> /etc/security/limits.conf

5.3 tuned 与中断亲和(可选但有效)

yum install -y tuned
systemctl enable --now tuned
tuned-adm profile throughput-performance

# IRQ 绑核(示例,按网卡中断号实际调整)
for i in $(grep -i eth0 /proc/interrupts | awk '{print $1}' | tr -d :); do
  printf "%x" 1 > /proc/irq/$i/smp_affinity
done

6. 基准前测(不测就等于没做)

6.1 fio 模板

cat > /root/fio-nvme-read-rand4k.fio <<'EOF'
[global]
name=randread_4k
ioengine=libaio
direct=1
bs=4k
iodepth=64
numjobs=8
time_based
runtime=60
group_reporting
filename=/data/vector/fio.test
rw=randread

[job1]
EOF
# 预创建 64G 测试文件
fallocate -l 64G /data/vector/fio.test

# 跑
fio /root/fio-nvme-read-rand4k.fio

6.2 关键指标对比(真实跑出来的数量级)

优化阶段 4k randread IOPS 平均延迟 p99 延迟
默认内核 + 单盘 ~420k 0.62ms 3.5ms
kernel-ml + RAID0 ~780k 0.38ms 2.2ms
调度 none + sysctl ~930k 0.31ms 1.6ms

这张表是我在三台配置近似的香港托管机上取的中位;不同厂牌 NVMe 可能±10~20%。

7. 选择与部署向量数据库(以 Qdrant 为例,Milvus 同理迁移)

我线上主推 Qdrant(Rust,HNSW、磁盘友好、API 简洁),单机延迟好控,Docker 起服务也轻。

7.1 Docker 部署(把最忙的目录放在最快的盘)

mkdir -p /data/vector/qdrant/storage /data/wal/qdrant
docker run -d --name qdrant \
  -p 6333:6333 \
  -v /data/vector/qdrant/storage:/qdrant/storage \
  -v /data/wal/qdrant:/qdrant/wal \
  qdrant/qdrant:latest

为什么存储放 /data/vector,WAL 放 /data/wal?

查询热点是向量段与索引,随机读为主;而 WAL/小 KV 元数据写多、顺序写多,两类 I/O 分盘最稳。

7.2 建库与参数(HNSW + on-disk)

# 创建 768 维、余弦距离的集合(简化示例)
curl -X PUT 'http://127.0.0.1:6333/collections/embeddings' \
  -H 'Content-Type: application/json' \
  -d '{
    "vectors": { "size": 768, "distance": "Cosine" },
    "optimizers_config": { "default_segment_number": 8 },
    "hnsw_config": { "m": 32, "ef_construct": 200 },
    "wal_config": { "wal_capacity_mb": 512, "wal_segments_ahead": 2 },
    "quantization_config": { "scalar": { "type": "int8", "always_ram": false }},
    "on_disk": true
  }'

经验值:

  • m=32 / ef_construct=200 在 1e7 级别向量下算是“稳妥中庸”的构图参数。
  • on_disk=true + 量化(int8)可把常驻内存压到可控范围,代价是 top-k 精确度略降;可按业务 A/B。
  • 查询时调 ef(如 64~256)在延迟和召回间找平衡。

7.3 导入与查询(Python 端)

# pip install qdrant-client==1.*
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct

client = QdrantClient(host="127.0.0.1", port=6333)

# 批量导入
points = []
for i in range(10000):
    vec = [0.0]*768
    vec[0] = i % 100 / 100.0
    points.append(PointStruct(id=i, vector=vec, payload={"k": i}))

client.upsert(collection_name="embeddings", points=points)

# 查询
query_vec = [0.0]*768
query_vec[0] = 0.42
res = client.search(collection_name="embeddings", query_vector=query_vec, limit=10, with_payload=False, params={"hnsw_ef": 128})
print(res)

如果你偏好 Milvus:用 Docker Compose 跑 standalone,把 minio/etcd 同样落在 /data/vector,WAL/rocksdb 落 /data/wal;索引用 IVF/HNSW 或 DiskANN,缓存 cache.capacity 设成物理内存的 30~50%,查询时调 nprobe/ef。

8. 联调压测(别让业务当你的压力机)

8.1 wrk 对 REST API 的烟囱测

# 用 wrk 模拟短连接 GET(你也可以用 vegeta/locust)
cat > search.lua <<'EOF'
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"
payload = '{"vector":[0.42' .. string.rep(",0",767) .. '],"limit":10,"params":{"hnsw_ef":128}}'
wrk.body = payload
request = function() return wrk.request() end
EOF

wrk -t8 -c256 -d60s --timeout 2s -s search.lua http://127.0.0.1:6333/collections/embeddings/points/search

8.2 关键观测(Grafana/命令行)

  • iostat -x 1:看 util、r_await、rareq-sz
  • nvme smart-log /dev/nvme0n1:看温度、媒体错误、可用备用块
  • docker stats:看 Qdrant 的 CPU、RSS
  • perf top/flamegraph(有条件就上):看热点是不是在 mmap pagefault 或 JSON 解析

8.3 压测结果样例(导入完成后的只读稳定期)

场景 QPS(avg) p50 p95 p99
默认部署(未分盘) 280 9ms 34ms 63ms
分盘 + 内核优化 520 6ms 18ms 36ms
调优 ef=128 → 96 610 5ms 14ms 28ms
三节点水平扩 ~1580 6~8ms 16~20ms 35~42ms

注:集群时把前端路由做成一致性哈希 + 粘性,能降低跨节点 cache miss。

9. 这些“坑”,我在香港机房都踩过

NVMe 温度 75℃ 以上降频
机箱前风墙看着足,但U.2 背板导风没打好。解决:加导风罩+把风扇曲线抬到 70%;温度回到 60~65℃,随机读 p99 从 3ms 回到 1.7ms。

PCIe 降速/降宽
有一次两块盘都只跑到 PCIe 3.0 x4。lspci -vv 看 LnkSta 一眼识破。原因是背板线缆插错了根端口侧。重插、固件更新解决。

在线 discard 导致尾延迟抽风
别加 discard。周更 fstrim 即可。

线程绑核与 NUMA
你把进程绑在 0~11 号核,NVMe 在另一个 NUMA,页表穿越的代价不划算。numactl --cpunodebind 绑到 NVMe 所在节点,延迟抖动立刻变小。

WAL 与查询混盘
Qdrant/Milvus 的 WAL 写入会把数据盘的随机写抬上去,检索尾延迟挤爆。分盘是第一原则。

容器默认 ulimit 太低
向量集合多了以后,经常“打开文件过多”。记得把容器的 nofile 提上去(前文 limits.conf)。

写入期间检索抖动
把导入批次做小、开启后端限速(Qdrant 的优化器参数/导入速率限流),并在业务层面把“强一致读”改“读后写延迟容忍”,就能平稳过渡。

10. 监控与日常运维清单(跑起来还得跑得久)

  • 磁盘健康:每周 nvme smart-log 存档;温度>70℃和媒体错误>0立刻报警
  • 碎片率/空洞:XFS 基本无忧;ext4 WAL 偶发抖动时 fstrim -av
  • 容量曲线:向量数据+索引一般 ≈ 1.2~1.6× 原始向量体积(量化后更小)
  • 备份:冷备份存 OSS;Qdrant 快照+WAL 限流;Milvus 走 MinIO 版本化
  • 变更窗口:索引参数重建需评估时长;建议在香港夜间生效(UTC+8 0:00~6:00)

11. 成本与容量预估(别等上线了才想起来)

向量规模 维度 索引(HNSW m=32) 量化 预计磁盘占用 建索引时间(单机)
1e6 768 int8 6080GB 10~20 min
1e7 768 int8 600800GB 2~4 h
5e7 768 很高 PQ/int8 3~5TB 10~18 h

这些是“机房里的平均值”,真实情况受 CPU/内核/盘型影响明显。

12. 一份可复制的最小落地清单(TL;DR)

  • 换内核到 5.x/6.x(CentOS 7 用 ELRepo)
  • 分三类盘位:RAID0(向量/索引)、单 NVMe(WAL/元数据)、系统盘
  • XFS + ext4,禁在线 discard,开 fstrim.timer
  • nvme none 调度,nvme_core.default_ps_max_latency_us=0,pcie_aspm=off
  • sysctl/limits:vm.max_map_count、nofile、dirty_* 调好
  • 部署 Qdrant(或 Milvus),存储/日志落对目录,HNSW + on-disk + 量化
  • 压测 wrk/fio,盯 p95/p99;冷热分离与导入限速
  • 温度/链路/NUMA 三件套常态化巡检

13. 收尾:天亮时的机房走道

天亮的时候,机房门外的走道像刚洗过的街道,空调的风从脚边掠过。我把最后一张压测曲线贴进维基,给客户发了三张图:fio 的 p99、Qdrant 的 p95、以及风扇曲线。那一刻你会发现,所谓“性能优化”并不神秘,无非是把每一种 I/O 都放在最适合它的介质上,让内核别帮倒忙,让空气流到该流的地方。

当晚运营同学回我一句:“高峰时段,检索没响一声。”

我把 KVM 从机柜里拔下来,合上机房的门,心里想的只有一句话:方法论是通用的,但细节要落在机房地板上。

附:命令速查(复制即用)

# 1) 查看链路与温度
lspci -vv | grep -A12 NVMe
nvme smart-log /dev/nvme0n1

# 2) 创建 mdraid0(数据)
mdadm --create /dev/md0 --level=0 --raid-devices=2 /dev/nvme0n1 /dev/nvme1n1 --chunk=1024
mkfs.xfs -f -m reflink=0 -d agcount=32 -l size=256m /dev/md0
mkdir -p /data/vector && echo '/dev/md0 /data/vector xfs defaults,noatime,inode64,logbufs=8,logbsize=256k 0 0' >> /etc/fstab

# 3) 准备 WAL 盘(ext4)
mkfs.ext4 -F -E stride=1024,stripe_width=2048 /dev/nvme2n1
mkdir -p /data/wal && echo '/dev/nvme2n1 /data/wal ext4 defaults,noatime,commit=120 0 0' >> /etc/fstab

# 4) 调度器与电源
for d in nvme0n1 nvme1n1 nvme2n1; do echo none > /sys/block/$d/queue/scheduler; done
systemctl enable --now fstrim.timer

# 5) sysctl 与 limits
sysctl --system
ulimit -n 1048576

如果你手上也有一台香港的 NVMe 服务器、也在为向量检索的尾延迟烦恼,照着这篇把“分盘—内核—文件系统—DB 参数—温度/NUMA”这五件事捋顺,先把 fio 的 p99 打到 2ms 以内;剩下的,就交给你的向量库与业务逻辑去折腾。祝你也能像我那天早晨一样,看着仪表盘,默默地把手机从“勿扰”调回正常。

目录结构
全文