Docker Compose 迁到单节点 Kubernetes:vLLM、Dify、Langfuse 逐个怎么搬
目录
本文按服务记录这次迁移,用来看清一个正在运行的容器怎样变成 Pod。机器是 prod102,地址 10.0.1.47,系统 Ubuntu 22.04.5。迁移前,Dify、vLLM、Xinference、New-API、Langfuse、Prometheus、Grafana、DCGM 和导航页都在 Docker 里提供服务。本机上的 Kubernetes 当时不能提供服务:它只是一台 worker,控制面在 10.0.1.46:6443,那台机器连不上。10.0.1.48 没有动。
切换是一批完成的,后面七章按依赖顺序来读,每一章都会写明它在这一批里的位置。完整脚本仍在 scripts/,这里只引用和本服务有关的命令。不要为了对照本文在现网重跑安装或迁移。
一、集群前提
1. 当时要保住的服务
| 端口 | 当时的容器 | 数据留在 |
|---|---|---|
| 8000 | vllm-qwen3.6-27b | /models/Qwen3.6-27B,符号链接到 /usr/cluster/models |
| 9997 | xinference-bge | /usr/cluster/dify/xinference |
| 3000 | new-api | /opt/new-api/data,库文件是 one-api.db |
| 8088 / 18443 | dify-nginx-1 | /usr/cluster/dify/src/docker/volumes 下的 Postgres、Redis、pgvector 和上传文件 |
| 3100 | langfuse-langfuse-web-1 | Langfuse 的 Postgres、ClickHouse、MinIO、Redis 数据卷 |
| 3001 / 9090 / 9400 | grafana、prometheus、dcgm-exporter | /usr/cluster/observability/metrics 和对应数据卷 |
| 8090 | llm-nav | /usr/cluster/llm-nav |
| 8777 | 宿主机 systemd server-status | /usr/cluster/server-status/app.py,不进集群 |
| 80 | 宿主机 Nginx | 不进集群 |
模型权重和数据库目录留在磁盘上。新 Pod 用 hostPath 挂回这些目录,所以里面的渠道、知识库、追踪记录和监控历史还是原来那一份。
2. 清掉失效的旧集群
旧 kubelet 是 1.22.12,CRI 走 dockershim。Calico 期望的 BGP 邻居是 10.0.1.46 和 10.0.1.48。Pod 网段 10.243.212.128/26 当时是黑洞路由,机器上还有 tunl0 和 kube-ipvs0。
kubeadm reset 连不上 10.0.1.46,列不出 Pod。CNI、iptables 和 IPVS 是后来手工清的。清理时带上 cri-dockerd 的 socket,让 kubeadm 对着当时的 Docker 运行时操作:
kubeadm reset --cri-socket unix:///run/cri-dockerd.sock备份在 /usr/cluster/k8s-reset-backup-20261012/,里面有 iptables、ip6tables、ipvs、路由、地址和清理前的容器名。旧控制面和 10.0.1.48 没有重置。清完之后,原来的 Docker 健康检查仍然是 200,然后才装新集群。不执行 docker rm -v,不执行 docker system prune。
3. 三次安装里真正生效的做法
安装脚本是 scripts/01-部署单节点.sh。它装 cri-dockerd 0.4.8 和 Kubernetes 1.37.1。仓库里这份已经是改过的版本,末尾仍会去下载 Flannel。Flannel 没有成为最终网络。
第一次停在 systemctl mask kubelet。/etc/systemd/system/kubelet.service 已经是一个真实单元文件,mask 报 File exists,set -e 让脚本退出。当时 cri-dockerd 0.4.8 已经装上,kubelet 是停用的,还没有 admin.conf。后来改成:
systemctl stop kubelet || true
systemctl disable kubelet || true
systemctl unmask kubelet || true第二次 kubeadm init 停在 wait-control-plane。https://10.0.1.47:6443 拒绝连接。cri-dockerd 默认去拉 registry.k8s.io/pause:3.10,访问超时,静态 Pod 起不来。scripts/04-补齐控制面.sh 先从阿里云拉 pause,再打上 cri-dockerd 认识的名字,并写 drop-in:
docker pull registry.aliyuncs.com/google_containers/pause:3.10.2
docker tag registry.aliyuncs.com/google_containers/pause:3.10.2 registry.k8s.io/pause:3.10[Service]
ExecStart=
ExecStart=/usr/local/bin/cri-dockerd --container-runtime-endpoint fd:// --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.10.2这个 drop-in 挂在正在使用的 cri-docker.service 上。同名的 /etc/systemd/system/cri-dockerd.service 是坏的,已经 mask。socket 听在 %t/cri-dockerd.sock。CoreDNS 和 kube-proxy 在这一步装上。脚本后半段下载 Flannel 时,GitHub 返回了 HTTP/2 帧错误。
第三次,scripts/05-安装Flannel.sh 拿到了 v0.27.4 的清单,命名空间、RBAC 和 DaemonSet 都创建了。ghcr.io/flannel-io/flannel:v0.27.4 的层下载停住。scripts/06-等节点就绪.sh 里的 docker pull 被后面的网桥脚本杀掉,退出码 143。
节点 Ready 用的是 scripts/07-单节点网桥.sh。它停掉 Flannel 拉取,删除 DaemonSet kube-flannel-ds,写入 /etc/cni/net.d/10-bridge.conflist,再重启 kubelet。网桥名 cni0,子网 10.244.0.0/24,落在 kubeadm 所写的 10.244.0.0/16 里面。插件是机器上已有的 bridge、host-local 和 portmap。
{
"cniVersion": "1.0.0",
"name": "k8s-pod-network",
"plugins": [
{
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"promiscMode": true,
"ipam": {
"type": "host-local",
"ranges": [[{"subnet": "10.244.0.0/24"}]],
"routes": [{"dst": "0.0.0.0/0"}]
}
},
{
"type": "portmap",
"capabilities": {"portMappings": true}
}
]
}重启后先打印 ready=False,随后 ready=True。kube-flannel 里的 RBAC 和 ConfigMap 可能还在。这份 conflist 还在时,不要再装 Flannel。
4. 真正切换的顺序
业务容器一直跑到这一步。scripts/03-迁入单节点.sh 在节点 Ready 之后按下面的顺序做。scripts/08-开始迁移.sh 只负责在后台启动服务器上的 /usr/cluster/k8s/migrate.sh。
- 起一个一次性 Pod
gpu-check,镜像用 vLLM 那份,命令是nvidia-smi -L,环境变量NVIDIA_VISIBLE_DEVICES=all。成功后删掉。这一步确认 Pod 能看见 GPU,同时说明不需要在清单里写nvidia.com/gpu。 - 容器还在跑的时候,从
dify-worker-1把langfuse_trace.py拷到/usr/cluster/k8s/patches/。新 Pod 的文件系统来自镜像,旧容器可写层里的文件不会自己过去。 scripts/02-生成清单.py对当时docker ps里每个业务容器做docker inspect,写出 Deployment、Service 和 Secret。Secret 在服务器上是0600,不放进这份仓库。- 把 Prometheus 配置里的
host.docker.internal:8000改成10.0.1.47:8000。文件还在宿主机上,旧容器和新 Pod 挂的是同一份。 - 先
kubectl apply命名空间、Secret 和 Service。这一步还不创建 Pod,所以还不会去抢宿主机端口。 - 给 CoreDNS 加上划线名到连字符名的 rewrite。Compose 的服务名可以有下划线,Kubernetes 的 Service 名不行。
- 把当时正在跑的业务容器名写入
/usr/cluster/k8s/stopped-containers.txt。除vllm-qwen3.6-27b以外执行docker stop -t 25,vLLM 最后执行docker stop -t 90。 - 容器停了、数据库文件还在磁盘上,这时把 New-API 渠道地址里的
172.17.0.1换成10.0.1.47。 - 再
kubectl apply四个命名空间的workloads.json,Pod 这才绑定 hostPort。
名单里有 28 个容器。已经退出的 dify-init_permissions-1 和若干 nvidia-smi 练习容器不在里面,因为生成器和停止列表都只看当时正在运行的容器。
CoreDNS 实际写入的规则:
agent_ssrf_proxy.dify.svc.cluster.local -> agent-ssrf-proxy.dify.svc.cluster.local
ssrf_proxy.dify.svc.cluster.local -> ssrf-proxy.dify.svc.cluster.local
api_websocket.dify.svc.cluster.local -> api-websocket.dify.svc.cluster.local
worker_beat.dify.svc.cluster.local -> worker-beat.dify.svc.cluster.local
agent_backend.dify.svc.cluster.local -> agent-backend.dify.svc.cluster.local
plugin_daemon.dify.svc.cluster.local -> plugin-daemon.dify.svc.cluster.local
local_sandbox.dify.svc.cluster.local -> local-sandbox.dify.svc.cluster.local
db_postgres.dify.svc.cluster.local -> db-postgres.dify.svc.cluster.local生成器对每个容器使用同一套形状:strategy: Recreate,nodeName: prod102,enableServiceLinks: false,hostAliases 把 host.docker.internal 指到 10.0.1.47。环境变量放进名为 <工作负载>-env 的 Secret,清单里只留 envFrom。有宿主机端口映射的,写成 Pod 的 hostPort。挂载写成 hostPath,文件和目录用 inspect 时看到的类型。共享内存大于 64MiB 的,补一个 8Gi 的 emptyDir 到 /dev/shm。Service 只声明容器端口,外部仍从原来的宿主机端口进来。
Recreate 是因为 hostPort 和 GPU 都不能有两个副本同时占用。enableServiceLinks: false 是为了避免 Kubernetes 按 Service 名注入一组环境变量,盖住应用原来的配置。
5. 回滚,以及 Docker 重启后的端口
回滚函数只挂在 New-API 和 Dify 的等待失败上。它删除四个 workloads.json,再按 stopped-containers.txt 执行 docker start。vLLM 等待失败,以及最后的健康检查失败,都是直接退出,不调用这个函数。
if [ "$na" != "1" ] || [ "$dy" != "1" ]; then
rollback
exit 1
fiif ! check_health final; then
echo MIGRATE_PARTIAL
exit 1
fi这次最后一步是 Xinference 的 /v1/models 返回 401。脚本因此打印 MIGRATE_PARTIAL 并以退出码 1 结束。回滚没有执行,Pod 保持运行。
若干旧容器的重启策略是 always:Dify 各容器、Langfuse 各容器、vllm-qwen3.6-27b 和 new-api。它们现在是 Exited。Docker 守护进程如果重启,always 会把它们再拉起来,和 Pod 的 hostPort 抢 3000、8000、8088 这些端口。unless-stopped 的会保持停止:xinference-bge、llm-nav、prometheus、grafana、dcgm-exporter。迁移时没有改这些重启策略。
二、按服务迁移
阅读顺序是 vLLM、Xinference、New-API、导航页、Dify、Langfuse、Prometheus / Grafana / DCGM。每一章的「切换」都是第 4 节那一批里的一段,容器是一起停的,Pod 是一起拉起的。
1. vLLM
它在停止名单的最后,超时 90 秒,用来等模型进程退出并把 8000 端口让出来。GPU 检查在停容器之前已经做过。
迁移前
容器名 vllm-qwen3.6-27b,镜像 docker.m.daocloud.io/vllm/vllm-openai:latest,重启策略 always。它不在 Compose 项目里,生成器按名字把它放进命名空间 llm,工作负载名 vllm。
宿主机 8000 映射到容器 8000。权重目录 /models/Qwen3.6-27B 挂到 /model。共享内存是 8GiB。入口是 vllm serve /model,参数保持 bfloat16、上下文 65536、gpu-memory-utilization 0.70、max-num-seqs 8、服务名 qwen3.6-27b。New-API 的渠道和 Dify 的对话都依赖这个端口。
清单里变成什么
Pod 继续使用同一条命令和同一个 hostPath,所以加载的还是磁盘上的权重,服务名仍是 qwen3.6-27b。HF_HUB_OFFLINE 和 NVIDIA_VISIBLE_DEVICES 在 Secret vllm-env 里。/dev/shm 用内存型 emptyDir,上限 8Gi,对应原来的 ShmSize。没有申请 nvidia.com/gpu,GPU 靠节点上的 NVIDIA 运行时和这个环境变量。
文件:config/vllm.yaml。
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm
namespace: llm
labels:
app: vllm
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: vllm
template:
metadata:
labels:
app: vllm
annotations:
llm.local/compose-service: "vllm"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: vllm
image: docker.m.daocloud.io/vllm/vllm-openai:latest
imagePullPolicy: IfNotPresent
command:
- "vllm"
- "serve"
args:
- "/model"
- "--trust-remote-code"
- "--dtype"
- "bfloat16"
- "--tensor-parallel-size"
- "1"
- "--enable-auto-tool-choice"
- "--tool-call-parser"
- "qwen3_xml"
- "--reasoning-parser"
- "qwen3"
- "--max-model-len"
- "65536"
- "--gpu-memory-utilization"
- "0.70"
- "--max-num-seqs"
- "8"
- "--served-model-name"
- "qwen3.6-27b"
- "--enable-prefix-caching"
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
envFrom:
- secretRef:
name: vllm-env
ports:
- containerPort: 8000
hostPort: 8000
protocol: TCP
volumeMounts:
- name: vol0
mountPath: "/model"
- name: dshm
mountPath: "/dev/shm"
volumes:
- name: vol0
hostPath:
path: "/models/Qwen3.6-27B"
type: Directory
- name: dshm
emptyDir:
medium: Memory
sizeLimit: 8Gi
---
apiVersion: v1
kind: Service
metadata:
name: vllm
namespace: llm
spec:
selector:
app: vllm
ports:
- name: p8000
port: 8000
targetPort: 8000
protocol: TCPService 的 8000 是集群内端口。宿主机上的 8000 来自 Pod 的 hostPort,所以原来访问 10.0.1.47:8000 的调用方式不变。
切换时做了什么
vLLM 留到其他容器都发出停止之后:
# 其余正在运行的业务容器一次 docker stop -t 25
docker stop -t 90 vllm-qwen3.6-27b端口空出来之后,和其他工作负载一起提交:
kubectl apply -f /usr/cluster/k8s/manifests/llm/workloads.json这个文件里同时有 vLLM、Xinference、New-API 和导航页。脚本随后单独等 :8000/health 变成 200,最多约 15 分钟。这一步失败会打印 VLLM_NOT_READY 并退出,不回滚。
怎么确认
写本文时,http://127.0.0.1:8000/health 和 http://127.0.0.1:8000/v1/models 都是 200。Pod 挂的仍是 /models/Qwen3.6-27B。Prometheus 能从 10.244.0.1 刮到 /metrics。
2. Xinference
它和其余容器一起 docker stop -t 25。最终健康检查把未带令牌的 401 当成失败,整批结果记成 MIGRATE_PARTIAL。
迁移前
容器名 xinference-bge,镜像 xprobe/xinference:latest,重启策略 unless-stopped。命令是 xinference-local -H 0.0.0.0 --log-level info。宿主机 9997 映射到容器 9997。/usr/cluster/dify/xinference 挂到 /data,里面是已经下载的 bge-m3 和 bge-reranker-v2-m3。Dify 的向量和重排走这个服务。它也不是 Compose 容器,生成器把它放进 llm,名字 xinference。
清单里变成什么
数据目录整份挂回去,所以模型文件还在 /data。镜像外临时装进旧容器可写层的包不会出现在新 Pod 里;要留住这种包,得像 Dify 的 Langfuse 补丁那样,在停止前拷出来再挂回去。XINFERENCE_HOME 和 NVIDIA_VISIBLE_DEVICES 在 Secret xinference-env 里。
文件:config/xinference.yaml。
apiVersion: apps/v1
kind: Deployment
metadata:
name: xinference
namespace: llm
labels:
app: xinference
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: xinference
template:
metadata:
labels:
app: xinference
annotations:
llm.local/compose-service: "xinference"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: xinference
image: xprobe/xinference:latest
imagePullPolicy: IfNotPresent
command:
- "xinference-local"
- "-H"
- "0.0.0.0"
- "--log-level"
- "info"
envFrom:
- secretRef:
name: xinference-env
ports:
- containerPort: 9997
hostPort: 9997
protocol: TCP
volumeMounts:
- name: vol0
mountPath: "/data"
volumes:
- name: vol0
hostPath:
path: "/usr/cluster/dify/xinference"
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: xinference
namespace: llm
spec:
selector:
app: xinference
ports:
- name: p9997
port: 9997
targetPort: 9997
protocol: TCP切换时做了什么
它在「其余容器」那一组里,超时 25 秒,然后随 llm/workloads.json 拉起。没有单独的等待循环。最后的 check_health 请求 http://127.0.0.1:9997/v1/models,只把 200、301、302、307、308 当成成功。
怎么确认
写本文时,不带令牌访问 /v1/models 仍是 401。401 表示进程在听 9997,这个接口要求管理员令牌。脚本因此没有打印 MIGRATE_OK。Pod 没有被删掉。
3. New-API
它在 25 秒那一组里被停止。渠道地址是在数据库文件已经脱离容器、Pod 还没起来的空隙里改的。
迁移前
容器名 new-api,镜像 calciumion/new-api:latest,重启策略 always。Compose 项目名是 new-api,生成器把它放进命名空间 llm。入口是 /new-api --log-dir /app/logs。宿主机 3000 映射到容器 3000。/opt/new-api/data 挂到 /data,/opt/new-api/logs 挂到 /app/logs。渠道、令牌和额度都在 /data/one-api.db。
迁移前,渠道要去访问宿主机上的 vLLM,地址写的是 Docker 桥上的 172.17.0.1。Pod 的网络已经不是那座桥。
清单里变成什么
SQLite 文件还在原来的 hostPath 上,所以用户、渠道和调用日志没有换一份新库。命令和日志目录也保持原样。
文件:config/new-api.yaml。
apiVersion: apps/v1
kind: Deployment
metadata:
name: new-api
namespace: llm
labels:
app: new-api
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: new-api
template:
metadata:
labels:
app: new-api
annotations:
llm.local/compose-service: "new-api"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: new-api
image: calciumion/new-api:latest
imagePullPolicy: IfNotPresent
command:
- "/new-api"
args:
- "--log-dir"
- "/app/logs"
envFrom:
- secretRef:
name: new-api-env
ports:
- containerPort: 3000
hostPort: 3000
protocol: TCP
volumeMounts:
- name: vol0
mountPath: "/data"
- name: vol1
mountPath: "/app/logs"
volumes:
- name: vol0
hostPath:
path: "/opt/new-api/data"
type: Directory
- name: vol1
hostPath:
path: "/opt/new-api/logs"
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: new-api
namespace: llm
spec:
selector:
app: new-api
ports:
- name: p3000
port: 3000
targetPort: 3000
protocol: TCP切换时做了什么
docker stop 之后、kubectl apply 工作负载之前,脚本打开磁盘上的库,只改 base_url 里的地址:
python3 - << 'PY'
import sqlite3
con = sqlite3.connect("/opt/new-api/data/one-api.db")
cur = con.cursor()
cur.execute("select id, base_url from channels")
changed = 0
for cid, url in cur.fetchall():
if url and "172.17.0.1" in url:
new = url.replace("172.17.0.1", "10.0.1.47")
cur.execute("update channels set base_url=? where id=?", (new, cid))
print("channel", cid, new)
changed += 1
con.commit()
print("channels_updated", changed)
PY然后它随 llm/workloads.json 拉起。等待循环把 :3000/api/status 的 200 和 Dify 首页的 307 绑在一起,最多约 8 分钟。这两个有一个等不到,才会回滚。
怎么确认
写本文时,:3000/api/status 是 200。渠道 vLLM-Qwen3.6-27B 的 base_url 是 http://10.0.1.47:8000,模型 qwen3.6-27b,状态为启用。令牌仍然在原来的 one-api.db 里,本文不列出密钥。
4. 导航页
它在 25 秒那一组里被停止,随 llm/workloads.json 拉起。页面文件在宿主机目录里,迁移没有改页面内容。
迁移前
容器名 llm-nav,镜像 nginx:latest,重启策略 unless-stopped。入口是 /docker-entrypoint.sh nginx -g daemon off;。宿主机 8090 映射到容器 80。/usr/cluster/llm-nav 挂到 /usr/share/nginx/html,而且是只读。链接指向本机其他服务的页面。
清单里变成什么
hostPort 写在容器端口 80 上,对外仍是 8090。只读挂载保留,Pod 改不了页面文件。
文件:config/nav.yaml。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nav
namespace: llm
labels:
app: nav
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nav
template:
metadata:
labels:
app: nav
annotations:
llm.local/compose-service: "nav"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: nav
image: nginx:latest
imagePullPolicy: IfNotPresent
command:
- "/docker-entrypoint.sh"
args:
- "nginx"
- "-g"
- "daemon off;"
envFrom:
- secretRef:
name: nav-env
ports:
- containerPort: 80
hostPort: 8090
protocol: TCP
volumeMounts:
- name: vol0
mountPath: "/usr/share/nginx/html"
readOnly: true
volumes:
- name: vol0
hostPath:
path: "/usr/cluster/llm-nav"
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: nav
namespace: llm
spec:
selector:
app: nav
ports:
- name: p80
port: 80
targetPort: 80
protocol: TCP切换时做了什么
docker stop -t 25 llm-nav
kubectl apply -f /usr/cluster/k8s/manifests/llm/workloads.json停止命令和同组其他容器是一次执行的。这里只标出这一个名字。
怎么确认
写本文时,http://127.0.0.1:8090/ 是 200。页面目录仍是 /usr/cluster/llm-nav。
5. Dify
Dify 是一套容器。下面用 Nginx 的完整清单代表「一个容器变成一个 Deployment」;其余容器用对照表。Nginx 是迁完之后唯一又改过配置的成员。
迁移前
这些容器都属于 Compose 项目 dify,重启策略是 always,镜像和数据如下。没有宿主机端口的,只在 Docker 网络里被别的容器用服务名访问。
| 原容器 | 工作负载 | 镜像 | 宿主机端口 | 容器端口 | 数据目录 | 依赖 |
|---|---|---|---|---|---|---|
dify-nginx-1 | nginx | nginx:latest | 8088→80,18443→443 | 80、443 | nginx/ 配置、证书和 certbot 目录 | api、web、api-websocket、plugin-daemon |
dify-web-1 | web | langgenius/dify-web:1.17.1 | 无 | 3000 | 无 | api |
dify-api-1 | api | langgenius/dify-api:1.17.1 | 无 | 5001 | volumes/app/storage | Postgres、Redis、pgvector、sandbox、插件 |
dify-api_websocket-1 | api-websocket | 同上 | 无 | 5001 | volumes/app/storage | 同上 |
dify-worker-1 | worker | 同上 | 无 | 5001 | volumes/app/storage | 同上 |
dify-worker_beat-1 | worker-beat | 同上 | 无 | 5001 | 无业务目录 | 同上 |
dify-plugin_daemon-1 | plugin-daemon | langgenius/dify-plugin-daemon:0.6.10-local | 5003 | 5003 | volumes/plugin_daemon | Postgres |
dify-agent_backend-1 | agent-backend | langgenius/dify-agent-backend:1.17.1 | 无 | 5050 | 无 | api、sandbox |
dify-local_sandbox-1 | local-sandbox | langgenius/dify-agent-local-sandbox:1.17.1 | 无 | 5004 | 两个 Docker 数据卷,工作区和 home | agent-ssrf-proxy |
dify-sandbox-1 | sandbox | langgenius/dify-sandbox:0.2.15 | 无 | 8194 | volumes/sandbox | ssrf-proxy |
dify-db_postgres-1 | db-postgres | postgres:15-alpine | 无 | 5432 | volumes/db/data | 无 |
dify-pgvector-1 | pgvector | pgvector/pgvector:pg16 | 无 | 5432 | volumes/pgvector/data | 无 |
dify-redis-1 | redis | redis:6-alpine | 无 | 6379 | volumes/redis/data | 无 |
dify-ssrf_proxy-1 | ssrf-proxy | ubuntu/squid:latest | 无 | 3128 | squid 配置和两个数据卷 | 无 |
dify-agent_ssrf_proxy-1 | agent-ssrf-proxy | ubuntu/squid:latest | 无 | 3128 | agent 用的 squid 配置和数据卷 | 无 |
dify-init_permissions-1 在迁移前已经退出,不在停止名单里,也没有对应的 Deployment。存储目录上的初始化标记还在 volumes/app/storage。
api、worker、api-websocket、worker-beat 都额外挂了 Langfuse 补丁。宿主机文件是 /usr/cluster/k8s/patches/langfuse_trace.py,容器路径是 /app/api/.venv/lib/python3.12/site-packages/dify_trace_langfuse/langfuse_trace.py。worker-beat 只有这一份补丁,没有 storage 目录。
数据库仍是原来的 Postgres 数据目录,用户 postgres,库名 dify。Redis 的启动参数里带有 requirepass,值留在服务器上的 Secret 里,这里不写。
对外地址是 http://10.0.1.47:8088。合同 Agent 的访问路径是 /agent/gQPVqCdcyKoW82iZ。
清单里变成什么
每个容器一个 Deployment 加一个 Service,命名空间 dify。工作负载名把下划线改成了连字符,原来的环境变量仍使用带下划线的主机名,所以才需要第 4 节的 CoreDNS rewrite。
Nginx 把配置目录挂回去,Pod 读到的就是后来改过的那份 default.conf。代表清单如下,文件是 config/dify-nginx.yaml。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: dify
labels:
app: nginx
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
annotations:
llm.local/compose-service: "nginx"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
command:
- "sh"
- "-c"
- "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh"
envFrom:
- secretRef:
name: nginx-env
ports:
- containerPort: 443
hostPort: 18443
protocol: TCP
- containerPort: 80
hostPort: 8088
protocol: TCP
volumeMounts:
- name: vol0
mountPath: "/etc/nginx/proxy.conf.template"
- name: vol1
mountPath: "/var/www/html"
- name: vol2
mountPath: "/etc/nginx/https.conf.template"
- name: vol3
mountPath: "/docker-entrypoint-mount.sh"
- name: vol4
mountPath: "/etc/letsencrypt/live"
- name: vol5
mountPath: "/etc/nginx/nginx.conf.template"
- name: vol6
mountPath: "/etc/nginx/conf.d"
- name: vol7
mountPath: "/etc/letsencrypt"
- name: vol8
mountPath: "/etc/ssl"
volumes:
- name: vol0
hostPath:
path: "/usr/cluster/dify/src/docker/nginx/proxy.conf.template"
type: File
- name: vol1
hostPath:
path: "/usr/cluster/dify/src/docker/volumes/certbot/www"
type: Directory
- name: vol2
hostPath:
path: "/usr/cluster/dify/src/docker/nginx/https.conf.template"
type: File
- name: vol3
hostPath:
path: "/usr/cluster/dify/src/docker/nginx/docker-entrypoint.sh"
type: File
- name: vol4
hostPath:
path: "/usr/cluster/dify/src/docker/volumes/certbot/conf/live"
type: Directory
- name: vol5
hostPath:
path: "/usr/cluster/dify/src/docker/nginx/nginx.conf.template"
type: File
- name: vol6
hostPath:
path: "/usr/cluster/dify/src/docker/nginx/conf.d"
type: Directory
- name: vol7
hostPath:
path: "/usr/cluster/dify/src/docker/volumes/certbot/conf"
type: Directory
- name: vol8
hostPath:
path: "/usr/cluster/dify/src/docker/nginx/ssl"
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: dify
spec:
selector:
app: nginx
ports:
- name: p443
port: 443
targetPort: 443
protocol: TCP
- name: p80
port: 80
targetPort: 80
protocol: TCPplugin-daemon 的 Deployment 只有 hostPort 5003。迁完之后给它的 Service 补了 5002,因为 Nginx 反代插件市场走 5002。现在这个 Service 同时有 p5003 和 p5002。
web 没有 volume。新 Pod 的前端来自镜像 langgenius/dify-web:1.17.1。
切换时做了什么
停止前先做了两件只和 Dify 有关的事:从 dify-worker-1 拷出 Langfuse 补丁;生成清单时把这份文件写进 api、worker、api-websocket、worker-beat 的 volume。
然后这 15 个容器进入 25 秒停止组,再随其他命名空间一起提交:
kubectl apply -f /usr/cluster/k8s/manifests/dify/workloads.jsonPod 起来之后,访问 8088 会挂住,Nginx 日志是 499。配置里的解析器还是 Docker 内嵌 DNS 127.0.0.11,Pod 里没有这个地址。改成 CoreDNS 10.96.0.10 之后出现 502,上游被解析成 http://127.0.53.53:3000/。Nginx 的 resolver 不使用 resolv.conf 的 search 域,短名 web 落到了 nss-myhostname。
CoreDNS 那组 rewrite 也帮不上这个忙:rewrite 改的是 web 这种短名不会命中的全域名,而且 Nginx 当时写的就是短名。最后改的是宿主机上的两份文件,default.conf 和 default.conf.template:
resolver 127.0.0.11 -> resolver 10.96.0.10
api:5001 -> api.dify.svc.cluster.local:5001
web:3000 -> web.dify.svc.cluster.local:3000
plugin_daemon:5002 -> plugin-daemon.dify.svc.cluster.local:5002Secret nginx-env 的 NGINX_SOCKET_IO_UPSTREAM 改为 api-websocket.dify.svc.cluster.local:5001。然后:
kubectl -n dify rollout restart deploy/nginx这些替换记在 scripts/09-修正Dify域名.sh。现网文件已经改完。脚本发现解析器已经是 10.96.0.10 且上游已经是 api.dify.svc.cluster.local:5001 时,打印 NGINX_ALREADY_OK 并退出,不再写文件。
怎么确认
写本文时,http://127.0.0.1:8088/ 是 307。这是首页跳转,说明 Nginx 已经能把请求交给 web。知识库、应用和上传文件在原来的 Postgres 与 volumes/app/storage 里。
plugin-daemon 启动过程中重启过 2 次,之后是 1/1 Running。dify-web 没有挂回「知识检索」的前端补丁,那个改动当时只在旧容器的可写层里。API 侧的 Langfuse 补丁还在,文件里有标记 # AGENT_TOOL_SPANS_V2。
Dify 里 getServerStatus 这个工具的 URL,迁移脚本没有改。库里仍可能是 http://172.17.0.1:8777。SSRF 放行包含 10.0.0.0/8,从 Pod 访问宿主机时应使用 http://10.0.1.47:8777。
6. Langfuse
Langfuse 也是一套容器。Web 没有自己的数据目录,追踪数据在 Postgres、ClickHouse 和 MinIO 里。这里贴 Web 的完整清单,其余用对照表。
迁移前
Compose 项目 langfuse,重启策略 always。
| 原容器 | 工作负载 | 镜像 | 宿主机端口 | 容器端口 | 数据目录 | 依赖 |
|---|---|---|---|---|---|---|
langfuse-langfuse-web-1 | langfuse-web | docker.langfuse.com/langfuse/langfuse:4 | 3100→3000 | 3000 | 无 | Postgres、ClickHouse、MinIO |
langfuse-langfuse-worker-1 | langfuse-worker | docker.langfuse.com/langfuse/langfuse-worker:4 | 127.0.0.1:3030 | 3030 | 无 | 同上 |
langfuse-postgres-1 | postgres | docker.io/postgres:17 | 无 | 5432 | langfuse_langfuse_postgres_data | 无 |
langfuse-clickhouse-1 | clickhouse | docker.io/clickhouse/clickhouse-server:25.12 | 无 | 8123、9000、9009 | 数据和日志两个卷 | 无 |
langfuse-minio-1 | minio | docker.io/minio/minio:latest | 9092→9000 | 9000 | langfuse_langfuse_minio_data | 无 |
langfuse-redis-1 | redis | docker.io/redis:7 | 无 | 6379 | langfuse_langfuse_redis_data | 无 |
这些数据卷都在 /usr/cluster/docker/volumes/<卷名>/_data。Worker 的 3030 只绑在 127.0.0.1,生成器把 hostIP: 127.0.0.1 留在了 Pod 上。
ClickHouse 原来没有发布到宿主机。生成器看到 PortBindings 是空的,就到容器里看正在听的端口,写成 Service 的 8123、9000、9009,不设 hostPort。集群里的 Web 和 Worker 通过 Service 名访问它。
Redis 启动参数里带有 requirepass。MinIO 和 Langfuse 的加密项在各自的 Secret 里。这些值不写入本文。
清单里变成什么
Web 可以换成一个新容器,因为会话和追踪不在它的可写层里。入口仍是 dumb-init -- ./web/entrypoint.sh,参数仍按有没有 NEXT_PUBLIC_LANGFUSE_CLOUD_REGION 选择要不要加载 dd-trace。
文件:config/langfuse-web.yaml。
apiVersion: apps/v1
kind: Deployment
metadata:
name: langfuse-web
namespace: langfuse
labels:
app: langfuse-web
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: langfuse-web
template:
metadata:
labels:
app: langfuse-web
annotations:
llm.local/compose-service: "langfuse-web"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: langfuse-web
image: docker.langfuse.com/langfuse/langfuse:4
imagePullPolicy: IfNotPresent
command:
- "dumb-init"
- "--"
- "./web/entrypoint.sh"
args:
- "/bin/sh"
- "-c"
- "if [ -n \"$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION\" ]; then node --import dd-trace/initialize.mjs ./web/server.js --keepAliveTimeout 110000; else node ./web/server.js --keepAliveTimeout 110000; fi"
envFrom:
- secretRef:
name: langfuse-web-env
ports:
- containerPort: 3000
hostPort: 3100
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: langfuse-web
namespace: langfuse
spec:
selector:
app: langfuse-web
ports:
- name: p3000
port: 3000
targetPort: 3000
protocol: TCPPostgres、ClickHouse、MinIO、Redis 各自把原来的卷挂回原来的容器路径。Web 和 Worker 不挂这些卷,它们通过环境变量里的主机名访问 Service。
切换时做了什么
六个容器都在 25 秒停止组里。Secret 和 Service 在停止前已经提交,工作负载在停止后提交:
kubectl apply -f /usr/cluster/k8s/manifests/langfuse/workloads.json没有单独改 Langfuse 的配置文件。Dify 能继续把追踪送到 10.0.1.47:3100,是因为 hostPort 没变,而且 Web 背后的数据库卷没换。
怎么确认
写本文时,http://127.0.0.1:3100/api/public/health 是 200。Postgres 数据卷仍是 langfuse_langfuse_postgres_data,ClickHouse 和 MinIO 的卷也还是迁移前那几个目录。
7. Prometheus、Grafana、DCGM
这三个容器在 25 秒停止组里,命名空间是 observability。真正改过内容的只有 Prometheus 的抓取目标。
迁移前
| 原容器 | 工作负载 | 镜像 | 宿主机端口 | 容器端口 | 数据目录 | 依赖 |
|---|---|---|---|---|---|---|
prometheus | prometheus | prom/prometheus:v3.2.1 | 9090 | 9090 | 配置文件和 observability_prometheus_data | 要抓 vLLM、DCGM |
grafana | grafana | grafana/grafana:11.5.2 | 3001→3000 | 3000 | provisioning、仪表盘目录和 observability_grafana_data | Prometheus |
dcgm-exporter | dcgm-exporter | nvidia/dcgm-exporter:3.3.8-3.6.0-ubuntu22.04 | 9400 | 9400 | 无 | 节点上的 GPU |
三者的重启策略都是 unless-stopped。Prometheus 的参数是 --config.file=/etc/prometheus/prometheus.yml、--storage.tsdb.path=/prometheus、保留 15 天。Grafana 的入口是 /run.sh。DCGM 的入口是 /usr/local/dcgm/dcgm-exporter-entrypoint.sh,没有宿主机目录。
迁移前,Prometheus 用 host.docker.internal:8000 找 vLLM。那个名字在 Docker 里由宿主机映射提供。Pod 里如果只靠这个名字,要另外写 hostAliases。脚本改的是配置文件里的地址,让抓取目标直接写成节点 IP。
清单里变成什么
三份清单分别是 config/prometheus.yaml、config/grafana.yaml、config/dcgm-exporter.yaml。抓取配置本身仍是 config/prometheus.yml,和服务器上 /usr/cluster/observability/metrics/prometheus.yml 是同一份内容。
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
namespace: observability
labels:
app: prometheus
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
annotations:
llm.local/compose-service: "prometheus"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: prometheus
image: prom/prometheus:v3.2.1
imagePullPolicy: IfNotPresent
command:
- "/bin/prometheus"
args:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=15d"
envFrom:
- secretRef:
name: prometheus-env
ports:
- containerPort: 9090
hostPort: 9090
protocol: TCP
volumeMounts:
- name: vol0
mountPath: "/etc/prometheus/prometheus.yml"
readOnly: true
- name: vol1
mountPath: "/prometheus"
volumes:
- name: vol0
hostPath:
path: "/usr/cluster/observability/metrics/prometheus.yml"
type: File
- name: vol1
hostPath:
path: "/usr/cluster/docker/volumes/observability_prometheus_data/_data"
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: observability
spec:
selector:
app: prometheus
ports:
- name: p9090
port: 9090
targetPort: 9090
protocol: TCPapiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: observability
labels:
app: grafana
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
annotations:
llm.local/compose-service: "grafana"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: grafana
image: grafana/grafana:11.5.2
imagePullPolicy: IfNotPresent
command:
- "/run.sh"
envFrom:
- secretRef:
name: grafana-env
ports:
- containerPort: 3000
hostPort: 3001
protocol: TCP
volumeMounts:
- name: vol0
mountPath: "/etc/grafana/provisioning/dashboards"
readOnly: true
- name: vol1
mountPath: "/var/lib/grafana/dashboards"
readOnly: true
- name: vol2
mountPath: "/var/lib/grafana"
- name: vol3
mountPath: "/etc/grafana/provisioning/datasources"
readOnly: true
volumes:
- name: vol0
hostPath:
path: "/usr/cluster/observability/metrics/grafana/provisioning/dashboards"
type: Directory
- name: vol1
hostPath:
path: "/usr/cluster/observability/metrics/grafana/dashboards"
type: Directory
- name: vol2
hostPath:
path: "/usr/cluster/docker/volumes/observability_grafana_data/_data"
type: Directory
- name: vol3
hostPath:
path: "/usr/cluster/observability/metrics/grafana/provisioning/datasources"
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: observability
spec:
selector:
app: grafana
ports:
- name: p3000
port: 3000
targetPort: 3000
protocol: TCPapiVersion: apps/v1
kind: Deployment
metadata:
name: dcgm-exporter
namespace: observability
labels:
app: dcgm-exporter
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: dcgm-exporter
template:
metadata:
labels:
app: dcgm-exporter
annotations:
llm.local/compose-service: "dcgm-exporter"
spec:
nodeName: prod102
enableServiceLinks: false
hostAliases:
- ip: 10.0.1.47
hostnames: ["host.docker.internal"]
containers:
- name: dcgm-exporter
image: nvidia/dcgm-exporter:3.3.8-3.6.0-ubuntu22.04
imagePullPolicy: IfNotPresent
command:
- "/usr/local/dcgm/dcgm-exporter-entrypoint.sh"
envFrom:
- secretRef:
name: dcgm-exporter-env
ports:
- containerPort: 9400
hostPort: 9400
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: dcgm-exporter
namespace: observability
spec:
selector:
app: dcgm-exporter
ports:
- name: p9400
port: 9400
targetPort: 9400
protocol: TCPGrafana 的 provisioning 和仪表盘目录是只读挂载,数据目录 observability_grafana_data 可写,所以账号和已保存的仪表盘还在。管理员口令在 Secret grafana-env 里。DCGM 不挂目录,GPU 指标靠镜像里的导出器和 Secret dcgm-exporter-env 中的 NVIDIA_VISIBLE_DEVICES。
Prometheus 现在的目标:
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: dcgm
static_configs:
- targets: ["dcgm-exporter:9400"]
- job_name: vllm
metrics_path: /metrics
static_configs:
- targets: ["10.0.1.47:8000"]dcgm-exporter:9400 能解析,是因为 Grafana 和 Prometheus 在同一个命名空间,短名会落到 observability.svc.cluster.local。Dify 的 Nginx 不能靠这个机制,它的 resolver 不读 search 域,所以上一章才改成全域名。
切换时做了什么
配置文件在容器还活着的时候就改好了。随后这三个容器进入 25 秒停止组,再提交:
kubectl apply -f /usr/cluster/k8s/manifests/observability/workloads.json新 Prometheus 一启动就读到已经改过的 yaml,时序数据仍写在原来的数据卷里。
怎么确认
写本文时,:9090/-/ready 和 :3001/api/health 都是 200。vLLM 的 /metrics 返回 200。DCGM 的 Service 端口是 9400。
三、功能操作案例
下面三件事跨着多个服务。产品章里已经写过各自的结果,这里把它们串成可以对照着做的步骤。现网已经是迁移之后的状态。
案例 1:一次看完端口、数据目录和健康检查
在服务器上执行:
export KUBECONFIG=/root/.kube/config
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl get svc -A节点应是 prod102、Ready、v1.37.1,运行时 docker://28.0.1。业务 Pod 在 llm、dify、langfuse、observability。docker ps 里不应再有第 1 节那些业务容器;docker ps -a 里它们是 Exited,名字与 /usr/cluster/k8s/stopped-containers.txt 一致。
抽查数据目录有没有被换成空卷:
kubectl -n llm get deploy vllm -o jsonpath='{.spec.template.spec.volumes}'
kubectl -n llm get deploy new-api -o jsonpath='{.spec.template.spec.volumes}'
kubectl -n dify get deploy db-postgres -o jsonpath='{.spec.template.spec.volumes}'
kubectl -n langfuse get deploy postgres -o jsonpath='{.spec.template.spec.volumes}'应当看到 /models/Qwen3.6-27B、/opt/new-api/data、/usr/cluster/dify/src/docker/volumes/db/data,以及 Langfuse 的 langfuse_langfuse_postgres_data。
然后看接口。期望写在每行命令旁边:
curl -s -o /dev/null -w 'new-api %{http_code}\n' http://127.0.0.1:3000/api/status
curl -s -o /dev/null -w 'vllm %{http_code}\n' http://127.0.0.1:8000/health
curl -s -o /dev/null -w 'models %{http_code}\n' http://127.0.0.1:8000/v1/models
curl -s -o /dev/null -w 'dify %{http_code}\n' http://127.0.0.1:8088/
curl -s -o /dev/null -w 'langfuse %{http_code}\n' http://127.0.0.1:3100/api/public/health
curl -s -o /dev/null -w 'grafana %{http_code}\n' http://127.0.0.1:3001/api/health
curl -s -o /dev/null -w 'prometheus %{http_code}\n' http://127.0.0.1:9090/-/ready
curl -s -o /dev/null -w 'nav %{http_code}\n' http://127.0.0.1:8090/
curl -s -o /dev/null -w 'xinference %{http_code}\n' http://127.0.0.1:9997/v1/models
curl -s -o /dev/null -w 'host80 %{http_code}\n' http://127.0.0.1:80/New-API、vLLM、Langfuse、Grafana、Prometheus、导航页期望 200。Dify 期望 307。Xinference 期望 401。宿主机 80 期望 200,它没有进集群。
server-status 仍由宿主机 systemd 提供。请求 http://127.0.0.1:8777/ 得到 404,因为根路径本来就没有处理函数。状态接口是 http://127.0.0.1:8777/server/status?server_name=gpu-server-01。
页面入口:Dify http://10.0.1.47:8088,合同 Agent http://10.0.1.47:8088/agent/gQPVqCdcyKoW82iZ,导航 http://10.0.1.47:8090,Langfuse http://10.0.1.47:3100,Grafana http://10.0.1.47:3001。
案例 2:把 Nginx 的两次失败当成排障步骤
这一段对应 Dify 那一章的切换。现网已经改完,scripts/09-修正Dify域名.sh 再执行会停在 NGINX_ALREADY_OK。
- 看 Nginx 日志。解析器若还是
127.0.0.11,请求会挂住,状态 499。Pod 里没有 Docker 的内嵌 DNS。 - 把
default.conf和default.conf.template的解析器改为resolver 10.96.0.10 valid=30s ipv6=off;,重启 Nginx。CoreDNS 的 ClusterIP 是10.96.0.10。 - 若上游变成
127.0.53.53,说明短名没有走集群 DNS。把api:5001、web:3000、plugin_daemon:5002换成*.dify.svc.cluster.local的全域名。两份文件都要改,否则下次渲染模板会盖回去。 - 确认
plugin-daemonService 同时有 5002 和 5003。WebSocket 上游是api-websocket.dify.svc.cluster.local:5001,这个值在 Secretnginx-env的NGINX_SOCKET_IO_UPSTREAM。 - 再请求
http://127.0.0.1:8088/,期望 307。
CoreDNS 里的下划线 rewrite 解决的是应用环境变量里的主机名。Nginx 这处失败要改的是它自己的配置文件。两处都要在,Dify 里的服务名才能对上。
案例 3:回滚命令
只在确定要回到原来的容器时使用。先确认 /usr/cluster/k8s/stopped-containers.txt 里有原来的 28 个容器名。现网没有走过这次回滚。
export KUBECONFIG=/root/.kube/config
MAN=/usr/cluster/k8s/manifests
kubectl delete -f "$MAN/llm/workloads.json" --ignore-not-found
kubectl delete -f "$MAN/dify/workloads.json" --ignore-not-found
kubectl delete -f "$MAN/langfuse/workloads.json" --ignore-not-found
kubectl delete -f "$MAN/observability/workloads.json" --ignore-not-found
while read -r name; do
[ -n "$name" ] || continue
docker start "$name" || true
done < /usr/cluster/k8s/stopped-containers.txt删除工作负载之后,Pod 释放 hostPort,原来的容器重新占用 3000、8000、8088、3100、9090、3001、9400、8090、9997。Service 和 Secret 还在的话,不会把数据目录删掉;数据本来就在宿主机路径上。
迁移脚本里的自动回滚只在 New-API 没到 200 或 Dify 没到 307 时触发。这次没有触发。最后的 MIGRATE_PARTIAL 来自 Xinference 的 401,当时 Pod 保持运行。
若 Docker 守护进程先被重启,而重启策略为 always 的旧容器已经起来,它们会先占住 hostPort,新 Pod 会起不来。那时候要先停掉这些 Exited 之后又被拉起的容器,再处理 Pod。
写文档时核对到的数据
- 节点
prod102Ready,Kubernetes v1.37.1,运行时 docker://28.0.1,地址 10.0.1.47。 - CNI 文件
/etc/cni/net.d/10-bridge.conflist,网桥cni0,子网 10.244.0.0/24。 /usr/cluster/k8s/stopped-containers.txt有 28 个容器名,状态为 Exited。- 业务 Pod 在
llm、dify、langfuse、observability。清单使用envFrom指向<工作负载>-env。 - Dify Nginx 解析器是
10.96.0.10,上游是*.dify.svc.cluster.local。plugin-daemonService 有 5002 和 5003。 - New-API 渠道
vLLM-Qwen3.6-27B的地址是http://10.0.1.47:8000。 - Prometheus 的 vLLM 目标是
10.0.1.47:8000,DCGM 目标是dcgm-exporter:9400。 - 健康检查:New-API、vLLM、Langfuse、Grafana、Prometheus、导航页为 200,Dify 为 307,Xinference 为 401,宿主机 80 为 200。
- Xinference 的 401 使迁移脚本记为
MIGRATE_PARTIAL,没有执行回滚。