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 服务器):

```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

```

一个常见的错误认知: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_key32 字节随机串任何人可读写删你全部向量(最严重的疏忽)
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_payloadtruepayload 全进内存,OOM 主因(尤其你这种带大量元数据的语料)
storage.wal.wal_capacity_mb256(写入高则 1024)WAL 写满会阻塞写入,表现为 upsert 超时
telemetry.enabledtrue拿不到 /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_keyPrometheus、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 = 明文传输密钥,等于没设。三个选择:

  1. 最省事:Qdrant 只绑 127.0.0.1,走 SSH 隧道或同机应用直连,不对外暴露端口;
  2. 标准做法:前面挂 Nginx/Caddy 处理 TLS,Qdrant 只听内网;
  3. 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 进程权限与审计

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 GB0.65 GB~21 GB(16GB 机器直接爆)
int8 量化 + HNSW 在内存5.0 GB0.65 GB~6 GB(够用)
binary 量化 + rescoring0.65 GB0.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/

三条硬要求:

  1. 异地:快照不能只躺在数据目录旁边,机器整体故障时一起没;
  2. 保留策略:至少 7 天滚动,升级前额外留一份带标签的;
  3. 必须演练:没恢复过的备份等于没有备份。每季度做一次到独立环境的恢复测试,记录实际耗时(这才是真实 RTO)。

6.4 RPO / RTO 参考

场景RPO(丢多少数据)RTO(多久恢复)手段
进程崩溃0(WAL 保证)分钟级systemd 自动重启
误删 collection到上次备份(≤24h)10~30 分钟快照恢复
磁盘损坏≤24h30~60 分钟快照 + 重建语料
整机丢失≤24h1~2 小时异地快照 + 语料重建

想压低 RPO 就加密快照频率(每小时一次对几百 MB 的库完全可行)。

第 7 章 监控与告警

7.1 三个端点的分工(别搞混)

端点内容注意
/metricsPrometheus 格式指标自建部署用这个;开了 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/qdrantchown -R qdrant:qdrant;用 unprivileged 镜像要 chown 1000:2000
启动很久才 ready正在恢复 WAL / 重建索引日志 recovery 关键字;/readyz正常,等它跑完;大库可能十几分钟
too many open filesnofile 限制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.ymlDocker 部署(含密钥注入与内存上限)
deploy/qdrant.servicesystemd 单元 + 安装步骤
deploy/prometheus-alerts.yml告警规则
ops/qdrant_ops.sh巡检 / 备份 / 恢复 / 监控脚本
dev/qdrant_helper.pyPython 开发封装(推荐直接抄进项目)