前面的监控系列文章:

这篇和 MinIO 一样是"内置 metrics 路线",但 Qdrant 没有像 MinIO 那样提供 MINIO_PROMETHEUS_AUTH_TYPE=public 的一键开关,API key 认证需要在 Prometheus 侧用 bearer_token_file 解决。

Qdrant 内置 metrics 链接到标题

Qdrant 从 v1.10 起在 API 端口 :6333 暴露 Prometheus 兼容的 metrics 端点:

GET http://<qdrant-host>:6333/metrics

默认需要 API key 认证。直接访问会返回 401 Unauthorized

$ curl -i http://localhost:6333/metrics
HTTP/1.1 401 Unauthorized
content-type: text/plain
{"status":{"error":"Unauthorized"}}

带 API key 后正常返回 200:

$ curl -H 'api-key: <YOUR_QDRANT_API_KEY>' http://localhost:6333/metrics
# HELP app_info information about qdrant server
# TYPE app_info gauge
app_info{name="qdrant",version="1.16.3"} 1
...

不需要额外部署 qdrant-exporter——社区 exporter 只是对内置端点的薄包装。

配置步骤 链接到标题

1. 准备 API key 文件 链接到标题

API key 不能明文写进 prometheus.yml(会进 git 仓库)。仓库内单独存一个文件,并加入 .gitignore

# 仓库目录
echo '<YOUR_QDRANT_API_KEY>' > monitor/prometheus/qdrant_api_key
chmod 600 monitor/prometheus/qdrant_api_key

.gitignore 追加:

monitor/prometheus/qdrant_api_key

2. Prometheus 添加 scrape job 链接到标题

prometheus.ymlscrape_configs 末尾新增:

- job_name: qdrant
  metrics_path: /metrics
  scheme: http
  bearer_token_file: /etc/prometheus/qdrant_api_key
  static_configs:
    - targets:
        - <qdrant-host>:6333
      labels:
        hostname: <qdrant-host>

部署到监控服务器时,API key 文件放在 prometheus 容器挂载的卷目录下,路径为 /etc/prometheus/qdrant_api_key(对应仓库的 monitor/prometheus/qdrant_api_key)。

3. 部署与重启 链接到标题

# 同步到监控服务器
scp monitor/prometheus/prometheus.yml <monitor-host>:/tmp/
scp monitor/prometheus/alerts.yml <monitor-host>:/tmp/
scp monitor/prometheus/qdrant_api_key <monitor-host>:/tmp/

ssh <monitor-host> "
  sudo cp /tmp/prometheus.yml /opt/monitor/prometheus/ &&
  sudo cp /tmp/alerts.yml /opt/monitor/prometheus/ &&
  sudo cp /tmp/qdrant_api_key /opt/monitor/prometheus/ &&
  sudo chown 65534:65534 /opt/monitor/prometheus/qdrant_api_key &&
  sudo chmod 600 /opt/monitor/prometheus/qdrant_api_key &&
  cd /opt/monitor && sudo docker compose restart prometheus
"

chown 65534:65534 不可省略,原因见第六章"踩坑 3"。

暴露的指标 链接到标题

始终暴露的指标(v1.16+) 链接到标题

无 collection / 无流量时也存在的指标:

类别 指标 用途
应用元信息 app_info{version} 标识版本
恢复模式 app_status_recovery_mode 0/1 标志
集群 cluster_enabled 是否启用集群
集合容量 collections_total 集合数量
集合容量 collections_vector_total 向量总数
副本 collection_active_replicas_min/max 活跃副本上下限
副本 collection_dead_replicas 失活副本
内存 memory_active_bytes 活跃内存
内存 memory_resident_bytes 常驻内存
内存 memory_allocated_bytes 已分配内存
内存 memory_metadata_bytes 元数据占用
内存 memory_retained_bytes 保留内存
进程 process_*(标准 Prometheus 指标) 进程级资源

按需暴露的指标 链接到标题

有 collection 或有流量时才出现:

指标 含义
rest_responses_total{status} REST 响应计数
rest_responses_duration_seconds_bucket REST 响应延迟分桶
grpc_responses_total{status} gRPC 响应计数
segments_total 段数量
pending_optimization_operations 待优化操作数

按需暴露的指标在无 collection 时不存在,Prometheus 表达式中涉及除法的部分需要 or vector(0)/vector(1) 兼容空集,否则空集除法会得到 NaN,不触发阈值告警(实际安全,但日志会有 warning)。

告警规则 链接到标题

alerts.yml 新增 qdrant_alerts 分组,共 11 条:

类别 告警名 触发条件 持续 严重度
可用性 QdrantDown up{job="qdrant"} == 0 1m critical
可用性 QdrantRecoveryMode app_status_recovery_mode == 1 1m warning
性能 QdrantHighErrorRate REST 错误率 > 5% 5m warning
性能 QdrantGRPCErrorRate gRPC 错误率 > 5% 5m warning
性能 QdrantHighAPILatency REST P99 > 1s 5m warning
容量 QdrantCollectionsCount collections_total > 100 10m warning
容量 QdrantVectorsTotal 总向量数 > 5e7 10m warning
容量 QdrantSegmentsHigh segments_total > 5000 10m warning
性能 QdrantOptimizerStuck 待优化操作 > 1000 15m warning
资源 QdrantHighResidentMemory 常驻内存 > 4 GB 10m warning
资源 QdrantHighActiveMemory 活跃内存 > 6 GB 10m warning

完整 YAML:

- name: qdrant_alerts
  interval: 30s
  rules:
    - alert: QdrantDown
      expr: up{job="qdrant"} == 0
      for: 1m
      labels:
        severity: critical
      annotations:
        summary: "Qdrant 已宕机"
        description: "Qdrant 服务已停止响应超过 1 分钟"

    - alert: QdrantRecoveryMode
      expr: app_status_recovery_mode{job="qdrant"} == 1
      for: 1m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant 处于恢复模式"
        description: "当前处于 recovery mode,写入可能被拒绝"

    - alert: QdrantHighErrorRate
      expr: |
        (
          sum by(hostname) (rate(rest_responses_total{job="qdrant",status="error"}[5m]))
          or vector(0)
        )
        /
        (
          sum by(hostname) (rate(rest_responses_total{job="qdrant"}[5m]))
          or vector(1)
        )
        > 0.05
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant REST API 错误率过高"
        description: "5 分钟内 REST API 错误率 {{ $value | printf \"%.2f\" }}%"

    - alert: QdrantGRPCErrorRate
      expr: |
        (
          sum by(hostname) (rate(grpc_responses_total{job="qdrant",status!~"ok|Ok|OK"}[5m]))
          or vector(0)
        )
        /
        (
          sum by(hostname) (rate(grpc_responses_total{job="qdrant"}[5m]))
          or vector(1)
        )
        > 0.05
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant gRPC 错误率过高"
        description: "5 分钟内 gRPC 错误率 {{ $value | printf \"%.2f\" }}%"

    - alert: QdrantHighAPILatency
      expr: histogram_quantile(0.99, sum by(le, hostname) (rate(rest_responses_duration_seconds_bucket{job="qdrant"}[5m]))) > 1
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant REST API P99 延迟过高"
        description: "P99 延迟: {{ $value | printf \"%.2f\" }}s"

    - alert: QdrantCollectionsCount
      expr: collections_total{job="qdrant"} > 100
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant 集合数超阈值"
        description: "当前集合数: {{ $value }}(阈值 100)"

    - alert: QdrantVectorsTotal
      expr: sum by(hostname) (collections_vector_total{job="qdrant"}) > 50000000
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant 向量总数超阈值"
        description: "总向量数: {{ $value | printf \"%.0f\" }}(阈值 5e7)"

    - alert: QdrantSegmentsHigh
      expr: segments_total{job="qdrant"} > 5000
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant Segments 数量过高"
        description: "段数: {{ $value }}(阈值 5000)"

    - alert: QdrantOptimizerStuck
      expr: max by(hostname) (pending_optimization_operations{job="qdrant"}) > 1000
      for: 15m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant 优化器疑似卡住"
        description: "挂起的优化操作: {{ $value }}"

    - alert: QdrantHighResidentMemory
      expr: memory_resident_bytes{job="qdrant"} > 4 * 1024 * 1024 * 1024
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant 常驻内存超过 4GB"
        description: "memory_resident_bytes: {{ $value | humanize1024 }}B"

    - alert: QdrantHighActiveMemory
      expr: memory_active_bytes{job="qdrant"} > 6 * 1024 * 1024 * 1024
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Qdrant 活跃内存超过 6GB"
        description: "memory_active_bytes: {{ $value | humanize1024 }}B"

踩坑记录 链接到标题

坑 1:/metrics 端点 401 链接到标题

现象:Prometheus target 状态 downlastError: 401 Unauthorized

原因:Qdrant 默认对 /metrics 端点也启用 API key 认证,没有 api-key header 会被拒。

解决:在 prometheus.ymlbearer_token_file 传 API key(不要写在 yml 里):

- job_name: qdrant
  bearer_token_file: /etc/prometheus/qdrant_api_key
  ...

调试时可以直接 curl 验证:

# 复现
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:6333/metrics
401

# 验证修复
$ curl -s -o /dev/null -w '%{http_code}\n' -H 'api-key: <KEY>' http://localhost:6333/metrics
200

坑 2:v1.16 指标去掉了 qdrant_ 前缀 链接到标题

现象:按老版本教程写的 qdrant_collections_total 表达式在 Prometheus 里查不到数据。

原因:Qdrant 1.16 起对指标命名空间做了调整——把 qdrant_ 前缀去掉了:

老版本(≤ 1.10) 新版本(≥ 1.16)
qdrant_collections_total collections_total
qdrant_rest_responses_total rest_responses_total
qdrant_memory_active_bytes memory_active_bytes
qdrant_segments_total segments_total

解决:先用 curl 实际拉一次 /metrics,把所有 qdrant_* 替换成无前缀的版本:

curl -H 'api-key: <KEY>' http://localhost:6333/metrics \
  | grep -E '^[a-z]' | awk '{print $1}' | sed 's/{.*//' | sort -u

执行后会得到 20 个无前缀指标名,按此修改告警表达式。

坑 3:bearer_token_file 权限被拒 链接到标题

现象:scrape target 仍然 down,但 lastError 变成了:

unable to read authorization credentials:
unable to read file /etc/prometheus/qdrant_api_key:
open /etc/prometheus/qdrant_api_key: permission denied

诊断:Prometheus 容器以 nobody 用户运行(uid=65534),而 scp 上去的 API key 文件 owner 默认是 root:600,nobody 读不到。

$ ssh <monitor-host> "sudo docker exec prometheus id"
uid=65534(nobody) gid=65534(nobody) groups=65534(nobody)

$ ssh <monitor-host> "sudo docker exec prometheus cat /etc/prometheus/qdrant_api_key"
cat: can't open '/etc/prometheus/qdrant_api_key': Permission denied

解决chown 65534:65534,mode 保留 600 即可(不用放宽到 644 暴露给其他用户):

sudo chown 65534:65534 /opt/monitor/prometheus/qdrant_api_key

改完后容器内能正常读取,target 状态变 up

$ ssh <monitor-host> "sudo docker exec prometheus cat /etc/prometheus/qdrant_api_key"
<YOUR_QDRANT_API_KEY>

这个坑是 Prometheus 容器化的常见问题——容器内进程以 nobody 运行,宿主机上的文件必须 chown 到 65534。MinIO 的 MINIO_PROMETHEUS_AUTH_TYPE=public 一行配置绕过了认证,所以没踩到;Qdrant 没有等价的开关,必须解决文件权限。

验证清单 链接到标题

部署完成后,按以下顺序验证:

# 1. promtool 校验配置语法
ssh <monitor-host> "sudo docker exec prometheus promtool check config /etc/prometheus/prometheus.yml"
# 预期:SUCCESS, 36 rules found

# 2. target health
ssh <monitor-host> "curl -s http://localhost:9090/api/v1/targets \
  | python3 -c \"import sys,json; d=json.load(sys.stdin); \
  [print(t['labels'].get('job'), t['health']) \
  for t in d['data']['activeTargets'] if t['labels'].get('job')=='qdrant']\""
# 预期:qdrant up

# 3. up 指标
ssh <monitor-host> "curl -s 'http://localhost:9090/api/v1/query?query=up{job=\"qdrant\"}'"
# 预期:value = 1

# 4. 关键业务指标
ssh <monitor-host> "curl -s 'http://localhost:9090/api/v1/query?query=collections_total{job=\"qdrant\"}'"
ssh <monitor-host> "curl -s 'http://localhost:9090/api/v1/query?query=memory_resident_bytes{job=\"qdrant\"}'"

# 5. 规则加载数
ssh <monitor-host> "curl -s http://localhost:9090/api/v1/rules \
  | python3 -c \"import sys,json; d=json.load(sys.stdin); \
  [print(g['name'], len(g['rules'])) \
  for g in d['data']['groups'] if g['name']=='qdrant_alerts']\""
# 预期:qdrant_alerts 11

# 6. 无 active 告警
ssh <monitor-host> "curl -s http://localhost:9090/api/v1/alerts \
  | python3 -c \"import sys,json; d=json.load(sys.stdin); \
  print('Active Qdrant alerts:', len([a for a in d['data']['alerts'] if a['labels'].get('job')=='qdrant']))\""
# 预期:0

总结 链接到标题

Qdrant 接入 Prometheus 的核心步骤:

  1. 仓库内 monitor/prometheus/qdrant_api_key 存 API key(.gitignore 忽略)
  2. prometheus.ymlqdrant job,用 bearer_token_file 引用
  3. alerts.yml 加 11 条 qdrant_alerts 规则
  4. 部署到监控服务器时 chown 65534:65534 解决容器 nobody 权限

和 MinIO 的对比:

MinIO Qdrant
metrics 端点开启方式 环境变量一行 默认开启
API key 必须用 bearer_token_file
容器权限坑 必须 chown 65534
指标前缀 minio_* 无前缀(v1.16+)
exporter 不需要 不需要

整体上 Qdrant 接入的工作量比 MinIO 多 30%——主要是 API key 认证和容器权限的额外配置。值得多花这点时间,因为 Qdrant 在 RAG、推荐、搜索场景里通常是核心依赖,宕机或性能退化直接影响业务。