深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
ESC

单节点 Kubernetes 部署 Milvus Standalone 踩坑实录

在家捣鼓一个 RAG 项目,需要用到向量数据库。把 Milvus 部署到家里的单节点 K8s 集群上。

clip_1776751156383_pmedc6.png

环境信息

我的实验环境是一台装了 Rocky Linux 10.1 的物理机,上面跑着一个单节点的 Kubernetes 集群:

  • OS: Rocky Linux 10.1
  • 容器运行时: cri-o
  • CNI: Flannel
  • Helm: v3.x

前置检查:存储和网络

部署 Milvus 之前,有两样东西必须先确认好:存储网络

Milvus 依赖 etcd 和 minio,两者都需要 PVC。先检查集群是否有默认的 StorageClass:

kubectl get sc

如果输出为空,说明没有配置存储类。对于单机测试环境,我用 hostPath 配了几个本地 PV,足够应付实验场景:

mkdir -p /home/wenjun/local-storage/pv-etcd
mkdir -p /home/wenjun/local-storage/pv-milvus
mkdir -p /home/wenjun/local-storage/pv-minio

cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: hostpath-pv-etcd
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: standard
hostPath:
path: /home/wenjun/local-storage/pv-etcd
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: hostpath-pv-milvus
spec:
capacity:
storage: 50Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: standard
hostPath:
path: /home/wenjun/local-storage/pv-milvus
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: hostpath-pv-minio
spec:
capacity:
storage: 500Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: standard
hostPath:
path: /home/wenjun/local-storage/pv-minio
EOF

注意:hostPath 的 capacity 只是声明,实际用多少取决于写入数据量,但要确保宿主机磁盘空间充足。

添加 Helm 仓库并安装

Milvus 官方提供了 Helm Chart,先添加仓库:

helm repo add zilliztech https://zilliztech.github.io/milvus-helm/
helm repo update

我选的是 Standalone 模式,所有组件跑在一个 Pod 里,配合 Woodpecker 作为消息队列。对于单机测试来说,这个组合既省资源又够用:

helm install milvus zilliztech/milvus \
--set image.all.tag=v2.6.14 \
--set cluster.enabled=false \
--set pulsarv3.enabled=false \
--set standalone.messageQueue=woodpecker \
--set woodpecker.enabled=true \
--set streaming.enabled=true \
--set etcd.replicaCount=1 \
--set minio.mode=standalone \
--set minio.replicas=1

安装完看一眼 Pod 状态:

kubectl get pods -o wide
kubectl get pvc

正常情况下应该看到三个 Pod 都在 Running 状态:

Pod 期望状态
milvus-etcd-0 1/1 Running
milvus-minio-xxx 1/1 Running
milvus-standalone-xxx 1/1 Running

但我的情况没那么顺利,下面三个坑一个接一个冒了出来。

踩坑一:镜像拉取失败(docker.io 无法访问)

现象:Pod 状态卡在 ImagePullBackOff,Events 里能看到连接 docker.io 超时的记录。

原因:我的网络环境访问不了 docker.io 官方仓库。

解决:给 cri-o 配置镜像代理。我用的是 DaoCloud 的镜像加速:

sudo tee /etc/containers/registries.conf.d/mirror.conf << 'EOF'
[[registry]]
prefix = "docker.io"
location = "docker.io"

[[registry.mirror]]
location = "docker.m.daocloud.io"
EOF

sudo systemctl restart crio

配置完重启 cri-o 后,删掉拉取失败的 Pod,让 kubelet 重新拉取镜像即可。

踩坑二:Pod 间网络不通(firewalld 拦截 CNI 流量)

这是最折腾的一个坑。

现象:etcd 和 minio 都正常启动了,但 milvus-standalone 一直在 CrashLoopBackOff。日志里反复出现这种报错:

dial tcp 10.244.0.xxx:2379: connect: no route to host

Pod 内部不仅连不上 etcd,DNS 解析也失败,ClusterIP 也 ping 不通。

原因firewalld 的 nftables 规则把 Flannel cni0 网桥上的流量给拦截了。加上 bridge-nf-call-iptables=1 这个内核参数,网桥流量会进入 nftables 后被丢弃,导致 Pod 间通信全断。

解决:在宿主机上以 root 权限执行以下操作:

# 1. 停止并禁用 firewalld
sudo systemctl stop firewalld
sudo systemctl disable firewalld

# 2. 清空残留的 nftables 规则
sudo nft flush ruleset

# 3. 重启 kube-proxy,让 K8s 重建 Service DNAT 规则
kubectl delete pod -n kube-system $(kubectl get pod -n kube-system -l k8s-app=kube-proxy -o jsonpath='{.items[0].metadata.name}')

重启 kube-proxy 后,Pod 间通信、DNS 解析、ClusterIP 访问全部恢复正常。milvus-standalone 也终于能连上 etcd 了。

建议提前执行 systemctl disable firewalld,避免节点重启后 firewalld 再次启动干扰网络。

踩坑三:etcd 权限错误

现象:etcd Pod 起不来,日志报错:

cannot access data directory: mkdir /bitnami/etcd/data: permission denied

原因:bitnami 的 etcd 镜像默认以非 root 用户运行,但 hostPath 挂载的目录权限不够。

解决:简单粗暴但有效:

chmod -R 777 /home/wenjun/local-storage/pv-etcd

然后删除 etcd Pod,让它重建。

暴露服务

默认部署的 Service 是 ClusterIP 类型,只能在集群内访问。我选的是 NodePort 方式,适合单机测试:

kubectl patch svc milvus -p '{"spec":{"type":"NodePort","ports":[{"name":"milvus","port":19530,"targetPort":19530,"nodePort":30001},{"name":"metrics","port":9091,"targetPort":9091,"nodePort":30002}]}}'

暴露后的访问地址:

用途 地址
Milvus gRPC/REST API http://<节点IP>:30001
Milvus WebUI http://<节点IP>:30002

连接测试

部署完成后,用 Python 客户端验证一下:

from pymilvus import MilvusClient

client = MilvusClient(
uri="http://192.168.1.58:30001", # 换成你的节点 IP
token="root:Milvus"
)

print(client.get_server_version()) # v2.6.14

# 创建测试集合
client.create_collection("test_collection", dimension=128)
print("Collection created")

或者用 REST API 查看数据库和集合列表:

# 查看数据库列表
curl -u root:Milvus http://192.168.1.58:30001/v2/vectordb/databases/list

# 查看集合列表
curl -u root:Milvus http://192.168.1.58:30001/v2/vectordb/collections/list

注意:直接用浏览器访问 http://<ip>:19530/ 会返回 404 page not found,因为这个端口提供的是 gRPC/REST API,不是 Web 根页面。

卸载清理

如果不再需要,可以一键卸载:

helm uninstall milvus
kubectl delete pvc data-milvus-etcd-0 milvus milvus-minio

如果要彻底清理 PV 和本地数据:

kubectl delete pv hostpath-pv-etcd hostpath-pv-milvus hostpath-pv-minio
rm -rf /home/wenjun/local-storage

部署参数说明

最后把这次用到的关键 Helm 参数汇总一下:

Helm 参数 说明
cluster.enabled false Standalone 模式,单 Pod 运行所有组件
image.all.tag v2.6.14 Milvus 镜像版本
pulsarv3.enabled false 禁用 Pulsar
woodpecker.enabled true 启用 Woodpecker 作为消息队列
streaming.enabled true 启用 Streaming 节点
etcd.replicaCount 1 etcd 单副本
minio.mode standalone MinIO 单机模式
minio.replicas 1 MinIO 单副本

小结

这次部署 Milvus 的经历再次验证了一个道理:本地测试环境永远不要低估网络和存储这两个基础问题

三个坑里面,firewalld 拦截 CNI 流量是最隐蔽也最耗时间的。表面上看是 Milvus 连不上 etcd,实际上是整个集群的 Pod 间网络都坏了。排查这类问题,最直接的方法就是在 Pod 里手动 curltelnet 其他服务的 ClusterIP,快速定位是 DNS 问题、路由问题还是防火墙问题。

如果你也在单节点 K8s 上部署 Milvus,希望这篇文章能帮你少踩几个坑。


参考文档

文章作者:阿文
文章链接: https://www.awen.me/post/8fecd9a4.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
您的评论由 AI 智能审核,一般1分钟内会展示,若不展示请确认你的评论是否符合社区和法律规范
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️