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

环境信息
我的实验环境是一台装了 Rocky Linux 10.1 的物理机,上面跑着一个单节点的 Kubernetes 集群:
- OS: Rocky Linux 10.1
- 容器运行时: cri-o
- CNI: Flannel
- Helm: v3.x
前置检查:存储和网络
部署 Milvus 之前,有两样东西必须先确认好:存储和网络。
Milvus 依赖 etcd 和 minio,两者都需要 PVC。先检查集群是否有默认的 StorageClass:
如果输出为空,说明没有配置存储类。对于单机测试环境,我用 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 权限执行以下操作:
sudo systemctl stop firewalld sudo systemctl disable firewalld
sudo nft flush ruleset
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", token="root:Milvus" )
print(client.get_server_version())
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 里手动 curl 或 telnet 其他服务的 ClusterIP,快速定位是 DNS 问题、路由问题还是防火墙问题。
如果你也在单节点 K8s 上部署 Milvus,希望这篇文章能帮你少踩几个坑。
参考文档
评论
0 条评论