
1. 为什么开发测试环境也要认真对待离线部署上个月同事在群里求助机房里的开发测试集群要整体重建新机器放在一个不能访问外网的网段里面还要跑一套数据看板 Superset 和一套 ONLYOFFICE 文档预览。往常装 Kubernetes 都是对着公网仓库一路 apt、docker pull 就完事真到了离线环境才发现第一步找包就能把人卡住。后来我把整套流程走了一遍从二进制、镜像、Helm Chart 到应用部署全部离线打通。这次选的版本是 Kubernetes v1.28.2配合 containerd 1.7.13。开发测试环境没有生产环境那么高的可用性要求但它的坑在于机器换了、网段变了、开发要用的中间件又多如果离线介质准备不齐后面每装一个应用都要重新折腾一次。1.1 你以为的开发测试环境可能比生产还难搞很多人默认开发测试环境 能联网的宽松环境实际情况恰恰相反。不少公司和机房的开发测试网段是独立隔离的为了省事或者合规直接不给外网路由。你在办公室开发时能随便拉镜像可机房里的机器根本访问不了 Docker Hub 或 Kubernetes 官方镜像仓库。还有一类场景是项目验收、漏洞扫描、等保测评要求所有组件必须用内部介质安装不能出现运行时从公网拉取的行为。这时候哪怕机器能访问外网也不能拉只能靠离线介质。所以给开发测试环境做离线部署不是自找麻烦而是提前把换机器重来的成本打下来。一次离线介质准备好后续扩容节点、重建环境、交付给其他团队都是一套固定动作。1.2 离线部署的本质是降级为供应链管理离线部署说白了就是把原来分散在公网各个仓库里的东西提前搬到一台有网的下载工作站上整理成标准介质包再运到内网目标机器上安装。Kubernetes 离线部署要管的包大体是三类二进制包kubeadm、kubelet、kubectl、containerd、runc、CNI 插件。镜像包Kubernetes 核心组件镜像、DNS、网络插件、以及开发测试要用的中间件和应用镜像。配置与编排文件kubeadm 配置、Helm Chart、Manifest YAML、本地镜像仓库和离线 Helm 仓库的初始化配置。把这三类盘清楚离线部署就成功了一大半。别上来就到处找一键离线安装脚本脚本多的是但脚本背后的版本匹配、镜像对应关系、私有仓库地址才是真正决定你能不能一次跑通的地方。2. 离线部署前必须盘清楚的版本、镜像与网络台账离线环境下改版本是件很痛苦的事。在线环境想升级就改一下镜像 tag离线环境每次改动都意味着重新做介质、重新拷盘、重新验包。所以动手之前花半小时把台账建好比装到一半再返工划算得多。2.1 版本选型从操作系统到容器运行时的匹配关系我这次统一采用 Ubuntu 22.04 containerd 1.7.13 Kubernetes v1.28.2。选这套组合的原因很直接内核较新对 cgroup v2 和 iptables 的支持都省心containerd 1.7 是长时间维护版本cri 集成成熟Kubernetes 1.28 不是最新的但功能特性稳定团队后续接中间件也不会碰到兼容性红线。各节点操作系统版本尽量一致不要在一个集群里混用 Ubuntu 和 CentOS。如果实在避免不了也要保证内核版本接近并且对 containerd 的 overlay 存储驱动做一次实际验证。一个容易忽略的坑是内核模块。Kubernetes 正常运行时依赖overlay和br_netfilter如果目标机器内核模块没加载网络转发和 Pod 通信都会出问题。这个我们在后面初始化步骤里会专门处理。2.2 把网络拓扑 仓库地址先画出来离线不等于节点之间没有网络。开发测试环境里控制节点、工作节点、镜像仓库、应用服务之间通常还是同一个内网。我的建议是固定一张表角色地址示例说明控制节点/工作节点192.168.10.11 ~ 10.13Kubernetes 集群节点双网卡或单网卡均可私有镜像仓库192.168.10.10:5000用 registry:2 容器跑承载所有 K8s 和应用镜像离线下载工作站10.0.0.8能访问公网的机器负责下载和打包不在集群内目标网段入口机器192.168.10.10接收外部介质启动 registry同时可以作为运维跳板镜像仓库地址要早定。后面 kubeadm 配置、containerd 配置、应用 values 里全都要写这个地址如果装到一半再改所有节点都要跟着改一遍。开发测试环境我用192.168.10.10:5000这种 IP 加端口的方式简单直接如果公司有自己的域名解析用registry.internal:5000更规范。2.3 依赖清单镜像只是其中一部分很多人以为离线部署就是拷镜像真正装的时候才发现还缺系统包、缺 CNI 二进制、缺 crictl。我按依赖类型整理了一个检查清单照着准备就行Kubernetes 官方镜像kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、coredns、pause。容器运行时containerd、runc、CNI 插件二进制包。网络插件镜像flannel 或 calico 相关的镜像。集群附加组件metrics-server、ingress-nginx、dashboard 等按需准备。开发测试应用镜像Superset、ONLYOFFICE Document Server、PostgreSQL、Redis、vLLM/Ollama 等。系统依赖conntrack、ebtables、ethtool、socat、iproute2、iptables。离线仓库组件registry:2 镜像或者 Harbor 离线安装包。Helm 及 Chart 包Helm 二进制、需要离线安装的 Chart 压缩包。系统依赖很容易被漏掉。如果目标机器有内网 apt/yum 源直接装如果没有最省事的办法是拿操作系统安装 ISO 挂载成本地源。千万别默认 kubeadm 二进制拷上去就能跑它依赖的这些用户态工具在干净系统上经常是缺的。3. 在有网机器上把离线介质一次性准备到位这部分在能上网的下载工作站上完成。准备介质不是把文件下载下来就完事还要校验、分类、做成可复用的目录结构。我的建议是打造一个/data/k8s-offline目录下面分成bin、images、charts、manifests四个子目录。/data/k8s-offline/ ├── bin/ # kubeadm、kubelet、kubectl、containerd、cni ├── images/ # 所有镜像 tar 包 ├── charts/ # Helm Chart 压缩包 └── manifests/ # 手工整理的 YAML、kubeadm 配置3.1 下载二进制包能下载到官方 tar 包就不要只拿 deb/rpmkubeadm、kubelet、kubectl 这三个可以直接从 Kubernetes release 页面下载裸二进制文件不用依赖包管理器。这样省掉了在不同发行版上处理依赖的问题。mkdir -p /data/k8s-offline/bin cd /data/k8s-offline/bin VERSIONv1.28.2 curl -L -O https://dl.k8s.io/release/$VERSION/bin/linux/amd64/kubeadm curl -L -O https://dl.k8s.io/release/$VERSION/bin/linux/amd64/kubelet curl -L -O https://dl.k8s.io/release/$VERSION/bin/linux/amd64/kubectl chmod x kubeadm kubelet kubectlcontainerd、runc、CNI 插件也一样直接从对应 GitHub Release 下载 Linux amd64 二进制压缩包CONTAINERD_VERSION1.7.13 RUNC_VERSION1.1.11 CNI_VERSIONv1.4.0 curl -L -O https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz curl -L -O https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/runc.amd64 curl -L -O https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-amd64-${CNI_VERSION}.tgz下载完不要急着拷贝先在下载机器上把版本号记下来。后面排查问题的时候第一件事就是确认这些二进制版本和 kubeadm init 配置里的kubernetesVersion一致不一致会出现很多莫名其妙的问题。3.2 拉取并导出 Kubernetes 核心镜像我建议先写一个kubeadm-init.yaml把镜像仓库地址定成最终要用的内网地址然后用 kubeadm 从配置里生成镜像清单。这样导出的镜像名直接就是内网仓库地址省去后期二次 tag。cat /data/k8s-offline/manifests/kubeadm-init.yaml EOF apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 nodeRegistration: criSocket: unix:///run/containerd/containerd.sock --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 imageRepository: 192.168.10.10:5000 controlPlaneEndpoint: 192.168.10.11:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 EOF然后生成镜像列表。注意这个列表是最终集群要从内网仓库拉取的镜像名但在下载机器上直接 pull 会失败因为内网仓库还没有这些镜像。所以正确顺序是先让 kubeadm 输出一份默认地址的清单把默认地址替换成本地临时 tagpull 完成后再重新 tag 回内网仓库地址。具体的脚本可以这样写cd /data/k8s-offline/images # 生成默认仓库的镜像清单用于 pull kubeadm config images list --kubernetes-version v1.28.2 upstream-images.list # 逐个 pull、tag、导出 REGISTRY192.168.10.10:5000 while read -r img; do name$(echo $img | awk -F/ {print $NF}) docker pull $img docker tag $img ${REGISTRY}/${name} docker save ${REGISTRY}/${name} | gzip k8s-${name}.tar.gz done upstream-images.list核心镜像清单中一定包含 pause。pause 镜像也叫 sandbox_image是 containerd 启动每个 Pod 前先拉取的基础镜像很多离线环境第一次 init 失败就是因为它没准备到。p8s 这个坑我们在排障部分再展开。3.3 下载网络插件和开发测试应用镜像Kubernetes 集群没有网络插件之前节点会一直处于 NotReady 状态。我这次用 flannel原因是开发测试环境网络简单flannel 的 VXLAN 后端够用镜像数量也比 calico 少。cd /data/k8s-offline/manifests curl -L -O https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml cd /data/k8s-offline/images grep -E image: /data/k8s-offline/manifests/kube-flannel.yml | awk {print $2} | sort -u flannel-images.list把这些网络插件镜像也按同样方式 pull、tag、导出。用 grep 从 YAML 里提取镜像列表比手写更不容易漏。开发测试应用镜像比如 Superset、ONLYOFFICE Document Server、PostgreSQL、Redis、vLLM 等下载思路完全一样。我的经验是不要一上来就下载一大批应用镜像先把 Kubernetes 基础设施镜像搞定集群能正常跑起来再按应用逐个补镜像。一次准备太多后面出问题不好定位。4. 目标节点初始化到集群拉起一条可复现的完整链路介质准备好之后剩下的就是在目标节点上执行安装。这一节我把每个步骤的命令和验证方法写清楚照着走一遍就能有一个最小可用集群。4.1 节点基础配置内核模块、系统参数和依赖包所有目标节点先做同一套基础初始化。# 加载内核模块 cat EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter # 配置网络转发参数 cat EOF | sudo tee /etc/sysctl.d/99-kubernetes.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sudo sysctl --system然后关闭 swap。Kubernetes 的 kubelet 默认对 swap 很敏感虽然新版有 NodeSwap 特性但开发测试环境没必要开。关闭后记得注释/etc/fstab里的 swap 行否则重启后又自动挂载回来kubelet 状态会反复异常。sudo swapoff -a接下来安装系统依赖。如果目标机器没有内网 apt 源可以用操作系统安装 ISO 挂载成本地源如果连 ISO 也拿不到就从下载工作站上把这些 deb 包连同依赖一起用外置介质带过去。sudo apt update sudo apt install -y conntrack ebtables ethtool socat iproute2 iptables4.2 安装 containerd 并处理 sandbox_imagecontainerd 是所有容器实例的直接管理进程kubelet 通过 CRI 协议和它通信。把下载好的 containerd 压缩包解压安装到/usr/local并把 runc 和 CNI 插件放到对应位置。cd /data/k8s-offline/bin sudo tar -C /usr/local -xzf containerd-1.7.13-linux-amd64.tar.gz sudo install -m 755 runc.amd64 /usr/local/bin/runc sudo mkdir -p /opt/cni/bin sudo tar -C /opt/cni/bin -xzf cni-plugins-linux-amd64-v1.4.0.tgz # 生成默认配置 sudo mkdir -p /etc/containerd sudo sh -c containerd config default /etc/containerd/config.toml修改config.toml两处关键内容。第一处是让 containerd 使用 systemd 作为 cgroup 驱动和 kubelet 的 cgroupfs 配置保持一致第二处是把 pause 镜像指向内网仓库。sudo sed -i s/SystemdCgroup false/SystemdCgroup true/ /etc/containerd/config.toml sudo sed -i s#sandbox_image .*#sandbox_image 192.168.10.10:5000/pause:3.9# /etc/containerd/config.toml如果私有仓库是 HTTP还要在config.toml里给仓库地址单独声明 endpoint否则 containerd 默认按 HTTPS 去连会直接 timeout。最简单的方式是追加这样一段[plugins.io.containerd.grpc.v1.cri.registry.mirrors.192.168.10.10:5000] endpoint [http://192.168.10.10:5000]启动 containerd 后用ctr验证一下 pause 镜像是否已经被导入。等会儿 kubeadm init 会立刻去拉这个镜像这里多花一分钟检查后面能少折腾一小时。sudo systemctl daemon-reload sudo systemctl enable --now containerd sudo ctr -n k8s.io images list | grep pause4.3 导入镜像并执行 kubeadm init把上一阶段打包的所有镜像 tar 包拷贝到目标节点逐个用ctr导入。这里必须带-n k8s.io命名空间因为 containerd 的 CRI 插件只认k8s.io这个命名空间。很多人习惯用ctr images import导入发现 kubectl 里还是 ImagePullBackOff就是因为导到了默认命名空间kubelet 根本看不到。sudo ctr -n k8s.io images import /data/k8s-offline/images/k8s-kube-apiserver.tar.gz sudo ctr -n k8s.io images import /data/k8s-offline/images/k8s-kube-controller-manager.tar.gz sudo ctr -n k8s.io images import /data/k8s-offline/images/k8s-kube-scheduler.tar.gz sudo ctr -n k8s.io images import /data/k8s-offline/images/k8s-kube-proxy.tar.gz sudo ctr -n k8s.io images import /data/k8s-offline/images/k8s-etcd.tar.gz sudo ctr -n k8s.io images import /data/k8s-offline/images/k8s-coredns.tar.gz sudo ctr -n k8s.io images import /data/k8s-offline/images/k8s-pause.tar.gz然后执行 initsudo kubeadm init --config /data/k8s-offline/manifests/kubeadm-init.yaml初始化成功后配置 kubectlmkdir -p $HOME/.kube sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config工作节点加入集群需要 join 命令。kubeadm init 成功后会直接输出最好用kubeadm token create --print-join-command再生成一次保存到本地文件。token 的有效期默认是 24 小时离线搭建如果中间隔了几天记得重新生成。4.4 安装网络插件让节点状态转为 Ready没有 CNI 插件时kubectl get nodes看到控制节点是 NotReadycoredns 也起不来。把 flannel 的镜像导入到所有节点或者推送到私有仓库然后修改kube-flannel.yml里的镜像地址为192.168.10.10:5000/flannel/flannel:v0.24.2之类最后 apply。sudo ctr -n k8s.io images import /data/k8s-offline/images/flannel.tar.gz kubectl apply -f /data/k8s-offline/manifests/kube-flannel.yml等一两分钟再看节点状态kubectl get nodes kubectl get pods -A如果节点状态是 Ready集群基础环境就算通了。到这一步Kubernetes 本身的离线部署已经完成接下来要解决的是开发测试环境里常用服务怎么离线跑起来。5. 开发测试应用的离线补给镜像仓库、Helm 源与常见应用落地集群基础通了开发测试工作才刚开始。你要在集群里装数据库、数据看板、文档服务甚至跑离线大模型推理验证。这些应用的镜像和 Chart 同样需要离线化。5.1 私有镜像仓库的两种搭法开发测试环境我强烈建议先部署一个私有镜像仓库而不是把所有应用镜像直接ctr import到节点上。原因很直接导入到本机的镜像只对当前节点有效Pod 调度到另一个节点时还是会去拉镜像一旦节点上没有就等着 ImagePullBackOff。最简单的方式是用 registry:2 起一个单容器仓库sudo docker load /data/k8s-offline/images/registry-2.tar.gz sudo docker run -d --name registry --restartalways \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ registry:2如果团队内需要镜像权限控制和 Web 管理界面用 Harbor。但 Harbor 的离线安装包本身也不小而且要依赖 Docker Compose开发测试环境如果就二三十个镜像registry:2 完全够用。把下载工作站上 tag 好的镜像推到私有仓库REGISTRY192.168.10.10:5000 while read -r img; do docker tag ${img##*/} ${REGISTRY}/${img##*/} docker push ${REGISTRY}/${img##*/} done /data/k8s-offline/images/upstream-images.list推完之后在任意一个集群节点上用 crictl 验证sudo crictl pull 192.168.10.10:5000/nginx:latest能拉下来说明 containerd 到私有仓库的通路没问题。后面应用镜像都能走这条链路。5.2 离线 Helm 源不求人也能装 Chart开发测试环境里很多应用都用 Helm 部署比如 Superset 有自己的官方 ChartONLYOFFICE 也有 Chart。离线环境不能直接helm repo add公网仓库但在下载工作站上把 Chart 先 pull 下来再拷贝到内网机器上完全可行。helm repo add superset https://apache.github.io/superset helm repo add onlyoffice https://onlyoffice.github.io/charts helm repo add bitnami https://charts.bitnami.com/bitnami helm pull superset/superset --version 0.12.0 helm pull onlyoffice/document-server --version 0.1.0 helm pull bitnami/postgresql --version 12.5.4 helm pull bitnami/redis --version 18.0.1如果团队里有多个人都要用可以在内网起一个 ChartMuseum 作为离线 Helm 仓库sudo docker run -d --name chartmuseum --restartalways \ -p 8080:8080 \ -v /data/chartmuseum:/charts \ chartmuseum/chartmuseum:latest然后在开发机上配置 Helm 源helm repo add internal http://192.168.10.10:8080 helm cm-push superset-0.12.0.tgz internal5.3 三个常见的开发测试离线应用落地方式Superset 离线部署Superset 依赖 PostgreSQL 和 Redis。离线环境下先把这三个镜像准备好再通过 Helm values 把镜像地址指到私有仓库helm install superset ./superset-0.12.0.tgz \ --set image.repository192.168.10.10:5000/superset \ --set image.taglatest \ --set postgresql.image.registry192.168.10.10:5000 \ --set redis.image.registry192.168.10.10:5000如果 Chart 内部还有依赖子 Chart比如postgresql-ha它可能默认从docker.io拉镜像。这时候最省事的办法是在 values 里统一覆盖global.imageRegistry192.168.10.10:5000前提是你已经把相关的镜像都推到了私有仓库。ONLYOFFICE Document Server 离线部署文档预览服务在开发测试环境很常见。ONLYOFFICE 的镜像比较大依赖 PostgreSQL、RabbitMQ、Redis离线部署时同样需要注意这些子 Chart 的镜像地址。helm install onlyoffice ./document-server-0.1.0.tgz \ --set image.repository192.168.10.10:5000/onlyoffice/documentserver \ --set image.taglatest \ --set postgresql.image.registry192.168.10.10:5000 \ --set rabbitmq.image.registry192.168.10.10:5000 \ --set redis.image.registry192.168.10.10:5000有一点容易被忽略ONLYOFFICE 要正常预览中文文档容器里得有中文字体。在线环境可以直接在 Pod 里 apt 装字体离线环境不行。所以离线部署时最好把fonts-noto-cjk字体包放到私有镜像里或者用 initContainer 挂载宿主机字体目录。开发测试环境我不建议做得太复杂直接在镜像制作阶段把字体打进去最省心。离线大模型推理验证开发测试环境现在还有个很常见的需求在内网验证大模型推理效果比如用 8B 参数的 DeepSeek 模型跑业务问答。这类模型权重动辄十几 GB 到几十 GB让每台节点都在线拉权重根本不现实。常规做法是在有网机器上提前拉好 vLLM 或 Ollama 的推理镜像并推送到私有仓库模型权重文件通过外置硬盘拷到内网服务器再挂载到 Pod 里。apiVersion: apps/v1 kind: Deployment metadata: name: vllm-deepseek-8b spec: replicas: 1 selector: matchLabels: app: vllm template: metadata: labels: app: vllm spec: containers: - name: vllm image: 192.168.10.10:5000/vllm/vllm-openai:latest command: [python3, -m, vllm.entrypoints.openai.api_server] args: [--model, /models/deepseek-8b, --port, 8000] resources: limits: nvidia.com/gpu: 1 volumeMounts: - name: models mountPath: /models volumes: - name: models hostPath: path: /data/models/deepseek-8b这个 YAML 的镜像地址、模型路径、GPU 资源限制都需要根据实际环境调整。重点在于推理镜像走私有仓库模型权重走外置介质两者分开不要全部塞进镜像 tar 包否则一个模型就要重新打包一次。6. 开发测试集群日常排障我踩过的坑和检查顺序离线集群排障和在线集群排障最大的区别是在线环境可以直接docker pull试一下离线环境不行每次验证都要看介质和仓库。我把自己踩过的高频问题整理成一个排查顺序遇到问题按这个顺序过一遍大部分都能解决。6.1 kubeadm init 卡在 kubelet-start先看 pause 镜像第一次 init 时最经典的失败是日志一直在等 kubelet 启动。打开另一个终端看 kubelet 日志journalctl -u kubelet -f --no-pager如果日志里反复出现container image 192.168.10.10:5000/pause:3.9 for sandbox image相关的记录大概率是 containerd 里根本没有这个镜像或者 containerd 访问不到私有仓库。先用 crictl 手动拉一次sudo crictl pull 192.168.10.10:5000/pause:3.9如果拉取超时检查 containerd config 里的 endpoint 是不是 HTTP。很多人在这里卡半天其实就是少写了http://前缀。6.2 镜像已经导入但 Pod 还是 ImagePullBackOff这个场景十有八九是命名空间导错了。containerd 有两个世界一个是底层 containerd 的默认命名空间一个是 Kubernetes 通过 CRI 使用的k8s.io命名空间。你用ctr images import不带-n k8s.io镜像确实导入成功了但 kubelet 完全看不到。检查命令是sudo ctr -n k8s.io images list | grep pause如果不带-n k8s.io能看到带了却看不到重新导入一遍即可。6.3 节点状态 Ready但 Pod 之间网络不通开发测试环境里最常见的问题是 Pod 网段和物理机网段冲突。我在 kubeadm 配置里用10.244.0.0/16如果目标机器所在的内网段也是10.244.0.0/16路由就会打架表现为某些 Pod 能通某些 Pod 间歇性超时。排查第一步不是看 flannel 配置而是先看主机路由表ip route | grep 10.244如果发现本机存在物理网卡占用了这个网段优先把 kubeadm 里的podSubnet改成一个内网不会用到的地址段比如172.20.0.0/16。开发测试环境改起来成本低趁早改。6.4 开发测试环境的常见坑位速查问题现象大概率原因排查命令kubeadm init 一直卡住kubelet 服务未启动或 pause 镜像缺失journalctl -u kubelet -fPod 一直是 ContainerCreatingCNI 插件没装或镜像没导入kubectl describe pod节点 NotReadyflannel/calico 没起来或内核模块没加载kubectl get pods -Acontainerd 拉私有仓库超时没写 http endpoint 或证书不对crictl pull镜像导入成功但 Pod 仍拉取失败ctr 导入到默认 namespacectr -n k8s.io images listHelm 安装报 registry 错误values 里没有把镜像地址全局覆盖helm get valuesGPU 资源 unavailable节点没有安装 NVIDIA 驱动/device pluginkubectl describe node6.5 最后一条经验把离线介质目录当作代码仓库管理整个离线部署做完之后一定不要把/data/k8s-offline随便扔在那里。建议把里面的images.list、charts/、manifests/全部纳入版本管理至少在一个共享目录里长期保存。开发测试环境的节点会换系统会重装你不可能每次都重新去公网下载一遍介质。我自己的习惯是每次新增应用镜像或升级组件后都在下载工作站上重新生成一次镜像清单并写一个简单的README.md记录版本号和验证时间。后面再有人接手机房环境直接照着 README 操作不用再从头趟一遍坑。