Qdrant 生产环境手册
覆盖:部署 → 配置 → 安全 → 调优 → 备份 → 监控 → 升级 → 故障排查
基线版本:Qdrant 1.19.x(2026-09 稳定版 1.19.1)|客户端:qdrant-client 1.19.x
开发用法另见Qdrant开发使用指南.md,配套配置与脚本在deploy/和ops/
第 0 章 三条版本红线(升级前必读)
Qdrant 的升级路径不是线性的,踩中这几条会直接起不来或数据读不出。
| 红线 | 内容 | 后果 |
|---|---|---|
| 不能跳版本 | 1.17 移除了 RocksDB 存储引擎,改用 Gridstore。1.15.x 及更早不能直接升到 1.17+ | 存储格式不兼容,启动即失败。必须逐个小版本升(1.15 → 1.16 → 1.17) |
| 旧端点已删 | 1.19 移除了 /search、/recommend、/discover 三个端点(1.10 起由统一 /query 取代) | 客户端报 404。升级前先把代码迁到 /query |
| 客户端要对齐 | 服务端 1.19 ↔ 客户端 1.19。客户端版本低于服务端太多会缺少新字段支持 | 新特性用不了或参数被静默忽略 |
版本选择建议:新部署直接用最新稳定版;已有部署跟随官方节奏一个一个升,不要跨版本。
检查当前版本:
curl -s http://127.0.0.1:6333/ | jq -r '.version'
第 1 章 部署形态怎么选
| 形态 | 适合场景 | 优点 | 代价 |
|---|---|---|---|
| macOS 单二进制 | 本地开发、跑评测 | 零依赖、零虚拟化开销、./qdrant 就跑起来 | 仅开发用,别拿来当生产 |
| Linux 单二进制 + systemd | 单机生产、2~8GB 内存小机器 | 最省资源;无容器运行时开销 | 升级要手动换二进制 |
| Docker / Compose | 单机生产、需要环境一致性 | 升级=改 tag,隔离干净,回滚快 | 容器运行时本身吃内存(Intel Mac 上尤其明显) |
| K8s + Operator | 集群、需要自动扩缩容 | 自动化程度最高 | 运维复杂度陡增,小规模纯亏 |
| Qdrant Cloud | 完全不想运维 | 免运维、有 SLA | 按量计费;数据出境需评估 |
给你的建议(结合你现有的 Intel MacBook 16GB + 国内 Linux 服务器):
- 本地开发:用官方 macOS 单二进制。不要为了跑 Qdrant 装 Docker Desktop——那东西本身就是你机器发热的主因之一。
```bash
curl -L https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-apple-darwin.tar.gz | tar -xz
./qdrant # REST 6333,Web UI: http://127.0.0.1:6333/dashboard
```
- 生产服务器:内存 ≤ 4GB 用 systemd 二进制;≥ 8GB 且已经有 Docker 运维习惯就用 Compose。
一个常见的错误认知:qdrant-client 的 --local 嵌入式模式(QdrantClient(path=...))不是生产方案。它是纯 Python + SQLite 实现,官方明确不推荐用于生产或压测,且数据格式与 server 版不兼容——./qdrant_local 目录搬不到生产服务器上。它只适合本地验证和评测。生产必须走 server 模式,数据用脚本重新导入(点 ID 幂等的话重导不会产生重复)。
第 2 章 单机生产部署
2.1 系统准备(Linux)
# 1) 文件描述符:Qdrant 是 mmap 重度用户,默认 1024 会在高负载下莫名报错
sudo tee /etc/security/limits.d/qdrant.conf <<'EOF'
qdrant soft nofile 65536
qdrant hard nofile 65536
EOF
# 2) 磁盘:向量检索是随机读密集,机械盘性能差一个数量级,能上 NVMe 就上
# 数据目录和快照目录必须分开挂载 —— 同一块盘,盘挂了数据和备份一起没
# 3) 关闭透明大页(THP)—— 会让 mmap 访问出现不可预测的延迟毛刺
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
# 4) 时间同步(快照/日志时间戳要能对上)
sudo timedatectl set-ntp true
2.2 目录规划(照抄即可)
/opt/qdrant/qdrant # 二进制
/etc/qdrant/config.yaml # 配置
/etc/qdrant/qdrant.env # 密钥(权限 600)
/var/lib/qdrant/storage/ # 数据(★ 独立盘)
/var/lib/qdrant/snapshots/ # 快照(★ 另一块盘或远端)
/var/lib/qdrant/temp/ # 临时文件
/var/log/qdrant/ # 日志(用 systemd 时在 journald)
2.3 首次启动自检(四步,缺一不可)
# ① 服务活着吗
curl -s http://127.0.0.1:6333/healthz # 期望: healthz check passed
# ② 就绪了吗(能服务请求)
curl -s http://127.0.0.1:6333/readyz # 期望: readyz check passed
# ③ 认证生效了吗(没配 key 时这步会返回 200,说明在裸奔)
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:6333/collections
# 期望 401;返回 200 = api_key 没生效,立刻回去查配置
# ④ 写入→检索→删除 全链路通不通
curl -s -X PUT http://127.0.0.1:6333/collections/_smoke \
-H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \
-d '{"vectors":{"size":4,"distance":"Cosine"}}'
curl -s -X PUT "http://127.0.0.1:6333/collections/_smoke/points?wait=true" \
-H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \
-d '{"points":[{"id":1,"vector":[0.1,0.2,0.3,0.4],"payload":{"t":"ok"}}]}'
curl -s -X POST "http://127.0.0.1:6333/collections/_smoke/points/query" \
-H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \
-d '{"query":[0.1,0.2,0.3,0.4],"limit":1}'
curl -s -X DELETE http://127.0.0.1:6333/collections/_smoke -H "api-key: $QDRANT_API_KEY"
2.4 Docker 部署
直接看 deploy/docker-compose.yml(已含密钥注入、内存上限、健康检查、日志轮转)。配 .env:
QDRANT_API_KEY=$(openssl rand -hex 32) # 写进 .env,别进 git
QDRANT_READ_ONLY_KEY=$(openssl rand -hex 32)
QDRANT_MEM_LIMIT=4g
第 3 章 配置详解(生产必改的 8 项)
完整模板见 deploy/production.yaml,这里只讲改了会出事的那些。
3.1 覆盖优先级(先搞清这个,否则改了不生效)
内置默认值 < config.yaml < 环境变量 QDRANT__XXX__YYY < 命令行参数
例:QDRANT__SERVICE__API_KEY=abc 等价于 service.api_key: abc,且优先级更高。所以密钥用环境变量注入,配置文件进 git 也不会泄密。
3.2 必改项清单
| 配置项 | 建议值 | 不改的后果 |
|---|---|---|
service.api_key | 32 字节随机串 | 任何人可读写删你全部向量(最严重的疏忽) |
service.read_only_api_key | 另一个随机串 | 监控系统得持有写权限,权限过大 |
service.host | 裸机 127.0.0.1 / 容器 0.0.0.0 | 裸机绑 0.0.0.0 + 无防火墙 = 公网裸奔 |
storage.storage_path | 独立数据盘 | 系统盘写满会导致整机异常 |
storage.snapshots_path | 另一块盘 | 同盘快照 = 盘挂了备份也挂 |
storage.on_disk_payload | true | payload 全进内存,OOM 主因(尤其你这种带大量元数据的语料) |
storage.wal.wal_capacity_mb | 256(写入高则 1024) | WAL 写满会阻塞写入,表现为 upsert 超时 |
telemetry.enabled | true | 拿不到 /telemetry 运行态,排障少一半信息 |
3.3 容易误解的三个参数
on_disk_payload — 设 true 时 payload 索引仍在内存。也就是说它省的是"存原文"的内存,不是索引的内存。所以字段越多、索引越多,省得越少。别指望它解决所有内存问题。
indexing_threshold_kb — 未索引数据累计到这个量才触发建 HNSW 索引。批量导入期间临时调大(如 1000000)能显著加快导入(避免边导边反复重建索引),导完必须调回(比如 20000)并等待优化器跑完,否则检索会一直扫未索引段——这是"导入后检索变慢"最常见的原因。
default_segment_number — 段数约等于可并行搜索的粒度。核多调大能提吞吐,但段越多小文件越多、内存与合并压力越大。2~4 核的机器保持 2,8 核以上可以试 4。不要设成核数那么多。
3.4 量化:内存不够的第一选择
| 方式 | 压缩比 | 召回损失 | 适用 |
|---|---|---|---|
scalar: int8 | ~4× | < 1% | 默认选它,几乎无痛 |
scalar: int8 + always_ram | ~4× | < 1% | 内存够放量化数据、原始向量留磁盘 |
binary | ~32× | 明显 | 必须配 rescoring/oversampling 才能用 |
| 1.19 TurboQuant 4-bit | ~8× | 小 | 新格式,想压得更狠时试 |
PATCH /collections/{name}
{
"quantization_config": {
"scalar": { "type": "int8", "quantile": 0.99, "always_ram": true }
}
}
关掉量化:把 quantization_config 设为 null 即可,不需要重建集合。
先量化,再考虑加机器。int8 省 75% 内存且不用重新嵌入;加机器要迁移数据、改架构、多一份运维。
第 4 章 安全加固
三层都要做,缺一层就等于没做。
4.1 网络层(最有效的一层)
# 裸机:绑回环,绝不 0.0.0.0
# config.yaml → service.host: 127.0.0.1
# systemd 单元里 Environment="QDRANT__SERVICE__HOST=127.0.0.1"
# 防火墙:只放行应用服务器
sudo ufw allow from 10.0.0.0/24 to any port 6333 proto tcp
sudo ufw allow from 10.0.0.0/24 to any port 6334 proto tcp
sudo ufw enable
Docker 的坑:-p 6333:6333 会绕过 ufw(Docker 直接写 iptables),必须写成 -p 127.0.0.1:6333:6333 或 -p 10.0.0.5:6333:6333。这是最常见的"我明明配了防火墙却被扫到"的原因。
4.2 认证层
# 生成两个密钥
openssl rand -hex 32 # 读写 key
openssl rand -hex 32 # 只读 key(给监控/报表)
| 角色 | key | 用途 |
|---|---|---|
| 应用读写 | api_key | 业务服务 |
| 监控抓取 / 只读报表 | read_only_api_key | Prometheus、BI |
轮换流程(不停机):
# 1. 服务端同时配置新旧两个 key 的能力有限,标准做法是「改配置 + 重启」
NEW=$(openssl rand -hex 32)
sudo sed -i "s#^(\s*QDRANT__SERVICE__API_KEY=).*#\1$NEW#" /etc/qdrant/qdrant.env
sudo systemctl restart qdrant # 重启约数秒
# 2. 验证:旧 key 401,新 key 200
curl -s -o /dev/null -w 'old:%{http_code}\n' -H "api-key: $OLD" http://127.0.0.1:6333/collections
curl -s -o /dev/null -w 'new:%{http_code}\n' -H "api-key: $NEW" http://127.0.0.1:6333/collections
# 3. 逐个改应用侧配置(.env / 密钥管理服务)
更平滑的方案:在反向代理层注入密钥(客户端不感知),轮换时只改代理配置。
4.3 传输层(TLS)
开了 api_key 不开 TLS = 明文传输密钥,等于没设。三个选择:
- 最省事:Qdrant 只绑 127.0.0.1,走 SSH 隧道或同机应用直连,不对外暴露端口;
- 标准做法:前面挂 Nginx/Caddy 处理 TLS,Qdrant 只听内网;
- Qdrant 原生
enable_tls: true+ 证书(集群内部通信也记得开cluster.p2p.enable_tls)。
# Nginx 反代要点(8000 → 6333)
location / {
proxy_pass http://127.0.0.1:6333;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 128m; # 大 batch upsert 会被 413 拦掉
proxy_read_timeout 300s; # 快照/大规模导入耗时长
proxy_buffering off; # 虽 Qdrant 主要是短请求,但保持关闭更稳
}
4.4 进程权限与审计
- 容器非 root:用
qdrant/qdrant:v1.19.1-unprivileged,或--user=1000:2000(数据目录需chown 1000:2000); - 审计日志(1.17+):写本地文件
<storage_path>/audit/,不写 stdout。容器部署要挂卷 + sidecar 收集,否则重启即丢; - /metrics 也要鉴权:开了 api_key 后抓取要带 key,Prometheus 用
authorization.credentials(填只读 key)。
4.5 安全自查清单(上线前逐条打勾)
[ ] api_key 已设,且不是 "changeme"/"123456" 这类弱值
[ ] curl 无 key 访问 /collections 返回 401
[ ] 6333/6334 未对公网开放(从外部机器 telnet 测试一下)
[ ] Docker 端口绑定写了 127.0.0.1 或内网 IP,不是 0.0.0.0
[ ] read_only_api_key 给监控用,应用不共用同一个 key
[ ] 容器以非 root 运行
[ ] 快照目录与应用数据目录不在同一块盘
[ ] 密钥不在 git 里(git log -S "api_key" 查一遍)
第 5 章 容量规划与调优
5.1 内存账簿(先算清账,再决定买什么)
Qdrant 的内存由四块组成:
| 组成 | 公式 | 说明 |
|---|---|---|
| 原始向量 | n × d × 4 字节 | n=向量数,d=维度。唯一真正线性的部分 |
| HNSW 图结构 | n × (m×2×4 + 8) ≈ n × 130 字节(m=16) | 不随维度变化,所以极致降维后它会占更大比例 |
| payload | 取决于字段数与索引 | on_disk_payload: true 可把原文挪到磁盘 |
| 段与运行时开销 | ~10~20% 余量 | 段元数据、正在合并的副本、请求缓冲 |
算一遍你的场景(500 万 × 1024 维,bge-m3 同源 dense+sparse):
| 配置 | 向量内存 | HNSW 图 | 合计估算 |
|---|---|---|---|
| float32 全内存 | 20.5 GB | 0.65 GB | ~21 GB(16GB 机器直接爆) |
| int8 量化 + HNSW 在内存 | 5.0 GB | 0.65 GB | ~6 GB(够用) |
| binary 量化 + rescoring | 0.65 GB | 0.65 GB | ~1.5 GB + 重排开销(需实测召回损失) |
结论:16GB 内存的机器,500 万块 1024 维必须开 int8 量化。这不是优化选项,是前置条件。
5.2 内存不够时的处理顺序(别跳步)
① 开 int8 量化 ← 省 75%,不动数据,不改架构
② on_disk_payload: true ← 省 payload 原文
③ 减少 payload 索引字段 ← 每个 keyword 索引都常驻内存
④ 1.19 三级内存策略:把冷集合设 cold 层
⑤ 减到单集合 + 提高分片/上 Milvus ← 真到亿级再考虑
5.3 HNSW 参数怎么调
| 参数 | 作用 | 调大的效果 | 建议 |
|---|---|---|---|
m | 每点连接数 | 召回↑、内存↑、建索引慢 | 16 通用最优;召回不够先调它 |
ef_construct | 建索引时搜索宽度 | 索引质量↑、构建慢 | 100;导入期可降 64 提速,导完重建 |
ef(查询时传) | 检索搜索宽度 | 召回↑、延迟↑ | 默认 128。这是你要调的第一参数,但注意 ef 提升有边际递减,且对已建成索引不需要重建 |
调优顺序:先确认过滤字段都有索引 → 再调查询 ef → 再考虑改 m 重建。
full_scan_threshold_kb:段小于此阈值直接暴力扫(更准)。小库上它意味着"根本没走 HNSW",这也是为什么你 35 块的小库延迟只有十几毫秒。
5.4 写入性能
# 推荐批量:100~1000 点/批;再大收益递减而内存峰值上升
client.upsert(collection_name=..., points=batch, wait=False) # 导入期用 False,吞吐翻倍
client.upsert(collection_name=..., points=last_batch, wait=True) # 最后一批用 True,确保落盘
| 手段 | 效果 | 代价 |
|---|---|---|
导入期 indexing_threshold_kb 调大 | 避免边导边重建索引,快数倍 | 导入期间检索慢(扫未索引段);导完必须调回 |
wait=False | 不等落盘,吞吐翻倍 | 返回成功≠已可检索 |
| 用 gRPC 而非 REST | 吞吐高、序列化开销小 | 需要 6334 端口 |
| 分多个并行客户端 | 提升并行度 | 过高会互相争抢,观察 CPU |
导入完成后必做:
# 把索引阈值调回正常值并触发优化器
curl -X PATCH http://127.0.0.1:6333/collections/{name} \
-H "api-key: $KEY" -H 'Content-Type: application/json' \
-d '{"optimizers_config":{"indexing_threshold":20000}}'
# 看优化器状态,等到所有段都 index 完成
curl -s http://127.0.0.1:6333/collections/{name} -H "api-key: $KEY" \
| jq '.result.optimizer_status, .result.segments_count, .result.indexed_vectors_count'
indexed_vectors_count 远小于 points_count = 索引还没建完,此时检索走暴力扫,慢且费 CPU。
第 6 章 备份与恢复
6.1 两种备份,各有职责(都要有)
| 类型 | 备份什么 | 恢复成本 | 适用 |
|---|---|---|---|
| 快照(物理) | collection / 全实例的二进制状态 | 分钟级 | ★ 灾难恢复、升级前回滚 |
| 源语料(逻辑) | chunks.jsonl + 构建脚本 | 小时级(要重新嵌入) | 换模型、改分块策略、跨版本重建 |
关键认知:你的分块语料是可重建的(有 build_chunks.py),所以逻辑层天然有备份。但快照仍必须做——重新嵌入 500 万块的成本和耗时不能接受作为常规恢复手段。
6.2 快照命令
KEY=your_api_key
# 单个 collection 快照(最常用)
curl -X POST http://127.0.0.1:6333/collections/kb_chunks_v1/snapshots -H "api-key: $KEY"
# → {"result":{"name":"kb_chunks_v1-2026-09-23-...snapshot","size":...}}
# 全实例快照(所有 collection 一起)
curl -X POST http://127.0.0.1:6333/snapshots -H "api-key: $KEY"
# 列出 / 删除
curl -s http://127.0.0.1:6333/collections/kb_chunks_v1/snapshots -H "api-key: $KEY" | jq -r '.result[].name'
curl -X DELETE http://127.0.0.1:6333/collections/kb_chunks_v1/snapshots/<snapshot_name> -H "api-key: $KEY"
# 恢复:把快照导回(可指定为新的 collection 名,便于先验证再切换)
curl -X PUT http://127.0.0.1:6333/collections/kb_chunks_v1/snapshots/recover \
-H "api-key: $KEY" -H 'Content-Type: application/json' \
-d '{"location":"file:///var/lib/qdrant/snapshots/kb_chunks_v1-xxx.snapshot"}'
# 从远端恢复(先上传到 Qdrant 能访问的 URL,或放本地路径恢复)
6.3 定时备份(用 ops/qdrant_ops.sh)
# crontab -e
# 每天 03:00 快照 + 保留 7 天
0 3 * * * /opt/qdrant/ops/qdrant_ops.sh backup-all >> /var/log/qdrant/backup.log 2>&1
# 每天 03:10 把快照同步到异地(另一个盘/对象存储)
10 3 * * * rsync -a --delete /var/lib/qdrant/snapshots/ /mnt/backup/qdrant-snapshots/
三条硬要求:
- 异地:快照不能只躺在数据目录旁边,机器整体故障时一起没;
- 保留策略:至少 7 天滚动,升级前额外留一份带标签的;
- 必须演练:没恢复过的备份等于没有备份。每季度做一次到独立环境的恢复测试,记录实际耗时(这才是真实 RTO)。
6.4 RPO / RTO 参考
| 场景 | RPO(丢多少数据) | RTO(多久恢复) | 手段 |
|---|---|---|---|
| 进程崩溃 | 0(WAL 保证) | 分钟级 | systemd 自动重启 |
| 误删 collection | 到上次备份(≤24h) | 10~30 分钟 | 快照恢复 |
| 磁盘损坏 | ≤24h | 30~60 分钟 | 快照 + 重建语料 |
| 整机丢失 | ≤24h | 1~2 小时 | 异地快照 + 语料重建 |
想压低 RPO 就加密快照频率(每小时一次对几百 MB 的库完全可行)。
第 7 章 监控与告警
7.1 三个端点的分工(别搞混)
| 端点 | 内容 | 注意 |
|---|---|---|
/metrics | Prometheus 格式指标 | 自建部署用这个;开了 api_key 要带 key |
/telemetry | 运行态快照(集合数/向量数/分片等) | 排障时看整体情况很方便 |
/sys_metrics | 集群基础设施指标 | Qdrant Cloud 专属,自建抓不到(常见误解) |
/healthz /livez /readyz | 健康检查三兄弟 | readyz 才能代表"可服务" |
7.2 必须告警的六件事
| 指标 | 阈值 | 为什么 |
|---|---|---|
| 磁盘使用率 | > 80% | 到 90% 会拒绝写入 |
| 优化器错误 | 出现即告警 | 索引构建失败 → 检索静默变慢 |
| 节点不可达 | 2 分钟 | 基础可用性 |
| 检索 P95 延迟 | > 500ms | 通常是漏建索引或段未合并 |
| 复制因子不足 | 低于目标 | 挂一个节点就可能丢数据 |
| 待处理集群操作堆积 | > 50 持续 10 分钟 | 分片迁移卡住 |
规则文件见 deploy/prometheus-alerts.yml。
反向告诫:不要对 page cache 内存占用告警。OS 把空闲内存用作缓存是正常且有益的行为(Qdrant 的 mmap 性能就靠它),对它告警会带来大量无效通知。要盯的是进程 RSS 是否逼近上限。
7.3 没有 Prometheus 时的轻量方案
你现有的栈是 Bark 推送,够用。用 ops/qdrant_ops.sh 的 watch 子命令,每 5 分钟跑一次,异常才推送:
*/5 * * * * /opt/qdrant/ops/qdrant_ops.sh watch >> /var/log/qdrant/watch.log 2>&1
它检查:服务可达性、磁盘水位、优化器状态、索引完成度、快照新鲜度(超过 36 小时未备份则告警)。快照新鲜度这一条很多团队不做,结果出事时才发现备份脚本早就静默失败了。
7.4 日志
log_level: INFO # 生产用 INFO;DEBUG 会打印每次请求,只临时开
logger:
format: json # 接 ELK/Loki 时开,便于结构化检索
排障三板斧:
journalctl -u qdrant --since "10 min ago" # systemd
docker logs --since 10m qdrant 2>&1 | grep -iE "error|warn" # 容器
journalctl -u qdrant | grep -i optimizer # 优化器专门看
第 8 章 日常运维与升级
8.1 巡检节奏
| 频率 | 检查项 | 命令 |
|---|---|---|
| 每日 | 服务健康、磁盘水位、优化器无报错、备份成功 | ops.sh status |
| 每周 | 集合数/向量数增长趋势、慢查询、快照可恢复性抽查 | ops.sh collections |
| 每月 | 内存与容量趋势对比(是否需要加量化/扩盘)、恢复演练 | 手工 |
| 每季 | 完整恢复演练 + 密钥轮换 | ops.sh restore-test |
8.2 集合级运维动作
| 动作 | 怎么做 | 注意事项 |
|---|---|---|
| 改 alias 指向(版本切换) | POST /collections/aliases | 秒级生效,业务代码零改动(见开发指南第 9 章) |
| 加 payload 索引 | PUT /collections/{name}/index | 大集合上会触发重建,在低峰期做 |
| 改索引阈值触发优化 | PATCH /collections/{name} | 同上 |
| 删除点 | POST /collections/{name}/points/delete | 内存要到优化器 vacuum 后才释放,不是立刻 |
| 增量更新 | 按 content_hash 比对后 upsert 变更点 | 点 ID 幂等,重复导入不会堆重复 |
关于删除的内存回收:Qdrant 靠优化器在 deleted_threshold(默认 20% 垃圾)达到后 vacuum 回收。所以删完数据内存不会立刻降,这是设计如此,不是 bug。急着回收就临时把 deleted_threshold 调小并等待优化器跑完。
8.3 升级流程(照做,别即兴)
# ---------- 升级前 ----------
qdrant_ops.sh backup-all # ① 全量快照
cp /etc/qdrant/config.yaml /etc/qdrant/config.yaml.bak.$(date +%F) # ② 备份配置
curl -s http://127.0.0.1:6333/ | jq -r .version # ③ 记录当前版本号
# ④ 读 release notes,确认升级路径合法(能不能跳版本!见第 0 章)
# ⑤ 确认应用侧没用已删除的旧端点(1.19 起 /search /recommend /discover 已删)
# ---------- 升级(Docker)----------
sed -i 's/qdrant:v1.19.0/qdrant:v1.19.1/' docker-compose.yml
docker compose pull && docker compose up -d
docker compose logs -f qdrant # 观察启动日志有无迁移报错
# ---------- 升级(二进制)----------
systemctl stop qdrant
cp /opt/qdrant/qdrant /opt/qdrant/qdrant.bak.$(date +%F)
curl -L <对应版本二进制> | tar -xz -C /opt/qdrant/
systemctl start qdrant
# ---------- 升级后验证 ----------
curl -s http://127.0.0.1:6333/ | jq -r .version # 版本对了吗
curl -s http://127.0.0.1:6333/readyz # 就绪了吗
# 跑你的 20 题评测集,对比升级前后 Recall —— 这是唯一能证明"没坏"的方法
python3 qdrant_import.py --eval
# ---------- 回滚预案(必须提前想好)----------
# 二进制:换回 .bak 二进制 + 配置;若存储格式已迁移,用升级前快照恢复
# Docker:改回旧 tag;同上
一条经验:Qdrant 的 小版本升级(1.19.0 → 1.19.1) 通常是安全的;跨小版本(1.18 → 1.19)必读 release notes;跨大版本迁移(1.15 → 1.17+)必须逐版本升。
8.4 扩缩容
| 需求 | 手段 | 备注 |
|---|---|---|
| 单机内存不够 | 先量化 → 再考虑加内存 | 量化不用迁数据 |
| 单机磁盘不够 | 扩盘 or 清旧快照 | 快照是最容易忘的磁盘黑洞 |
| 需要高可用 | 上集群:cluster.enabled: true,分片 + 副本 | 副本数 ≥2 才有意义 |
| 读并发扛不住 | 加副本 + 读亲和(1.19 X-Qdrant-Route-Affinity) | 副本只提读,不提写 |
| 写入吞吐不够 | 分片数 + 批量写 + gRPC | 分片数建集合时定好,之后改要 resharding |
第 9 章 故障排查手册(症状 → 原因 → 处理)
9.1 启动类
| 症状 | 最可能原因 | 排查 | 处理 |
|---|---|---|---|
| 启动即退出,日志提示存储版本不兼容 | 跨版本升级(尤其 1.15→1.17+) | 日志里的 storage version / RocksDB 关键字 | 逐个小版本升;已升坏用升级前快照恢复 |
Address already in use | 端口被占(常见:上次进程没死干净) | lsof -i:6333 / ss -lntp | grep 633 | 杀掉旧进程;容器用 docker ps -a 找残留 |
Permission denied 打开 storage | 数据目录权限/属主不对 | ls -ln /var/lib/qdrant | chown -R qdrant:qdrant;用 unprivileged 镜像要 chown 1000:2000 |
| 启动很久才 ready | 正在恢复 WAL / 重建索引 | 日志 recovery 关键字;/readyz | 正常,等它跑完;大库可能十几分钟 |
too many open files | nofile 限制 | ulimit -n、cat /proc/$(pidof qdrant)/limits | 提到 65536(systemd 用 LimitNOFILE) |
9.2 检索类
| 症状 | 最可能原因 | 排查 | 处理 |
|---|---|---|---|
| 检索结果为空,但不报错 | 向量维度与集合 size 不一致(最隐蔽的坑) | GET /collections/{name} 看 config.params.vectors.size | 维度必须严格等于模型输出维度。写小了不报错但结果全乱 |
| 结果为空 | filter 字段名写错 / 值类型不匹配(字符串 vs 数字) | 去掉 filter 再查,能出结果就是 filter 问题 | 用 /points/scroll 抽样确认 payload 里的真实字段名与类型 |
| 结果为空 | 查询的 collection 名被 alias 指向了旧集合 | GET /aliases | 确认 alias 指向 |
| 检索慢(几百 ms 以上) | 过滤字段漏建 payload 索引 → 全量扫描 | GET /collections/{name} 看 payload_schema | 给 filter 用到的字段建索引(开发指南第 3 章) |
| 检索慢 | indexed_vectors_count << points_count | 同上 | 索引没建完,等优化器或调回阈值触发 |
| 检索慢 | 段太多碎片化 | segments_count 偏大 | 调 default_segment_number,等优化器合并 |
| 召回不准 | 无稀疏通道,精确词(如"第三十九条")匹配不上 | 对比纯向量 vs 混合检索结果 | 开 dense+sparse 混合 + RRF 融合 |
| 召回不准 | 代码/表格块缺自然语言摘要 | 看 vector_text 是不是纯代码 | 给富内容块补一句语义摘要(实测 R@5 90%→100%) |
ef 调很大仍不准 | 数据本身没入库或 payload 错 | count 核对总数 | 先排除数据问题再调参数 |
9.3 内存 / 性能类
| 症状 | 最可能原因 | 处理 |
|---|---|---|
进程被 OOM Killer 杀掉(dmesg 有 oom) | payload 全内存 + 无量化 + 段过多 | 开 int8 量化、on_disk_payload: true、给容器/服务设 MemoryMax |
| 内存持续上涨不回落 | 优化器未完成合并;或删除点未 vacuum | 等优化器;急则临时调小 deleted_threshold |
| CPU 100% 持续 | 优化器在重建索引,或检索在扫未索引段 | 看 optimizer_status;导入后记得把 indexing_threshold 调回 |
| 写入超时 / 卡住 | WAL 写满、优化器占用、磁盘 IO 饱和 | 调大 wal_capacity_mb;错峰写入;检查磁盘 iostat |
| 磁盘只涨不降 | 快照堆积 + 旧段未回收 | 清理旧快照(ops.sh prune-snapshots);确认优化器在跑 |
| 快照创建失败 | 磁盘空间不足 / snapshots_path 权限 | df -h;chown;快照目录独立盘 |
9.4 认证 / 权限类
| 症状 | 原因 | 处理 |
|---|---|---|
| 401 Unauthorized | 未带 key / key 错误 | 请求头加 api-key: xxx |
| 403 Forbidden | 用只读 key 执行写操作 | 写操作用主 key;监控只用只读 key |
| 本机 curl 通、应用不通 | 应用连的是旧 IP/端口,或被防火墙拦 | 从应用机器 telnet 10.0.0.5 6334 测通 |
| Prometheus 抓不到指标 | /metrics 也要鉴权 | scrape 配置加 authorization.credentials |
| 客户端连不上 gRPC | 只放行了 6333,没放行 6334 | 防火墙同时放行两个端口 |
9.5 集群类
| 症状 | 原因 | 处理 |
|---|---|---|
节点 not ready | 该节点落后于共识,或磁盘满 | 看该节点日志;GET /cluster 看状态 |
| 分片迁移卡住 | 网络不通 / 目标磁盘满 | GET /cluster 看 pending_operations;修网络/磁盘 |
| 共识失同步 | Raft 状态异常 | POST /cluster/recover(先确认没有正在进行的迁移) |
| 副本数低于目标 | 节点掉线 | 恢复节点;期间可用性降低但不一定不可用 |
第 10 章 命令速查表
# 环境
export Q="http://127.0.0.1:6333"
export KEY="你的api_key"
AUTH=(-H "api-key: $KEY")
# ==================== 健康与状态 ====================
curl -s $Q/healthz # 活否
curl -s $Q/readyz # 就绪否
curl -s $Q/ | jq # 版本 + 基本信息
curl -s $Q/telemetry "${AUTH[@]}" | jq # 运行态遥测
curl -s $Q/metrics "${AUTH[@]}" | grep '^# HELP' | head -40 # 指标清单
# ==================== 集合管理 ====================
curl -s $Q/collections "${AUTH[@]}" | jq -r '.result.collections[].name'
curl -s $Q/collections/{name} "${AUTH[@]}" \
| jq '{points:.result.points_count, indexed:.result.indexed_vectors_count, segs:.result.segments_count, opt:.result.optimizer_status, size:.result.config.params.vectors.size}'
curl -s $Q/collections/{name}/exists "${AUTH[@]}"
curl -X DELETE $Q/collections/{name} "${AUTH[@]}"
# ==================== 计数与抽样 ====================
curl -s -X POST $Q/collections/{name}/points/count "${AUTH[@]}" \
-H 'Content-Type: application/json' -d '{"exact":true}' | jq .result.count
curl -s -X POST $Q/collections/{name}/points/scroll "${AUTH[@]}" \
-H 'Content-Type: application/json' \
-d '{"limit":3,"with_payload":true,"with_vector":false}' | jq
# ==================== 检索 ====================
curl -s -X POST $Q/collections/{name}/points/query "${AUTH[@]}" \
-H 'Content-Type: application/json' \
-d '{"query":[0.1,0.2,0.3],"limit":5,"with_payload":true}' | jq
# 带过滤 + 指定 ef
# -d '{"query":[..],"limit":5,"params":{"hnsw_ef":128},
# "filter":{"must":[{"key":"chunk_type","match":{"value":"code"}}]}}'
# ==================== 索引与优化 ====================
curl -X PUT $Q/collections/{name}/index "${AUTH[@]}" \
-H 'Content-Type: application/json' \
-d '{"field_name":"chunk_type","field_schema":"keyword"}'
curl -s $Q/collections/{name}/index "${AUTH[@]}" | jq # 列出所有索引
curl -X PATCH $Q/collections/{name} "${AUTH[@]}" \
-H 'Content-Type: application/json' \
-d '{"optimizers_config":{"indexing_threshold":20000}}'
# ==================== 快照与备份 ====================
curl -X POST $Q/collections/{name}/snapshots "${AUTH[@]}" | jq .result.name
curl -X POST $Q/snapshots "${AUTH[@]}" | jq .result.name # 全实例
curl -s $Q/collections/{name}/snapshots "${AUTH[@]}" | jq -r '.result[].name'
curl -X DELETE $Q/collections/{name}/snapshots/{snap} "${AUTH[@]}"
curl -X PUT $Q/collections/{name}/snapshots/recover "${AUTH[@]}" \
-H 'Content-Type: application/json' \
-d '{"location":"file:///var/lib/qdrant/snapshots/xxx.snapshot"}'
# ==================== Alias(版本切换)====================
curl -s $Q/aliases "${AUTH[@]}" | jq
curl -X POST $Q/collections/aliases "${AUTH[@]}" \
-H 'Content-Type: application/json' \
-d '{"actions":[{"delete_alias":{"alias_name":"kb_current"}},
{"create_alias":{"collection_name":"kb_chunks_v2","alias_name":"kb_current"}}]}'
# ==================== 集群 ====================
curl -s $Q/cluster "${AUTH[@]}" | jq
curl -X POST $Q/cluster/recover "${AUTH[@]}"
附:上线前检查清单
【部署】
[ ] 版本写死,未使用 latest
[ ] 数据目录与快照目录在不同物理盘
[ ] nofile 已放开到 65536
[ ] MEM 上限已设(容器 mem_limit / systemd MemoryMax)
[ ] 日志轮转已配(避免磁盘被日志写满)
【安全】
[ ] api_key + read_only_api_key 已设,无 key 访问返回 401
[ ] 端口未对公网开放,Docker 绑定写了 127.0.0.1 或内网 IP
[ ] 容器非 root 运行
[ ] 密钥不在 git 中
【数据】
[ ] 集合已按语义分块入库,payload 索引已建(filter 用到的字段全覆盖)
[ ] 导入后 indexing_threshold 已调回,indexed_vectors_count ≈ points_count
[ ] 量化已按内存情况开启
[ ] 评测集已跑,Recall@5 达标(≥90%)
【运维】
[ ] 定时快照 + 异地同步已配
[ ] 至少完成一次恢复演练(真实计时)
[ ] 监控告警已配(磁盘、优化器错误、服务不可达、快照新鲜度)
[ ] 升级流程与回滚预案已写成文档
相关文件
| 文件 | 说明 |
|---|---|
Qdrant开发使用指南.md | 建集合、混合检索、过滤、多租户、快照 API 的代码写法 |
deploy/production.yaml | 生产配置模板(逐项中文注释) |
deploy/docker-compose.yml | Docker 部署(含密钥注入与内存上限) |
deploy/qdrant.service | systemd 单元 + 安装步骤 |
deploy/prometheus-alerts.yml | 告警规则 |
ops/qdrant_ops.sh | 巡检 / 备份 / 恢复 / 监控脚本 |
dev/qdrant_helper.py | Python 开发封装(推荐直接抄进项目) |