NanoCloud 私有化材料

私有化部署运维手册 (K8s 离线交付)

飞书 Wiki 同步子文档,revision 3。

私有化部署运维手册 (K8s 离线交付)

定位:本手册是 NanoCloud 私有化离线交付的一站式部署文档。

1. 交付模型与阅读路径

1.1 适用范围

项说明
受众客户现场 / 驻场运维(中文)
交付形态离线 tar 包(nanocloud--.tar.gz),安装全程不依赖公网
主路径单机离线(airgap):profile=standalone + install.sh --airgap
底座包内 k3s 由 foundation/base-install.sh 安装(vendored install.sh + airgap 镜像);禁止现场裸跑 curl … get.k3s.io
镜像分发registry-first:seed registry:2.8.3 → 集群内 registry(NodePort 30500)→ push → 节点 mirror → Helm
不涵盖Compose 轨(仓库根 deployment/),与本手册无关

1.2 部署形态(profile)对照

勿跨 profile 混用步骤。

profile场景存储入口章节
standalone单机无集群(主路径)local-path§6
k3s-ha1 server + 多 agent(工作负载多节点)Longhorn§7
k8s-bare已有自建集群(RKE2/kubeadm 等)集群默认 SC§8
eks / ack / tke云托管云盘 SC§8 + 对应 platform/*.yaml

勿跨 profile 混用步骤。 当前 k3s-ha 只自动加入 agent,不追加 etcd/server;它提供工作负载和
Longhorn 副本冗余,不是控制面 HA。控制面故障仍会导致 Kubernetes API 单点不可用。

1.3 角色分工

角色谁做什么
构建机交付方 / 有外网环境package.sh 打离线包;需 Docker/buildx、可拉基础镜像
目标机客户现场(通常无外网)解包 → 写 Site Contract → install.sh --airgap → 验收
管理入口目标机或可访问集群的跳板包内 bin/kubectl / bin/helm;k3s 默认 KUBECONFIG=/etc/rancher/k3s/k3s.yaml

2. 交付物清单与校验

2.1 获取交付包

交付物由 CI release:package(或 scripts/package.sh)产出并上传 OSS(oss://zbl-nanocloud/releases//)。
物料必须在目标服务器直接下载并校验,禁止先下载到个人电脑再中转到客户环境。
现场下载使用只读 RAM 账号(凭据见交付邮件 / offline-deploy §0.1;勿用于 CI 上传)。

nanocloud--amd64.tar.gz        # 必下
nanocloud--amd64.tar.gz.sha256 # 必下
release.lock.yaml                   # 亦含在包内 release.lock
index.json                          # 可选

目标服务器示例:

cd /tmp
ossutil ls "oss://zbl-nanocloud/releases//"
ossutil cp "oss://zbl-nanocloud/releases//nanocloud--amd64.tar.gz" .
ossutil cp "oss://zbl-nanocloud/releases//nanocloud--amd64.tar.gz.sha256" .
sha256sum -c nanocloud--amd64.tar.gz.sha256

2.2 校验与解包

#  :为安装版本号 比如:0.4.15
# 1) 完整性校验(必须通过) 
sha256sum -c nanocloud--amd64.tar.gz.sha256 

# 2) 解包
mkdir -p /opt/nanocloud
tar -xzf nanocloud--amd64.tar.gz -C /opt/nanocloud
cd /opt/nanocloud/nanocloud--amd64

# 3) 包内 helm/crane 加入 PATH(sudo 不继承用户 PATH)
export PATH="$PWD/bin:$PATH"

若解包后出现大量 ._*(macOS AppleDouble):find /opt/nanocloud -name '._*' -delete。

2.3 包内容核对

chart/  platform/  foundation/  images/  scripts/  db/  release.lock
bin/helm  bin/kubectl  bin/crane                  # 完整 release 包
scripts/restore.sh                                # 人工审批的灾难恢复入口
foundation/k3s/{k3s, k3s-airgap-images-*.tar.gz, install.sh}   # vendored;现场勿 curl get.k3s.io
foundation/storage/charts/longhorn-*.tgz         # k3s-ha addon
foundation/cert/cert-manager.yaml                # addon
foundation/addons.images.list                    # addon 镜像清单
images/{*.tar, MANIFEST.txt, SHA256SUMS}         # 除四个现场手工物料外的应用+基础镜像

**0.4.33 的 MySQL 相关法务边界:**完整离线包不携带下列四个镜像归档,仅通过 images/MANUAL_IMAGES.txt 声明文件名和镜像契约。交付方已将对应归档发布到 oss://zbl-nanocloud/releases/0.4.33/manual-images/;部署前必须直接下载到目标机 /srv/nanocloud/mysql-images,校验后通过 --manual-images-dir 交给安装器。

禁止随包携带的归档包内数量要求现场来源
nanocloud_db-migrator_1.2.0.tar0版本 OSS manual-images/ → /srv/nanocloud/mysql-images
nanocloud_backup-tools_1.0.0.tar0版本 OSS manual-images/ → /srv/nanocloud/mysql-images
mysql_8.0.tar0版本 OSS manual-images/ → /srv/nanocloud/mysql-images
container-registry.oracle.com_mysql_community-router_8.4.6.tar0版本 OSS manual-images/ → /srv/nanocloud/mysql-images

0.4.33 下载与校验:

mkdir -p /srv/nanocloud/mysql-images
ossutil cp -r oss://zbl-nanocloud/releases/0.4.33/manual-images/ /srv/nanocloud/mysql-images/
cd /srv/nanocloud/mysql-images
sha256sum -c SHA256SUMS
/opt/nanocloud/nanocloud-0.4.33-amd64/bin/crane validate --tarball nanocloud_db-migrator_1.2.0.tar
/opt/nanocloud/nanocloud-0.4.33-amd64/bin/crane validate --tarball nanocloud_backup-tools_1.0.0.tar
/opt/nanocloud/nanocloud-0.4.33-amd64/bin/crane validate --tarball mysql_8.0.tar
/opt/nanocloud/nanocloud-0.4.33-amd64/bin/crane validate --tarball container-registry.oracle.com_mysql_community-router_8.4.6.tar

目录必须恰好包含四个声明归档;缺文件、额外错版文件、archive 校验失败或现场 Registry digest 无法读取时均应终止安装。

红线检查:

  • release.lock禁止空 digest(客户包勿设 ALLOW_EMPTY_DIGESTS=1);空 digest = 拒绝安装,向发版方索包。
  • 交付包内不应残留 chart/values.local.yaml(本地联调文件)。若有会被 install.sh 自动叠加,导致镜像变 :latest、组件被禁用——有则改名或删除。
  • (k3s-ha)addons.images.list / images/MANIFEST* 必须含 CSI sidecar 与 engine 族(csi-attachercsi-provisionercsi-resizercsi-snapshottercsi-node-driver-registrarlivenessprobelonghorn-enginelonghorn-instance-manager 等 ≥13 条 longhornio/*),仅 grep longhorn 通过不算数——缺镜像会导致 Longhorn CSI ImagePullBackOff(补齐方法见 §14.5)。

3. 环境要求与预检

3.1 目标机要求

项要求
OSUbuntu 22.04 / 24.04 或同类(systemd);内核 ≥ 4.18
架构与包名一致(amd64 或 arm64),节点同构
资源全栈参考 ≥ 8C / 16G;磁盘建议 ≥ 200G SSD(lab 小盘须缩小 PVC,见 §14.6)
Kubernetes≥ 1.24(自带集群时;contract-check 硬门)
时钟NTP 同步(JWT/TLS 敏感)
swap建议关闭(os-preflight 检查)
端口6443(apiserver)、10250、flannel;业务 NodePort:80/443(HTTP/HTTPS)、1883/1884(MQTT)、30500(registry)
权限root / sudo
工具包内自带 bin/helm、bin/kubectl、bin/crane;不依赖现场在线安装这些工具

3.2 预检

解包后、安装前可单独执行(install.sh 也会自动跑):

sudo bash foundation/os-preflight.sh --profile standalone   # 或 k3s-ha

4. 出包(构建机)

仅交付方操作;现场可跳过本节。

在仓库 k8s/ 目录(或带 k8s/ 前缀自仓库根)执行,默认打 full 包(含 k3s airgap 段):

# 全量:构建应用/工具镜像 + 拉取基础镜像 + 出包
bash k8s/scripts/package.sh --arch amd64

# 复用本机已有镜像(不重复 build)
bash k8s/scripts/package.sh --arch amd64 --from-local
选项含义
--arch amd64|arm64|native目标架构(默认 amd64)
--version 须与 chart/Chart.yaml 的 version / appVersion 一致
--kind full|foundation|platform|app默认 full
--from-local使用本地已有镜像
--images-dir 使用预置 docker-archive + MANIFEST
--no-k3s不含 k3s(客户 standalone 包不要用)
--keep-images打包后保留临时 images 目录

产物:k8s/dist/nanocloud--.tar.gz(及 .sha256)。macOS 打包设 COPYFILE_DISABLE=1,避免 ._* AppleDouble 进包。


5. 现场配置:Site Contract

cp platform/standalone.yaml /tmp/site.yaml
vi /tmp/site.yaml

5.1 必填与关键项

键要求
network.publicAddress必填真实域名或 IP;占位符(example.com / CHANGE_ME)会被 preflight / chart schema 拒绝
images.registry--airgap 时由安装器注入 :30500;多机建议固定为首节点 IP 或 VIP;values 各镜像引用不要重复写 registry 前缀(helper 会拼成双前缀 404)
密钥 / 口令可留空:首装由 Helm 生成并写入 Secret nanocloud-credentials;生产建议在 gitignored 现场文件中显式设置
infra.mqtt.auth.enabled保持 true;禁止为绕过 robot-service 就绪而关 auth
backup.*生产必须开启并配置离机备份目标(见 §11);lab 可关
persistence.*磁盘不足时在首次安装前缩小(示例见 §14.6);registry PVC 不在 chart values 内

站点示例骨架见包内 chart/values.site.example.yaml。

**Longhorn 分层存储。**仅 k3s-ha 首装可配置 foundation.longhorn.tiering。启用时必须同时设置 platform.storageClass=longhorn-ssd 和 persistence.rustfs.storageClass=longhorn-hdd;完整配置、宿主机挂载责任边界和验收命令见 §20。

5.2 values 加载顺序(install.sh)

chart/values.yaml → platform/.yaml → chart/values.local.yaml(若存在)
  → --values 文件 →(airgap)注入 images.registry + release.lock digests

lock 注入在 --values之后,故 overlay 里写 images.digests.* 无法覆盖 lock;lab 需覆盖时用 --allow-digest-override --digest-override 。


6. 安装 A:单机 standalone(主路径)

6.1 一键安装(推荐)

cd /opt/nanocloud/nanocloud--amd64
export PATH="$PWD/bin:$PATH"

sudo env PATH="$PATH" bash scripts/install.sh \
  --profile standalone \
  --airgap \
  --values /tmp/site.yaml \
  --manual-images-dir /srv/nanocloud/mysql-images

k3s 由脚本安装(foundation/base-install.sh --airgap,使用包内二进制 + airgap 镜像,INSTALL_K3S_SKIP_DOWNLOAD=true)。

6.2 安装器内部顺序(standalone)

  1. 叠加 values / Site Contract preflight(弱口令、备份契约等)。
  2. 执行 foundation/os-preflight.sh。
  3. 用包内二进制和 K3s airgap 镜像安装 K3s;等待 API、Node Ready、默认 StorageClass 可用。
  4. 从离线包导入 registry:2.8.3,部署站点 registry(ns=nanocloud-registry,NodePort 30500)。
  5. 等 registry Ready,执行 push-images.sh 推送全部应用、基础组件和 addon 镜像。
  6. 执行 configure-registry-mirror.sh,写入 registry 配置并重启 K3s。
  7. 在镜像推送和 registry 配置完成后安装 cert-manager;启用 kube-vip 时同样在此阶段安装。
  8. 注入动态解析的 images.registry 与 release.lock digests,安装 Gateway API CRD 并检查升级窗。
  9. 执行 helm upgrade --install(PVC 走 local-path),等待 post-install 迁移与授权 Job 收敛。
  10. 执行 migrate/grants 与离线镜像来源硬门禁;由部署人员完成 §9 人工验收(无 helm test)。

0.4.11 原始包曾在第 5 步之前安装 cert-manager,导致其访问 quay.io 并超时。任何 addon
都不得早于站点 registry 填充完成启动。

standalone 不装 Longhorn、不跑 agent join。

6.3 干跑(可选)

sudo env PATH="$PATH" bash scripts/install.sh \
  --profile standalone --airgap --values /tmp/site.yaml --dry-run

6.4 常用安装选项

--manual-images-dir :指定 MANUAL_IMAGES.txt 声明的四个 MySQL 相关归档目录;0.4.33 现场统一使用 /srv/nanocloud/mysql-images,单节点和多节点均必须传入。

选项 / 环境变量含义
--values 现场 overlay(可重复)
--namespace / --release默认均为 nanocloud
--dry-run只渲染 / 预检,不落资源
--force-reinstall卸载失败/残留 release 后重装;无 release 时清 orphan 账本 CM
--recover-failed将 failed/pending release 回滚到上一 good revision
--skip-k3s已有集群时跳过 k3s bootstrap
--skip-contract-check专家:跳过集群契约硬门(打印 WARN)
--allow-digest-override + --digest-override 仅 lab:lock 注入后覆盖 digests
NANOCLOUD_DEV=1lab 逃逸:跳过多项预检(非生产支持标志)
REGISTRY_ENDPOINT覆盖站点 registry(默认 :30500)
LEGACY_IMPORT_IMAGES=1遗留:逐节点全量 ctr import(不推荐)

--allow-insecure-defaults已移除;本地 lab 用 NANOCLOUD_DEV=1。


7. 安装 B:多节点 K3s(默认 1 控制面 + 2 工作节点,HA 按需开启)

与 standalone 的关键差异:业务 Helm 只在三台计划节点 Ready 且 Longhorn 就绪后执行。profile 名继续为 k3s-ha,但默认 controlPlaneHA=false:首节点是唯一 control-plane/embedded-etcd server,另外两台是 worker agent。需要时才显式开启 HA,把同两台节点从 agentNodes 移到 serverNodes;旧 joinNodes 仅保留为 agent 兼容别名。

存储模型:默认兼容模式仍使用 platform.storageClass: longhorn。新建私有化站点可按 §20 启用 SSD/HDD 分层:除 RustFS 外的嵌入式基础组件使用默认类 longhorn-ssd,RustFS 显式使用 longhorn-hdd。nanocloud-registry全程使用 local-path,不迁入 Longhorn;已有未分层集群不得通过修改 values 原地切换 PVC 的 StorageClass。

7.1 前置:SSH 免密(人工完成)

自动加入支持 server 与 agent。安装器只消费已有 BatchMode SSH,不配置密钥——首节点上须已通过 ssh root@ true。已有 agent 被列入 serverNodes 时,脚本会先 cordon/drain,再以完全一致的关键网络参数转为 embedded-etcd server,Ready 后自动 uncordon。

7.2 Site Contract

foundation:
  k3s:
    serverIP: ""
    # 默认不覆盖 clusterCIDR / serviceCIDR,使用 K3s 内建 CIDR
    controlPlaneHA: false
    agentNodes:
      - ""
      - ""
    # sshUser: root
    # packageDir: /opt/nanocloud/nanocloud--amd64

# 按需开启三控制面 HA:
# controlPlaneHA: true
# serverNodes: ["", ""]
# agentNodes: []

环境变量等价项:默认模式使用 K3S_AGENT_NODES=,;开启 HA 时使用 K3S_CONTROL_PLANE_HA=1 与 K3S_SERVER_NODES=,,并清空 agent 列表。K3S_SERVER_IP= 指定首节点,K3S_JOIN_NODES 仍是 agent 别名。产品默认不传 Pod/Service CIDR 参数,由 K3s 使用内建 CIDR;只有客户 Site Contract 明确要求时才覆盖,且两者不得相同或重叠。

旋钮含义
controlPlaneHA / K3S_CONTROL_PLANE_HA默认 false;只有显式为 true 才允许追加 embedded-etcd server
serverNodes / K3S_SERVER_NODESHA 模式必须恰好两个;默认模式禁止配置
agentNodes / K3S_AGENT_NODES默认模式必须恰好两个;HA 模式必须为空;旧 joinNodes 仅为别名
SKIP_K3S_JOIN=1跳过自动加入,但 Site Contract 仍必须描述三节点,安装器仍检查 Ready 与角色
FOUNDATION_VIP=启用 kube-vip,并自动把 VIP 加入所有 server 的 API TLS SAN
SKIP_LONGHORN=1跳过 Longhorn;仅限明确不需要分布式业务存储的场景

7.3 首节点一键

cd /opt/nanocloud/nanocloud--amd64
export PATH="$PWD/bin:$PATH"

sudo env PATH="$PATH" bash scripts/install.sh \
  --profile k3s-ha \
  --airgap \
  --values /tmp/site-k3s-ha.yaml \
  --manual-images-dir /srv/nanocloud/mysql-images

执行顺序:首个 embedded-etcd server 以 K3s 默认 CIDR(客户显式覆盖时才传 CIDR)启动 → seed/启动现场 registry → push 全部镜像并配置首节点 mirror → 安装 cert-manager 与可选 kube-vip → 默认加入两个 agentNodes;仅在 controlPlaneHA=true 时加入两个 serverNodes → 等三节点 Ready 并校验默认 1 个或 HA 3 个 etcd 角色 → base-install --longhorn-only → Gateway CRD → Helm → migrate/grants 与镜像来源硬门禁 → 人工验收。

7.4 手工加入 server / agent(自动加入关闭时)

# 首节点记录 token
sudo cat /var/lib/rancher/k3s/server/node-token

# 加入节点:放二进制与 airgap 镜像
sudo mkdir -p /usr/local/bin /var/lib/rancher/k3s/agent/images
sudo cp foundation/k3s/k3s /usr/local/bin/k3s && sudo chmod +x /usr/local/bin/k3s
sudo cp foundation/k3s/k3s-airgap-images-* /var/lib/rancher/k3s/agent/images/

# 额外 embedded-etcd server:关键参数必须与首节点完全一致
sudo INSTALL_K3S_SKIP_DOWNLOAD=true sh foundation/k3s/install.sh server \
  --server https://:6443 --token  --disable=traefik \
  --cluster-cidr 10.10.6.0/24 --service-cidr 10.10.7.0/24 \
  --kube-controller-manager-arg node-cidr-mask-size-ipv4=26 \
  --kubelet-arg max-pods=60 --tls-san 

# 纯 agent
sudo INSTALL_K3S_SKIP_DOWNLOAD=true K3S_URL=https://:6443 \
  K3S_TOKEN= sh foundation/k3s/install.sh --kubelet-arg max-pods=60

# 每个节点都必须写 mirror
sudo bash scripts/configure-registry-mirror.sh --registry :30500

7.5 HA / CIDR 验收

验收不能只看 Node 角色,必须覆盖控制面读写故障注入。先核对 3 个 server 的 PodCIDR 分别属于目标网段、Service ClusterIP 属于独立 Service CIDR,再停止一个 server 的 K3s,在另一 server 使用本地 kubeconfig 轮询 /readyz,确认 etcd ok 后创建临时 ConfigMap;最后恢复被停节点并确认 3 个 etcd server 全部 Ready。

kubectl get nodes -o custom-columns=NAME:.metadata.name,ETCD:.metadata.labels.node-role\.kubernetes\.io/etcd,PODCIDR:.spec.podCIDR,MAXPODS:.status.capacity.pods
kubectl get svc -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,CLUSTER-IP:.spec.clusterIP
kubectl get --raw=/readyz?verbose | grep 'etcd ok'

**故障注入保护:**测试脚本必须用 trap 或等价机制保证失败也会恢复被停服务;未完成 etcd 快照和资源导出前不得在已有站点修改集群 CIDR。

铁律:mirror 与 disable-default-registry-endpoint 只是纵深防御,不是严格离线的正确性基础。
2026-08-04 实测 K3s v1.30.2 在站点 registry 返回 404 后仍可能请求 registry-1.docker.io。
NanoCloud、cert-manager、Longhorn、kube-vip 等受管镜像必须直接渲染为
${SITE_REGISTRY}/...@sha256:...;发现受管 Pod(含 initContainer)不是站点 registry 前缀,安装必须失败。


8. 安装 C:既有集群与云托管

8.1 k8s-bare(已有自建集群)

cp platform/k8s-bare.yaml /tmp/site.yaml
vi /tmp/site.yaml   # publicAddress + images.registry(外部 registry 显式填写)

# 端口冲突检查(有输出=冲突,改 overlay 中 gateway.traefik/mqtt nodePort)
kubectl get svc -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{" "}{.spec.ports[*].nodePort}{"\n"}{end}' | grep -E '30080|30443|1883|1883'

sudo env PATH="$PATH" bash scripts/install.sh \
  --profile k8s-bare --airgap --values /tmp/site.yaml
# 在线场景(可拉镜像)则去掉 --airgap,走 contract-check 硬门

要求:集群 ≥1.24、一个默认 StorageClass(WaitForFirstConsumer 可用)、kubeconfig 就绪。
注意 GatewayClass 是集群级单例——与既有 release 冲突时在 overlay 改 gateway.className(如 traefik-)独占一个 class。

8.2 云托管(eks / ack / tke)

infra 走云产品:对应 platform/.yaml overlay 关内嵌、填外部端点(RDS/OSS/MQTT 等),其余流程同 k8s-bare。细节以各 overlay 注释为准。


9. 安装后验收

Chart 已移除helm test / nanocloud-verify。安装器会做 migrate/grants 门禁;现场验收由部署同学按下列步骤手工完成。

9.1 总览

export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
export PATH="/opt/nanocloud/nanocloud--amd64/bin:$PATH"

bash scripts/status.sh -n nanocloud
kubectl get pods -n nanocloud
kubectl get pods -n nanocloud-registry
helm status nanocloud -n nanocloud

9.2 数据库迁移链(六个 platform-chain 库)

当前平台迁移链包含 MySQL 三库 db_x2robot_cloud / db_x2robot_analytics / db_x2robot_v2,
以及 PostgreSQL 三库 xrai-core / db_label_service / data_pipeline。首装走 post-install baseline,
升级走 pre-upgrade 增量 SQL;六库的 Goose applied 版本均须达到交付包声明的链头(0.4.12 基线为 10000)。
Airflow 与 Prefect 是 component-self,只由 pg-ensure 建库并由组件自身管理 schema,不属于平台 Goose 链。

NS=nanocloud
kubectl exec -n $NS mysql-0 -- sh -c \
  'MYSQL_PWD=$MYSQL_ROOT_PASSWORD mysql -uroot --ssl-mode=DISABLED -e "SHOW DATABASES"'
# 应看到上述 MySQL 三库;PostgreSQL 还应存在 xrai-core、db_label_service、data_pipeline、airflow、prefect

kubectl exec -n $NS mysql-0 -- sh -c \
  'MYSQL_PWD=$MYSQL_ROOT_PASSWORD mysql -uroot --ssl-mode=DISABLED -N -e \
   "SELECT version_id,is_applied FROM db_x2robot_cloud.goose_db_version ORDER BY id DESC LIMIT 3"'
# goose_db_version 无 comment 列;勿查 comment(ERROR 1054)

同时逐库检查 PostgreSQL 的 goose_db_version,并确认 data_pipeline.meta.collection_report_refresh_states
存在。collection-report 只检查端口或数据库存在仍可能在迁移窗口失败,必须等待这张精确业务表。

9.3 analysis-grants(analysis 开启时)

hook Job -analysis-grants 在 migrate 后为业务账号授权(ALL on analytics + SELECT on v2),带 schema gate(库不存在直接失败):

kubectl exec -n nanocloud mysql-0 -- sh -c \
  'MYSQL_PWD=$MYSQL_ROOT_PASSWORD mysql -uroot --ssl-mode=DISABLED -e \
   "SHOW GRANTS FOR '"'"''"'"'@'"'"'%'"'"'"'

9.4 入口与镜像前缀

# HTTP 入口(Traefik NodePort;Host = platform.|admin. + publicAddress)
curl -sS -o /dev/null -w '%{http_code}\n' -H "Host: platform." http://:30080/
curl -sS -o /dev/null -w '%{http_code}\n' -H "Host: admin."    http://:30080/
# 期望 200

# 版本账本
kubectl get cm -n nanocloud nanocloud-release -o jsonpath='{.data}'; echo

# 所有受管主容器和 initContainer 必须带动态解析出的站点 registry 前缀
for ns in nanocloud cert-manager longhorn-system; do
  kubectl get pods -n "$ns" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range .spec.initContainers[*]}{"  init "}{.image}{"\n"}{end}{range .spec.containers[*]}{"  main "}{.image}{"\n"}{end}{end}' 2>/dev/null
done

# 启动门禁应成功,主容器不应因数据库竞态发生重启
kubectl get pods -n nanocloud -o jsonpath='{range .items[*]}{.metadata.name}{" init="}{range .status.initContainerStatuses[*]}{.name}{":"}{.state.terminated.exitCode}{" "}{end}{" restart="}{range .status.containerStatuses[*]}{.name}{":"}{.restartCount}{" "}{end}{"\n"}'

# collection-report 最近调度应连续 Complete
kubectl get cronjob,job -n nanocloud | grep collection-report

# APISIX HMAC Consumer Secret(输出包含 app_secret,请勿粘贴到工单或群聊)
# 获取并解码 hmac-consumers.json
kubectl -n nanocloud get secret apisix-secret -o jsonpath='{.data.hmac-consumers\.json}' | base64 -d; echo

# 解码后用 jq 解析并格式化 JSON(需要 jq)
kubectl -n nanocloud get secret apisix-secret -o jsonpath='{.data.hmac-consumers\.json}' | base64 -d | jq .

# 如遇 JSONPath 转义兼容问题,可改用 go-template
kubectl -n nanocloud get secret apisix-secret -o go-template='{{index .data "hmac-consumers.json"}}' | base64 -d | jq .

9.5 验收勾选清单

  • 包 sha256 校验通过;release.lock 无空 digest

  • os-preflight 无 FAIL

  • k3s Ready;nanocloud-registry/registry Available;images/pushed.manifest 存在

  • (k3s-ha)全部计划节点 Ready,Longhorn CSI 族 Pod Running;未分层模式 default SC = longhorn;分层模式 default SC = longhorn-ssd,且 RustFS PVC = longhorn-hdd

  • 各节点 /etc/rancher/k3s/registries.yaml 指向站点 registry

  • nanocloud ns 工作负载就绪;helm status = deployed;三个受管 namespace 的主容器/initContainer 镜像前缀正确

  • 六个 platform-chain 库 Goose 链头正确;Airflow/Prefect schema 就绪;analysis grants(若启用)

  • initContainer 全部成功;新建业务 Pod 主容器 restartCount=0;collection-report 最近任务连续 Complete

  • 管理员登录成功;用户、租户及租户绑定数据符合 Site Contract/初始化预期

  • 部署同学手工验收通过(§9.1–§9.4)

  • Host 访问 platform / admin 均 200


9.6 日志部署与验收

日志能力由 Helm Chart 随平台统一部署,不需要在每个业务服务中增加 Sidecar。启用后,每个 Kubernetes 节点运行一个 Fluent Bit DaemonSet Pod,只采集当前 NanoCloud namespace 的容器标准输出,并写入 MinIO/S3。日志按日期和业务服务分层,最终对象路径为 nanocloud-logs/YYYYMMDD//.json.gz,例如 nanocloud-logs/20260807/xrai-core/xxx.json.gz。

**默认资源边界。**每节点请求 20m CPU / 32Mi 内存,上限 100m CPU / 96Mi 内存;内存缓冲上限为 32Mi,节点磁盘缓冲上限为 256Mi,上传并发为 1。磁盘缓冲位于独立 hostPath,仅在 MinIO 短时不可用时吸收积压,不占用业务容器的内存限制;达到上限后采集器按自身存储策略处理积压,不应阻塞业务进程写标准输出。

**保留策略。**Chart 的 post-install / post-upgrade 生命周期 Job 会创建 nanocloud-logs bucket,并配置 7 天对象过期和 1 天未完成分片清理。无需额外部署 CronJob 清理日志;旧目录结构中的对象也受同一 bucket 生命周期管理。

**离线依赖与 S3 选择。**日志采集器以 Fluent Bit 镜像随 airgap bundle 交付,不要求现场通过公网安装宿主机依赖;相关镜像必须进入镜像清单、写入 release.lock 并从站点 registry 拉取。s3.endpoint="" 时使用平台内嵌 MinIO;不使用内嵌 MinIO 时必须填写外部 MinIO/S3 endpoint,并通过独立 Secret 提供凭证,不要只修改 bucket 名称或在 values 中写明文密钥。

logging:
  enabled: true
  bucket: nanocloud-logs
  retentionDays: 7
  memoryBufferLimit: 32Mi
  localBufferSize: 256Mi
  uploadConcurrency: 1
  s3:
    endpoint: ""                 # 空表示使用平台内嵌 MinIO
    manageBucketLifecycle: true
PKG=/opt/nanocloud/nanocloud--amd64
NS=nanocloud
RELEASE=nanocloud
export PATH="$PKG/bin:$PATH"
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

# 三节点场景应为 DESIRED/CURRENT/READY=3;Pod 必须分别落在三个节点
kubectl get nodes -o wide
kubectl -n "$NS" get daemonset "${RELEASE}-log-agent" -o wide
kubectl -n "$NS" get pods -l app.kubernetes.io/name=log-agent -o wide

# 镜像必须来自动态现场 registry 且带 @sha256,不得回退公网
kubectl -n "$NS" get daemonset "${RELEASE}-log-agent" \
  -o jsonpath='{.spec.template.spec.containers[0].image}'; echo

# 配置中的对象路径应为 /%Y%m%d/$TAG[1]/$UUID.json.gz
kubectl -n "$NS" get configmap "${RELEASE}-fluent-bit" \
  -o jsonpath='{.data.fluent-bit\.conf}' | grep s3_key_format

# 分别检查每个 DaemonSet Pod;每个节点都应出现上传成功记录
for pod in $(kubectl -n "$NS" get pods \
  -l app.kubernetes.io/name=log-agent -o name); do
  echo "===== $pod ====="
  kubectl -n "$NS" logs "$pod" --since=15m \
    | grep 'Successfully uploaded object' | tail -5
done

# 先核对 Chart 渲染的目标规则:ID=nanocloud-log-retention、Status=Enabled、Days=7
kubectl -n "$NS" get configmap "${RELEASE}-log-lifecycle" \
  -o jsonpath='{.data.lifecycle\.json}'; echo

还必须从 MinIO 读取实际对象和实际 ILM,不能只检查 ConfigMap。以下临时 Pod 使用当前交付包锁定的 mc 镜像,凭据通过 Secret 引用注入,不在命令或日志中显示。

cd "$PKG"
SITE_REGISTRY=$(helm get values "$RELEASE" -n "$NS" -o json \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["images"]["registry"])')
MC_DIGEST=$(awk '$1 == "mc:" && match($0,/sha256:[0-9a-f]+/) {print substr($0,RSTART,RLENGTH); exit}' release.lock)
MC_IMAGE="${SITE_REGISTRY}/minio/mc@${MC_DIGEST}"
test -n "$SITE_REGISTRY" && test -n "$MC_DIGEST"

kubectl delete pod log-storage-audit -n "$NS" --ignore-not-found
kubectl apply -f - /dev/null
          echo '===== recent log objects ====='
          mc find logs/nanocloud-logs --name '*.json.gz' --newer-than 1d
          echo '===== actual lifecycle ====='
          mc ilm export logs/nanocloud-logs
      env:
        - name: S3_ACCESS_KEY
          valueFrom:
            secretKeyRef:
              name: nanocloud-credentials
              key: minio-root-user
        - name: S3_SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: nanocloud-credentials
              key: minio-root-password
EOF

kubectl wait -n "$NS" --for=jsonpath='{.status.phase}'=Succeeded \
  pod/log-storage-audit --timeout=180s
kubectl logs log-storage-audit -n "$NS"
kubectl delete pod log-storage-audit -n "$NS"

验收结果必须同时满足:DaemonSet 为 3/3、三个节点均有成功上传记录、MinIO 中存在 nanocloud-logs/YYYYMMDD//.json.gz,且实际 ILM 包含 nanocloud-log-retention、Enabled 和 Expiration.Days=7。仅看到 ConfigMap 中写了 7 天,不代表生命周期已在对象存储侧生效。

若 DaemonSet 在某个节点长期停留于 ImagePullBackOff,先检查该节点的 registry mirror、包内 images/pushed.manifest、release.lock 中的 Fluent Bit digest,以及渲染后的镜像是否以现场 registry 为前缀。不要通过改回 Docker Hub 地址绕过离线链路。

# 查看采集器错误、重试和上传失败
kubectl -n "$NS" logs daemonset/"${RELEASE}-log-agent" --since=30m | grep -Ei "error|retry|failed|buffer" | tail -100

# 查看 DaemonSet 是否有节点未就绪
kubectl -n "$NS" get daemonset "${RELEASE}-log-agent" -o wide
kubectl -n "$NS" get pods -l app.kubernetes.io/name=log-agent -o wide

# 查看节点本地缓冲目录(按实际节点进入)
sudo du -sh /var/lib/nanocloud/log-agent/buffer 2>/dev/null || true
sudo ls -lh /var/lib/nanocloud/log-agent/ 2>/dev/null || true

# 外部 S3 场景:确认 endpoint 和凭证 Secret 已注入,不要回退公网
kubectl -n "$NS" get secret -o name | grep -E "log|s3|object"
  • 日志采集 DaemonSet 在全部节点 Ready,镜像来自现场 registry 且 digest 与 release.lock 一致

  • MinIO 中已出现 nanocloud-logs/YYYYMMDD// 对象,生命周期规则为 7 天

10. AI 管线

Chart 默认 services.pipeline.enabled=true,会部署 AI 管线并开启文档约定的 MinIO 制品链路。不需要该能力时,
在 Site Contract 显式设为 false 并重跑 install.sh:

services:
  pipeline:
    enabled: true
    datahubApiKey: ""                       # 空则首装 lookupOrRand 写入 nanocloud-credentials
    vlm:
      arkApiKey: ""           # caption / instruction 需要
      dashscopeApiKey: ""                   # 按需
    # Caption、Instruction 和制品物化链路默认开启。
    # Smart QC、Tagging、Importer、通知及策略类业务默认关闭。
项说明
镜像包内需含 data-hub / flowforge / artifact-writer
建库data_pipeline 由平台迁移 Job db-migrate-data-pipeline(weight -10)entrypoint 幂等创建;prefect 由 hook Job pg-ensure(weight -6)幂等创建,晚开也安全(2026-07-29 起替代 data-pipeline-ensure)
表结构data_pipeline schema 唯一通道 = 平台 goose 链(db-migrate-data-pipeline,goose validate && up);原 data-hub -mode migrate 通道已退役(migration-rebaseline-plan.md)
工作负载data-hub、prefect-server、FlowForge resident/Caption/Instruction worker、artifact-writer
核心调度Dispatcher、Reconciler 和 Instruction Artifact Sync 默认开启;Smart QC、Tagging 等业务仍默认关闭
入口Data Hub 不暴露 HTTP 路由;Prefect UI 由 gateway.expose.prefect 控制,且未接入 NanoCloud RBAC
FlowForgeworker 使用集群内 http://prefect-server:4200/api,不连接云端 Prefect
凭证kubectl get secret nanocloud-credentials -n nanocloud -o jsonpath='{.data.datahub-api-key}' | base64 -d; echo

启用后核验:

kubectl get job  -n nanocloud -l 'app.kubernetes.io/component in (db-migrate-data-pipeline,pg-ensure)'
kubectl get deploy -n nanocloud data-hub prefect-server flowforge-qc-worker artifact-writer
kubectl exec -n nanocloud postgres-0 -- sh -c \
  'PGPASSWORD=$POSTGRES_PASSWORD psql -U "$POSTGRES_USER" -d postgres -tAc \
   "SELECT datname FROM pg_database WHERE datname IN ('\''data_pipeline'\'','\''prefect'\'') ORDER BY datname"'

11. 备份开通(生产必做)

生产 profile 契约要求 backup.enabled=true + 非空 backup.target.endpoint(install 预检)。
目标必须离机:与集群同机/同盘的备份不构成灾难恢复能力。验证环境可临时指向内嵌 MinIO,上线前必须改离机端点。

11.1 A 轨架构速览

每日 02:00 CronJob -site-export(concurrencyPolicy=Forbid,失败重试 3 次):

mysqldump/pg_dump(C1 库清单)→ 版本账本导出 → tar + zstd → age 公钥加密
  → mc 推送离机目标(S3 契约,只写凭据)→ 保留策略(日 7 + 周 4 + 月 12)
  • C1 库清单:MySQL db_x2robot_cloud,db_x2robot_v2,db_x2robot_analytics;PG xrai-core,db_label_service,airflow,data_pipeline,prefect;ClickHouse x2robot_analysis。
  • payload:dbs/(dump)+ ledger/(账本快照)+ keys/(记名不记值密钥清单)。
  • age 私钥永不进集群(日常路径);仅恢复演练 Job 按需挂载。

11.2 开通步骤

# ① 在集群外的受控运维机生成 age 密钥对;私钥权限仅限当前用户
umask 077
age-keygen -o backup-age-private-key.txt
age-keygen -y backup-age-private-key.txt > backup-age-public-key.txt

# 公钥必须为单行 age1...;私钥以 AGE-SECRET-KEY- 开头
# 只允许把公钥写入集群,backup-age-private-key.txt 必须离线保管,禁止上传服务器/集群
cat backup-age-public-key.txt

NS=nanocloud

# ② 创建或更新 age 公钥 Secret(键名必须为 publicKey)
kubectl -n "$NS" create secret generic backup-age-public-key \
  --from-file=publicKey=backup-age-public-key.txt \
  --dry-run=client -o yaml | kubectl apply -f -

# 验证公钥格式;输出应以 age1 开头
kubectl -n "$NS" get secret backup-age-public-key \
  -o jsonpath='{.data.publicKey}' | base64 -d
echo

# ③ 创建或更新备份目标凭据 Secret(外部 S3/OSS/MinIO)
# AK/SK 由目标存储管理员提供,建议仅授予目标 bucket 的最小读写权限
kubectl -n "$NS" create secret generic backup-target-credentials \
  --from-literal=accessKey=[REDACTED]' \
  --from-literal=secretKey=[REDACTED]' \
  --dry-run=client -o yaml | kubectl apply -f -

# 内部 minio  获取
kubectl -n "$NS"  get secret nanocloud-credentials \
    -o jsonpath='{.data.minio-root-user}' | base64 -d

# 内部 minio  获取
kubectl -n "$NS"  get secret nanocloud-credentials \
    -o jsonpath='{.data.minio-root-password}' | base64 -d

# 只检查键名和字节数,不输出凭据明文
kubectl -n "$NS" describe secret backup-target-credentials

Site Contract 追加后重跑 install.sh:

backup:
  enabled: true
  target:
    endpoint: https://s3.   # 离机!
    bucket: nanocloud-backup
    prefix: site-export
    credentialsSecret: backup-target-credentials
  # retention: { daily: 7, weekly: 4, monthly: 12 }
  # convenienceCopy: { enabled: true, keep: 7 }   # 可选:平台 MinIO 便利副本(非 DR)

内部存储:使用内嵌 MinIO

仅为跑通备份 CronJob 和恢复流程时,可将目标临时设为同 namespace 的 http://minio:9000。这份副本与业务处于同一集群、同一故障域,不属于离机备份,也不能作为生产灾难恢复依据。

backup:
  enabled: true
  target:
    endpoint: http://minio:9000
    bucket: nanocloud-backup
    prefix: site-export
    credentialsSecret: backup-target-credentials

backup-target-credentials 和 backup-age-public-key 不会由 Chart 自动创建。走内部存储可将现有平台 MinIO 凭据转换为备份目标要求的键名;命令不会输出凭据明文:

NS=nanocloud

# 从平台 Secret 读取内部 MinIO 管理凭据;变量只存在于当前 shell
MINIO_USER=$(kubectl -n "$NS" get secret nanocloud-credentials \
  -o jsonpath='{.data.minio-root-user}' | base64 -d)
MINIO_PASSWORD=$(kubectl -n "$NS" get secret nanocloud-credentials \
  -o jsonpath='{.data.minio-root-password}' | base64 -d)

# 转换成备份 Job 要求的 accessKey / secretKey 键名;重复执行会更新 Secret
kubectl -n "$NS" create secret generic backup-target-credentials \
  --from-literal=accessKey=[REDACTED] \
  --from-literal=secretKey=[REDACTED] \
  --dry-run=client -o yaml | kubectl apply -f -

unset MINIO_USER MINIO_PASSWORD

# 只验证 Secret 存在、键名正确,不输出凭据明文
kubectl -n "$NS" describe secret backup-target-credentials

# 内部 MinIO 只替代备份目标,不替代加密步骤;仍须按上文创建 backup-age-public-key
kubectl -n "$NS" get secret backup-age-public-key

离线演练:不修改包内 Chart,使用包内 backup-tools 生成 age 密钥

本流程用于干净环境验收。只新增站点 overlay 与 Kubernetes Secret,不修改交付包内 Chart、release.lock 或镜像清单。backup-tools 必须由当前交付包的 release.lock 锁定,并从现场 registry 以 digest 拉取;禁止临时访问公网镜像。

先在已完成离线安装的首节点执行以下命令。PKG 指向当前版本解包根目录;PROFILE 和 SITE_VALUES 必须与首次安装保持一致。

PKG=/opt/nanocloud/nanocloud--amd64
NS=nanocloud
RELEASE=nanocloud
PROFILE=k3s-ha
SITE_VALUES=/tmp/site-k3s-ha.yaml

export PATH="$PKG/bin:$PATH"
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
cd "$PKG"

test -s release.lock
test -s "$SITE_VALUES"
kubectl get nodes

# 从已部署 Helm values 读取动态现场 registry;不得写死节点 IP
SITE_REGISTRY=$(helm get values "$RELEASE" -n "$NS" -o json \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["images"]["registry"])')
BACKUP_DIGEST=$(awk '$1 == "backup:" && match($0,/sha256:[0-9a-f]+/) {print substr($0,RSTART,RLENGTH); exit}' release.lock)
test -n "$SITE_REGISTRY"
test -n "$BACKUP_DIGEST"
BACKUP_IMAGE="${SITE_REGISTRY}/nanocloud/backup-tools@${BACKUP_DIGEST}"
printf 'backup image: %s\n' "$BACKUP_IMAGE"

密钥生成 Pod 的日志包含 age 私钥,因此必须直接写入权限为 0600 的受控文件,不在终端回显。Pod 日志落盘后立即删除;集群只保存公钥 Secret。

KEY_DIR=/root/nanocloud-preserve
KEY_FILE="$KEY_DIR/backup-age-private-key.txt"

install -d -m 700 "$KEY_DIR"
umask 077
kubectl delete pod age-keygen -n "$NS" --ignore-not-found
kubectl run age-keygen -n "$NS" \
  --image="$BACKUP_IMAGE" --restart=Never --command -- age-keygen
kubectl wait -n "$NS" --for=jsonpath='{.status.phase}'=Succeeded \
  pod/age-keygen --timeout=120s
kubectl logs age-keygen -n "$NS" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
kubectl delete pod age-keygen -n "$NS"

# 只解析公钥;不得 cat 私钥文件
AGE_PUBLIC_KEY=$(sed -n 's/^# public key: //p' "$KEY_FILE" | head -1)
case "$AGE_PUBLIC_KEY" in age1*) ;; *) echo 'age 公钥解析失败' >&2; exit 1;; esac

kubectl create secret generic backup-age-public-key -n "$NS" \
  --from-literal=publicKey="$AGE_PUBLIC_KEY" \
  --dry-run=client -o yaml | kubectl apply -f -
unset AGE_PUBLIC_KEY

# 只检查 Secret 键和长度,不输出公钥或私钥
kubectl describe secret backup-age-public-key -n "$NS"

随后按上方“内部 MinIO:复用平台凭据创建目标 Secret”创建 backup-target-credentials,并在包外新建站点 overlay。内部 MinIO 仅用于证明导出、加密、上传和回读校验链路,不构成离机容灾。

backup:
  enabled: true
  target:
    endpoint: http://minio:9000
    bucket: nanocloud-backup
    prefix: site-export
    credentialsSecret: backup-target-credentials
  retention:
    enabled: true
    daily: 7
    weekly: 0
    monthly: 0
BACKUP_VALUES=/tmp/backup-test-values.yaml
test -s "$BACKUP_VALUES"

cd "$PKG"
sudo env PATH="$PATH" KUBECONFIG="$KUBECONFIG" \
  bash scripts/install.sh --profile "$PROFILE" --airgap \
  --values "$SITE_VALUES" --values "$BACKUP_VALUES"

kubectl get cronjob "${RELEASE}-site-export" -n "$NS"
kubectl get cronjob "${RELEASE}-site-export" -n "$NS" \
  -o jsonpath='{.spec.jobTemplate.spec.template.spec.containers[?(@.name=="site-export")].image}'; echo

最后一条命令输出必须是现场 registry 前缀并带 @sha256:。重跑安装器会按受支持入口重新校验离线物料和当前 release;请在维护窗口执行并保留完整终端记录。

上线前必须把 backup.target.endpoint、目标凭据和 bucket 切换到独立于 NanoCloud 集群的 S3/OSS/MinIO,并重新执行一次手动备份与恢复验证。

11.3 手动触发与验证

NS=nanocloud
RELEASE=nanocloud
CRONJOB="${RELEASE}-site-export"
JOB="${CRONJOB}-manual-$(date +%Y%m%d%H%M%S)"

kubectl get cronjob "$CRONJOB" -n "$NS"
kubectl create job -n "$NS" --from="cronjob/$CRONJOB" "$JOB"

if ! kubectl wait -n "$NS" --for=condition=complete "job/$JOB" --timeout=30m; then
  kubectl describe job "$JOB" -n "$NS"
  kubectl logs "job/$JOB" -n "$NS" --all-containers --prefix=true --tail=300
  exit 1
fi

# 完整保留验收日志;末行应包含“A 轨备份成功”和 backupId
kubectl logs "job/$JOB" -n "$NS" -c site-export --timestamps

# 应同时看到 data 与 annotation 两处 backup.last,location 指向 site-export/
kubectl get configmap "${RELEASE}-release" -n "$NS" \
  -o jsonpath='{.data.backup\.last}'; echo
kubectl get configmap "${RELEASE}-release" -n "$NS" \
  -o go-template='{{ index .metadata.annotations "nanocloud.io/backup-last" }}'; echo

# 完成取证后再清理临时 Job
kubectl delete job "$JOB" -n "$NS"

成功日志必须覆盖:MySQL 3 库、PostgreSQL 5 库、启用时的 ClickHouse 库、账本与记名不记值的密钥清单、tar.zst 压缩、age 加密、上传后 SHA-256 回读一致,以及保留轮换结果 日 7 / 周 0 / 月 0。若日志只显示 Job Complete,但缺少回读校验或 backup.last,不得判定备份闭环通过。

排障速查:Job 起不来且报 backup-age-public-key 缺失 → 公钥 Secret 未建或键名错;推送 403 → 目标凭据/权限不符;dump 连不上库 → nanocloud-credentials 口令与 infra values 不一致。

11.4 B 轨(对象复制 + 月度演练,按需)

默认关。生产开备份时可显式打开:

  • 源桶版本化:objectTrack.enabled=true 时 minio-init 对业务桶 mc version enable + ILM noncurrent-expire-days = graceDays(默认 7)。
  • 对象复制:mode: mirror(mc mirror --overwrite,不开 --remove)或 replication(mc replicate add + 水位探测);副本 Secret backup-object-replica-credentials;禁止把 replica endpoint 指回平台 MinIO 自身。
  • 月度演练:drill.enabled=true 需 age 私钥 Secret(backup-age-private-key,键 privateKey)——与日常"私钥不进集群"路径不同,现场拒绝则不要开。演练深度:抽一份 A 轨 → 解密 → checksum → 副本抽样 → 写 drill.last;不做隔离库全量导入。
  • drill.requireForUpgrade: true 且严格模式时,空/fail/≥45 天演练记录会让升级门 FAIL。

12. 升级与回退

12.1 升级模型(先迁移后滚动)

升级 = 用新交付包在同一 Site Contract 上重跑同一条 install.sh 命令。helm hook 保证顺序:

pre-upgrade -20  backup-gate    备份门(G1 快照桩 / G2 每日备份新鲜度 / G3 账本可读 / G4 演练年龄 / G5 复制滞后)
pre-upgrade -10  db-migrate-*   平台迁移链 goose validate && up(仅 platform-chain 库)
主资源滚动更新(Deployment / StatefulSet)

关键性质:

  • 先迁移后滚动:schema 先于代码升级;迁移只增不改(破坏性变更拆 expand/contract 两个 Release)。
  • 备份门:备份过期时门即时补跑一次完整 A 轨(产物前缀 backups/,与每日备份的 site-export/ 不同,都正常);补跑失败才拦升级,且主资源零改动。备份未开通(backup.enabled=false)时门直接跳过——生产升级前务必先开通每日备份,否则失去回滚锚点。
  • 升级窗:新包 release.lock 的 minUpgradeFrom 与集群账本比较,`from --namespace $NS --airgap --values /tmp/site.yaml --dry-run # 与正式安装同输入渲染比对

### 12.3 执行与验证

cd /opt/nanocloud/nanocloud--amd64 export PATH="$PWD/bin:$PATH" sudo env PATH="$PATH" bash scripts/install.sh
--profile --airgap --values /tmp/site.yaml


观察(另开终端):`kubectl get jobs -n $NS -w`(先 backup-gate 后 db-migrate);  
`kubectl logs -n $NS -l job-name=-db-migrate- -f`。

升级后验证:goose 链头 = 新包链头;账本 `release` / `lastUpgradeAt` 回写;全 Pod Running;  
镜像 digest 与新包 `release.lock` 一致且仍以动态站点 registry 为前缀;业务冒烟同 §9。

### 12.4 失败恢复与回退

| 场景 | 处置 |
|-|-|
| `Prior release in state 'pending-*'` | 上次操作中断:`--recover-failed` 优先;无 good revision 再 `--force-reinstall` |
| 备份门失败 | 先手动触发一次备份(§11.3),成功后再升级 |
| 迁移 hook 失败 | release 标 failed、主资源零改动;看 hook Pod 日志定位 SQL,修复后重跑 install.sh |
| 升级后业务异常、schema 向后兼容 | 安装器可回滚到上一 Helm revision;数据库迁移不回退,完成后按 §9 复验 |
| 已前进 schema 不兼容或数据损坏 | **不做 goose down,也不自动恢复数据库**;审批后用 `scripts/restore.sh` 指定 backupId 恢复,再用旧交付包重装 |

---

## 13. 卸载与清场

export PATH="$PWD/bin:$PATH"

bash scripts/uninstall.sh --platform --namespace nanocloud # 卸平台(保留业务 PVC) bash scripts/uninstall.sh --platform --purge --namespace nanocloud # 并删该 ns 全部 PVC(交互确认) bash scripts/uninstall.sh --foundation # 仅打印拆 k3s 指引(不自动执行)


| 对象 | 位置 | 说明 |
|-|-|-|
| Helm release / 业务 PVC | ns `nanocloud` | `--purge` 只删**该 ns** 的 PVC |
| 版本账本 CM `nanocloud-release` | ns `nanocloud` | 带 keep policy,helm uninstall **不删**;重装受阻时手删或 `--force-reinstall` |
| Registry PVC | ns `nanocloud-registry`,名 `registry-data` | **不在**`--purge` 范围;彻底清场另行删除 |
| k3s | 本机 | 确认后手工 `sudo bash /usr/local/bin/k3s-uninstall.sh`(agent:`k3s-agent-uninstall.sh`) |


彻底清场示例(**不可逆,生产慎用**):

sudo bash scripts/uninstall.sh --platform --purge --namespace nanocloud kubectl delete ns nanocloud-registry --ignore-not-found kubectl delete configmap nanocloud-release -n nanocloud --ignore-not-found sudo bash /usr/local/bin/k3s-uninstall.sh


**三节点彻底清场顺序:**先卸业务和 Longhorn,确认 CSI 挂载已释放;再先卸两个 agent,最后卸首节点 server。反向操作可能让 Longhorn globalmount 或 kubelet mount 卡住 K3s 卸载。

1. 首节点执行平台 `--purge`,删除站点 Registry,并卸载 Longhorn。
2. 在 `172.16.10.242`、`172.16.10.243` 依次运行 `/usr/local/bin/k3s-agent-uninstall.sh`。
3. 在 `172.16.10.241` 运行 `/usr/local/bin/k3s-uninstall.sh`。
4. 逐节点确认没有 kubelet、Longhorn 或 K3s 残留挂载,再清理明确的旧数据目录。

每台节点验收;systemd 返回 inactive/unknown,findmnt 无输出

systemctl is-active k3s k3s-agent || true findmnt -rn -o TARGET | grep -E '/var/lib/(kubelet|longhorn|rancher/k3s)' || true

仅在确认是测试环境且数据已批准删除后执行

rm -rf /var/lib/longhorn /var/lib/rancher/k3s /var/lib/kubelet

再次确认目录与挂载均不存在

test ! -e /var/lib/longhorn test ! -e /var/lib/rancher/k3s findmnt -rn -o TARGET | grep -E '/var/lib/(kubelet|longhorn|rancher/k3s)' && exit 1 || true


---

## 14. 故障排查

### 14.1 通用诊断入口

helm status nanocloud -n nanocloud kubectl get jobs -n nanocloud --sort-by=.metadata.creationTimestamp kubectl get pods -n nanocloud --field-selector=status.phase!=Running,status.phase!=Succeeded kubectl logs -n nanocloud -l job-name= --tail=200


`helm STATUS=failed` 时**不要只看 DESCRIPTION 提到的最后一个 Job**——列全部 hook/Job。成功的 hook Job 可能已被 `hook-delete-policy` 删除,属正常。常见 hook:`jwt-keygen`、`db-migrate-*`、`analysis-grants`、`minio-init`、`backup-gate`(升级时)。

### 14.2 镜像拉取 / registry

| 现象 | 处置 |
|-|-|
| registry Pod ImagePullBackOff | `sudo bash scripts/seed-registry-image.sh --images-dir images`(import 后 `ctr tag` 短名);确认 socket `/run/k3s/containerd/containerd.sock` |
| push 失败 connection refused | registry 未 Ready / 防火墙:`kubectl -n nanocloud-registry rollout status deploy/registry`;放行 **30500** |
| 业务 Pod ImagePullBackOff | mirror 未生效 / 未 push / 前缀错:重跑 `configure-registry-mirror.sh --registry :30500`;查 `images/pushed.manifest` |
| 双前缀 not found(`…/…/…`) | values 镜像引用重复写了 registry 前缀;由 `images.registry` 拼接,引用保持短名 |
| IfNotPresent 仍拉不到,ctr 能看到 | 镜像导进了系统 containerd 而非 k3s 的;用包内脚本(自动探测 k3s socket)重导 |
| tag 能拉、`@sha256:` 失败 | digest 问题,见 §14.4 |
| jwt-keygen DeadlineExceeded | 多为 toolbox digest 拉取失败(`@sha256:` 引用,tag 预拉无效);先修 digest(§14.4) |
| `release.lock` 空 digest | 非客户包 / lab 逃逸;拒绝安装,向发版方索取完整 digest 包 |

### 14.3 数据库 / hook

| 错误 / 现象 | 根因与处置 |
|-|-|
| `ERROR 1049` Unknown database | 库尚未被 migrate 创建:查 db-migrate hook 日志,确认三库注册且 Job 成功 |
| `ERROR 1044` Access denied | 业务账号缺授权:查 `analysis-grants` Job(须在 migrate 之后成功);verify-schema 会查 grants |
| `pending-install` + 无 Job + 只有 cloud 库 | **Helm `--wait` 死锁**(Chart /dev/null
cat /etc/rancher/k3s/registries.yaml

# 3) 对比 lock digest 与 registry 实际 digest
REG=:30500
LOCK_DIGEST=$(grep -E '^[[:space:]]+toolbox:' release.lock | grep -oE 'sha256:[a-f0-9]+')
TAG_DIGEST=$(PATH=$PWD/bin:$PATH crane digest --insecure ${REG}/nanocloud/toolbox:1.0.0)
curl -sS -o /dev/null -w '%{http_code}\n' -I "http://${REG}/v2/nanocloud/toolbox/manifests/${LOCK_DIGEST}"

修复(推荐 A):

# A) 从包重 push,使 registry 同时有 tag 与 lock digest
export PATH="$PWD/bin:$PATH"
bash scripts/push-images.sh --registry "$REG" --images-dir images

# B) registry 无法重 push 时 — helm 覆盖为 registry 实际 digest
helm upgrade nanocloud ./chart -n nanocloud --reuse-values \
  --set "images.digests.toolbox=${TAG_DIGEST}"
kubectl delete job -n nanocloud -l app.kubernetes.io/component=jwt-keygen --ignore-not-found

# 长期:重跑 configure-registry-mirror.sh(写 registries + config + restart)
sudo bash scripts/configure-registry-mirror.sh --registry ${REG}

lab 还可用 install.sh --allow-digest-override --digest-override digests-override.yaml 在 lock 注入后覆盖。

14.5 Longhorn(k3s-ha)

  • CSI / engine ImagePullBackOff:包内 addons.images.list 缺 CSI sidecar / engine 族。有网跳板补齐后重 push(v1.7.0 完整集见 §2.3 清单):

    REG=:30500
    for img in longhornio/csi-attacher:v4.6.1 longhornio/csi-provisioner:v4.0.1 longhornio/csi-resizer:v1.11.1 longhornio/csi-snapshotter:v7.0.2 longhornio/csi-node-driver-registrar:v2.11.1 longhornio/livenessprobe:v2.13.1 longhornio/longhorn-engine:v1.7.0 longhornio/longhorn-instance-manager:v1.7.0; do
      ./bin/crane copy "docker.io/${img}" "${REG}/${img}" --insecure
    done
    sudo bash foundation/base-install.sh --profile k3s-ha --longhorn-only
  • registry PVC Pending:local-path 磁盘不足 / provisioner 异常(registry 不迁 Longhorn):kubectl -n nanocloud-registry get pvc,events;kubectl get sc local-path。

14.6 磁盘 / PVC 过小(lab 小盘)

0.4.33 的 k3s-ha 默认业务 PVC 请求合计约 115Gi。Longhorn 默认每卷 2 副本,仅副本数据即需要约 230Gi,还未计入 storage-minimal-available-percentage=25、Registry local-path、系统组件、快照和增长空间。本次三台约 99Gi 系统盘在默认值下出现 insufficient storage / ReplicaSchedulingFailure,不能通过只缩小 RustFS 一个卷解决。

# 仅用于本次约 100Gi/节点的 0.4.33 测试环境;生产不得直接复制
persistence:
  mysql:      { size: 10Gi }
  postgres:   { size: 5Gi }
  redis:      { size: 2Gi }
  rustfs:     { size: 10Gi }
  emqx:       { size: 2Gi }
  kafka:      { size: 2Gi }
  clickhouse: { size: 10Gi }
  airflow:    { size: 5Gi }

上述测试 overlay 的逻辑请求总量为 46Gi,按 2 副本约占 92Gi,最终 8 个 Longhorn volume 均为 attached/healthy。生产容量必须按“PVC 请求总量 × 副本数 + 最小保留空间 + 快照 + 预期增长”规划,优先增加独立数据盘;不得照搬测试容量,也不得通过放宽 Longhorn over-provisioning 或 minimal-available 阈值掩盖物理容量不足。Registry PVC 仍使用 local-path,不计入业务 Longhorn PVC,但必须单独预留系统盘空间。

14.7 节点 Muttipathd 条件异常

# 每个节点都需要执行 最好提前和存储管理员确认

cp -a /etc/multipath.conf \
  /etc/multipath.conf.bak.$(date +%Y%m%d%H%M%S)

cat >/etc/multipath.conf /dev/null || true
systemctl reload multipathd || systemctl restart multipathd

# 验证配置
grep -n -A10 -B2 blacklist /etc/multipath.conf
systemctl is-active multipathd
multipath -ll

# 更新配置后可继续执行安装脚本

14.8 其他高频项

现象处置
Prior release in state 'pending-*'--recover-failed 或 --force-reinstall
helm 已卸但重装被账本拦(minUpgradeFrom 空)kubectl delete cm nanocloud-release -n nanocloud;或 --force-reinstall(无 release 时自动清 orphan 账本)
部署后镜像全变 :latest、组件消失包内残留 chart/values.local.yaml 被自动叠加:删除/改名后重装
helm: command not found(sudo 下)sudo env PATH="$PWD/bin:$PATH" bash scripts/install.sh …,或 ./bin/helm
NodePort 冲突改 overlay 中 Traefik / MQTT nodePort,或查占用
GatewayClass "traefik" exists and cannot be imported集群级单例被占:overlay 改 gateway.className: traefik- 独占
robot-service Ready 失败、:8000 refused进程等 MQTT 连上才听 HTTP;勿关 MQTT auth,核对 nanocloud-credentials 注入
CRD apply / 渲染报 YAML 错,目录有 ._*macOS AppleDouble:find /opt/nanocloud -name '._*' -delete;打包侧 COPYFILE_DISABLE=1
前端 nginx.conf mount「not a directory」frontend-nginx ConfigMap 未生成(旧 chart 模板缺陷);升级包并确认 CM 存在

**日志告警忽略边界。**短时的单次上传失败可以观察重试是否恢复;但如果持续忽略 MinIO/S3 不可达、磁盘缓冲增长或 DaemonSet 非 Ready,超过 256Mi 本地缓冲上限后,尚未上传的日志可能丢失,且 Pod 删除后无法追溯。处理顺序:先确认 log-agent Pod、registry mirror 和 endpoint/Secret,再检查 MinIO/S3 连通性与生命周期配置;恢复后用 9.6 节的上传成功日志、对象路径和生命周期查询命令完成闭环验收。


附录:命令速查与版本钉扎

15.1 命令速查

# —— 构建机 ——
bash k8s/scripts/package.sh --arch amd64
bash k8s/scripts/package.sh --arch amd64 --from-local

# —— 目标机:预检 / 安装 ——
sudo bash foundation/os-preflight.sh --profile standalone
export PATH="$PWD/bin:$PATH"
sudo env PATH="$PATH" bash scripts/install.sh --profile standalone --airgap --values /tmp/site.yaml
sudo env PATH="$PATH" bash scripts/install.sh --profile standalone --airgap --values /tmp/site.yaml --dry-run
sudo env PATH="$PATH" bash scripts/install.sh --profile standalone --airgap --values /tmp/site.yaml --force-reinstall

# —— 状态 / 测试 ——
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
bash scripts/status.sh -n nanocloud
helm status nanocloud -n nanocloud
kubectl get pods,svc,gateway,httproute -n nanocloud
kubectl get pods,pvc -n nanocloud-registry

# —— 备份 ——
kubectl create job -n nanocloud --from=cronjob/-site-export manual-backup-$(date +%Y%m%d%H%M)

# —— 卸载 ——
bash scripts/uninstall.sh --platform --namespace nanocloud
bash scripts/uninstall.sh --platform --purge --namespace nanocloud
bash scripts/uninstall.sh --foundation

# —— 拆步(一般不必)——
sudo bash foundation/base-install.sh --profile standalone --airgap
sudo bash scripts/seed-registry-image.sh --images-dir images
sudo bash scripts/push-images.sh --registry ":30500" --images-dir images
sudo bash scripts/configure-registry-mirror.sh --registry ":30500"


# 获取密码:
# 默认用户名:admin 密码获取如下
kubectl get secret nanocloud-credentials \
  -n nanocloud \
  -o jsonpath='{.data.user-service-password}' | base64 -d
echo

15.2 组件版本钉扎(foundation f2026.07)

组件版本组件版本
k3sv1.30.2+k3s1cert-managerv1.15.3
Longhornv1.7.0registry2.8.3
kube-vipv0.8.2helm(包内工具)v3.16.4

以交付包 release.lock 的 foundation.components 段为最终依据。

实测补充:nano.x2robot.com、备份日志与 HTTPS

本节记录 2026-08-10 在 39.108.97.7 三节点 K3s 环境的实测结果,并给出当前 Chart 启用 HTTPS 的安全路径。运行版本为 NanoCloud 0.4.18,入口为 Chart 自管 Traefik + Gateway API,Service 类型为 LoadBalancer(K3s ServiceLB)。

16.1 当前状态与域名结果

检查项实测结果
业务域名network.publicAddress=nano.x2robot.com,协议暂为 http
HTTPRouteplatform、admin、api、oss、minio-console、airflow、prefect 共 7 条,全部 Accepted=True、ResolvedRefs=True
DNS7 个 FQDN 的 A 记录均解析到 39.108.97.7
备份手工触发 CronJob 成功;3 个 MySQL 库、5 个 PostgreSQL 库、45 张 ClickHouse 表、版本账本和 2 份 C0 密钥进入 age 加密载荷,上传后 checksum 一致
日志nanocloud-log-agent 为 3/3;日志对象持续写入 nanocloud-logs,ILM 为 7 天
HTTPS当前尚未启用:tls-nanocloud Secret 不存在,Gateway 只有 web:HTTP:8000 listener

域名按服务名前缀统一派生:

platform.nano.x2robot.com
admin.nano.x2robot.com
api.nano.x2robot.com
oss.nano.x2robot.com
minio-console.nano.x2robot.com
airflow.nano.x2robot.com
prefect.nano.x2robot.com

入口验收命令如下。API 根路径返回 404、MinIO 匿名根路径返回 403 都属于后端语义,不代表路由失败。

kubectl get httproute -n nanocloud \
  -o custom-columns=NAME:.metadata.name,HOSTNAMES:.spec.hostnames[*]

for host in platform admin minio-console airflow prefect; do
  curl -sS -o /dev/null -w "$host=%{http_code}\n" \
    -H "Host: ${host}.nano.x2robot.com" http://39.108.97.7/
done

for host in api oss; do
  curl -sS -o /dev/null -w "$host=%{http_code}\n" \
    -H "Host: ${host}.nano.x2robot.com" http://39.108.97.7/
done

16.2 备份与日志复核命令

# 定时策略与最近执行
kubectl get cronjob nanocloud-site-export -n nanocloud -o wide
kubectl get cm nanocloud-release -n nanocloud \
  -o jsonpath='{.data.backup\.last}{"\n"}'

# 用 CronJob 原模板做一次现场验收
job="nanocloud-site-export-manual-$(date +%H%M%S)"
kubectl create job -n nanocloud \
  --from=cronjob/nanocloud-site-export "$job"
kubectl wait -n nanocloud --for=condition=complete \
  "job/$job" --timeout=20m
kubectl logs -n nanocloud "job/$job" --tail=100

# 日志 Agent 与最近上传结果
kubectl get ds nanocloud-log-agent -n nanocloud
for pod in $(kubectl get pod -n nanocloud \
  -l app.kubernetes.io/name=log-agent -o name); do
  kubectl logs -n nanocloud "$pod" --since=10m | \
    grep -E 'Successfully uploaded|error|failed|retry' | tail -n 30
done

# 在内嵌 MinIO 中检查对象和 7 天 ILM;命令不输出访问密钥
kubectl exec -n nanocloud minio-0 -- sh -c '
  mc alias set audit http://127.0.0.1:9000 \
    "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null
  mc ls --recursive audit/nanocloud-backup | tail -n 20
  mc ls --recursive audit/nanocloud-logs | tail -n 20
  mc ilm rule ls audit/nanocloud-logs
'

实测注意:修改域名触发 Helm 滚动更新 MinIO 时,Fluent Bit 曾在约 30 秒内出现 PutObject request failed;MinIO Ready 后三节点均自动重试并继续输出 Successfully uploaded object,对象数继续增长。验收不能只看 DaemonSet 3/3,必须同时检查 Agent 日志与目标桶的新对象时间。

16.3 HTTPS 前置条件与 Chart 行为

启用 HTTPS 前必须同时满足 DNS、网络、证书和 Helm 配置四个条件。80/443 应从客户端网络到达 Traefik;证书必须覆盖上面的 7 个 FQDN,可使用一张 *.nano.x2robot.com 通配符证书,或使用包含 7 个名称的多 SAN 证书。完整证书链和私钥必须匹配,私钥不得写入代码仓或飞书文档。

# DNS 与公网入口
for host in platform admin api oss minio-console airflow prefect; do
  fqdn="${host}.nano.x2robot.com"
  printf '%-42s ' "$fqdn"
  dig +short A "$fqdn"
done

# 端口连通性
nc -vz 39.108.97.7 80
nc -vz 39.108.97.7 443

# 证书有效期、签发者与 SAN
openssl x509 -in fullchain.pem -noout \
  -subject -issuer -dates -ext subjectAltName

# 证书与私钥公钥指纹必须相同
openssl x509 -in fullchain.pem -pubkey -noout | \
  openssl pkey -pubin -outform der | openssl sha256
openssl pkey -in privkey.pem -pubout -outform der | openssl sha256

当前 Chart 的关键值如下:network.publicAddress 派生主机名,network.domains.protocol=https 更新应用侧公开 URL,tls.secretName 使 Gateway 生成 websecure:HTTPS:8443 listener,gateway.redirectHttpToHttps=true 才会把 80 永久跳转到 443。Traefik Service 对外仍暴露 80/443,容器和 Gateway 内部端口为 8000/8443。

16.4 推荐路径:客户证书或站点 CA(适合离线交付)

完全离线或不允许 ACME 出网的站点,推荐先由客户 PKI 签发证书,再创建 Kubernetes TLS Secret。先创建 Secret,再切换 Helm,避免 Gateway 引用不存在的证书。

# 1. 创建或更新 TLS Secret;fullchain.pem 应包含完整链
kubectl -n nanocloud create secret tls tls-nanocloud \
  --cert=fullchain.pem --key=privkey.pem \
  --dry-run=client -o yaml | kubectl apply -f -

kubectl -n nanocloud get secret tls-nanocloud \
  -o jsonpath='{.type}{"\n"}'

# 2. 在 gitignored 的 Site Contract 中加入以下内容
umask 077
vi /root/nanocloud-preserve/site-k3s-ha.yaml
network:
  publicAddress: nano.x2robot.com
  domains:
    protocol: https

tls:
  secretName: tls-nanocloud
  certManager:
    enabled: false
  create: false

gateway:
  redirectHttpToHttps: true

已有环境可在维护窗口执行 Helm 升级。--reuse-values 保留 release.lock 注入的站点 Registry 与 digest;不要只用一组零散 --set 替代现场 Site Contract。

export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
export PKG=/opt/nanocloud/nanocloud-0.4.18-amd64

"$PKG/bin/helm" upgrade nanocloud "$PKG/chart" \
  -n nanocloud --reuse-values \
  -f /root/nanocloud-preserve/site-k3s-ha.yaml \
  --wait --timeout 30m

kubectl -n nanocloud get gateway nanocloud \
  -o jsonpath='{range .spec.listeners[*]}{.name}{":"}{.protocol}{":"}{.port}{"\n"}{end}'

16.5 cert-manager 自动签发的限制

Chart 可以通过 tls.certManager.enabled=true 创建多 SAN Certificate,并引用指定 Issuer。但 0.4.18 的 K3s 离线底座仅安装了 cert-manager v1.15.3,当前 controller 没有 --enable-gateway-api;同时 Chart Traefik 明确设置 --providers.kubernetesingress=false,Gateway 的 HTTP listener 端口为 8000,而 cert-manager v1.15 HTTP-01 Gateway solver 要求启用 GatewayAPI feature gate 并匹配 80 端口 listener。因此当前 K3s profile 不应直接使用默认 ClusterIssuer/letsencrypt 假设。

如需自动签发,优先使用客户 DNS 提供商支持的 ACME DNS-01 ClusterIssuer;它不依赖 HTTP listener,但需要 DNS API 凭据和访问 ACME 服务的出网能力。Issuer Ready 后再配置:

network:
  publicAddress: nano.x2robot.com
  domains:
    protocol: https

tls:
  secretName: tls-nanocloud
  certManager:
    enabled: true
    issuerRef:
      kind: ClusterIssuer
      name: letsencrypt-dns01
      group: cert-manager.io
  create: false

gateway:
  redirectHttpToHttps: false
# 第一阶段:签发期间保留 HTTP,不启用强制跳转
kubectl wait --for=condition=Ready \
  clusterissuer/letsencrypt-dns01 --timeout=120s

export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
export PKG=/opt/nanocloud/nanocloud-0.4.18-amd64
"$PKG/bin/helm" upgrade nanocloud "$PKG/chart" \
  -n nanocloud --reuse-values -f /tmp/https-dns01.yaml \
  --wait --timeout 30m

kubectl -n nanocloud wait --for=condition=Ready \
  certificate/tls-nanocloud --timeout=10m
kubectl -n nanocloud describe certificate tls-nanocloud

# 第二阶段:证书 Ready 后再开启 80 -> 443 跳转
"$PKG/bin/helm" upgrade nanocloud "$PKG/chart" \
  -n nanocloud --reuse-values \
  --set gateway.redirectHttpToHttps=true \
  --wait --timeout 30m

如坚持使用 HTTP-01,必须先在底座中持久化 cert-manager v1.15 的 --enable-gateway-api,并调整 Gateway/Service 端口契约后完成独立演练;不要用一次性的 Deployment patch 代替交付包修复。cert-manager v1.15 的 Gateway API solver 要求见 官方 HTTP-01 文档。

16.6 HTTPS 验收与回退

# 资源状态
kubectl -n nanocloud get gateway,httproute,certificate
kubectl -n nanocloud get secret tls-nanocloud \
  -o jsonpath='{.type}{"\n"}'

# HTTP 应跳转到 HTTPS
curl -sS -D - -o /dev/null \
  http://platform.nano.x2robot.com/

# HTTPS 验收不能使用 -k
for host in platform admin api oss minio-console airflow prefect; do
  curl -sS -o /dev/null -w "$host=%{http_code}\n" \
    "https://${host}.nano.x2robot.com/"
done

# 校验证书链、有效期与 SAN
echo | openssl s_client \
  -connect platform.nano.x2robot.com:443 \
  -servername platform.nano.x2robot.com 2>/dev/null | \
  openssl x509 -noout -subject -issuer -dates -ext subjectAltName

# 查看 Helm 历史;失败时回退到切换 HTTPS 前的 revision
"$PKG/bin/helm" history nanocloud -n nanocloud
"$PKG/bin/helm" rollback nanocloud  \
  -n nanocloud --wait --timeout 30m

回退 Helm 后还应把 Site Contract 的 network.domains.protocol 恢复为 http,并确认 Gateway 只剩 web listener。TLS Secret 可暂时保留以便重新启用;确认不再使用且已有可恢复副本后,才允许删除。

16.7 MinIO Console 401、hostAliases 修复与升级窗口

2026-08-10 在 39.108.97.7 的 HTTP 环境中,访问 POST /api/v1/login 返回 401 Unauthorized。该现象不是账号密码错误:使用 nanocloud-credentials 中同一组 root 凭据直接访问 MinIO API 成功,但 Console 登录仍返回 invalid Login。

**根因。**内嵌 MinIO 同时存在内部地址和公开地址。业务服务使用 infra.minio.endpoint=minio:9000;Console 使用 MINIO_SERVER_URL=http://oss.nano.x2robot.com,以保证浏览器重定向和公开对象地址保持为外部可达域名。问题发生时,MinIO Pod 把该公开域名解析到公网 IP 39.108.97.7,Pod 内回环访问公网 80 端口被拒绝:

Get "http://oss.nano.x2robot.com/.../?location=":
dial tcp 39.108.97.7:80: connect: connection refused

因此不能简单把 MINIO_SERVER_URL 改成 http://minio:9000。正确模型是保留公开 Host,同时让 MinIO Pod 内部把公开域名解析到 Traefik Service ClusterIP:

外部客户端:oss.nano.x2robot.com -> 公网入口 -> Traefik -> minio:9000
MinIO Pod: oss.nano.x2robot.com -> Traefik ClusterIP -> minio:9000

47 环境对照。47.107.155.43 使用 HTTPS,Chart 已自动把 oss.nanocloud-cluster.x2robot.cn 和 minio-console.nanocloud-cluster.x2robot.cn 映射到动态查询得到的 Traefik ClusterIP,所以 Console 登录返回 204。39 环境使用 HTTP,而旧模板只有 protocol=https 才执行同一自动注入逻辑,导致 HTTP 路径遗漏。

Chart 修复。chart/templates/infra/minio.yaml 已移除仅 HTTPS 生效的条件。只要启用 Chart Traefik,HTTP/HTTPS 都通过 Helm lookup 查询 service/traefik 的 ClusterIP,并自动注入公开 OSS 域名;不得写死节点 IP或 10.43.x.x。显式 infra.minio.hostAliases 仍优先,配置空数组可关闭自动注入。

hostAliases:
  - ip: ""
    hostnames:
      - "oss.nano.x2robot.com"
      - "minio-console.nano.x2robot.com"

39 环境使用修复后的 Chart 执行 --reuse-values 升级,server-side dry-run 与运行态均解析为当前 Traefik ClusterIP。验收命令如下,命令不会输出 Secret 值:

# 验证动态映射
kubectl get pod minio-0 -n nanocloud \
  -o jsonpath='{.spec.hostAliases}{"\n"}'

# 验证 Console 首页和路由
curl -sS -o /dev/null -w '%{http_code}\n' \
  http://minio-console.nano.x2robot.com/
kubectl get httproute nanocloud-minio-console -n nanocloud

# 在 MinIO Pod 内验证公开 OSS 域名确实可以完成 API 认证
# 账号密码从 Secret 读取到当前 shell,禁止 echo 或粘贴到工单
u=$(kubectl get secret nanocloud-credentials -n nanocloud \
  -o jsonpath='{.data.minio-root-user}' | base64 -d)
p=$(kubectl get secret nanocloud-credentials -n nanocloud \
  -o jsonpath='{.data.minio-root-password}' | base64 -d)
kubectl exec -n nanocloud minio-0 -- \
  mc alias set publiccheck http://oss.nano.x2robot.com "$u" "$p"
kubectl exec -n nanocloud minio-0 -- mc admin info publiccheck >/dev/null
unset u p
验收项39 环境结果
Helmrevision 4,deployed
Console 首页连续返回 200
Console 登录当前 root 凭据返回 204
公开 OSS 域名回连MinIO Pod 内 mc admin info 成功
集群状态三节点 Ready,无异常 Pod
日志回传MinIO 恢复后,三节点 Fluent Bit 持续输出 Successfully uploaded object

**升级窗口说明:**MinIO 为单副本 StatefulSet,并使用 Longhorn RWO 卷。修改 PodSpec 会重建 MinIO;旧卷解除、新节点重新挂载以及 Endpoint 恢复期间,Console 曾出现约 15 秒 503 Service Unavailable,日志 Agent 同期短暂出现 S3 上传失败,随后自动重试恢复。现场变更应安排维护窗口,且验收必须同时检查 Console、MinIO Endpoint 和日志对象持续增长。

**首装边界。**Helm 首次安装第一次渲染时,service/traefik 可能尚不存在,lookup 无法获得 ClusterIP;本次闭环验证的是“已有 Traefik Service 的升级路径”。正式宣告冷安装闭环前,还需让安装器在 Traefik Service 创建后执行一次幂等 Helm 收敛,或提供无需二次渲染的内部解析方案。不得通过写死 ClusterIP 规避该问题。

17. 2026-08-12:K3s HA、目标网段、HTTPS 与 RustFS RC 实测

测试环境为 root@39.108.97.7(n1:172.16.10.241,n2:172.16.10.242,n3:172.16.10.243),K3s v1.30.2+k3s1。变更前已创建 etcd 快照并导出 Kubernetes/Helm 资源。三节点阶段完成了 HA、目标网段、HTTPS 与 RustFS RC 的专项验收;随后按验证目标收敛为单控制面,并恢复与原三节点环境一致的完整业务及 Infra 组件,唯一对象存储变更为 rustfs/rustfs:rc。最终完整栈结果见 17.5。

17.1 关键决策

  • 用户输入的 Pod CIDR 与 Service CIDR 都是 10.10.6.0/24,Kubernetes 不允许重叠;最终采用 Pod 10.10.6.0/24、Service 10.10.7.0/24。
  • Pod /24 按每节点 /26 划分,max-pods=60;该配置最多支持 4 个节点。
  • 控制面使用 3 个 embedded-etcd server;旧 joinNodes 继续作为 agent 兼容别名。
  • 内嵌对象存储切换为 rustfs/rustfs:rc,但保留 minio Service、Secret key、PVC data-minio-0、端口和初始化脚本契约。

17.2 实测结果

检查项结果
控制面n1/n2/n3 均为 control-plane,etcd,master,三个本地 API 的 etcd ready 均通过
Pod CIDRn1=10.10.6.0/26,n2=10.10.6.64/26,n3=10.10.6.128/26;实际 Pod 获得 10.10.6.x
Service CIDRkubernetes=10.10.7.1、kube-dns=10.10.7.10,RustFS 与 HTTPS 验收 Service 也获得 10.10.7.x
HA 故障注入停止 n1 后 2 秒内 n2 API Ready,etcd ok,并成功写入 ConfigMap;恢复 n1 后 3 个 etcd server 全部 Ready
RustFS RCminio-0 Ready;原 init-buckets.sh 成功完成建桶、版本化、noncurrent 7 天 ILM、应用用户和 readwrite policy
S3 应用账号以旧 Secret key 对应的应用 AK/SK 完成 PUT、GET、stat,内容一致并返回有效 VersionID
HTTPSHTTP 返回 308,HTTPS health 返回 200;证书 SAN 覆盖 nano.x2robot.com、*.nano.x2robot.com 与 39.108.97.7

17.3 兼容与限制

  • 本地同卷演练已验证“旧 MinIO 写对象 → 同一卷启动 RustFS → mc cat 内容一致”;原 mc 初始化逻辑也完整通过。
  • RustFS 以 UID/GID 10001 非 root 运行;Chart 通过 fsGroup=10001 与 OnRootMismatch 兼容原 PVC 权限。
  • MinIO 未加密的 xl.meta 数据属于本次兼容范围;MinIO SSE 加密对象不在兼容承诺内,存在 SSE 时必须先做独立迁移演练。
  • rc tag 只用于选择版本;正式离线包必须按架构锁定 platform manifest digest,不能直接拿多架构 index digest 替代。
  • 本轮公网 IP 上可用的 VIP 候选地址已被占用,因此控制面故障测试使用 n2/n3 的直接 API 地址完成;生产现场仍应分配未占用 VIP,并把它加入所有 server 的 TLS SAN。

17.4 代码与回归

实现分支:codex/k3s-ha-rustfs-rc。本地发布门禁结果为 87 passed / 0 failed;standalone、k3s-ha、k8s-bare、ACK、EKS、TKE 六个 profile 均可渲染,k3s-ha Helm lint 通过。仓库运行手册已补充 HA/CIDR、离线自签 HTTPS 与 RustFS/MinIO 兼容迁移说明。

17.5 单节点完整业务栈最终验收(2026-08-12)

最终环境保留 n1 单控制面,n2、n3 的 K3s 均为 disabled/inactive。组件集合与原三节点部署保持一致,存储类因单节点验证改用 local-path;RustFS 继续复用原数据目录并绑定 20Gi Retain PV。

检查项最终结果
集群与网段仅 nanocloud-n1 Ready;Pod CIDR=10.10.6.0/24,Service CIDR=10.10.7.0/24,Kubernetes Service IP=10.10.7.1
Helmnanocloud-0.4.22,revision 5,状态 deployed
业务与 Gateway23 个 Deployment 全部 1/1:核心四服务、分析中心、Acquisition、Airflow 三组件、Data Hub、FlowForge 四类 Worker、Artifact Writer、Prefect、Connect、APISIX、Traefik 与两个前端均 Ready
Infra 与日志7 个 StatefulSet 全部 1/1:MySQL、PostgreSQL、Redis、RustFS、EMQX、Kafka、ClickHouse;日志 DaemonSet 1/1
镜像来源完整 Helm 渲染共 28 个镜像,27 个来自深圳 ACR;唯一例外严格为 rustfs/rustfs:rc。运行时资源复核无 Docker Hub 残留
RustFS S3使用原业务 AK/SK 完成 PUT、GET、stat、delete;六个业务桶可见,x2robot-cloud 版本化启用
CDCMySQL source、PostgreSQL source、NDC ClickHouse sink、ALS ClickHouse sink 四个 Connector 及全部 task 均为 RUNNING
Gateway HTTPGateway PROGRAMMED=True;platform、admin、airflow、prefect 返回 200,OSS/Console 匿名访问返回 403;公网 39.108.97.7:80 按 Host 路由返回 200
存储7 个 PVC 全部 Bound;RustFS 的 data-minio-0 保留原名称和兼容契约

**HTTPS 边界。**同一测试日已在三节点专项阶段验证 HTTP 308 跳转与 HTTPS health 200。最终完整栈为了与原三节点站点参数保持一致,恢复为 HTTP;因此本次完整业务恢复不重复启用 TLS,但 HTTPS Chart 配置与专项验收结论仍有效。

**修复记录。**正式离线包中的旧 Data Hub 镜像缺少当前 Chart 的 caption_no_ai_direct_label 参数,旧 Connect 镜像缺少 OffsetVersion SMT。已从当前源码构建 amd64 兼容镜像并推送 ACR,定时任务随后成功,四个 CDC Connector 注册和就绪检查全部通过。

现场切换前备份保存在 /var/backups/nanocloud-rustfs-full-switch-20260812-170448;部署值和 Chart 位于 /opt/nanocloud-rustfs-validation-20260812。

18. 2026-08-12:最终交付契约(默认三节点与 RustFS/rc 离线)

本节记录当前分支的最终交付口径,与第 17 章的历史现场实测区分。本节变更仅在本地仓库完成,未重建、升级或修改 39.108.97.7,该环境继续保留给现有测试人员使用。

18.1 三节点默认与 HA 开关

k3s-ha profile 继续部署三台机器,但默认 controlPlaneHA=false:1 个 control-plane/embedded-etcd server 加 2 个 worker agent。Site Contract 必须恰好给出两个 agentNodes,安装器不会把缺失节点列表降级为单节点。

需要开启控制面 HA 时,显式设置 controlPlaneHA=true,把同两个 IP 移入 serverNodes 并清空 agentNodes。首节点始终通过 --cluster-init 创建 embedded-etcd,因此可以后续把已有 agent drain 并转换为 server,无需重建首节点。未开开关却配置 server、默认 worker 数量不是 2、HA server 数量不是 2 或 HA 同时配置 agent,都会在 SSH 与集群变更前失败。

18.2 默认 CIDR

部署脚本默认不传 --cluster-cidr 和 --service-cidr,由 K3s 使用内建默认 CIDR。10.10.6.0/24 与 10.10.7.0/24 只作为客户 Site Contract 示例,不是产品默认配置。

18.3 RustFS / rc 资源与离线镜像

项目最终契约
服务端镜像rustfs/rustfs:rc
管理工具镜像rustfs/rc:v0.1.31
Kubernetes 资源StatefulSet/Service/PVC/Secret/初始化 Job 全部使用 rustfs 命名,不保留旧 MinIO 对象名
离线包两个镜像都必须按目标架构进入镜像归档、images/SHA256SUMS 与 release.lock
现场拉取安装器先推送到自建 Registry,再把所有运行引用渲染为 /rustfs/...@sha256:

离线门禁会检查两项固定原始引用、归档 checksum、锁定 digest、Helm 全量渲染的现场 Registry 前缀,以及 release.lock 中每个 digest 在现场 Registry 的可访问性。运行中不得直接从 Docker Hub 或其他公网 Registry 拉取 RustFS/rc。

18.4 本地回归结果

发布包静态回归为 88 passed / 0 failed;standalone、k3s-ha、k8s-bare、ACK、EKS、TKE 六个 profile 全部渲染通过,Helm lint 通过。拓扑负例已覆盖:未显式开启 HA 的 serverNodes、默认仅一个 agent、HA 同时带 agent 均会被拒绝。

带 Docker 的补充自检完成 87 项后,最后一项因构建机专用阿里云 VPC ACR 授权端点连接超时失败;失败镜像为 image-sz-registry-vpc.cn-shenzhen.cr.aliyuncs.com/nanocloud/docker:27-cli,不涉及 RustFS/rc 或现场自建 Registry 的代码契约。


19. 2026-08-17:0.4.33 单节点与三节点离线部署演练

本节记录在专属测试环境 39.108.97.7 完成的 0.4.33 实际部署。演练依次完成旧环境彻底清场、单节点 standalone 安装与验收、再次清场,以及默认 1 控制面 + 2 工作节点的 k3s-ha 安装与验收。结论来自安装日志、Kubernetes、Helm、Longhorn、现场 Registry 和入口请求。

19.1 版本包与 OSS 物料

项目实测结果
版本0.4.33
OSS 交付包oss://zbl-nanocloud/releases/0.4.33/nanocloud-0.4.33-amd64.tar.gz
包大小5,877,236,830 bytes
包 SHA256253da0b9b0456a95f4ff9971768148e813ba3014160320c1f3a7c67f0ff7e230,目标机复核通过
现场镜像目录/srv/nanocloud/mysql-images
现场镜像 OSSoss://zbl-nanocloud/releases/0.4.33/manual-images/

19.2 四个 MySQL 相关现场镜像

四个归档不进入主离线包,部署前下载到安装器指定目录。四个 tar 均通过 SHA256SUMS 和 crane validate --tarball;OSS 对象带 x-oss-meta-sha256 元数据,上传后已逐个回读核对。

文件大小SHA256
nanocloud_db-migrator_1.2.0.tar81,202,68891bf6a178384b4651c0ebc1910a7331de3d593e8c6112f4418ef609d3c86ac0e
nanocloud_backup-tools_1.0.0.tar57,745,40801299bbdb4b4d942dc423d8083e6714eea8ef00c5395c4c8ebf73854f5f5827d
mysql_8.0.tar233,600,5123cda3048ee8600101936696681f4b398d0d5106ac8329a4393f7c838109ac806
container-registry.oracle.com_mysql_community-router_8.4.6.tar93,329,4080fa8833ee7ffcf101240272e85cfd96a60de7dbfdb888b219be0b958039f4717

安装时统一传入 --manual-images-dir /srv/nanocloud/mysql-images。安装器将常规归档和四个现场归档一起推送到 172.16.10.241:30500,再以现场 Registry 返回的 digest 生成最终 Helm 覆盖。

19.3 清场与单节点部署

清场先卸两个 agent,再卸首节点 server;同时删除旧业务、Registry、Longhorn 数据、K3s/Kubelet 数据目录和残留挂载。最终三台机器的 K3s 服务均为 inactive,旧目录及挂载不存在。

bash scripts/install.sh \
  --profile standalone \
  --airgap \
  --values /root/nanocloud-preserve/site-standalone-0.4.33.yaml \
  --manual-images-dir /srv/nanocloud/mysql-images

单节点安装一次完成:站点 Registry 共推送 48 个镜像,默认 Registry endpoint 已禁用,全部工作负载 Ready,版本账本为 0.4.33,平台 HTTP 请求返回 200。单节点验收完成后再次执行完整清场,为多节点首装提供干净环境。

19.4 三节点部署与容量修正

bash scripts/install.sh \
  --profile k3s-ha \
  --airgap \
  --values /root/nanocloud-preserve/site-k3s-ha-0.4.33.yaml \
  --manual-images-dir /srv/nanocloud/mysql-images

节点为 172.16.10.241、172.16.10.242、172.16.10.243。本次 controlPlaneHA=false:n1 是唯一 control-plane/embedded-etcd,n2、n3 是 worker agent;k3s-ha 在这里表示多节点工作负载和 Longhorn 副本,不表示控制面 HA。

首次按默认 PVC 安装时,RustFS 100Gi 因 insufficient storage 失败;只把 RustFS 缩到 20Gi 后仍失败。原因是 0.4.33 默认业务 PVC 请求总量约 115Gi,2 副本需要约 230Gi,而三台约 99Gi 系统盘扣除 Longhorn 保留空间后的可调度容量不足。

persistence:
  mysql:      { size: 10Gi }
  postgres:   { size: 5Gi }
  redis:      { size: 2Gi }
  rustfs:     { size: 10Gi }
  emqx:       { size: 2Gi }
  kafka:      { size: 2Gi }
  clickhouse: { size: 10Gi }
  airflow:    { size: 5Gi }

最终测试 overlay 的逻辑请求总量为 46Gi,2 副本约占 92Gi。该配置仅用于小盘实验,不是新的生产默认值;生产必须通过增加独立数据盘和容量预算解决。

19.5 最终验收

检查项终态
节点3/3 Ready;n1 control-plane/etcd,n2、n3 worker
工作负载全部 Deployment、StatefulSet 达到期望副本;失败 Pod 为 0
Longhorn8 个 volume 均 attached/healthy,每卷 2 副本
镜像来源nanocloud、cert-manager、longhorn-system 的受管镜像均使用 172.16.10.241:30500 前缀
节点 mirror三台节点均配置现场 Registry mirror,并设置 disable-default-registry-endpoint: true
Helm / 账本Revision 1、deployed;版本账本为 0.4.33
入口https://platform.39.108.97.7 返回 200;HTTP 返回 301;使用现场自签名证书
磁盘占用最终约 n1 64%、n2 27%、n3 30%

19.6 离线边界与证据

交付包和四个现场镜像均在安装开始前准备完成,安装全程使用 --airgap、现场 Registry、节点 mirror 和受管镜像前缀硬门禁。本次没有通过防火墙或路由策略做公网出口隔离测试,因此结论是“离线物料路径与离线安装链通过”,不能表述为“严格无公网出口已证明”。如项目要求严格断网证据,需在安装窗口额外执行出口阻断和访问审计。

本次测试 Site Contract 关闭备份,因此不能把工作负载 Ready 视为备份闭环通过。单节点日志为 /root/nanocloud-0.4.33-stage/install-standalone.log,多节点最终日志为 /root/nanocloud-0.4.33-stage/install-k3s-ha-final-capacity.log;Site Contract 分别保存在 /root/nanocloud-preserve/site-standalone-0.4.33.yaml 和 /root/nanocloud-preserve/site-k3s-ha-0.4.33.yaml。

19.7 演练结论

0.4.33 已在同一专属环境完成“彻底清场 → 单节点离线安装与验收 → 再次清场 → 三节点离线安装 → Longhorn 容量修正 → 工作负载、镜像门禁、存储副本和入口验收”的闭环。阻塞点不是镜像或安装器流程,而是默认 PVC 与小盘环境的物理容量不匹配;正式交付必须在首装前完成容量计算。


20. 2026-08-15:Longhorn SSD/HDD 分层存储交付契约

本节记录当前本地代码已经实现并通过静态回归的分层存储方案。K8s 轨当前对象存储组件是 RustFS,不是 MinIO;因此现场所称“MinIO 使用机械盘”在当前交付中对应“RustFS 数据卷使用机械盘”。本节变更尚未在 39.108.97.7 执行存储迁移,该环境继续使用既有未分层 Longhorn 磁盘和 PVC。

20.1 存储分配模型

对象StorageClass磁盘池
MySQL、PostgreSQL、ClickHouse、Redis、EMQX、Kafkalonghorn-ssdLonghorn 标签为 ssd 的路径
RustFS 数据卷longhorn-hddLonghorn 标签为 hdd 的机械盘 RAID/LVM 挂载路径
nanocloud-registrylocal-path保持部署引导盘,不迁入 Longhorn

两个自定义 StorageClass 都使用 2 副本、Retain、Immediate 和 dataLocality=best-effort。启用分层后,longhorn-ssd 是集群默认类,RustFS 必须显式指定 longhorn-hdd。未启用分层的现有 Site Contract 继续使用原 longhorn,兼容行为不变。

20.2 自动化边界

安装器自动完成现场必须提前完成
读取 Site Contract 中的 SSD/HDD 路径确认物理盘、RAID 卡、逻辑盘和 Linux 设备的实际对应关系
为每个 Ready 节点注册名为 ssd、hdd 的 Longhorn 磁盘并添加同名标签机械盘完成 RAID/LVM、ext4 或 XFS 格式化,并持久挂载到统一路径
等待两个 Longhorn 磁盘均达到 Ready=True、Schedulable=True把挂载写入 /etc/fstab,逐节点重启或等价验证后确认仍然存在
创建 longhorn-ssd、longhorn-hdd 并设置默认类确认 SSD 与 HDD 路径实际位于不同块设备/逻辑卷,并完成容量与性能验收

**安装器不会自动识别磁盘介质,也不会分区、格式化或挂载磁盘。**如果 /mnt/longhorn-hdd 没有真正挂载,Longhorn 可能在系统 SSD 上创建同名目录并显示 Ready。因此逐节点 findmnt 是现场硬门禁,不能只看 Longhorn 状态。

服务器配置截图只能证明采购规格中包含 SSD、SATA HDD 和 RAID 卡,不能证明现场 Linux 设备名、RAID 级别、逻辑卷、文件系统和挂载点已经配置完成,因此安装器不能基于截图或设备名称安全推断磁盘类型。

20.3 Site Contract

foundation:
  longhorn:
    tiering:
      enabled: true
      ssdPath: /var/lib/longhorn
      hddPath: /mnt/longhorn-hdd

platform:
  storageClass: longhorn-ssd

persistence:
  rustfs:
    storageClass: longhorn-hdd
    size: 2Ti  # 示例,按现场容量预算填写

安装前 preflight 会检查:profile 必须为 k3s-ha;两个路径必须是不同的绝对路径且不能包含空格;SKIP_LONGHORN=1 不能与分层模式并用;平台默认类必须是 longhorn-ssd;RustFS 必须显式使用 longhorn-hdd。同名环境变量 LONGHORN_TIERING_ENABLED、LONGHORN_SSD_PATH、LONGHORN_HDD_PATH 可覆盖安装期路径。

20.4 宿主机安装前验收

以下命令必须在每个会运行 Longhorn 的节点上执行:

findmnt -T /var/lib/longhorn -no TARGET,SOURCE,FSTYPE
findmnt -T /mnt/longhorn-hdd -no TARGET,SOURCE,FSTYPE
df -hT /var/lib/longhorn /mnt/longhorn-hdd
findmnt -T /var/lib/longhorn -no SOURCE
findmnt -T /mnt/longhorn-hdd -no SOURCE

两条 SOURCE 必须不同,文件系统必须为 ext4 或 XFS。HDD 路径应是 RAID/LVM 提供的受管理文件系统;Longhorn 不会把一个卷自动分片到 12 块裸 HDD。当前 2 副本契约至少需要 2 个 Ready 节点,生产建议 3 个节点;SSD 池和 HDD 池要分别按 PVC request × 2 副本 + 保留空间 + 快照 + 增长率计算容量,不能互相借用可调度容量。

20.5 安装后验收

kubectl get storageclass longhorn-ssd longhorn-hdd -o yaml

kubectl -n longhorn-system get nodes.longhorn.io \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range $n,$d := .spec.disks}{"  "}{$n}{" path="}{$d.path}{" tags="}{$d.tags}{"\n"}{end}{end}'

kubectl -n nanocloud get pvc \
  -o custom-columns=NAME:.metadata.name,CLASS:.spec.storageClassName,STATUS:.status.phase

预期 longhorn-ssd 的 diskSelector=ssd 且为默认类,longhorn-hdd 的 diskSelector=hdd;data-rustfs-0 使用 longhorn-hdd,其余 NanoCloud 嵌入式基础组件 PVC 使用 longhorn-ssd。所有 PVC 应为 Bound,对应 Longhorn volume 应为 healthy、2 副本。

20.6 已有环境迁移边界

PVC 的 storageClassName 不能按本方案原地修改。Longhorn 默认磁盘注解也只对尚无既有磁盘的 Longhorn Node 生效;安装器发现已有磁盘布局不完全等于配置的 ssd/hdd 路径和标签时会拒绝自动迁移。

已有环境必须单独执行数据迁移评审:先完成可恢复备份并停止写入,再创建目标 StorageClass/PVC、恢复数据和逐组件验收。不能把修改 values 后执行 Helm upgrade 当作数据迁移方案。当前 39.108.97.7 正属于已有未分层环境,所以本次没有对其执行存储切换。

20.7 当前代码与回归事实

本地代码已新增两类 StorageClass、RustFS 独立 StorageClass 覆盖、Site Contract schema、安装前一致性检查、Longhorn 节点磁盘标签配置、已有布局拒绝逻辑和离线回归用例。实际 Helm 渲染结果为:MySQL、PostgreSQL、ClickHouse、Redis、EMQX、Kafka 使用 longhorn-ssd,RustFS 使用 longhorn-hdd。

当前离线交付静态回归结果为 103 passed / 0 failed;Bash 语法、JSON Schema、Helm lint/template、无 PyYAML 的离线解析回退均通过。以上是本地代码验证结果,不等同于目标服务器真实 HDD 挂载、故障重建和负载性能验收。

Longhorn 能力依据:Storage Tags、Multiple Disks、StorageClass Parameters。

21. zbl 站点:0.4.33 三节点离线部署与运维基线(2026-08-18)

**当前结论:**NanoCloud 0.4.33 已在 10.10.3.11 / 10.10.3.12 / 10.10.3.13 完成三节点离线部署,Helm revision 16 为 deployed。所有 Deployment/StatefulSet Ready,RustFS 3/3,离线镜像前缀不变量通过。

本节记录 zbl.sysxlzx.xyz 当前站点。前文 39.108.97.7、172.16.10.x 等内容属于历史现场,不应与本节参数混用。

21.1 现场拓扑与生产配置

角色主机名内网 IP
K3s server / etcd、registry、平台入口nanocloud-node110.10.3.11
K3s agentnanocloud-node210.10.3.12
K3s agentnanocloud-node310.10.3.13
  • 系统:Ubuntu 24.04.3 LTS、x86_64;每节点约 112 vCPU、62GiB 内存、930GiB NVMe 系统盘、88TB XFS 数据盘。
  • 域名:zbl.sysxlzx.xyz;DNS 上游为 10.10.3.1。
  • 时区:主机、Pod、日志、所有 CronJob 统一为 Asia/Shanghai。
  • 默认登录用户为 user,使用 SSH 公钥;特权操作走 sudo 或 su -。
  • 正式站点文件:/srv/nanocloud/site.yaml;离线包:/home/user/nanocloud-0.4.33-amd64;四个 MySQL 现场镜像:/srv/nanocloud/mysql-images。
cd /home/user/nanocloud-0.4.33-amd64
sudo env SKIP_K3S_JOIN=1 bash scripts/install.sh \
  --profile k3s-ha \
  --values /srv/nanocloud/site.yaml \
  --skip-k3s \
  --airgap \
  --manual-images-dir /srv/nanocloud/mysql-images

现场升级必须继续使用以上完整离线入口;不得省略 --airgap 或人工镜像目录,也不得把生产拓扑配置放在 /tmp。

21.2 存储池设计

层级StorageClass / 路径用途
SSDlonghorn-ssd(默认)MySQL、PostgreSQL、Redis、Kafka、EMQX、ClickHouse 等 infra
HDDlonghorn-hdd-single;/data/nanocloud/longhorn-hdd仅 RustFS
本地盘local-pathK3s 底座和现场 registry,不作为业务默认持久化类

RustFS Pool 0 为三节点各 80Ti,原始容量 240Ti。三节点纠删码按约 2 个数据分片 + 1 个校验分片估算,业务可用容量约 160Ti,尚未扣除文件系统和运行水位预留。“80Ti 校验”是对象数据的等宽校验分片容量预算,不是单个独立校验文件。

RustFS PVC 使用 longhorn-hdd-single,由 RustFS 跨节点纠删码承担冗余,避免 Longhorn 多副本与应用层纠删码双重放大。

当前 StorageClass 列表中 local-path、longhorn、longhorn-hdd、longhorn-ssd、longhorn-static 及 RustFS 专用 longhorn-hdd-single 各有用途,不应仅因数量多而删除。

后续加盘与均衡

  1. 三节点先准备同规格磁盘、挂载点和 Longhorn HDD disk tag,确认容量可调度。
  2. 只在 /srv/nanocloud/site.yaml 的 infra.rustfs.additionalPools 末尾追加 Pool;禁止删除、重排或编辑既有 Pool。
  3. 备份站点文件和 nanocloud-rustfs-pool-contract 后,在维护窗口执行完整离线安装命令。
  4. 等待全部 RustFS StatefulSet、Pod、PVC 收敛,再做只读 Pool 检查。
  5. 显式启动 RustFS rebalance 并持续观察。加盘不会自动创建 Pool,也不能假设历史对象会自动均衡。

追加 Pool 会改变全部 RustFS Pod 的端点列表并触发滚动更新;旧 PVC 不迁移,但仍属于有影响升级,必须安排维护窗口并保留离机备份。

21.3 本环境备份

当前备份目标是同一 RustFS 集群中的 nanocloud-backup 桶,只证明导出、加密、上传和回读链路,不构成离机灾备。生产投运前必须切换到独立故障域的 S3/OSS/MinIO,并完成恢复演练。

  • CronJob:nanocloud-site-export,每日 02:00,timeZone=Asia/Shanghai,禁止并发。
  • age 私钥仅保存在 node1 的 /srv/nanocloud/backup-keys/age-private.key,root-only;集群内只保存公钥 Secret。
  • 手动验收 Job:nanocloud-site-export-manual-20260818-1439,状态 Complete。
  • backupId:site-export-2026-08-18T1440+08:00。
  • 验收覆盖 MySQL 3 库、PostgreSQL 5 库、ClickHouse 45 表、版本账本、记名不记值的密钥清单;age 加密后上传,远端回读 SHA-256 一致。
kubectl get cronjob nanocloud-site-export -n nanocloud -o wide
kubectl get configmap nanocloud-release -n nanocloud \
  -o jsonpath='{.data.backup\.last}'; echo
kubectl get job nanocloud-site-export-manual-20260818-1439 -n nanocloud

禁止在终端、日志或文档中输出备份 AK/SK 和 age 私钥;Secret 只检查名称、键名、长度和引用关系。

21.4 临时监控服务

nanocloud-health-monitor 仅用于本次离线部署验收和正式监控上线前的过渡期人工巡检,不是生产监控服务。它复用离线包内 toolbox 镜像,不访问公网;K3s metrics-server 继续提供 kubectl top 的瞬时 CPU/内存。

  • ClusterIP:nanocloud-health-monitor:8080;端点:/healthz、/metrics。
  • 指标:节点总数/Ready、异常 Pod、不可用 Deployment、未就绪 StatefulSet、未绑定 PVC、失败 Job、RustFS 期望/Ready、副本备份开关及最近成功时间。
  • RBAC 仅允许资源 get/list 和读取 nanocloud-release 账本;容器非 root,无宿主机挂载。
kubectl rollout status deployment/nanocloud-health-monitor -n nanocloud
kubectl exec -n nanocloud deployment/nanocloud-health-monitor -- \
  wget -qO- http://127.0.0.1:8080/metrics
kubectl top nodes
kubectl top pods -n nanocloud --sort-by=memory

临时监控验收:节点 3/3 Ready、RustFS 3/3、不可用 Deployment/StatefulSet=0、未绑定 PVC=0、失败 Job=0,最近成功备份时间指标为 1787035223。该组件不保存历史曲线、不提供 Dashboard 或告警通知,不能作为生产监控验收依据;正式监控需将 Prometheus、Alertmanager、Grafana、日志组件及依赖镜像和 digest 纳入离线包后部署。

21.5 部署问题与解决方案复盘

问题根因解决方案
DNS/NTP 初始不可用bond4 没有上游 DNS,timesyncd 无法解析 NTP三节点配置 10.10.3.1,恢复 DNS 与时间同步
浏览器提示证书不安全私网入口使用内部 CA,终端未信任;公网 ACME 验证条件不成立统一域名入口并向受管终端分发站点 CA;若需公网可信证书,先满足 ACME 可验证条件
升级拉取公网 PostgreSQL未使用完整离线入口,release.lock/registry 覆盖缺失回滚后仅使用 install.sh --airgap
agent 拉取本地 registry 失败三节点 mirror 配置不一致三节点统一 registry mirror 并重启验收
四个 MySQL 镜像 digest 不一致现场镜像不属于原 release.lock 基线固定目录并由 --manual-images-dir 后置注入实际 digest
RustFS 单副本 SC 被预检拒绝初版分层门禁只接受 longhorn-hdd将 longhorn-hdd-single 纳入契约并校验三节点 disk tag
StorageClass 更新失败Kubernetes StorageClass 字段不可变对齐声明契约,不直接修改不可变对象
旧 Longhorn engine image 指向公网历史零引用对象残留核对引用数后清理旧对象,保持现场 registry 闭环
RustFS 扩容缺少拓扑保护原 Chart 只有单 Pool实现 append-only Pool 与集群内 Pool Contract,Pool 0 原位兼容
监控容器首次重启toolbox 的 BusyBox 未编译 httpd applet改用镜像已包含的 Python HTTP server,重新执行完整离线安装并验证 revision 16

投产前剩余风险

  • 当前 K3s 只有 1 个 server/etcd,三节点是 1 控制面 + 2 agent,并非控制面高可用。
  • 备份仍与业务 RustFS 同故障域,必须迁移到离机目标并完成恢复演练。
  • 内部 CA 需要向浏览器终端分发信任。
  • 当前只有临时健康检查;正式监控所需的历史存储、Grafana 和告警通知渠道尚未建设。

On this page

私有化部署运维手册 (K8s 离线交付)1. 交付模型与阅读路径1.1 适用范围1.2 部署形态(profile)对照1.3 角色分工2. 交付物清单与校验2.1 获取交付包2.2 校验与解包2.3 包内容核对3. 环境要求与预检3.1 目标机要求3.2 预检4. 出包(构建机)5. 现场配置:Site Contract5.1 必填与关键项5.2 values 加载顺序(install.sh)6. 安装 A:单机 standalone(主路径)6.1 一键安装(推荐)6.2 安装器内部顺序(standalone)6.3 干跑(可选)6.4 常用安装选项7. 安装 B:多节点 K3s(默认 1 控制面 + 2 工作节点,HA 按需开启)7.1 前置:SSH 免密(人工完成)7.2 Site Contract7.3 首节点一键7.4 手工加入 server / agent(自动加入关闭时)7.5 HA / CIDR 验收8. 安装 C:既有集群与云托管8.1 k8s-bare(已有自建集群)8.2 云托管(eks / ack / tke)9. 安装后验收9.1 总览9.2 数据库迁移链(六个 platform-chain 库)9.3 analysis-grants(analysis 开启时)9.4 入口与镜像前缀9.5 验收勾选清单9.6 日志部署与验收10. AI 管线11. 备份开通(生产必做)11.1 A 轨架构速览11.2 开通步骤内部存储:使用内嵌 MinIO离线演练:不修改包内 Chart,使用包内 backup-tools 生成 age 密钥11.3 手动触发与验证11.4 B 轨(对象复制 + 月度演练,按需)12. 升级与回退12.1 升级模型(先迁移后滚动)每台节点验收;systemd 返回 inactive/unknown,findmnt 无输出仅在确认是测试环境且数据已批准删除后执行再次确认目录与挂载均不存在14.5 Longhorn(k3s-ha)14.6 磁盘 / PVC 过小(lab 小盘)14.7 节点 Muttipathd 条件异常14.8 其他高频项附录:命令速查与版本钉扎15.1 命令速查15.2 组件版本钉扎(foundation f2026.07)实测补充:nano.x2robot.com、备份日志与 HTTPS16.1 当前状态与域名结果16.2 备份与日志复核命令16.3 HTTPS 前置条件与 Chart 行为16.4 推荐路径:客户证书或站点 CA(适合离线交付)16.5 cert-manager 自动签发的限制16.6 HTTPS 验收与回退16.7 MinIO Console 401、hostAliases 修复与升级窗口17. 2026-08-12:K3s HA、目标网段、HTTPS 与 RustFS RC 实测17.1 关键决策17.2 实测结果17.3 兼容与限制17.4 代码与回归17.5 单节点完整业务栈最终验收(2026-08-12)18. 2026-08-12:最终交付契约(默认三节点与 RustFS/rc 离线)18.1 三节点默认与 HA 开关18.2 默认 CIDR18.3 RustFS / rc 资源与离线镜像18.4 本地回归结果19. 2026-08-17:0.4.33 单节点与三节点离线部署演练19.1 版本包与 OSS 物料19.2 四个 MySQL 相关现场镜像19.3 清场与单节点部署19.4 三节点部署与容量修正19.5 最终验收19.6 离线边界与证据19.7 演练结论20. 2026-08-15:Longhorn SSD/HDD 分层存储交付契约20.1 存储分配模型20.2 自动化边界20.3 Site Contract20.4 宿主机安装前验收20.5 安装后验收20.6 已有环境迁移边界20.7 当前代码与回归事实21. zbl 站点:0.4.33 三节点离线部署与运维基线(2026-08-18)21.1 现场拓扑与生产配置21.2 存储池设计后续加盘与均衡21.3 本环境备份21.4 临时监控服务21.5 部署问题与解决方案复盘投产前剩余风险