跳转至

INCIDENT-20260918-01 — 命名空间迁移期排障记录

  • 主题目录:ns_migration/
  • 记录对象:2026-09-18 凌晨「命名空间统一为 server」变更期间的 3 个阻塞问题
  • 等级:P3(变更期故障)——变更时目标组件无有效数据(GitLab projects=0),无数据损坏,但变更窗口被拉长约 30 分钟
  • 当前状态:已恢复。Traefik 与 GitLab 全栈均运行于 server 命名空间,验证通过
  • 关联文档:SOP.md、RUNBOOK.md
  • 说明:本文件是台账与交接用。日常排障手册见 RUNBOOK.md 第 8 章;部署期的 5 起问题另见 gitlab-deploy-v2/INCIDENT-20260917-01

1. 概要

项 内容
发生时间 2026-09-18 01:06 – 01:22(CST)
影响面 GitLab 全栈重建过程(Traefik 迁移部分未受影响)
业务影响 GitLab 停机时间由预期的约 15 分钟延长到约 25 分钟
数据影响 无(迁移前已复核 GitLab 为空:projects=0、Gitaly 无仓库、MinIO 仅空桶)
问题数 3 个(PV 交叉绑定、CNPG 无法 bootstrap、集群级残留阻塞部署)

2. 影响范围

  • 受影响服务:GitLab 及其依赖(PostgreSQL / Redis / MinIO)—— 全部在重建中,尚未对外提供服务。
  • 不受影响:Traefik(已于 00:38 完成迁移并验证)、监控栈(本次明确排除)、集群其他命名空间。
  • 数据损坏:无。三个 local PV 的回收策略均为 Retain,且旧数据目录以改名方式保留(*.bak.20260918005523),未做任何删除。

3. 时间线

时间 事件
00:55 下线旧 GitLab(卸载 release + 删除 4 个命名空间),旧 PV 转入 Released
00:55 节点数据目录改名保留、删除旧 PV 并按原定义重建(三个 PV 回到 Available)
01:06 在 server 内创建 MinIO / PostgreSQL(CNPG) / Redis
01:07 问题 1 暴露:发现 gitlab-pg-1 绑定到 pv-redis-ssd、redis-data 绑定到 pv-postgres-ssd(交叉绑定)
01:09 删除两个 PVC 并清空 claimRef,改为显式指定 volumeName 重绑
01:10 重建 CNPG Cluster —— 问题 2 暴露:状态长期停在 Setting up primary / BootstrapPending
01:12 观察到 operator 每秒重复输出 Creating new Job jobName=gitlab-pg-1-initdb,而该 Job 早已 Complete
01:15 判定根因为「预建 PVC 干扰 CNPG 状态机」,改为 PV claimRef 预绑定方案
01:15 处置中又发现新 PVC 抢占了 pv-gitaly-ssd(20Gi),遂把该 PV 也一并预绑定给 repo-data-gitlab-gitaly-0
01:16 CNPG Cluster 达到 Cluster in healthy state,gitlab-pg-1 → pv-postgres-ssd 绑定正确
01:13 首次 helm install gitlab 失败 —— 问题 3 暴露:报 GatewayClass 归属校验错误
01:20 确认 GatewayClass gitlab-gw 卡在删除中(finalizer + 原 controller 已随旧命名空间消失),手工摘除 finalizer
01:21 重新部署
01:22 release deployed,随后 migrations Job 完成、登录页 200、/-/health 返回 GitLab OK
01:35 文档注记与备份台账收尾完成

4. 根因

4.1 问题 1:两个同规格 local PV 被交叉绑定

现象:gitlab-pg-1(PostgreSQL)绑到了 pv-redis-ssd,redis-data(Redis)绑到了 pv-postgres-ssd。

证据

NAME          STATUS   VOLUME
gitlab-pg-1   Bound    pv-redis-ssd          ← 应为 pv-postgres-ssd
redis-data    Bound    pv-postgres-ssd       ← 应为 pv-redis-ssd

根因:两个 PV 容量(5Gi)、accessModes、storageClassName(local-ssd)完全相同,Deployment/CNPG 创建 PVC 时由调度器先到先得地随机挑选 Available 的 PV,控制器并不理解「这个卷叫 postgres 就该给 PG 用」。

次生问题:修正时删除 PVC 并用 claimRef: null 释放后,新 PVC 又抢占了当时唯一 Available 的 pv-gitaly-ssd(20Gi,满足 5Gi 申请),造成第二次错位。

4.2 问题 2:CNPG 因预建 PVC 而无法 bootstrap

现象:Cluster/gitlab-pg 长期处于 Setting up primary;Initialized=False (BootstrapPending);initdb Job 早已 Complete,但 operator 持续重复创建同名 Job。

证据

status.conditions:
- type: Initialized   status: "False"   reason: BootstrapPending
  message: Cluster has not been bootstrapped yet

operator 日志(每秒重复):
{"msg":"Creating new Job","jobName":"gitlab-pg-1-initdb","primary":true}

根因:为让 PostgreSQL 用上正确的卷,我先手工创建了名为 gitlab-pg-1 的 PVC(并指定 volumeName)。而 CNPG(CloudNativePG)对实例 PVC 有归属校验,只认由自己创建/带其标签的 PVC;面对这个"外来"的同名 PVC,它既不接管也不报错,而是判定 bootstrap 未完成,于是无限重试 initdb。

结论:CNPG 的实例 PVC 绝不可预建。要在重建场景下控制卷的归属,正确做法是给 PV 设 claimRef 预绑定(见 5.2)。

4.3 问题 3:集群级 GatewayClass 卡在删除中,阻塞新 release

现象:helm install gitlab 失败:

Error: Unable to continue with install: GatewayClass "gitlab-gw" in namespace "" exists and
cannot be imported into the current release: invalid ownership metadata; annotation validation
error: key "meta.helm.sh/release-namespace" must equal "server": current value is "gitlab"

证据

$ kubectl get gatewayclass gitlab-gw -o jsonpath='{.metadata.deletionTimestamp} {.metadata.finalizers}'
2026-09-17T16:50:31Z ["gateway-exists-finalizer.gateway.networking.k8s.io"]

根因:GatewayClass 是集群级资源,前一次 helm uninstall 与命名空间删除都无法立即清掉它;它被 Gateway API 的标准 finalizer gateway-exists-finalizer 拦住(设计上要求其下所有 Gateway 先删除)。而负责处理该 finalizer 的 Envoy Gateway controller 已随旧命名空间被删除,导致对象永久停在 deleting。Helm 随后拒绝把带旧注解(release-namespace: gitlab)的对象纳入新 release(server),部署中断。

5. 处置过程

5.1 问题 1 的处置(两轮)

# 第一轮:解除交叉绑定
kubectl -n server delete cluster gitlab-pg --wait=false
kubectl -n server delete statefulset redis
kubectl -n server delete pvc gitlab-pg-1 redis-data
kubectl patch pv pv-postgres-ssd -p '{"spec":{"claimRef":null}}'
kubectl patch pv pv-redis-ssd    -p '{"spec":{"claimRef":null}}'
# 用 volumeName 显式指定(此轮做法,后被证明对 CNPG 不适用)
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: gitlab-pg-1, namespace: server}
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: local-ssd
  volumeName: pv-postgres-ssd
  resources: {requests: {storage: 5Gi}}
YAML
# 第二轮(最终方案):PV claimRef 预绑定 —— 不预建 PVC,只限定"哪个 PVC 名可以绑这个 PV"
kubectl patch pv pv-gitaly-ssd   -p '{"spec":{"claimRef":null}}'   # 必须先清空(去掉 uid),否则停留在 Released
kubectl patch pv pv-postgres-ssd -p '{"spec":{"claimRef":null}}'
kubectl patch pv pv-gitaly-ssd --type=merge \
  -p '{"spec":{"claimRef":{"apiVersion":"v1","kind":"PersistentVolumeClaim","namespace":"server","name":"repo-data-gitlab-gitaly-0"}}}'
kubectl patch pv pv-postgres-ssd --type=merge \
  -p '{"spec":{"claimRef":{"apiVersion":"v1","kind":"PersistentVolumeClaim","namespace":"server","name":"gitlab-pg-1"}}}'

关键点:claimRef 中只写 apiVersion/kind/namespace/name 四个字段(不写 uid),PV 状态为 Available 但只接受该名字的 PVC 绑定。用 --type=merge 直接改已有 claimRef 不会清掉旧 uid(实测报 patched (no change) 且状态仍为 Released),必须先 claimRef: null 再设置。

5.2 问题 2 的处置

删除手工 PVC,改用 5.1 的 PV 预绑定,让 CNPG 自行创建 PVC:

kubectl -n server delete cluster gitlab-pg --wait=false
kubectl -n server delete job gitlab-pg-1-initdb --ignore-not-found
kubectl -n server delete pvc gitlab-pg-1 --ignore-not-found
# 预绑定 pv-postgres-ssd → server/gitlab-pg-1(见 5.1)
kubectl apply -f <Cluster 定义>          # 重建后 CNPG 自建 PVC,一次成功

5.3 问题 3 的处置

# 1) 确认没有任何 Gateway 仍引用它
kubectl get gateways -A                     # 输出 No resources found
# 2) 该 finalizer 本是等待 controller 清理 Gateway 后自动移除;但 controller 已不存在,
#    属"无人处理"的孤儿 finalizer,只能手工摘除
kubectl patch gatewayclass gitlab-gw --type=merge -p '{"metadata":{"finalizers":null}}'
# 3) 确认对象已清除后重新部署
kubectl delete gatewayclass gitlab-gw --ignore-not-found
helm upgrade --install gitlab ./gitlab -n server -f values-gitlab.yaml --timeout 900s

6. 恢复验证

# 存储绑定语义正确
kubectl -n server get pvc --no-headers | awk '{print $1, $2, $3}'
# 预期:gitlab-pg-1→pv-postgres-ssd  redis-data→pv-redis-ssd  repo-data-gitlab-gitaly-0→pv-gitaly-ssd

# 数据库集群健康
kubectl -n server get cluster gitlab-pg          # 预期 Cluster in healthy state

# 服务可用
kubectl -n server get job | grep migrations      # 预期 Completed
kubectl -n server run gl-check --rm -i --restart=Never --image=curlimages/curl:latest --quiet -- \
  sh -c 'curl -s -H "Host: gitlab.rainchen.top" http://envoy-server-gitlab-gw-80b44f81/-/health'
# 预期 GitLab OK

以上均已实测通过。

7. 复盘

有效的做法

  • 迁移前坚持复核数据为空(projects=0),并据此选择"重建路径"而非"数据搬迁路径" —— 否则问题 1、2 的排障代价会成倍上升。
  • local PV 采用 Retain + 数据目录改名保留,全程未做删除,保证了可回滚。
  • 每步都先看对象状态(get pvc / get cluster / operator 日志)再动手,问题定位较快。

拖慢恢复的做法

  • 把 CNPG 当成普通 StatefulSet 处理,想用"预建 PVC 指定卷"这种常规手段控制绑定 —— 这是问题 2 的直接原因,且这一轮探索消耗了约 5 分钟。
  • 第一次修 PV 交叉绑定时,只关注了 PG 与 Redis 两个卷,忽略了同时处于 Available 的 pv-gitaly-ssd,导致新 PVC 被它抢走,多返工一轮。
  • 部署前没有先清理旧 release 的集群级残留(GatewayClass / ClusterRole),问题 3 直到 helm install 报错才暴露。

留待体系化改进

  • 这类"变更前清理集群级残留"的检查项,应固化为 SOP 的前置检查步骤(本次已补进 SOP.md 第 5 章与附录 B)。
  • claimRef 预绑定的具体写法(字段、顺序、--type=merge 的坑)已写入 RUNBOOK.md 场景 3。

8. 行动项

事项 责任人 期限 状态
将「变更前清理旧 release 集群级残留(GatewayClass / ClusterRole / CRD 实例)」写入各变更 SOP 的前置检查 rain 2026-09-26 待办
为主机侧存储准备检查脚本:PV 与 PVC 的对应关系一键核对(含 claimRef 预绑定正确性) rain 2026-09-26 待办
评估是否将三个 local PV 的用途写入注释/标签(如 app.kubernetes.io/purpose)以便人工识别 rain 2026-10-03 待办
评估多节点入口方案(消除 k8s-master-01 单点,同时避免 hostPort 调度冲突) rain 2026-10-10 待办
清理 k8s-node-01:/mnt/ssd/k8s/*.bak.20260918005523(现已证明新实例运行正常,确认无需回滚后可删) rain 2026-09-26 待办

附:关联变更

变更 文档
本次命名空间统一变更 ns_migration/SOP.md
GitLab 首次部署期的 4 个阻塞问题 + 1 起凭据失误 gitlab-deploy-v2/INCIDENT-20260917-01-gitlab-部署与迁移期排障.md
Traefik 迁移到 server traefik_deploy/SOP.md

来源:/home/rain/Documents/k8s_ins/ns_migration/INCIDENT-20260918-01-ns迁移期排障.md | 最后更新:2026-09-19 16:53(由 build/sync.py 同步,请勿在站点侧修改)