Ubuntu 单机从驱动装到 Kubernetes:H20 上跑 vLLM、Dify 和监控
目录
本文按一台刚装好的 Ubuntu 22.04.5 来写。机器就是这台学习机:主机名 prod102,地址 10.0.1.47,内核 5.15.0-130-generic,数据盘 /usr/cluster,显卡 NVIDIA H20 550.120。读者从驱动和 Docker 开始,装好 Kubernetes 1.37.1,再把 vLLM、Xinference、New-API、Dify、Langfuse、Prometheus、Grafana、DCGM 和导航页放进这个单节点。
写文档时这台机器已经在跑。下面的脚本会在发现现有集群或现有容器时直接退出。不要在现网上再执行一遍。
登录密码、数据库密码和 dify-gateway 令牌按现网值写在本文。Langfuse 的 NEXTAUTH_SECRET、SALT、ENCRYPTION_KEY、Redis 口令、MinIO 口令和 kubeadm 加入令牌不写。合同原文不放进仓库。
一、安装配置
1. 目标
浏览器打开 Dify。对话从 Dify 到 New-API,再到 vLLM 的 qwen3.6-27b。知识库向量和重排走 Xinference 的 bge-m3、bge-reranker-v2-m3。追踪进 Langfuse。显卡进 Prometheus 和 Grafana。
四个命名空间:
| 命名空间 | 工作负载 |
|---|---|
llm | vLLM、Xinference、New-API、导航页 |
dify | Dify 1.17.1 全套 |
langfuse | Langfuse 4、Postgres、ClickHouse、Redis、MinIO |
observability | Prometheus、Grafana、DCGM Exporter |
两张卡共用这一张 H20。Pod 里设置 NVIDIA_VISIBLE_DEVICES=all,靠 Docker 的 nvidia 运行时看见显卡。清单里不写 nvidia.com/gpu,否则没有时间分片的设备插件会把第二份 GPU 工作负载卡住。
getServerStatus 留在宿主机 systemd,监听 0.0.0.0:8777。Dify 工具地址用 http://10.0.1.47:8777。宿主机 80 端口的 Nginx 不属于这套大模型服务。
2. 系统、驱动和目录
核对过的版本:驱动 nvidia-driver-550 550.120-0ubuntu0.22.04.1,nvidia-container-toolkit 1.17.8-1。nvidia-smi 看到 NVIDIA H20,显存 97871 MiB。
/models 是指向 /usr/cluster/models 的符号链接。Docker 数据根在 /usr/cluster/docker。根分区只留系统,模型和镜像都放数据盘。
文件:scripts/01-准备系统.sh
#!/bin/bash
# 空白的 Ubuntu 22.04 使用。不要在已经跑着业务的 prod102 上再执行。
set -eu
export DEBIAN_FRONTEND=noninteractive
if [ "$(hostname)" = "prod102" ] && [ -f /root/.kube/config ]; then
echo "prod102 上已经有集群。这份脚本是给空机器的,停在这里。"
exit 1
fi
apt-get update
apt-get install -y ca-certificates curl gnupg python3
apt-get install -y nvidia-driver-550
if ! nvidia-smi >/dev/null 2>&1; then
echo "驱动已安装。重启后再继续,直到 nvidia-smi 能看到 NVIDIA H20。"
exit 0
fi
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
> /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt-get update
apt-get install -y nvidia-container-toolkit=1.17.8-1
modprobe br_netfilter || true
cat > /etc/modules-load.d/k8s.conf << 'EOF'
br_netfilter
EOF
cat > /etc/sysctl.d/k8s.conf << 'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system >/dev/null
mkdir -p /usr/cluster/models /usr/cluster/docker /opt/new-api/data /opt/new-api/logs \
/usr/cluster/dify/xinference /usr/cluster/server-status /usr/cluster/llm-nav \
/usr/cluster/observability/metrics
ln -sfn /usr/cluster/models /models
echo PREP_OK驱动装完如果 nvidia-smi 还看不见卡,先重启,再跑后面的脚本。
3. Docker
服务器上的 Docker 是 28.0.1。docker info 里数据根是 /usr/cluster/docker,cgroup 驱动是 systemd,默认运行时是 nvidia。/etc/docker/daemon.json 里目前有四条镜像加速。空白机器要把数据根和 nvidia 运行时一起写进去,否则数据会落到 /var/lib/docker。
文件:scripts/02-安装Docker.sh
#!/bin/bash
# 空白机器安装 Docker 28.0.1。数据放在 /usr/cluster,默认运行时用 nvidia。
# 不要在已经跑着业务的 prod102 上执行。
set -eu
export DEBIAN_FRONTEND=noninteractive
if [ "$(hostname)" = "prod102" ] && [ -S /var/run/docker.sock ]; then
echo "prod102 上 Docker 已经在跑。这份脚本是给空机器的,停在这里。"
exit 1
fi
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update
ver=$(apt-cache madison docker-ce | awk '/28.0.1/{print $3; exit}')
if [ -z "$ver" ]; then
echo "apt 里没有 docker-ce 28.0.1"
exit 1
fi
apt-get install -y "docker-ce=$ver" "docker-ce-cli=$ver" containerd.io
mkdir -p /usr/cluster/docker /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
"data-root": "/usr/cluster/docker",
"exec-opts": ["native.cgroupdriver=systemd"],
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime"
}
},
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me",
"https://docker.m.daocloud.io",
"https://hub-mirror.c.163.com"
]
}
EOF
systemctl enable docker
systemctl restart docker
docker info --format 'root={{.DockerRootDir}} cgroup={{.CgroupDriver}} runtime={{.DefaultRuntime}}'
echo DOCKER_OK装完核对这三行:
root=/usr/cluster/docker cgroup=systemd runtime=nvidia4. 模型权重
权重目录 /models/Qwen3.6-27B。里面有 ModelScope 的 configuration.json,也有 Hugging Face 模型卡。模型 ID 是 Qwen/Qwen3.6-27B,config.json 里的架构是 Qwen3_5ForConditionalGeneration。
文件:scripts/04-下载模型.sh
#!/bin/bash
# 把 Qwen3.6-27B 下到 /models。目录里已有 ModelScope 的 configuration.json
# 和 Hugging Face 模型卡。不要在权重已经齐的机器上再下一份。
set -eu
if [ -f /models/Qwen3.6-27B/config.json ]; then
echo "权重已在 /models/Qwen3.6-27B"
exit 0
fi
mkdir -p /usr/cluster/models
ln -sfn /usr/cluster/models /models
pip3 install -U modelscope
modelscope download --model Qwen/Qwen3.6-27B --local_dir /models/Qwen3.6-27B
test -f /models/Qwen3.6-27B/config.json
echo MODEL_OKvLLM 挂载这个目录到容器内 /model,并设置 HF_HUB_OFFLINE=1,启动后不再访问外网拉权重。
5. Kubernetes 单节点
版本:kubelet / kubeadm / kubectl 1.37.1-1.1,cri-dockerd 0.4.8。接口 https://10.0.1.47:6443,只绑在这块网卡上。kubeconfig 在 /root/.kube/config,权限 600。使用 kubectl 前要带上 KUBECONFIG=/root/.kube/config。
容器运行时走现有 Docker,socket 是 unix:///run/cri-dockerd.sock。pause 镜像用阿里云的 registry.aliyuncs.com/google_containers/pause:3.10.2,并打上 registry.k8s.io/pause:3.10 这个标签。这个标签和 drop-in 要在 kubeadm init 之前就位。
Pod 网段在 kubeadm 里写成 10.244.0.0/16。本机网桥实际分配 10.244.0.0/24,网关 10.244.0.1,网桥名 cni0。Service 网段 10.96.0.0/12,CoreDNS 是 10.96.0.10。节点名 prod102,装完后去掉 control-plane 的 NoSchedule 污点,业务 Pod 才能调度上来。网络用这份网桥配置,安装脚本里不部署 Flannel。
文件:scripts/03-安装单节点.sh
#!/bin/bash
# 空白机器安装 Kubernetes 1.37.1 单节点。
# 运行时是 cri-dockerd 0.4.8,网络是本机网桥。不要再装 Flannel。
# 不要在已经跑着业务的 prod102 上执行。
set -eu
export DEBIAN_FRONTEND=noninteractive
export KUBECONFIG=/root/.kube/config
if [ "$(hostname)" = "prod102" ] && [ -f /etc/kubernetes/admin.conf ]; then
echo "prod102 上控制面已经在。这份脚本是给空机器的,停在这里。"
exit 1
fi
KVER=1.37.1-1.1
IMG_REPO=registry.aliyuncs.com/google_containers
WORKDIR=/usr/cluster/k8s
mkdir -p "$WORKDIR" /etc/cni/net.d /opt/cni/bin
echo "=== cri-dockerd 0.4.8 ==="
if ! /usr/local/bin/cri-dockerd --version 2>/dev/null | grep -q '0.4.8'; then
curl -fL --retry 3 -o "$WORKDIR/cri-dockerd-0.4.8.amd64.tgz" \
https://github.com/Mirantis/cri-dockerd/releases/download/v0.4.8/cri-dockerd-0.4.8.amd64.tgz
tar -xzf "$WORKDIR/cri-dockerd-0.4.8.amd64.tgz" -C "$WORKDIR"
install -m 0755 "$WORKDIR/cri-dockerd/cri-dockerd" /usr/local/bin/cri-dockerd
fi
if [ ! -f /lib/systemd/system/cri-docker.service ] && [ ! -f /etc/systemd/system/cri-docker.service ]; then
cat > /etc/systemd/system/cri-docker.socket << 'EOF'
[Unit]
Description=CRI Docker Socket for the API
PartOf=cri-docker.service
[Socket]
ListenStream=%t/cri-dockerd.sock
SocketMode=0660
SocketUser=root
SocketGroup=root
[Install]
WantedBy=sockets.target
EOF
cat > /etc/systemd/system/cri-docker.service << 'EOF'
[Unit]
Description=CRI Interface for Docker Application Container Engine
After=network-online.target docker.service
Wants=network-online.target
Requires=cri-docker.socket
[Service]
Type=notify
ExecStart=/usr/local/bin/cri-dockerd --container-runtime-endpoint fd://
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
Delegate=yes
KillMode=process
[Install]
WantedBy=multi-user.target
EOF
fi
systemctl mask cri-dockerd.service >/dev/null 2>&1 || true
systemctl daemon-reload
systemctl enable cri-docker.socket cri-docker.service
echo "=== pause 镜像,写在 kubeadm init 之前 ==="
docker pull "$IMG_REPO/pause:3.10.2"
docker tag "$IMG_REPO/pause:3.10.2" registry.k8s.io/pause:3.10
mkdir -p /etc/systemd/system/cri-docker.service.d
cat > /etc/systemd/system/cri-docker.service.d/pause.conf << 'EOF'
[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
EOF
systemctl daemon-reload
systemctl restart cri-docker.socket
systemctl restart cri-docker.service
echo "=== kubeadm 1.37.1 ==="
install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.37/deb/Release.key \
| gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
cat > /etc/apt/sources.list.d/kubernetes.list << 'EOF'
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.37/deb/ /
EOF
apt-get update
systemctl stop kubelet || true
systemctl disable kubelet || true
apt-get install -y -o Dpkg::Options::=--force-confnew \
kubelet="$KVER" kubeadm="$KVER" kubectl="$KVER" conntrack socat
apt-mark hold kubelet kubeadm kubectl
systemctl unmask kubelet || true
cat > "$WORKDIR/kubeadm-config.yaml" << 'EOF'
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 10.0.1.47
bindPort: 6443
nodeRegistration:
criSocket: unix:///run/cri-dockerd.sock
imagePullPolicy: IfNotPresent
name: prod102
kubeletExtraArgs:
- name: node-ip
value: 10.0.1.47
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.37.1
imageRepository: registry.aliyuncs.com/google_containers
controlPlaneEndpoint: 10.0.1.47:6443
networking:
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
apiServer:
certSANs:
- 10.0.1.47
- prod102
extraArgs:
- name: bind-address
value: 10.0.1.47
EOF
echo "=== 网桥,写在 init 之前,节点才能 Ready ==="
cat > /etc/cni/net.d/10-bridge.conflist << 'EOF'
{
"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}
}
]
}
EOF
kubeadm config images pull --config "$WORKDIR/kubeadm-config.yaml"
kubeadm init --config "$WORKDIR/kubeadm-config.yaml" --ignore-preflight-errors=FileExisting-tc
mkdir -p /root/.kube
cp /etc/kubernetes/admin.conf /root/.kube/config
chmod 600 /root/.kube/config
systemctl restart kubelet
kubectl taint nodes prod102 node-role.kubernetes.io/control-plane:NoSchedule- || true
kubectl wait --for=condition=Ready node/prod102 --timeout=180s
kubectl get nodes -o wide
echo INSTALL_OK节点 Ready 之后,kubectl get node 应看到 prod102、v1.37.1、docker://28.0.1。
6. 工作负载
共同约定:strategy: Recreate,nodeName: prod102,imagePullPolicy: IfNotPresent,enableServiceLinks: false。需要访问宿主机时,hostAliases 把 host.docker.internal 指到 10.0.1.47。对外端口用 hostPort,和原来的端口保持一致。vLLM 不设就绪探针,模型加载要几分钟。
文件:scripts/05-部署组件.sh、config/namespaces.yaml、config/llm.yaml、config/observability.yaml、config/dify-nginx.yaml
#!/bin/bash
# 空白机器上创建命名空间和工作负载。不要在现网 prod102 上执行。
set -eu
export KUBECONFIG=/root/.kube/config
ROOT=$(cd "$(dirname "$0")/.." && pwd)
if kubectl get deploy -n llm vllm >/dev/null 2>&1; then
echo "llm/vllm 已经在。这份脚本是给空集群的,停在这里。"
exit 1
fi
kubectl apply -f "$ROOT/config/namespaces.yaml"
kubectl apply -f "$ROOT/config/llm.yaml"
kubectl apply -f "$ROOT/config/observability.yaml"
kubectl apply -f "$ROOT/config/dify-nginx.yaml"
echo APPLY_OK
echo "Dify 和 Langfuse 的其余工作负载见本文的服务表。数据库口令在 config/数据库口令.env。"
echo "Langfuse 加密密钥和 Redis 口令不要写进仓库,在服务器上生成后做成 Secret。"apiVersion: v1
kind: Namespace
metadata:
name: llm
---
apiVersion: v1
kind: Namespace
metadata:
name: dify
---
apiVersion: v1
kind: Namespace
metadata:
name: langfuse
---
apiVersion: v1
kind: Namespace
metadata:
name: observabilityapiVersion: 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
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"
env:
- name: HF_HUB_OFFLINE
value: "1"
- name: NVIDIA_VISIBLE_DEVICES
value: all
- name: NVIDIA_DRIVER_CAPABILITIES
value: all
ports:
- containerPort: 8000
hostPort: 8000
volumeMounts:
- name: model
mountPath: /model
- name: dshm
mountPath: /dev/shm
volumes:
- name: model
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
---
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
spec:
nodeName: prod102
enableServiceLinks: false
containers:
- name: xinference
image: xprobe/xinference:latest
imagePullPolicy: IfNotPresent
command: ["xinference-local", "-H", "0.0.0.0", "--log-level", "info"]
env:
- name: XINFERENCE_HOME
value: /data
- name: XINFERENCE_MODEL_SRC
value: modelscope
- name: NVIDIA_VISIBLE_DEVICES
value: all
- name: NVIDIA_DRIVER_CAPABILITIES
value: all
ports:
- containerPort: 9997
hostPort: 9997
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
hostPath:
path: /usr/cluster/dify/xinference
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: xinference
namespace: llm
spec:
selector:
app: xinference
ports:
- name: p9997
port: 9997
targetPort: 9997
---
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
spec:
nodeName: prod102
enableServiceLinks: false
containers:
- name: new-api
image: calciumion/new-api:latest
imagePullPolicy: IfNotPresent
command: ["/new-api"]
args: ["--log-dir", "/app/logs"]
ports:
- containerPort: 3000
hostPort: 3000
volumeMounts:
- name: data
mountPath: /data
- name: logs
mountPath: /app/logs
volumes:
- name: data
hostPath:
path: /opt/new-api/data
type: DirectoryOrCreate
- name: logs
hostPath:
path: /opt/new-api/logs
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: new-api
namespace: llm
spec:
selector:
app: new-api
ports:
- name: p3000
port: 3000
targetPort: 3000
---
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
spec:
nodeName: prod102
containers:
- name: nav
image: nginx:latest
imagePullPolicy: IfNotPresent
command: ["/docker-entrypoint.sh"]
args: ["nginx", "-g", "daemon off;"]
ports:
- containerPort: 80
hostPort: 8090
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
hostPath:
path: /usr/cluster/llm-nav
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: nav
namespace: llm
spec:
selector:
app: nav
ports:
- name: p80
port: 80
targetPort: 80apiVersion: 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
spec:
nodeName: prod102
enableServiceLinks: false
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
ports:
- containerPort: 9090
hostPort: 9090
volumeMounts:
- name: config
mountPath: /etc/prometheus/prometheus.yml
subPath: prometheus.yml
- name: data
mountPath: /prometheus
volumes:
- name: config
hostPath:
path: /usr/cluster/observability/metrics/prometheus.yml
type: File
- name: data
hostPath:
path: /usr/cluster/docker/volumes/observability_prometheus_data/_data
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: prometheus
namespace: observability
spec:
selector:
app: prometheus
ports:
- name: p9090
port: 9090
targetPort: 9090
---
apiVersion: 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
spec:
nodeName: prod102
enableServiceLinks: false
containers:
- name: grafana
image: grafana/grafana:11.5.2
imagePullPolicy: IfNotPresent
command: ["/run.sh"]
env:
- name: GF_SECURITY_ADMIN_USER
value: admin
- name: GF_SECURITY_ADMIN_PASSWORD
value: Gf-gpu-2026
- name: GF_SERVER_ROOT_URL
value: http://10.0.1.47:3001
ports:
- containerPort: 3000
hostPort: 3001
volumeMounts:
- name: datasources
mountPath: /etc/grafana/provisioning/datasources
- name: dashboards-prov
mountPath: /etc/grafana/provisioning/dashboards
- name: dashboards
mountPath: /var/lib/grafana/dashboards
- name: data
mountPath: /var/lib/grafana
volumes:
- name: datasources
hostPath:
path: /usr/cluster/observability/metrics/grafana/provisioning/datasources
type: Directory
- name: dashboards-prov
hostPath:
path: /usr/cluster/observability/metrics/grafana/provisioning/dashboards
type: Directory
- name: dashboards
hostPath:
path: /usr/cluster/observability/metrics/grafana/dashboards
type: Directory
- name: data
hostPath:
path: /usr/cluster/docker/volumes/observability_grafana_data/_data
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: observability
spec:
selector:
app: grafana
ports:
- name: p3000
port: 3000
targetPort: 3000
---
apiVersion: 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
spec:
nodeName: prod102
enableServiceLinks: false
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"]
env:
- name: NVIDIA_VISIBLE_DEVICES
value: all
- name: NVIDIA_DRIVER_CAPABILITIES
value: all
ports:
- containerPort: 9400
hostPort: 9400
---
apiVersion: v1
kind: Service
metadata:
name: dcgm-exporter
namespace: observability
spec:
selector:
app: dcgm-exporter
ports:
- name: p9400
port: 9400
targetPort: 9400apiVersion: 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
spec:
nodeName: prod102
enableServiceLinks: false
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
command: ["sh", "-c"]
args:
- "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\\r$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh"
ports:
- containerPort: 80
hostPort: 8088
- containerPort: 443
hostPort: 18443
volumeMounts:
- name: entry
mountPath: /docker-entrypoint-mount.sh
- name: confd
mountPath: /etc/nginx/conf.d
- name: nginx-conf
mountPath: /etc/nginx/nginx.conf.template
- name: proxy-conf
mountPath: /etc/nginx/proxy.conf.template
- name: https-conf
mountPath: /etc/nginx/https.conf.template
- name: ssl
mountPath: /etc/ssl
- name: certbot-conf
mountPath: /etc/letsencrypt
- name: certbot-live
mountPath: /etc/letsencrypt/live
- name: certbot-www
mountPath: /var/www/html
volumes:
- name: entry
hostPath:
path: /usr/cluster/dify/src/docker/nginx/docker-entrypoint.sh
type: File
- name: confd
hostPath:
path: /usr/cluster/dify/src/docker/nginx/conf.d
type: Directory
- name: nginx-conf
hostPath:
path: /usr/cluster/dify/src/docker/nginx/nginx.conf.template
type: File
- name: proxy-conf
hostPath:
path: /usr/cluster/dify/src/docker/nginx/proxy.conf.template
type: File
- name: https-conf
hostPath:
path: /usr/cluster/dify/src/docker/nginx/https.conf.template
type: File
- name: ssl
hostPath:
path: /usr/cluster/dify/src/docker/nginx/ssl
type: Directory
- name: certbot-conf
hostPath:
path: /usr/cluster/dify/src/docker/volumes/certbot/conf
type: DirectoryOrCreate
- name: certbot-live
hostPath:
path: /usr/cluster/dify/src/docker/volumes/certbot/conf/live
type: DirectoryOrCreate
- name: certbot-www
hostPath:
path: /usr/cluster/dify/src/docker/volumes/certbot/www
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: dify
spec:
selector:
app: nginx
ports:
- name: p80
port: 80
targetPort: 80
- name: p443
port: 443
targetPort: 443vLLM 参数:bfloat16,上下文 65536,gpu-memory-utilization 0.70,max-num-seqs 8,服务名 qwen3.6-27b,共享内存 8Gi。/dev/shm 用 Memory 类型的 emptyDir。
Xinference 第一次启动要初始化管理员,用户名 admin,密码 Xinf-bge-2026。然后在容器里安装 sentence-transformers:
kubectl -n llm exec deploy/xinference -- pip install -i https://pypi.tuna.tsinghua.edu.cn/simple sentence-transformers加载两个模型时 n_gpu 写 1,download_hub 写 modelscope。不带令牌访问 /v1/models 会返回 401,这表示进程在、接口要求登录。
POST http://10.0.1.47:9997/v1/models?wait_ready=true
{
"model_name": "bge-m3",
"model_type": "embedding",
"n_gpu": 1,
"download_hub": "modelscope"
}POST http://10.0.1.47:9997/v1/models?wait_ready=true
{
"model_name": "bge-reranker-v2-m3",
"model_type": "rerank",
"n_gpu": 1,
"download_hub": "modelscope"
}bge-m3 的向量维度是 1024。
Dify 源码和官方 Docker 文件放在 /usr/cluster/dify/src/docker,版本 1.17.1。公开地址六项都是 http://10.0.1.47:8088:CONSOLE_API_URL、CONSOLE_WEB_URL、SERVICE_API_URL、APP_API_URL、APP_WEB_URL、FILES_URL。Nginx 对外 8088,TLS 对外 18443。向量库用 pgvector。数据库用户 postgres,库名 dify,口令见下一节。
Dify Nginx 的解析器写 10.96.0.10。上游用集群全域名,短名在 Nginx 里不会走 Pod 的 search 域:
| 上游 | 地址 |
|---|---|
| API | api.dify.svc.cluster.local:5001 |
| Web | web.dify.svc.cluster.local:3000 |
| WebSocket | api-websocket.dify.svc.cluster.local:5001 |
| 插件 | plugin-daemon.dify.svc.cluster.local:5002 |
plugin-daemon 的 Service 要同时暴露 5002 和 5003。镜像 EXPOSE 只有 5003,Nginx 代理的是 5002。
其余 Dify、Langfuse 工作负载用同一套 Deployment 模板:镜像和数据目录按下表挂 hostPath,环境变量放进名为 <应用名>-env 的 Secret。数据库口令用本文的值。Langfuse 加密项和 Redis 口令在服务器上生成,权限 600,不写进仓库。
| 命名空间 | 名字 | 镜像 | 数据目录 |
|---|---|---|---|
| dify | api / worker / api-websocket | langgenius/dify-api:1.17.1 | /usr/cluster/dify/src/docker/volumes/app/storage |
| dify | worker-beat | 同上 | 追踪补丁文件 |
| dify | web | langgenius/dify-web:1.17.1 | 无 |
| dify | db-postgres | postgres:15-alpine | volumes/db/data |
| dify | pgvector | pgvector/pgvector:pg16 | volumes/pgvector/data |
| dify | redis | redis:6-alpine | volumes/redis/data |
| dify | sandbox | langgenius/dify-sandbox:0.2.15 | volumes/sandbox |
| dify | plugin-daemon | langgenius/dify-plugin-daemon:0.6.10-local | volumes/plugin_daemon |
| dify | ssrf-proxy / agent-ssrf-proxy | ubuntu/squid:latest | ssrf_proxy 配置 |
| dify | agent-backend | langgenius/dify-agent-backend:1.17.1 | 无 |
| dify | local-sandbox | langgenius/dify-agent-local-sandbox:1.17.1 | Docker volume _data |
| langfuse | langfuse-web | docker.langfuse.com/langfuse/langfuse:4 | 无,hostPort 3100 |
| langfuse | langfuse-worker | docker.langfuse.com/langfuse/langfuse-worker:4 | hostPort 127.0.0.1:3030 |
| langfuse | postgres | docker.io/postgres:17 | langfuse_langfuse_postgres_data |
| langfuse | clickhouse | docker.io/clickhouse/clickhouse-server:25.12 | ClickHouse 数据卷 |
| langfuse | redis | docker.io/redis:7 | Redis 数据卷 |
| langfuse | minio | docker.io/minio/minio:latest | hostPort 9092 到容器 9000 |
api、worker、worker-beat、api-websocket 把 config/langfuse_trace.py 挂到:
/app/api/.venv/lib/python3.12/site-packages/dify_trace_langfuse/langfuse_trace.py文件里的标记是 # AGENT_TOOL_SPANS_V2。工具调用的 Observation 名字来自这段补丁。
内网放行写在 Dify 环境里,只认这一行赋值,不要去改注释里的同名文字:
SSRF_PROXY_ALLOW_PRIVATE_IPS=172.17.0.0/16,10.0.0.0/8Prometheus 抓取文件是 config/prometheus.yml。vLLM 目标是 10.0.1.47:8000,DCGM 目标是 dcgm-exporter:9400。Grafana 数据源是同命名空间的 http://prometheus:9090。看板目录在服务器 /usr/cluster/observability/metrics/grafana/dashboards。
端口:
| 地址 | 服务 |
|---|---|
http://10.0.1.47:3000 | New-API |
http://10.0.1.47:8000 | vLLM |
http://10.0.1.47:9997 | Xinference |
http://10.0.1.47:8088 | Dify |
http://10.0.1.47:3100 | Langfuse |
http://10.0.1.47:3001 | Grafana |
http://10.0.1.47:9090 | Prometheus |
http://10.0.1.47:9400 | DCGM |
http://10.0.1.47:8090 | 导航页 |
http://10.0.1.47:8777 | getServerStatus |
7. 服务器状态接口
程序用系统 Python 听在宿主机 8777。systemd 单元名 server-status。
文件:scripts/06-服务器状态.sh、config/server-status/app.py
#!/bin/bash
# 在宿主机安装 getServerStatus。它不是容器,端口 8777。
# 不要停掉现网上已经 active 的 server-status。
set -eu
if systemctl is-active --quiet server-status; then
echo "server-status 已在运行"
exit 0
fi
ROOT=$(cd "$(dirname "$0")/.." && pwd)
mkdir -p /usr/cluster/server-status
cp "$ROOT/config/server-status/app.py" /usr/cluster/server-status/app.py
cp "$ROOT/config/server-status/server-status.service" /etc/systemd/system/server-status.service
systemctl daemon-reload
systemctl enable server-status
systemctl restart server-status
curl -fsS "http://127.0.0.1:8777/server/status?server_name=gpu-server-01"
echo
echo STATUS_OK#!/usr/bin/env python3
"""作业 1 的模拟服务器状态接口。operationId 对应 Langfuse 里的 getServerStatus。"""
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == "/health":
self._send({"status": "ok"})
return
if parsed.path == "/server/status":
name = (parse_qs(parsed.query).get("server_name") or ["unknown"])[0]
self._send(
{
"server": name,
"cpu": "45%",
"memory": "62%",
"gpu_util": "78%",
"gpu_memory": "18.2/24 GB",
}
)
return
self.send_response(404)
self.end_headers()
def _send(self, payload):
body = json.dumps(payload, ensure_ascii=False).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
print("%s - %s" % (self.address_string(), fmt % args), flush=True)
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", 8777), Handler).serve_forever()OpenAPI 的 servers.url 写 http://10.0.1.47:8777,operationId 是 getServerStatus。完整文件是 config/server-status-openapi.yaml。返回值是固定的模拟数:CPU 45%,内存 62%,GPU 利用率 78%,显存 18.2/24 GB。
8. 账号和口令
| 系统 | 账号 | 密码 |
|---|---|---|
| New-API | admin | L4o4986784$ |
| Dify | admin@example.com | Dify-rag-2026 |
| Grafana | admin | Gf-gpu-2026 |
| Langfuse | admin@example.com | Lf-trace-2026 |
| Xinference | admin | Xinf-bge-2026 |
数据库口令在 config/数据库口令.env:
# 可以写进文档的数据库口令。Redis 口令、Langfuse 加密密钥不在这里。
DIFY_POSTGRES_USER=postgres
DIFY_POSTGRES_PASSWORD=difyai123456
DIFY_POSTGRES_DB=dify
PGVECTOR_PASSWORD=difyai123456
LANGFUSE_POSTGRES_USER=postgres
LANGFUSE_POSTGRES_PASSWORD=ebb9cb3a5365bc5ffd41349860dc48c8
LANGFUSE_POSTGRES_DB=postgres
CLICKHOUSE_USER=clickhouse
CLICKHOUSE_PASSWORD=b5e2a69b99ac76c35f7f8d5742ce2ea6
CLICKHOUSE_DB=defaultdify-gateway 是用户 admin 的第 3 号令牌,状态启用,额度无限,永不过期,只允许模型 qwen3.6-27b。完整密钥:
sk-C0P9V9kwPfXDvQCfaVzSLCJRUmioNmxYud1uqsLXkBqZTFxKNew-API 渠道 id 1,名字 vLLM-Qwen3.6-27B,地址 http://10.0.1.47:8000,模型 qwen3.6-27b。额度换算:QuotaPerUnit=500000,500000 额度等于 1 美元。
Dify 登录接口的密码要先做 base64,请求头 X-CSRF-Token 要等于 cookie 里的 csrf_token。
二、功能操作案例
案例 1:New-API 渠道和 dify-gateway
- 打开
http://10.0.1.47:3000,用 admin 登录。 - 渠道指向
http://10.0.1.47:8000,模型名qwen3.6-27b。 - 令牌页确认 dify-gateway。密钥就是上一节那一把。额度无限,过期时间
-1,模型限制只放行qwen3.6-27b。 - 金额和过期都在这把令牌上改。Dify 的 Agent 页面没有这两项。关掉无限额度后,花费记在令牌上;保持无限时,花费记在 admin 余额上。
以后新建的 Agent 或工作流,只要还用工作区默认的 qwen3.6-27b,对话都走这把令牌。向量和重排不走它,它们走 Xinference。
案例 2:Dify 公开地址、模型和内网放行
初始化管理员 admin@example.com / Dify-rag-2026。六项公开 URL 都带 :8088。
文件:scripts/07-接入模型.py
#!/usr/bin/env python3
"""记录工作区模型注册。对话走 New-API 的 dify-gateway,向量和重排走 Xinference。
现网已经注册过。再跑一次如果返回 Can't add same credential,停下来即可。
不要创建第二把名为 dify-gateway 的令牌。
"""
import json
import urllib.request
DIFY = "http://127.0.0.1:8088"
NEW_API = "http://10.0.1.47:3000/v1"
XINFERENCE = "http://10.0.1.47:9997"
GATEWAY_KEY = "sk-C0P9V9kwPfXDvQCfaVzSLCJRUmioNmxYud1uqsLXkBqZTFxK"
llm = {
"model": "qwen3.6-27b",
"model_type": "llm",
"credentials": {
"mode": "chat",
"context_size": "65536",
"max_tokens_to_sample": "8192",
"function_calling_type": "tool_call",
"stream_function_calling": "supported",
"display_name": "qwen3.6-27b",
"endpoint_url": NEW_API,
"api_key": GATEWAY_KEY,
},
}
embedding = {
"model": "bge-m3",
"model_type": "text-embedding",
"credentials": {
"server_url": XINFERENCE,
"model_uid": "bge-m3",
},
}
rerank = {
"model": "bge-reranker-v2-m3",
"model_type": "rerank",
"credentials": {
"server_url": XINFERENCE,
"model_uid": "bge-reranker-v2-m3",
},
}
def show(title, body):
print(title)
print(json.dumps(body, ensure_ascii=False, indent=2))
show("对话模型", llm)
show("向量模型", embedding)
show("重排模型", rerank)
print("保存:POST /console/api/workspaces/current/model-providers/<provider>/models/credentials")
print("供应商:langgenius/openai_api_compatible/openai_api_compatible")
print("供应商:langgenius/xinference/xinference")
print("然后把这三项设成工作区默认模型。")对话模型上下文 65536,单次输出上限 8192,函数调用类型 tool_call。默认重排模型是 bge-reranker-v2-m3。
SSRF_PROXY_ALLOW_PRIVATE_IPS=172.17.0.0/16,10.0.0.0/8。工具请求经过 Squid 的 3128。从 api Pod 访问 http://10.0.1.47:8777/server/status?server_name=gpu-server-01 应返回上面的模拟指标。
案例 3:合同知识库和 Agent
合同文件先在 Dify 页面上传。仓库里不放原文。
知识库名称 阿里云合同条款。高质量索引,bge-m3,中文,自动分段,混合检索,向量权重 0.7,关键词权重 0.3,top_k 4,打开 Rerank,模型 bge-reranker-v2-m3。权限 all_team_members。现网这份库是 1 个文档、27190 字、75 个片段,id 71042108-5f9a-4856-a0db-4359e8e7fa98。
Agent 名称同样是 阿里云合同条款。现网 id 01a1236c-4cc3-7fb9-bfce-c730a318bdf9,应用 d2e3f57b-3901-45de-bd9a-d51feb849f6e。访问地址:
http://10.0.1.47:8088/agent/gQPVqCdcyKoW82iZ知识检索卡片名 Retrieval 1。查询模式用 generated_query,这样模型会按用户问题调用 knowledge_base_search。set_name 填 Retrieval 1。
文件:scripts/08-合同知识库.py
#!/usr/bin/env python3
"""合同知识库和 Agent 提示词。合同原文不在仓库里,先在页面上传。
现网知识库 id 是 71042108-5f9a-4856-a0db-4359e8e7fa98。
空白环境上传后用接口返回的新 id,不要拿这个旧 id 再索引一份。
"""
DATASET_NAME = "阿里云合同条款"
PROVIDER = "langgenius/xinference/xinference"
SYSTEM_PROMPT = """# 角色
你是【阿里云框架合同解析智能Agent】,服务采购、IT、法务。只解析阿里云框架协议、服务条款、补充协议、订单、账单、SLA、免责、费用、终止、数据安全。其他问题请用户找对应负责人。
# 检索
需要合同原文时,调用一次 knowledge_base_search:set_name 为「Retrieval 1」,query 只用条款主题加关键词。
本轮对话已经引用过该条款原文时,直接作答。
没有命中时,只回复:【知识库未检索到本合同相关条款,请查阅原件或咨询法务/阿里云客户经理】。
# 作答
结论必须带章节或条款号,并摘一句原文。立场是甲方。框架与订单或补充协议冲突时,写明以订单/补充协议为准。
一问一答只答所问:加粗结论、一句原文、风险色标、一条建议。
用户要求总结或报告时,输出三块:【核心信息摘要】【关键条款清单】【风险清单与建议】。报告优先看:主体与期限、计费与最低消费、SLA 与赔付上限、数据与保密、责任上限与间接损失、终止与数据留存、管辖与单方变更。
🔴高风险:责任上限过低、免除重大损失、数据迁出限制、无 SLA 赔付、自动续费无提前告知、单方变更条款。
🟡中风险:表述模糊、期限不清、流程繁琐、赔付条件苛刻。
🟢低风险:常规商业条款。
每条风险包含:原文摘抄、风险说明、我方建议。
只做条款解读、提取、对比、风险识别和摘要。签署结论留给法务。
每次回复末尾加:本内容仅为合同条款辅助解析,不构成法律意见,合同签署前务必由公司法务最终审核。
只有用户要求总结或报告时,最后再问:是否需要整理成精简关键条款清单?"""
retrieval_model = {
"search_method": "hybrid_search",
"reranking_enable": True,
"reranking_mode": "reranking_model",
"reranking_model": {
"reranking_provider_name": PROVIDER,
"reranking_model_name": "bge-reranker-v2-m3",
},
"top_k": 4,
"score_threshold_enabled": False,
"weights": {
"weight_type": "customized",
"vector_setting": {
"vector_weight": 0.7,
"embedding_provider_name": PROVIDER,
"embedding_model_name": "bge-m3",
},
"keyword_setting": {"keyword_weight": 0.3},
},
}
init_body = {
"name": DATASET_NAME,
"indexing_technique": "high_quality",
"doc_form": "text_model",
"doc_language": "Chinese",
"embedding_model": "bge-m3",
"embedding_model_provider": PROVIDER,
"process_rule": {"mode": "automatic"},
"retrieval_model": retrieval_model,
}
knowledge_set = {
"id": "retrieval-1",
"name": "Retrieval 1",
"description": "阿里云框架合同原文及附件",
"query": {"mode": "generated_query"},
"retrieval": {
"mode": "multiple",
"top_k": 4,
"reranking_mode": "reranking_model",
"reranking_enable": True,
"reranking_model": {"provider": PROVIDER, "model": "bge-reranker-v2-m3"},
},
"metadata_filtering": {"mode": "disabled"},
}
print("系统提示词字数", len(SYSTEM_PROMPT))
print("创建知识库 POST /console/api/datasets/init")
print("查询模式用 generated_query。user_query 会在页面打开时查固定句子。")
print("保存草稿 PUT /console/api/agent/<id>/composer ,save_strategy=save_to_current_version")
print("发布 POST /console/api/agent/<id>/publish")系统提示词 674 个字符,标记句是「本轮对话已经引用过」。保存草稿用 save_to_current_version。发布走 /publish。save_as_new_version 会被拒绝。
配置页要出现「知识检索」。Dify 1.17.1 的前端把 ENABLE_AGENT_KNOWLEDGE_RETRIEVAL 编译成关闭。官方 web 镜像重新创建后,这个开关回到关闭,需要再改前端包里的 !1 为 !0。现网这次迁进集群时,web Pod 用的是原镜像,页面补丁没有带进去。
召回可以拿这三句看分:保密信息约 0.94,承诺保底消费约 0.87,责任上限约 0.05。责任上限分数低,是因为合同里没有贴得很近的表述。检索不到就按提示词回复知识库没有这条,不编造。
案例 4:Langfuse 里看全链路和 Token
Langfuse 登录 http://10.0.1.47:3100,admin@example.com / Lf-trace-2026。Dify 应用的追踪开关在运行时配置里,关掉时提问不会送出追踪。合同 Agent 要先打开开关再提问。
一条已经对上的问答:
| 项 | 值 |
|---|---|
| trace / message | 373f2dcb-fd1e-4a4a-84d4-871050011a4b |
| 时间 | 2026-10-10 14:58(北京时间) |
| 用户原话 | 其实吧 这只是纠纷的时候才会刨根稳定的一个合同 平时除了折扣没啥用 |
| 模型 | qwen3.6-27b |
| token | 提示 26763,补全 862,合计 27625 |
| New-API 日志 | 第 125 条,额度 13813 |
| 金额 | 13813 / 500000 = 0.027626 美元 |
同一分钟还有一笔额度 1692,合 0.003384 美元。两笔合计 0.031010 美元。Dify 的模型凭证没有单价,messages.total_price 是 0。新的对话会带上 token;美元数看 New-API 的额度,除非在模型凭证里填上单价。
案例 5:getServerStatus 出现在 Langfuse
工具按 config/server-status-openapi.yaml 注册到「运维巡检 Agent」。现网这个 Agent 的 id 是 01a124c7-9fce-70a8-9efa-41b2a6f5b7e7,应用 e4d614f6-de22-4f4c-b58e-caeb0504b121。服务器地址用 http://10.0.1.47:8777。
在对话里让它查询 gpu-server-03。Langfuse 里 Observation 的 Name 应等于 getServerStatus。这个名字来自 OpenAPI 的 operationId,再由 # AGENT_TOOL_SPANS_V2 写进追踪。
案例 6:Grafana 看显卡
打开 http://10.0.1.47:3001,admin / Gf-gpu-2026。数据源 Prometheus 指向 http://prometheus:9090。DCGM 在 9400 提供显存、温度、利用率。vLLM 的 /metrics 由 Prometheus 向 10.0.1.47:8000 抓取。看板文件放在 /usr/cluster/observability/metrics/grafana/dashboards。
写文档时核对到的数据
- 系统 Ubuntu 22.04.5,内核 5.15.0-130-generic,Docker 28.0.1,Kubernetes 1.37.1,cri-dockerd 0.4.8。
- 节点
prod102Ready,内部地址 10.0.1.47,运行时 docker://28.0.1。 - 显卡 NVIDIA H20,驱动 550.120。
- 模型目录
/models/Qwen3.6-27B,符号链接到/usr/cluster/models。 - 上述端口在宿主机上由对应 Pod 的 hostPort 提供。
- 合同知识库、Agent、站点 code、令牌和数据库口令按当时库里的记录写入。加密密钥和 Redis 口令没有写入。