主题
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 存储后端。出现以下严重故障:
- PostgreSQL 无法完成数据库 Schema 加载 —
db:schema:load在 NFS 上挂起,1080+ 张表的创建极度缓慢 - db:schema:load 报
out of shared memory— 分区表创建时锁耗尽共享内存 - Webservice 和 Sidekiq Pod 反复被 K8s 探针杀死 — Rails 预加载耗时 3-5 分钟,超过默认 liveness/readiness 超时
- 节点 192.168.122.31 处于 NotReady 状态 — 集群调度能力受限
- 大量 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 postgres2.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'
# 返回 2002.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"
# 节点最终自行恢复,无需手动干预三、根因分析
| # | 根因 | 影响 |
|---|---|---|
| 1 | NFS I/O 性能不足 | PostgreSQL db:schema:load 无法完成,GitLab 无法初始化 |
| 2 | max_locks_per_transaction 过低 (64) | 分区表锁耗尽,schema load 失败 |
| 3 | 缺少 startupProbe | K8s liveness/readiness 在 Rails 预加载期间误杀 Pod |
| 4 | Sidekiq 探针端口/路径错误 | 3808 不存在,/readiness 返回 404,Pod 永远不 Ready |
| 5 | stale 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
EOF4.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
EOF4.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=120s4.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: 105.3 Webservice 探针配置
yaml
startupProbe:
httpGet:
path: /-/readiness
port: 8080
initialDelaySeconds: 30
failureThreshold: 60 # 600秒 (60 * 10s) 超时
periodSeconds: 10
timeoutSeconds: 55.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+ 分钟。如果没有 startupProbe,livenessProbe 会在应用启动期间误杀 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 在本地存储上的长期稳定性