Files
admin 68c0a320ed refactor(k8s): 删除 K8s 命名的死接口 LivePod;修正 K8s 文档横幅重复
1) src/supervisor/spawner.ts:LivePod —— 注释此前已中性化,但**类型名来自 K8s 的
   Pod**;全仓只有声明那一处出现(0 引用 = 死代码)⇒ 整块删除。属 cf8b7b1 的漏项。

2) docs/k8s-deploy.md · docs/k8s-deployment.md:cf8b7b1 里那行状态横幅被**重复
   插入 2 次并挤在同一行**(生成脚本当时不幂等,每跑一次多插一条)⇒ 收敛为 1 条。
   脚本已补「H1 后已有横幅就不再插」的负向断言 + 一条把重复收敛成 1 条的自愈规则。

验证:tsc --noEmit exit 0;npm test 36 测试 / 35 通过 / 0 失败 / 1 跳过;
LivePod 全仓(含 lib/)0 命中;两份文档横幅各 1 条。
2026-09-15 06:51:48 +08:00

21 KiB
Raw Permalink Blame History

K8s 踩坑记录(模式 B)— 部署流程 + 根因排查

⚠️ 2026-09-15:模式 B 的 K8s 后端代码已从本仓下线(k8s-spawner / leader / reconcile / k8s-user-fs / tcp-bridge / web/file-service 六个模块 + 2 个测试 + 1 个脚本)。备份:D:\github\_dsh_shenxian_K8s后端备份_20260915\。本文按「历史设计稿 + 未来可选路线」保留;文中的 dshs file-service / dshs tcp-bridge 两条子命令现已不存在,照着敲会 command not found。 🧭 ← 返回 README · 分步教程:K8s 部署教程

记录模式 B 实机部署踩到的坑与根因:先在阿里云 2C2G 单机 k3s 上 PoC 验证,后在 ACK 智能托管上完整落地(§5–§8 含部署流程实录)。 想直接照着部署,从 k8s-deployment.md 开始;本文按「症状 → 根因 → 修复」组织,用于卡住时对查。


1. 环境 / k3s 安装

1.1 cgroup v1 导致 k3s v1.36+ kubelet 拒启

  • 症状:k3s v1.36 启动几秒后退出,systemctl 反复 auto-restart。日志:
    Shutdown request received: "kubelet exited: ... kubelet is configured to not run on a host using cgroup v1 ..."
    
  • 根因:K8s 1.36 移除了 cgroup v1 支持;阿里云 Linux 3(RHEL8 系)默认跑 cgroup v1。
  • 修复(二选一):
    • 启用 cgroup v2(改内核参数 + 重启):
      grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"
      grubby --info=ALL | grep args      # 确认参数已写入
      reboot
      # 重启后验证
      stat -fc %T /sys/fs/cgroup/        # 输出 cgroup2fs 才对
      
    • 或降级 k3s v1.31(还支持 cgroup v1)。

1.2 swap 未关

  • 症状:kubelet 起不来(fail-swap-on 默认 true)。
  • 修复:
    swapoff -a
    # 持久化(可选):注释 /etc/fstab 里的 swap 行
    sed -i '/[[:space:]]swap[[:space:]]/s/^/#/' /etc/fstab
    

1.3 SELinux 依赖缺失(RHEL8 系)

  • 症状:k3s 安装脚本报:
    nothing provides container-selinux >= 3:2.191.0-1 needed by k3s-selinux-...
    
  • 修复:跳过 SELinux RPM(PoC 不需要):
    curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \
      INSTALL_K3S_MIRROR=cn INSTALL_K3S_SKIP_SELINUX_RPM=true sh -s - --disable traefik --disable metrics-server --disable servicelb --disable local-storage
    

2. 镜像源(中国区)

2.1 docker hub 被墙

  • 症状:registry-1.docker.io 403 / 超时,busybox/node/socat 镜像拉不动。
  • 修复:给 k3s 的 containerd 配国内镜像 /etc/rancher/k3s/registries.yaml:
    mirrors:
      docker.io:
        endpoint:
          - "https://docker.m.daocloud.io"
          - "https://docker.1ms.run"
    
    然后 systemctl restart k3s。
  • 注意:镜像拉取极慢(一个 29MB 镜像拉了 13 分钟),批量拉取要有耐心或换更快的镜像源。

2.2 raw.githubusercontent 被墙

  • 症状:kubectl apply -f https://raw.githubusercontent.com/longhorn/... 拉不到 manifest。
  • 修复:加 ghproxy 前缀:
    curl -sL "https://ghfast.top/https://raw.githubusercontent.com/longhorn/longhorn/v1.7.2/deploy/longhorn.yaml" -o /tmp/longhorn.yaml
    

2.3 alpine/socat tag 写错

  • 症状:alpine/socat:1.8.0.0-r0 拉取 403。
  • 根因:该 tag 不存在。
  • 修复:用 alpine/socat:1.8.0.0(或 latest)。 ⚠️ 内部设计稿 §4.3 里写的 1.8.0.0-r0 需按此改正;本项目已改用自带的 Node tcp-bridge(见 §7.5),不再依赖 socat 镜像。

2.4 ghcr.io 直连被墙(CNPG/Longhorn 镜像)

  • 症状:CloudNativePG operator 镜像 ghcr.io/cloudnative-pg/cloudnative-pg 卡 "Pulling" 十几分钟不动。
  • 修复:registries.yaml 里给 ghcr.io 也配镜像:
    ghcr.io:
      endpoint:
        - "https://ghcr.m.daocloud.io"
    

2.5 CloudNativePG poolers CRD apply 报 annotation 超长

  • 症状:kubectl apply -f cnpg.yaml 报 CRD poolers.postgresql.cnpg.io is invalid: metadata.annotations: Too long,operator 崩溃 no matches for kind "Pooler"。
  • 根因:client-side apply 写的 last-applied-configuration annotation 超过 256KB。
  • 修复:kubectl apply --server-side --force-conflicts -f cnpg.yaml(server-side 不写那个超长 annotation)。

3. Longhorn

3.1 磁盘保留比例过高

  • 症状:卷副本创建失败 No available disk candidates,卷状态 faulted。
  • 根因:Longhorn 默认保留 30% 磁盘;磁盘 88% 满时「可用 < 保留量」,拒绝调度副本。
  • 修复:把保留/最小可用降到 5%:
    kubectl -n longhorn-system patch settings.longhorn.io storage-reserved-percentage-for-default-disk --type=merge -p '{"value":"5"}'
    kubectl -n longhorn-system patch settings.longhorn.io storage-minimal-available-percentage --type=merge -p '{"value":"5"}'
    

3.2 iscsid 未启动

  • 症状:卷卡在 attaching,消费 Pod 一直 ContainerCreating。
  • 修复(装完 iscsi-initiator-utils 后记得启动):
    dnf install -y iscsi-initiator-utils
    systemctl enable --now iscsid
    

3.3 单节点 3 副本调度不了

  • 症状:2 个副本 Failed to schedule replica,卷卡 attaching。
  • 根因:Longhorn 默认 hard anti-affinity,同一卷的副本必须在不同节点;单节点只能调度 1 个。
  • 修复:副本降到 1:
    kubectl -n longhorn-system patch volumes.longhorn.io <volume-name> --type=merge -p '{"spec":{"numberOfReplicas":1}}'
    kubectl -n longhorn-system patch settings.longhorn.io default-replica-count --type=merge -p '{"value":"1"}'
    

3.4 RWX 卷需 nfs-utils

  • 症状:RWX 卷挂载失败(RWX 走 share-manager / NFS ganesha)。
  • 修复:装 nfs-utils(提供 mount.nfs):
    dnf install -y nfs-utils
    

3.5 内存预算(重要)

  • Longhorn 自身 ~600M。2C2G 上 k3s + Longhorn 已占 ~1.4G,无法再跑 CloudNativePG 3 实例(需 ~1.5G)。
  • PoC item 4(CNPG + pgbench)需 ≥4G 机器,2G 必 OOM。

4. 方案修正(PoC 实测推翻/修正的结论)

4.1 flannel 实际强制 NetworkPolicy(§3.5 修正)

  • 实测 k3s v1.31 flannel 下 NetworkPolicy 开箱即用(k3s 内嵌 kube-router netpol 控制器,不再以 DaemonSet 形式;attacker 被拒,删策略后立刻恢复连通)。
  • §3.5「k3s 默认 flannel 不强制」不成立。
  • Cilium 的选型理由应改为:L7 策略 / eBPF 性能 / Hubble 可观测(这些 kube-router 没有),而不是「基础强制」。

4.2 gid ≠ fsGroup(§6.0 措辞)

  • 实测 init Job 建目录 0700 属主 uid 正确,但 gid=0(不是 fsGroup=100001)——因 §4.3 的 Pod 只设 runAsUser+fsGroup、没设 runAsGroup,进程默认 gid=0。
  • 不影响 0700 安全边界(0700 已把 group 权限关成 ---)。若要对齐 gid,需加 runAsGroup:<uid>。

4.3 socat tag(§4.3 修正)

  • alpine/socat:1.8.0.0-r0 → alpine/socat:1.8.0.0。见 §2.3。

5. 完整部署流程(ACK 实跑记录,2026-08-23)

在阿里云 ACK 智能托管模式 上跑通控制面 Phase 2 的完整步骤。k8s 方案里的 「3 server HA / kube-vip / MetalLB / Cilium」在 ACK 上由托管能力替代(§5.5 映射)。 每用户 DSH Pod(Phase 3)、cert-manager、Helm chart 尚未落地,仍待补。

5.1 集群与基础组件

  1. 建 ACK 集群(智能托管模式):网络插件 DataPath V2(eBPF)+ 勾选 NetworkPolicy;服务转发模式 IPVS。
  2. 装 CNPG operator:manifest 用 ghproxy 下载(§2.2),kubectl apply --server-side --force-conflicts(§2.5 annotation 超长坑)。
  3. ghcr.io 换源:CNPG operator 与 Postgres 镜像在阿里云拉不动(§2.4),换 ghcr.m.daocloud.io。

5.2 部署步骤

  1. kubectl apply -f deploy/00-namespace.yaml
  2. kubectl apply -f deploy/01-dsh-pg.yaml(Postgres 集群;storageClass 用拓扑感知 alicloud-disk-topology-alltype,size ≥20GiB)
  3. 等 dsh-pg 进入 Cluster in healthy state;读连接串: kubectl -n dsh get secret dsh-pg-app -o jsonpath='{.data.uri}' | base64 -d
  4. 推镜像到 ACR:CI master push 自动推(需 ACR_USERNAME/ACR_PASSWORD secret),或 workflow_dispatch。
  5. 建三个 secret(不在 deploy/ YAML 里,因含动态值):
    • dsh-acr-pull(docker-registry 类型,ACR 凭证)
    • dsh-secret(共享加密密钥,key)
    • dsh-pg(url = 上面读到的 URI)
  6. kubectl apply -f deploy/02-control-plane.yaml
  7. bootstrap admin:kubectl -n dsh exec deploy/dsh-orchestrator -- node lib/cli.js bootstrap-admin --username admin --password '<p>'
  8. 按 §5.3 验收。

5.3 验收清单(Phase 2)

  • 注册 → 审核 → 登录 → 管理台/域名 API 全通。
  • 访问控制:普通用户访问管理台 403、域名 API 200。
  • kill 一个控制面副本 → Deployment 自动重建(3/3)、Service 不中断。
  • 数据在 Postgres,删副本/重启不丢。

5.4 踩坑记录(ACK 实测新增)

坑 修复
ghcr.io 被墙:CNPG operator / Postgres 镜像拉不动 换 ghcr.m.daocloud.io(§2.4)
ESSD 最小 20GiB:size: 10Gi 报 less than minimum 20GiB storage size: 20Gi
CNPG 重建集群密码漂移:dsh-pg-app 重新生成密码 读最新 URI 重建 dsh-pg secret;生产用固定密码
控制面启动依赖 Postgres 就绪:ECONNREFUSED 崩 等 Postgres healthy 再部署,或接受 CrashLoopBackOff 重试
Auto Mode 节点有 taint,CNPG pod 调度失败 GOATScaler 自动扩出无 taint 节点

5.5 ACK 与 k8s 方案的映射

k8s.md 定案 ACK 等价
3 server HA + kube-vip 托管控制面(免费)
MetalLB(L2 ARP 云上不生效) SLB / ALB
Cilium Terway DataPath V2(eBPF + NetworkPolicy)
Longhorn RWX / RWO NAS / ESSD 云盘
CloudNativePG CloudNativePG(自建)或 RDS PG 高可用版

6. PoC 实测基线(item 4:CNPG on Longhorn)

机器:阿里云 4C16G,Ubuntu 24.04,SSD 40G。k3s v1.31 + Longhorn v1.7.2(副本 1)+ CNPG v1.24.1,3 实例 healthy。

pgbench(scale 5,30s,4 clients / 2 threads):

负载 tps latency average 说明
select-only(-S) 19164.7 0.209 ms 纯读,数据 ~70MB 全进内存缓冲,未打到磁盘
TPC-B 混合(含写) 705.3 5.671 ms 写落到 Longhorn 磁盘,这才是 I/O 基线

结论:CNPG on Longhorn 跑通(3 实例、pgbench 正常)。写路径 latency ~5.6ms / tps ~705,是 Longhorn 单副本的 I/O 成本。要对照「节点本地 NVMe + PG 主备」判断是否可接受,需换 NVMe 盘 + 更大 scale(让数据集超过内存)再压一次。

另:pgbench 手动跑法(kubectl run --rm -i 在后台/非 tty 会话会卡 stdin,别用):

kubectl run pgbench-init --restart=Never --image=postgres:16 -n <ns> \
  --env PGHOST=<cluster>-rw --env PGUSER=dsh --env PGPASSWORD=<pw> --env PGDATABASE=dsh \
  -- pgbench -i -s 5
# 等 pod phase=Succeeded 后 kubectl logs;跑完 kubectl delete pod

7. Phase 3 集群联调踩坑(2026-08-23,第二阶段)

控制面 deployMode=k8s + leader election + file sidecar + 每用户 Pod 在 ACK 上联调的实跑记录。前一个阶段(§5)只把控制面 3 副本 + CNPG 跑通了。

7.1 部署顺序(缺一步就挂,按序执行)

  1. deploy/00-namespace.yaml → 01-dsh-pg.yaml(Postgres healthy)→ 读 dsh-pg-app URI 建 dsh-pg/dsh-secret/dsh-acr-pull secret。
  2. deploy/03-rbac.yaml(SA + Role + lease 权限)。漏了它,控制面 Deployment 报 FailedCreate: serviceaccount "dsh-orchestrator" not found,滚动更新卡死。
  3. deploy/02-control-plane.yaml(含 imagePullPolicy: Always,同名 tag 否则节点 缓存旧镜像)。
  4. NAS:控制台建 CNFS(容器网络文件系统)→ 应用 deploy/05-storage.yaml 引 CNFS → 04-pvc.yaml → 08-bootstrap.yaml(chmod 1777 PVC 根)→ 最后 07-psa.yaml。
  5. 每用户 Pod 依赖 dsh-acr-pull(§7.3)。

7.2 NAS:用 CNFS,别手写 server 参数

  • 手写 alicloud-nas StorageClass 的 server 必须是挂载目标域 + :/,且 不含 FileSystemId 前缀。我给错成 <FSID>.<NAS_MOUNT_TARGET> (带 FSID 前缀)→ provisioner 报 CreateDir: IllegalCharacters;去掉 FSID 前缀 后 PVC 才 Bound。
  • 但挂载目标还会变(重建 NAS 后后缀漂移),正确姿势是控制台建 CNFS (storage.alibabacloud.com/v1beta1 ContainerNetworkFileSystem),StorageClass 用 containerNetworkFileSystem: nas 参数引用,provisioner 自动拿到当前挂载目标。
  • bootstrap Job 第一次卡 ContainerCreating 无事件 = NFS 挂载挂起(挂载目标错/VPC 不通); 目标对了会报 mount.nfs: Connection reset by peer(通常是安全组/访问组或目标刚就绪)。

7.3 每用户 Pod 镜像拉取与资源

  • 控制面镜像在私有 ACR,生成的 files Pod / DSH Pod / watchdog Job 必须带 imagePullSecrets: [dsh-acr-pull]——控制面 Deployment 有,但生成的 Pod 不继承。 漏了就是 Init:ImagePullBackOff insufficient_scope。代码里 config imagePullSecret 默认 dsh-acr-pull,已在 K8sSpawner 生成的三类 Pod/Job 全部带上。
  • bootstrap Job 也需 imagePullSecrets + 用控制面镜像(busybox 从 docker.io 拉不动)。
  • files sidecar 内存 64Mi 跑不起 node:22-slim + Fastify(OOMKilled → Pod 不 Ready → Headless DNS 无 A 记录 → 控制面 ENOTFOUND),提到 256Mi。
  • files sidecar 以用户 uid 跑且无 home,homedir() 落到 /,resolveConfig 会尝试 mkdir /.dshs → EACCES 崩。runFileService 已钉 dataRoot=/tmp + 占位 encryptionSecret 跳过写文件。

7.5 docker.io 被墙 → socat sidecar 换成 Node tcp-bridge

  • ACK 节点拉不动 docker.io(busybox/alpine/socat 都超时),而 alpine/socat 正是 每用户 DSH Pod 的 sidecar 镜像。改为控制面镜像里的 tcp-bridge 子命令(net 纯转发 8081→8080),DSH Pod 只依赖 ACR 里已有的 dsh + dshs 两个镜像 + imagePullSecret,彻底去掉 docker.io 依赖。
  • 测鉴权/带 cookie 的接口用 --resolve 走域名,别用 kubectl port-forward:设了 cookieDomain=.dsh.example.com 后 cookie 不会发到 127.0.0.1。

7.6 每用户 Pod 的 uid 与就绪探针

  • uid 错位:注册/建 admin 若先 initUserRoot(建 files Pod)再 createUser,此时 用户不存在,resolveUid 回退 hash uid,而 createUser 给的是 baseUid+row_id—— files Pod 用 hash uid 建 0700 目录,DSH Pod 用 row_id uid 访问 → EACCES。已改成先 建用户再 initUserRoot。
  • 就绪探针:DSH 只监听 loopback,tcpSocket 8080 探的是 Pod IP、永远不 Ready → Headless 无 A 记录 → 子域 502。改成容器内 node -e http.get(127.0.0.1:8080) exec 探针。
  • 子域代理端口:Headless Service(clusterIP: None)没有 kube-proxy 做 DNAT,代理 必须直连 Pod 的 targetPort(sidecar 8081),不能连 Service port 80——否则 502。 endpointFor 返回 8081。
  • cookie 跨子域:dsh.<domain> 登录后跳 <user>.dsh.<domain>,SameSite=Lax 实测 不带 cookie → 401;改 SameSite=None; Secure(App 已 HTTPS-only)+ secureCookies=true。
  • 代理 502:控制面 keep-alive 池缓存旧 DSH Pod IP,重建 Pod 后命中旧 IP 副本 → 502; 连接出错时 agent.destroy() + agent:false 重试一次重新 DNS。

7.4 域名入口被阿里云备案拦截

  • 腾讯云入口机 nginx 反代到 ACK 公网 SLB(<SLB_IP>)时,Host: dsh.example.com 被阿里云返回 403 Non-compliance ICP Filing(Server: Beaver)——域名在腾讯云备案、 未在阿里云备案,走阿里云公网 SLB 的 80/443 被备案校验拦。直连 SLB IP(不带域名 Host) 则 200 正常。
  • 解法:给域名在阿里云做 ICP 备案,或入口走 NodePort/专线绕开公网 SLB 备案层,或 控制面域名解析子域改用「SLB 直连 + 腾讯云 nginx 透传 Host」之外的方案(待定)。

8. Phase 4 加固 / 可观测(2026-08-23)

8.1 代码加固(已落地,随镜像生效)

  • readOnlyRootFilesystem: true + emptyDir /tmp:DSH dsh/sidecar、file sidecar、 watchdog、控制面容器全部只读 rootfs,/tmp 走 emptyDir(PSA restricted 纵深)。
  • 空闲回收:reconcile(仅 leader)对每个期望 main 检查 hasActiveSession,会话全过期 → stop Pod + 删期望态,不自动拉起(复用 sessionTtlSeconds,默认 7 天)。
  • egress 收敛:DSHS_EGRESS_CIDRS(逗号分隔 CIDR)非空时,每用户 DSH Pod 的 443 egress 从 0.0.0.0/0 收敛为白名单(仍 except 内网)。默认空 = 保持现状。 ⚠️ DeepSeek API 走 CDN,IP 会漂移,收敛前先 dig +short api.deepseek.com 核对并定期重查。

8.2 etcd encryption-at-rest(ACK 托管,控制台操作)

  1. ACK 控制台 → 集群 → 托管控制面 → 开启 etcd 加密(encryption-at-rest)。
  2. 开启后必须全量重写存量 Secret,否则旧 Secret 仍明文:
    kubectl get secrets -A -o json | kubectl replace -f -
    
  3. 验证(应只剩 k8s:enc:aescbc:v1: 前缀的密文):
    kubectl get secrets --all-namespaces -o json | grep -v 'k8s:enc:aescbc' || echo "all encrypted"
    
    覆盖 dsh-key-*(每用户 API key)、dsh-pg(DB DSN)、dsh-secret(共享加密密钥)。

8.3 可观测(Prometheus + Loki + 告警)

  • 指标:控制面已可暴露 /metrics(未接入则用 prom-client 加一个);ACK 托管 Prometheus (ARMS)在控制台开通后,用 ServiceMonitor 抓 dsh-orchestrator 的 3080。 关键指标:控制面副本数、Postgres 连接数、每用户 Pod 数、崩溃计数。
  • 日志:Loki + Promtail(或 ACK SLS)接入控制面与每用户 Pod 日志。
  • 告警规则(Prometheus):控制面副本 < 3、Postgres 不可写、DSH Pod 崩溃率、NAS 容量水位。 示例(自建 Prometheus 时用):
    apiVersion: monitoring.coreos.com/v1
    kind: PrometheusRule
    metadata: { name: dsh-alerts, namespace: dsh }
    spec:
      groups:
        - name: dsh
          rules:
            - alert: DshControlPlaneDown
              expr: count(up{app="dsh-orchestrator"}) < 3
              for: 5m
            - alert: DshPostgresUnavailable
              expr: pg_up == 0
              for: 2m
    
    注:托管 Prometheus/Loki 实际接入需在 ACK 控制台开通,本轮交付清单与步骤。

8.4 Web 安全审计(代码审计结论,2026-08-23)

  • XSS(已修):desktop.html/admin.html 把文件名 e.name、插件名/描述 plugin.name/plugin.description、密钥名 k.name、用户名 u.username 直接拼进 innerHTML/insertAdjacentHTML。文件名与插件描述不受字符集约束 → 存储型 XSS。 已加 esc() 转义 <>&"' 后再拼接。
  • 输入校验(已确认安全):用户名 ^[a-zA-Z0-9_-]+$、域名 isValidDomain (小写字母数字+连字符,防 nginx 注入)、密钥名 ^[A-Za-z0-9\-_ .]{1,32}$、路径 resolveWithinRoot/safeFilename。
  • CSRF(残余低风险,未修):会话 cookie 为 SameSite=None 后跨站请求也会带 cookie,但所有状态变更端点都是 application/json + 无 CORS 头,跨站 fetch 会触发 preflight 被浏览器拦;真正暴露的是无 body 的 POST /api/auth/logout(logout CSRF, 低危)。后续若要加固,给状态变更端点加 CSRF token / 自定义头校验。
  • OWASP ZAP 基线扫描:未在本轮执行(需 ZAP 工具/容器)。已定位的 XSS 已人工修, ZAP 可作为 CI 门禁后续接入。

8.5 联调后期修的 4 个根因 bug(2026-08-23)

症状 根因 修复
leader 不接管,lease 挂旧 pod 数小时 client-node patchNamespacedLease 走 JSON Patch(要数组),传对象被 Go 拒 400 被静默吞 改 replaceNamespacedLease 全量 PUT + RV 乐观并发
admin DSH 反复 502 endpointFor 返回 Service DNS,A 记录 ~30s TTL,Pod 重启后这 30s 内仍解析旧 IP endpointFor 直接返回 podIP,每次请求实时读 Pod
模型无法请求(EAI_AGAIN) ACK DNS 是 node-local-dns(label k8s-app=node-local-dns),NP 规则却选 k8s-app=kube-dns → DNS 全被拦 NP DNS egress 改 0.0.0.0/0(可移植)
keepAlive 池销毁后不可复用 agent.destroy() 只销毁不替换,后续请求首跳全失败 destroy 后 new Agent() 替换,再短连接重试一次