Skip to content

GitLab PostgreSQL 从 NFS 迁移到本地存储 & 集群恢复全记录

日期: 2026-08-21 环境: RKE2 v1.35.6 / 6 节点集群 / GitLab Helm Chart 涉及节点: sza122028.local (192.168.122.28)、sza122031.local (192.168.122.31)


一、问题背景

GitLab 集群在 6 节点 RKE2 环境中运行,PostgreSQL 使用 NFS 存储后端。出现以下严重故障:

  1. PostgreSQL 无法完成数据库 Schema 加载db:schema:load 在 NFS 上挂起,1080+ 张表的创建极度缓慢
  2. db:schema:load 报 out of shared memory — 分区表创建时锁耗尽共享内存
  3. Webservice 和 Sidekiq Pod 反复被 K8s 探针杀死 — Rails 预加载耗时 3-5 分钟,超过默认 liveness/readiness 超时
  4. 节点 192.168.122.31 处于 NotReady 状态 — 集群调度能力受限
  5. 大量 stale ReplicaSet 残留 — 导致不必要的 Pod 竞争资源

二、排查过程

2.1 PostgreSQL 存储瓶颈诊断

bash
# 检查 PostgreSQL Pod 日志
kubectl logs -n gitlab postgres-xxx -c postgres

# 观察到:
# - db:schema:load 持续挂起,单表创建耗时数秒
# - NFS I/O 延迟极高,GitLab schema 有 1080+ 张表

# 检查 NFS 性能
kubectl exec -n gitlab postgres-xxx -- \
  dd if=/dev/zero of=/var/lib/postgresql/data/testfile bs=1M count=100 oflag=dsync
# 结果: 写入速度极慢,确认 NFS 是瓶颈

# 检查 PV/PVC 状态
kubectl get pv -n gitlab | grep postgres
kubectl get pvc -n gitlab | grep postgres

2.2 共享内存问题诊断

bash
# db:schema:load 报 out of shared memory
# 检查 PostgreSQL 当前配置
kubectl exec -n gitlab postgres-xxx -- psql -U gitlab -c "SHOW max_locks_per_transaction;"
# 结果: 默认值 64,对于 GitLab 的大量分区表远远不够

# 检查共享内存使用
kubectl exec -n gitlab postgres-xxx -- psql -U gitlab -c "
  SELECT name, setting, unit FROM pg_settings
  WHERE name IN ('shared_buffers', 'max_locks_per_transaction', 'max_connections');
"

2.3 K8s 探针问题诊断

bash
# 检查 Webservice/Sidekiq Pod 事件
kubectl describe pod -n gitlab gitlab-webservice-default-xxx
kubectl describe pod -n gitlab gitlab-sidekiq-all-in-1-v2-xxx

# 观察到:
# - Liveness probe failed: Get "http://...": context deadline exceeded
# - Container killed by liveness probe
# - Pod 处于 CrashLoopBackOff 循环

# 检查 Rails 预加载时间
kubectl logs -n gitlab gitlab-webservice-default-xxx -c webservice
# 日志显示 Rails environment 加载需要 3-5+ 分钟

# 检查 Sidekiq readiness 端口
kubectl exec -n gitlab gitlab-sidekiq-xxx -c sidekiq -- cat /proc/net/tcp
# 发现 sidekiq_exporter 只监听 3807,但 readiness probe 配置的是 3808

# 确认 metrics 端点
kubectl exec -n gitlab gitlab-sidekiq-xxx -c sidekiq -- ruby -e \
  'require "net/http"; puts Net::HTTP.get_response(URI("http://localhost:3807/")).code'
# 返回 404

kubectl exec -n gitlab gitlab-sidekiq-xxx -c sidekiq -- ruby -e \
  'require "net/http"; r = Net::HTTP.get_response(URI("http://localhost:3807/metrics")); puts r.code'
# 返回 200

2.4 节点 192.168.122.31 NotReady 排查

bash
# 检查节点状态
kubectl get nodes -o wide
# sza122031.local NotReady

# 检查节点 taint
kubectl get node sza122031.local -o jsonpath='{.spec.taints}'

# 检查 kubelet 日志
ssh 192.168.122.31 "journalctl -u rke2-agent -n 50 --no-pager"
# 节点最终自行恢复,无需手动干预

三、根因分析

#根因影响
1NFS I/O 性能不足PostgreSQL db:schema:load 无法完成,GitLab 无法初始化
2max_locks_per_transaction 过低 (64)分区表锁耗尽,schema load 失败
3缺少 startupProbeK8s liveness/readiness 在 Rails 预加载期间误杀 Pod
4Sidekiq 探针端口/路径错误3808 不存在,/readiness 返回 404,Pod 永远不 Ready
5stale ReplicaSet 残留旧 Pod 与新 Pod 竞争调度资源

四、解决方案 & 操作步骤

4.1 Step 1: 创建本地存储 StorageClass

bash
cat <<'EOF' | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
EOF

4.2 Step 2: 创建本地 PV 和 PVC

bash
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-postgres-pv
spec:
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /data/gitlab-postgres
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - sza122028.local
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data-local
  namespace: gitlab
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  storageClassName: local-storage
EOF

4.3 Step 3: 在目标节点创建数据目录

bash
ssh sza122028.local "mkdir -p /data/gitlab-postgres && chmod 700 /data/gitlab-postgres"

4.4 Step 4: 迁移 PostgreSQL Deployment

bash
# 备份当前 deployment 配置
kubectl get deploy postgres -n gitlab -o yaml > /tmp/postgres-deploy-backup.yaml

# Patch Deployment: 更新 volume 使用本地存储 + 绑定节点
kubectl patch deploy postgres -n gitlab --type='json' -p='
[
  {"op": "replace", "path": "/spec/template/spec/volumes", "value": [
    {
      "name": "postgres-data",
      "persistentVolumeClaim": {
        "claimName": "postgres-data-local"
      }
    }
  ]},
  {"op": "replace", "path": "/spec/template/spec/nodeSelector", "value": {
    "kubernetes.io/hostname": "sza122028.local"
  }}
]'

4.5 Step 5: 优化 PostgreSQL 参数

bash
# 等待 PostgreSQL Pod 启动
kubectl wait --for=condition=Ready pod -l app=postgres -n gitlab --timeout=120s

# 进入 PostgreSQL Pod
POD=$(kubectl get pods -n gitlab -l app=postgres -o jsonpath='{.items[0].metadata.name}')

# 修改 max_locks_per_transaction 为 512,shared_buffers 为 512MB
kubectl exec -n gitlab $POD -c postgres -- bash -c "
  echo \"max_locks_per_transaction = 512\" >> /var/lib/postgresql/data/postgresql.auto.conf
  echo \"shared_buffers = 512MB\" >> /var/lib/postgresql/data/postgresql.auto.conf
"

# 重启 PostgreSQL
kubectl rollout restart deploy postgres -n gitlab
kubectl rollout status deploy postgres -n gitlab --timeout=120s

4.6 Step 6: 执行 db:schema:load

bash
# 获取新的 PostgreSQL Pod 名称
POD=$(kubectl get pods -n gitlab -l app=postgres -o jsonpath='{.items[0].metadata.name}')

# 确认参数已生效
kubectl exec -n gitlab $POD -c postgres -- psql -U gitlab -c "
  SHOW max_locks_per_transaction;
  SHOW shared_buffers;
"

# 创建数据库(如果不存在)
kubectl exec -n gitlab $POD -c postgres -- psql -U gitlab -c "
  SELECT 'CREATE DATABASE gitlabhq_production'
  WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'gitlabhq_production')
\gexec"

# 执行 schema load(预计需要数分钟)
kubectl exec -n gitlab $POD -c postgres -- bash -c "
  cd /srv/gitlab && RAILS_ENV=production bin/rake db:schema:load
"

# 验证
kubectl exec -n gitlab $POD -c postgres -- psql -U gitlab -d gitlabhq_production -c "
  SELECT count(*) as tables FROM pg_tables WHERE schemaname='public';
  SELECT count(*) as migrations FROM schema_migrations;
"
# 预期: 1080+ tables, 2163+ migrations

# 运行迁移
kubectl exec -n gitlab $POD -c postgres -- bash -c "
  cd /srv/gitlab && RAILS_ENV=production bin/rake db:migrate
"

4.7 Step 7: 修复 Webservice 探针(添加 startupProbe)

bash
kubectl patch deploy gitlab-webservice-default -n gitlab --type='json' -p='
[
  {"op": "add", "path": "/spec/template/spec/containers/0/startupProbe", "value": {
    "httpGet": {
      "path": "/-/readiness",
      "port": 8080,
      "scheme": "HTTP"
    },
    "initialDelaySeconds": 30,
    "periodSeconds": 10,
    "failureThreshold": 60,
    "timeoutSeconds": 5
  }},
  {"op": "replace", "path": "/spec/template/spec/containers/0/livenessProbe/failureThreshold", "value": 30},
  {"op": "replace", "path": "/spec/template/spec/containers/0/livenessProbe/periodSeconds", "value": 10},
  {"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/failureThreshold", "value": 30},
  {"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/periodSeconds", "value": 10}
]'

4.8 Step 8: 修复 Sidekiq 探针(端口 + 路径 + startupProbe)

bash
# 修复 Sidekiq 探针: 端口 3808 → 3807, 路径 /readiness → /metrics
kubectl patch deploy gitlab-sidekiq-all-in-1-v2 -n gitlab --type='json' -p='
[
  {"op": "add", "path": "/spec/template/spec/containers/0/startupProbe", "value": {
    "httpGet": {
      "path": "/metrics",
      "port": 3807,
      "scheme": "HTTP"
    },
    "initialDelaySeconds": 30,
    "periodSeconds": 10,
    "failureThreshold": 60,
    "timeoutSeconds": 5
  }},
  {"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/httpGet/port", "value": 3807},
  {"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/httpGet/path", "value": "/metrics"},
  {"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/failureThreshold", "value": 30},
  {"op": "replace", "path": "/spec/template/spec/containers/0/livenessProbe/httpGet/port", "value": 3807},
  {"op": "replace", "path": "/spec/template/spec/containers/0/livenessProbe/httpGet/path", "value": "/metrics"},
  {"op": "replace", "path": "/spec/template/spec/containers/0/livenessProbe/failureThreshold", "value": 30}
]'

4.9 Step 9: 清理 stale ReplicaSet

bash
# 查看所有 ReplicaSet
kubectl get rs -n gitlab | grep -E 'sidekiq|webservice'

# Scale 旧的 ReplicaSet 到 0
kubectl scale rs -n gitlab gitlab-sidekiq-all-in-1-v2-5bf9bc9648 --replicas=0
kubectl scale rs -n gitlab gitlab-sidekiq-all-in-1-v2-656b558dcd --replicas=0

# 或者直接清理所有 0 副本的 ReplicaSet
kubectl get rs -n gitlab -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.replicas}{"\n"}{end}' | \
  awk '$2 == "0" {print $1}' | xargs -I{} kubectl delete rs -n gitlab {}

4.10 Step 10: 最终验证

bash
# 全部 Pod 状态
kubectl get pods -n gitlab -o wide

# 预期输出:
# postgres-xxx                          1/1  Running
# gitlab-webservice-default-xxx          2/2  Running  (每个副本)
# gitlab-sidekiq-all-in-1-v2-xxx         1/1  Running
# gitlab-gitaly-default-{0,1,2}          1/1  Running
# gitlab-gitlab-shell-xxx                1/1  Running
# redis-xxx                              1/1  Running
# minio-xxx                              1/1  Running

# 数据库完整性
POD=$(kubectl get pods -n gitlab -l app=postgres -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n gitlab $POD -c postgres -- psql -U gitlab -d gitlabhq_production -c "
  SELECT count(*) as tables,
         (SELECT count(*) FROM schema_migrations) as migrations
  FROM pg_tables WHERE schemaname='public';
"

# Sidekiq 队列处理验证
kubectl logs -n gitlab gitlab-sidekiq-all-in-1-v2-xxx -c sidekiq --tail=20 | \
  grep -E 'Listening|started|queue'

# 所有节点状态
kubectl get nodes -o wide

五、关键配置汇总

5.1 PostgreSQL 参数

ini
# /var/lib/postgresql/data/postgresql.auto.conf
max_locks_per_transaction = 512    # 默认 64,GitLab 分区表需要更多
shared_buffers = 512MB             # 默认 128MB,提升缓存性能

5.2 Sidekiq 探针配置

yaml
readinessProbe:
  httpGet:
    path: /metrics          # 注意: 不是 /readiness
    port: 3807              # 注意: 不是 3808
  failureThreshold: 30
  periodSeconds: 10
startupProbe:
  httpGet:
    path: /metrics
    port: 3807
  failureThreshold: 60      # 600秒 (60 * 10s) 超时
  periodSeconds: 10
livenessProbe:
  httpGet:
    path: /metrics
    port: 3807
  failureThreshold: 30
  periodSeconds: 10

5.3 Webservice 探针配置

yaml
startupProbe:
  httpGet:
    path: /-/readiness
    port: 8080
  initialDelaySeconds: 30
  failureThreshold: 60      # 600秒 (60 * 10s) 超时
  periodSeconds: 10
  timeoutSeconds: 5

5.4 本地存储架构

┌─────────────────────────────────────────────┐
│  sza122028.local (192.168.122.28)           │
│                                             │
│  /data/gitlab-postgres/  (hostPath)         │
│    └── PostgreSQL 数据文件                   │
│                                             │
│  PV: local-postgres-pv (20Gi)               │
│  PVC: gitlab/postgres-data-local            │
│  StorageClass: local-storage                │
│  nodeAffinity: sza122028.local              │
└─────────────────────────────────────────────┘

六、踩坑总结 & 经验教训

🔑 经验 1: NFS 不适合 PostgreSQL

GitLab 的 schema 有 1080+ 张表,涉及大量分区表。在 NFS 上执行 db:schema:load 会因为网络延迟导致操作挂起。生产环境的 PostgreSQL 应始终使用本地存储或高性能块存储。

🔑 经验 2: startupProbe 是大型 Rails 应用的必需品

GitLab 的 Puma/Sidekiq 进程预加载 Rails 环境需要 3-5+ 分钟。如果没有 startupProbelivenessProbe 会在应用启动期间误杀 Pod。

startupProbe: 给应用足够的启动时间,启动成功后自动禁用
livenessProbe: 仅在 startupProbe 成功后才开始检测

🔑 经验 3: Sidekiq 的 readiness 端口容易配错

GitLab 的 sidekiq_exporter 监听端口 3807,而非 GitLab Chart 默认配置的 3808。同时 readiness 路径应该是 /metrics 而非 /readiness(后者返回 404)。

🔑 经验 4: max_locks_per_transaction 对分区表至关重要

GitLab 使用大量分区表(如 audit_events),创建分区需要大量表锁。默认值 64 远远不够,建议设为 512。

🔑 经验 5: 及时清理 stale ReplicaSet

每次 patch Deployment 都会产生新的 ReplicaSet。如果不清理旧的,会导致多个版本的 Pod 同时运行,争抢资源。


七、回滚方案

如果需要回退到 NFS 存储:

bash
# 1. 恢复 NFS PVC
kubectl get pvc postgres-data -n gitlab   # 确认原 PVC 仍在

# 2. Patch Deployment 恢复 NFS volume
kubectl patch deploy postgres -n gitlab --type='json' -p='
[
  {"op": "replace", "path": "/spec/template/spec/volumes", "value": [
    {
      "name": "postgres-data",
      "persistentVolumeClaim": {
        "claimName": "postgres-data"
      }
    }
  ]},
  {"op": "replace", "path": "/spec/template/spec/nodeSelector", "value": null}
]'

# 3. 从本地存储同步数据回 NFS(如需要)
# ssh sza122028.local "rsync -avz /data/gitlab-postgres/ /nfs/gitlab-postgres/"

# 4. 恢复探针默认配置
kubectl patch deploy gitlab-sidekiq-all-in-1-v2 -n gitlab --type='json' -p='
[
  {"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/httpGet/port", "value": 3808},
  {"op": "replace", "path": "/spec/template/spec/containers/0/readinessProbe/httpGet/path", "value": "/readiness"}
]'

八、后续待办

  • [ ] 测试 GitLab Web UI 功能(登录、页面加载、项目创建)
  • [ ] 考虑将 PostgreSQL 数据从 hostPath 定期备份到 NFS
  • [ ] 评估是否需要将本地存储方案持久化到 GitLab Helm Chart values
  • [ ] 监控 PostgreSQL 在本地存储上的长期稳定性