跳到主要内容

5 篇博文 含有标签「Milvus」

查看所有标签

Docker 卷挂载后是空的?etcd 数据全在可写层,容器重建即丢

· 阅读需 6 分钟

磁盘清理时用 docker ps -as 巡检各容器可写层,etcd 容器赫然 385M——而它挂载的数据卷 etcd_data 里只有 8K。当时磁盘清理没敢动它:一旦 recreate,Milvus 的全部元数据就随可写层一起蒸发。

在开发 AI客服 时遇到此问题——7×24小时AI客服,快速解答产品使用问题;它的知识库检索跑在 Milvus 上,而 Milvus 的元数据全靠这个 etcd。

TL;DR​

compose 的 volumes: 只负责「把卷挂到容器某个路径」,应用往哪写由它自己的参数决定。etcd 没设 data-dir 时默认写到工作目录下的 default.etcd——挂载点 /etcd 它根本没用,385M 数据全在可写层,docker compose up -d 的任何一次重建都会把它带走。修复用 etcd 原生迁移路径:在线快照 → restore → 停机换数据 → 补 ETCD_DATA_DIR → 带卷重建,零丢失,全程停机不到两分钟。

问题现象​

compose 里的 etcd 服务看起来是「正确」的——卷挂了:

  etcd:
image: quay.io/coreos/etcd:v3.5.5
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
# ... 没有任何 data-dir 相关配置
volumes:
- etcd_data:/etcd

但两个数字对不上:

$ docker ps -as --format "{{.Names}}\t{{.Size}}" | grep etcd
rag-service-etcd-1 385MB (virtual 199MB) ← 可写层 385M

$ docker exec rag-service-etcd-1 du -sh /etcd
8K /etcd ← 挂载的卷是空的

$ docker exec rag-service-etcd-1 ls -la / | grep etcd
drwx------ 3 root root 4096 default.etcd ← 数据在这:容器根目录

根因​

挂载 ≠ 使用。 volumes: etcd_data:/etcd 只保证「卷被挂到 /etcd 这个路径」,至于 etcd 往哪写,由它自己的 --data-dir 参数决定。etcd 的缺省 data-dir 是工作目录下的 default.etcd——这份 compose 既没设 ETCD_DATA_DIR 环境变量,也没传 --data-dir 命令行参数,于是 etcd 在容器根目录自建了 default.etcd,把全部数据写进了可写层。挂载点 /etcd 从第一天起就是空的。

这类「卷是摆设」最阴险的地方在于平时完全无症状:服务正常跑、数据正常读写、监控全绿。只有当你需要 docker compose up -d --force-recreate(换配置、回收可写层、迁移宿主机)的那一刻,可写层被整体丢弃,数据才消失——而那通常是你在做紧急运维、最不需要第二起事故的时候。

解决方案​

用 etcd 原生的快照迁移路径,在线快照保证一致性,短暂停机只出现在最后的数据切换。

步骤 1:在线一致性快照​

docker exec rag-service-etcd-1 sh -c \
'ETCDCTL_API=3 etcdctl --endpoints=http://127.0.0.1:2379 snapshot save /tmp/etcd-snap.db'
docker cp rag-service-etcd-1:/tmp/etcd-snap.db /root/etcd-snap.db

snapshot save 对运行中的 etcd 是安全的(不走文件复制,走 Raft 后端快照),不停机、不锁写。

步骤 2:restore 成目标 data-dir 结构​

docker run --rm -v /root:/host quay.io/coreos/etcd:v3.5.5 \
etcdctl snapshot restore /host/etcd-snap.db --data-dir /host/etcd-restored

restore 会生成带完整 member/ 结构的数据目录(快照本身不能直接当 data-dir 用)。

步骤 3:停机切换,数据入卷​

docker stop rag-service-etcd-1
rm -rf /var/lib/docker/volumes/etcd_data/_data/* # 卷是空的,清掉挂载残留
cp -a /root/etcd-restored/. /var/lib/docker/volumes/etcd_data/_data/

步骤 4:补上缺失的配置,带卷重建​

  etcd:
environment:
- ETCD_DATA_DIR=/etcd # ← 缺的就是这一行
# ...
volumes:
- etcd_data:/etcd
docker compose up -d etcd    # 配置变更触发 recreate,新容器以卷为 data-dir

步骤 5:验证​

docker exec rag-service-etcd-1 etcdctl --endpoints=http://127.0.0.1:2379 endpoint health
du -sh /var/lib/docker/volumes/etcd_data/_data # 数据应在卷里
docker ps -as | grep etcd # 可写层应回落到 KB 级

本次修复后:可写层 385M → 8KB,数据落卷 123M,上游 Milvus 自动重连、元数据读写正常。

注意事项

  • 给 stateful 容器(etcd/postgres/redis/minio)做巡检时,把「可写层大小 vs 卷大小」当固定对账项:docker ps -as 的 SIZE 一栏异常膨胀而卷很空,几乎必然是数据没落卷。
  • 判断「数据在哪」要追进程实际读写的路径(docker top 看参数、进容器找数据目录),不能只看 compose 有没有 volumes 行——有挂载不等于被使用。
  • 单节点 etcd 的快照 restore 会重建 member 元数据,只适用于单节点场景;多节点集群的迁移请走成员变更流程。
  • 同类排查方法论见 容器日志吃满服务器磁盘?docker system df 的 reclaimable 是误报——docker ps -as 可写层观察是同一把刀;挂载路径被覆盖的另一形态见 Docker Volume 覆盖 Bind Mount。

常见问题​

Docker 挂载卷后目录是空的怎么回事?​

两种常见原因:一是空卷挂载遮盖了镜像内原有目录(Docker 的既有行为);二是应用的数据目录参数根本没指向挂载点,数据写进了容器可写层——本文的 etcd 案例就是这种。用 du 分别看卷目录和容器可写层的大小,一眼可辨。

etcd 数据怎么安全迁移到新的 data-dir?​

etcdctl snapshot save 在线做一致性快照(不停机),etcdctl snapshot restore --data-dir 生成带 member 结构的目标目录,停容器后把数据放到新位置、以新 data-dir 启动,最后 endpoint health 加上游重连验证。全程停机只在切换那一段。

compose 里挂了卷为什么没生效?​

volumes: 只把卷挂到容器的某个路径;应用写哪里由它自己的配置决定——etcd 看 data-dir,postgres 看 PGDATA,redis 看配置文件里的 dir。挂载点和应用配置对不上,卷就只是个空目录,数据全在可写层,重建即丢。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

Milvus 报 invalid collection name?collection 名首字符必须字母/下划线,UUID 不能直接拼

· 阅读需 6 分钟

在按租户给向量库的 collection 加前缀、用 {tenant_id}_{collection} 拼名时,第一个请求就被 Milvus 顶了回来——invalid collection name: the first character ... must be an underscore or letter,接口直接 500。

在开发 AI客服 时遇到此问题——7×24小时AI客服,快速解答产品使用问题,提供功能指导和最佳实践。

TL;DR​

Milvus 对 collection 名有严格校验:首字符必须是字母或下划线、其余只允许 [a-zA-Z0-9_](禁连字符)、长度 ≤255,违反就报 invalid collection name(错误码 1100)。UUID 首字符常是数字、且自带连字符 -,两点都踩雷,所以不能把 tenant_id 这种 UUID 直接拼进 collection 名做隔离。改用原始名 + tenant 字段过滤即可。

问题现象​

带 collection=system_product_help 的查询接口返回 500,rag-service 日志只吐一行:

pymilvus.exceptions.MilvusException: code=1100,
Invalid collection name: 00000000-0000-0000-0000-000000000001_system_product_help.
the first character of a collection name must be an underscore or letter

诡异的是,同一参数的另一个纯查日志接口(/query-logs)却返回 200——因为它只读 PostgreSQL,根本没碰 Milvus;只有真正会去 Milvus has_collection 的路径才触发校验。

根因​

代码里用 f"{tenant_id}_{collection}" 拼出 collection 名,例如 00000000-0000-0000-0000-000000000001_system_product_help。这个名字同时犯了两条规:

00000000-0000-0000-0000-000000000001_system_product_help
^ ^ ^
│ │ └─ 下划线后 OK,但……
│ └─── 连字符 `-` 违规
└────────────────── 首字符是数字 `0` 违规(必须字母/下划线)

Milvus 的 collection 名校验规则(源码 nameutil.go,正则 ^[a-zA-Z_][a-zA-Z0-9_]*$,长度 ≤255):

规则要求
首字符字母或下划线 _
其余字符仅 [a-zA-Z0-9_](字母、数字、下划线)
禁止连字符 -、空格、点号、其他特殊符号
长度1–255 个字符

UUID 几乎必然违规:标准形式 8-4-4-4-12 含 4 个连字符,且首段常以数字开头。把这样的前缀拼到 collection 名上,has_collection / describe_collection / 创建语句都会被服务端拒掉,抛 code 1100。

更要命的是:因为拼出来的名从来就不合法,所谓的"按租户前缀隔离"从未真正生效过——库里实际存在的 collection 全是没前缀的原始名,拼接逻辑与真实数据系统性脱节,纯写代码时的一个想当然。

解决方案​

别把 tenant_id 拼进 collection 名。 collection 一律用原始名,租户隔离交给普通字段:

from pymilvus import MilvusClient

client = MilvusClient(uri="http://localhost:19530")

# ❌ 错误:UUID 拼前缀,首字符是数字 + 含连字符 → code 1100
tenant_id = "00000000-0000-0000-0000-000000000001"
bad_name = f"{tenant_id}_system_product_help" # 非法

# ✅ 正确:collection 用原始名,tenant_id 作为 schema 字段过滤
client.create_collection(
collection_name="system_product_help", # 合法、稳定
schema=client.create_schema(auto_id=True, enable_dynamic_field=False),
)
# 写入与查询时用 tenant_id 字段做过滤,而不是改 collection 名
client.insert(
collection_name="system_product_help",
data=[{"tenant_id": tenant_id, "text": "...", "vector": [...]}],
)

如果你确实需要"一段可读前缀"做多租户或环境隔离,把任意字符串转成安全 slug 再拼:

import re

def safe_slug(raw: str) -> str:
# 非 [a-zA-Z0-9_] 一律替成下划线,首字符若非字母/下划线则补一个
s = re.sub(r"[^a-zA-Z0-9_]", "_", raw)
if not re.match(r"^[a-zA-Z_]", s):
s = "_" + s
return s[:255] # 截到长度上限内

name = f"{safe_slug(tenant_id)}_system_product_help" # 合法

排查这类 500 时,第一眼先看 PM2 / 服务日志里的 MilvusException——错误码和"first character must be ..."这句提示基本能直接定位到名字不合法,不必往业务逻辑深处找。

顺带一提,rag-service 这类依赖 Milvus 的服务,本身还有"容器没配 restart 策略、崩溃后整条 RAG 链路起不来"的坑,见 Docker Compose 服务重启后起不来?检查 restart 策略;混合检索那侧则要注意 RRF 分数与相似度阈值不兼容。

注意事项​

注意事项

  • 连字符是最隐蔽的坑:很多团队习惯用 tenant-env-docs 这种 kebab-case 命名,在 Milvus 里全部非法。命名一律用下划线 snake_case。
  • 不只是 collection 名:database 名、partition 名、字段名的校验规则相近(首字符、允许字符集),用 UUID 或连字符拼接时都要先过一遍校验。
  • 隔离用字段,不要用 collection 数量:按租户各建一个 collection 会让 collection 数量随租户线性膨胀,超出 Milvus 管理舒适区。把 tenant_id 做成普通字段 + 过滤,或用 partition key,是更稳的隔离方式。
  • 校验在服务端:pymilvus 客户端不一定对每个调用都前置校验,非法名可能直到请求落到 Milvus 才报 1100,本地单测容易漏。

常见问题​

Milvus collection 名有哪些命名规则?​

首字符必须是字母或下划线,其余字符仅允许字母、数字、下划线([a-zA-Z0-9_]),禁止连字符和空格,最长 255 个字符。Milvus 在服务端用正则强制校验,违反就抛 invalid collection name(错误码 1100),创建和寻址都会失败。命名用 snake_case 最保险。

Milvus collection 名长度上限是多少?​

255 个字符。超出会被校验拒绝并报 invalid collection name(错误码 1100)。实际命名通常远短于此,真正卡上限的往往是把长 UUID 或多段路径拼了进去——这恰恰说明你不该把这类动态串拼进 collection 名。

Milvus collection 名为什么不能用 UUID 作前缀?​

UUID 标准形式首段常以数字开头(违反"首字符必须字母/下划线"),且自带 4 个连字符 -(不在允许字符集),两点都违反命名规则。把 tenant_id 拼成 collection 前缀做隔离是常见误用:不仅名字非法,还会让 collection 数量随租户膨胀。正确做法是把 tenant_id 放进普通字段或分区键,collection 用稳定的原始名。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

修复 Milvus 混合检索 RRF 分数与相似度阈值不兼容

· 阅读需 3 分钟

在 RAG 知识库项目中调试混合检索评分问题,以下是完整排查过程。

TL;DR​

Milvus 混合检索的加权融合分数 = 0.7 * dense_score + 0.3 * sparse_score,理论最大值约 0.7。如果用 min_similarity=0.7 过滤,结果几乎全被剔除。解决方案:将阈值降到 0.3,或根据融合策略动态调整。