一、故障背景
本文记录一次 GitLab 15.2.0 在 Ubuntu 18.04 服务器上的故障排查及恢复过程。
此次故障的主要现象是:GitLab 服务能够启动,但访问项目页面时出现 HTTP 500 Internal Server Error。排查发现,问题涉及 PostgreSQL 数据库表记录缺失、表结构不完整以及系统 NVMe 硬盘 I/O 错误。
最终通过 PostgreSQL 逻辑备份抢救、重建项目统计信息、迁移数据库到另一块硬盘,并重新配置 GitLab Omnibus,使数据库恢复到可连接、可查询的状态,GitLab Rails 也能正常读取全部项目统计记录。
需要说明:本文记录的服务器恢复到了服务正常启动、Rails 项目统计查询成功的阶段。实际项目网页和 Git Clone/Push 的最终验证,应在对应环境中另外执行。
1.1 服务器环境
| 项目 | 配置 |
|---|---|
| 操作系统 | Ubuntu 18.04.6 LTS |
| GitLab 版本 | 15.2.0 Omnibus |
| PostgreSQL 版本 | 13.6 |
| 系统硬盘 | 512 GB NVMe SSD |
| 数据硬盘 | 2 TB HDD |
| GitLab 地址 | http://192.168.10.93:8081(示例内网地址) |
| PostgreSQL 原目录 | /var/opt/gitlab/postgresql/data |
| Git 仓库目录 | /disk/data/gitlab/git-data/repositories |
故障前,系统硬盘存放 Ubuntu 和 PostgreSQL,Git 仓库存放在单独的机械硬盘中。
二、故障现象:GitLab 服务正常,但项目访问报 500
首先执行:
sudo gitlab-ctl status
发现 PostgreSQL、Puma、Sidekiq、Gitaly、Redis、Nginx 等服务均显示运行。
然而,访问 GitLab 项目首页仍会出现:
500 Internal Server Error
这表明 GitLab 进程启动成功,不代表 Rails 应用能够正确读取数据库。
2.1 查看 Rails 日志
可以使用以下命令检查错误:
sudo gitlab-ctl tail puma
或者:
sudo tail -n 100 /var/log/gitlab/gitlab-rails/production.log
此次故障中,Rails 日志出现类似错误:
ActionView::Template::Error
undefined method 'commit_count' for nil:NilClass
app/presenters/project_presenter.rb:194
这是非常关键的线索。
commit_count 是 GitLab 项目统计数据中的字段。报错说明 Rails 在生成项目页面时,原本期望获得一个统计对象,但实际拿到了 nil。
因此需要重点检查数据库中的 project_statistics 表。
三、检查 PostgreSQL 数据库
3.1 连接 GitLab 数据库
GitLab Omnibus 自带 PostgreSQL 客户端,可以执行:
sudo gitlab-psql -d gitlabhq_production
也可以使用完整路径:
sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql \
-h /var/opt/gitlab/postgresql \
-d gitlabhq_production
3.2 检查核心数据表
执行:
SELECT COUNT(*) FROM projects;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM project_statistics;
SELECT COUNT(*) FROM namespace_statistics;
本次检查结果:
| 数据表 | 记录数 |
|---|---|
| projects | 6 |
| users | 14 |
| project_statistics | 0 |
| namespace_statistics | 0 |
发现问题:数据库中有 6 个 GitLab 项目,但项目统计表竟然没有任何记录。
正常情况下,每个项目应当具有对应的 project_statistics 记录,否则部分 GitLab 页面会因统计对象不存在而报错。
3.3 检查表结构
执行:
\d public.project_statistics
发现当前 project_statistics 表缺少 namespace_id、wiki_size、packages_size、uploads_size 等字段,同时缺少多个索引和外键。
这与 GitLab 15.2.0 安装包中提供的数据库参考结构不一致。
可通过以下文件对照:
/opt/gitlab/embedded/service/gitlab-rails/db/structure.sql
这里要注意:不能因为某个字段不存在,就盲目添加。必须根据实际 GitLab 版本核对字段名称、类型、默认值、索引以及约束。
四、发现更严重的问题:NVMe 硬盘出现 I/O 错误
最初以为只是 GitLab 数据库逻辑问题,但进一步检查系统日志后,发现 NVMe 有严重的读取错误。
执行:
sudo dmesg -T | grep -Ei \
'critical medium error|I/O error|EXT4-fs error'
日志中出现:
blk_update_request: critical medium error,
dev nvme0n1, sector 267299976
blk_update_request: critical medium error,
dev nvme0n1, sector 878282560
并且在后续恢复期间仍然出现新的读取错误。
这说明系统硬盘存在持续的 I/O 风险。
即便运行:
sudo smartctl -H /dev/nvme0n1
得到 SMART PASSED,也不能忽略内核实际报告的介质读取失败。
注意:SMART PASSED 不等于磁盘完全健康。
因此,本次处理不能简单地在原 PostgreSQL 数据目录里修复数据,还需要将数据库迁移到可靠的存储设备。
五、PostgreSQL 逻辑备份失败:定位损坏数据表
首先尝试使用 pg_dump 备份数据库。
sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump \
-h /var/opt/gitlab/postgresql \
-U gitlab-psql \
-d gitlabhq_production \
-Fc \
-f /disk/gitlab_recovery_check/gitlab_rescue.dump
但出现:
ERROR: could not read block 13 in file
"base/16386/22610": Input/output error
进一步排查确认,该错误与 raw_usage_data 表关联的 TOAST 索引有关。
5.1 什么是 PostgreSQL TOAST?
PostgreSQL 使用 TOAST 机制存储较大的字段内容。
当某些字段超过普通数据页适合存储的大小时,PostgreSQL 可能将其拆分并存放在独立的 TOAST 关系中。
因此,即使普通表还能查询,其关联的 TOAST 数据或索引仍可能出现读写问题。
5.2 抢救可读取的数据
为了优先保护 GitLab 核心数据,本次暂时跳过损坏表的内容:
sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump \
-h /var/opt/gitlab/postgresql \
-U gitlab-psql \
-d gitlabhq_production \
-Fc \
--exclude-table-data=public.raw_usage_data \
--file=/disk/gitlab_recovery_check/gitlab_rescue.dump
此命令成功生成约 7 MB 的自定义格式数据库备份。
重要说明:这不是完整无损备份。
--exclude-table-data 会跳过指定表的数据,所以被排除的历史数据不会出现在恢复后的数据库中。使用此方法应当明确数据损失范围,而不能将其作为常规备份方案。
备份生成后,应使用 pg_restore --list 检查,并尽可能恢复到独立测试数据库中验证。
六、备份 Git 仓库和 GitLab 配置
GitLab 数据不只有 PostgreSQL 数据库,还包括 Git 仓库、附件、LFS 文件、CI/CD 数据以及配置密钥。
本次在机械硬盘创建救援目录:
sudo mkdir -p /disk/gitlab_safe_backup
sudo chmod 700 /disk/gitlab_safe_backup
6.1 备份配置
sudo tar -czpf \
/disk/gitlab_safe_backup/gitlab_config.tar.gz \
-C /etc gitlab
其中应重点保留:
/etc/gitlab/gitlab.rb
/etc/gitlab/gitlab-secrets.json
6.2 备份 Git 仓库
sudo tar -cpf \
/disk/gitlab_safe_backup/repositories.tar \
-C /disk/data/gitlab/git-data \
repositories
本次 Git 仓库归档约 13 GB。
实际生产环境应避免备份期间发生仓库写入,或者采用可保证一致性的备份方案。
6.3 验证归档
sudo tar -tf \
/disk/gitlab_safe_backup/repositories.tar \
> /dev/null
echo $?
返回 0 说明 tar 归档可读,但不代表所有 Git 仓库对象一定完整。
注意:本案例中的 /disk 同时存放原 Git 仓库和救援副本,因此仅属于应急救援。长期备份必须放到其他物理设备或异机。
七、将 PostgreSQL 迁移到机械硬盘
考虑到 NVMe 持续出现读取错误,决定将 PostgreSQL 从系统 SSD 迁移至机械硬盘 /dev/sda2。
7.1 检查目标硬盘
findmnt /disk
df -h /disk
sudo smartctl -H /dev/sda
本次结果:
/dev/sda2 ext4
Available: 1.7 TB
SMART: PASSED
随后创建 PostgreSQL 新目录:
sudo mkdir -p /disk/data/gitlab-postgresql
sudo chown gitlab-psql:gitlab-psql \
/disk/data/gitlab-postgresql
sudo chmod 700 /disk/data/gitlab-postgresql
7.2 初始化独立 PostgreSQL 13.6
原数据库使用:
Encoding: UTF8
Collation: zh_CN.UTF-8
因此,新数据库保持一致:
sudo -u gitlab-psql /opt/gitlab/embedded/bin/initdb \
-D /disk/data/gitlab-postgresql/data \
-U gitlab-psql \
-E UTF8 \
--locale=zh_CN.UTF-8 \
--auth-local=peer \
--auth-host=scram-sha-256
这里创建的是一个全新的数据库集群,而不是复制损坏的旧数据文件。
7.3 临时使用独立端口
为了避免与生产 PostgreSQL 的 5432 端口冲突,新实例临时使用 55432。
在新实例的 postgresql.conf 中设置:
port = 55432
listen_addresses = ''
unix_socket_directories = '/disk/data/gitlab-postgresql/socket'
随后启动:
sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_ctl \
-D /disk/data/gitlab-postgresql/data \
-l /disk/data/gitlab-postgresql/startup.log \
-w start
验证新实例:
sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql \
-h /disk/data/gitlab-postgresql/socket \
-p 55432 \
-d postgres \
-c "SHOW data_directory;"
成功显示:
/disk/data/gitlab-postgresql/data
至此,旧生产数据库和新恢复数据库可以暂时并行存在,互不使用同一数据目录。
八、恢复 GitLab 数据库
8.1 创建应用角色
新 PostgreSQL 中创建 gitlab 角色:
sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql \
-h /disk/data/gitlab-postgresql/socket \
-p 55432 \
-d postgres \
-c "CREATE ROLE gitlab LOGIN;"
8.2 创建目标数据库
sudo -u gitlab-psql /opt/gitlab/embedded/bin/createdb \
-h /disk/data/gitlab-postgresql/socket \
-p 55432 \
-O gitlab \
-T template0 \
gitlabhq_production
8.3 恢复救援数据库
sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_restore \
-h /disk/data/gitlab-postgresql/socket \
-p 55432 \
-U gitlab-psql \
-d gitlabhq_production \
--no-owner \
--no-acl \
--role=gitlab \
--exit-on-error \
/disk/data/gitlab-postgresql/gitlab_rescue.dump
需要注意,示例要求备份文件已复制到 PostgreSQL 系统用户可读取的目录。
本次恢复后检查结果为:
| 表 | 数据量 |
|---|---|
| projects | 6 |
| users | 14 |
| project_statistics | 0 |
| namespace_statistics | 0 |
这与原救援备份中的数据相符。
九、修复 project_statistics 和 namespace_statistics
这是解决本次 GitLab 500 错误的核心数据库修复步骤。
由于原数据库中统计表为空,且 project_statistics 缺少 GitLab 15.2 需要的部分字段,必须先补齐结构再恢复记录。
以下 SQL 是本次环境采用的修复思路示例,不是所有 GitLab 版本都适用的通用迁移脚本。
9.1 补齐缺失字段
ALTER TABLE public.project_statistics
ADD COLUMN IF NOT EXISTS namespace_id integer,
ADD COLUMN IF NOT EXISTS shared_runners_seconds bigint NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS shared_runners_seconds_last_reset timestamp without time zone,
ADD COLUMN IF NOT EXISTS packages_size bigint NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS wiki_size bigint,
ADD COLUMN IF NOT EXISTS uploads_size bigint NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS container_registry_size bigint NOT NULL DEFAULT 0;
9.2 恢复项目统计记录
从 projects 表读取真实项目 ID 和 Namespace ID:
INSERT INTO public.project_statistics (
project_id,
namespace_id,
commit_count,
repository_size,
storage_size,
lfs_objects_size,
build_artifacts_size,
pipeline_artifacts_size,
created_at,
updated_at
)
SELECT
p.id,
p.namespace_id,
0, 0, 0, 0, 0, 0,
NOW(),
NOW()
FROM projects p
WHERE NOT EXISTS (
SELECT 1
FROM project_statistics ps
WHERE ps.project_id = p.id
);
这会为缺少统计记录的项目创建对象。
这里把统计数初始化为零,只是修复数据结构与对象关联。真实的提交数量、仓库大小并未因此恢复,还需要后续从 Git 仓库重新计算。
9.3 恢复 Namespace 统计
INSERT INTO public.namespace_statistics (namespace_id)
SELECT n.id
FROM namespaces n
WHERE NOT EXISTS (
SELECT 1
FROM namespace_statistics ns
WHERE ns.namespace_id = n.id
);
本次修复结果:
projects 6
project_statistics 6
namespaces 22
namespace_statistics 22
9.4 补齐关键索引和约束
例如:
CREATE UNIQUE INDEX IF NOT EXISTS
index_project_statistics_on_project_id
ON project_statistics(project_id);
CREATE INDEX IF NOT EXISTS
index_project_statistics_on_namespace_id
ON project_statistics(namespace_id);
同时恢复缺失的非空约束及外键:
ALTER TABLE project_statistics
ALTER COLUMN namespace_id SET NOT NULL,
ALTER COLUMN commit_count SET NOT NULL,
ALTER COLUMN repository_size SET NOT NULL;
ALTER TABLE project_statistics
ADD CONSTRAINT fk_rails_12c471002f
FOREIGN KEY (project_id)
REFERENCES projects(id)
ON DELETE CASCADE;
本次还恢复了 repository_size、storage_size、wiki_size、packages_size 等相关索引。
建议所有数据库修复操作在事务内执行,并先在测试库验证。 生产环境不能直接复制上述 SQL 而不核对当前表结构,特别要避免重复外键、字段类型冲突和统计记录重复。
十、解决 PostgreSQL Peer 认证失败
数据库恢复之后,还有一个问题:GitLab 应用用户无法直接连接新 PostgreSQL。
旧实例使用:
local all all peer map=gitlab
并在 pg_ident.conf 中配置:
gitlab git gitlab
gitlab mattermost gitlab_mattermost
gitlab /^(.*)$ \1
而新实例默认使用普通:
local all all peer
这两者不同。
GitLab Rails 通常以 Linux 用户 git 运行,但访问数据库时使用角色 gitlab。因此,需要正确设置 Peer 用户映射。
新数据库认证配置应与实际 GitLab 服务用户对应,不能为了方便直接改成 trust。
10.1 目录权限问题
本次还有一个容易忽略的问题:
/disk/data/gitlab-postgresql
最初权限为 700,仅允许 gitlab-psql 访问。
虽然 PostgreSQL Socket 文件为可访问状态,但 Linux 用户 git 无法穿越其上级目录。
最终通过 ACL 给 git 用户授予必要的目录权限:
sudo setfacl -m u:git:--x \
/disk/data/gitlab-postgresql
sudo setfacl -m u:git:r-x \
/disk/data/gitlab-postgresql/socket
这样既能保证连接,又不需要开放 PostgreSQL 数据目录。
验证:
sudo -u git /opt/gitlab/embedded/bin/psql \
-h /disk/data/gitlab-postgresql/socket \
-p 55432 \
-U gitlab \
-d gitlabhq_production \
-c "SELECT COUNT(*) FROM projects;"
返回:
6
说明 GitLab 应用用户可以正常连接新数据库。
十一、正式切换 GitLab Omnibus
当新数据库恢复成功,并且已经通过独立连接验证后,可以进入停机切换。
11.1 停止 GitLab
sudo gitlab-ctl stop
确认 PostgreSQL、Rails、Sidekiq、Gitaly 等主服务均已停止。
同时关闭测试 PostgreSQL:
sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_ctl \
-D /disk/data/gitlab-postgresql/data \
-m fast \
-w stop
不要让两个 PostgreSQL 进程同时使用一个数据目录。
11.2 修改 GitLab 配置
编辑:
/etc/gitlab/gitlab.rb
本次最终使用的关键配置为:
postgresql['dir'] = '/disk/data/gitlab-postgresql'
postgresql['unix_socket_directory'] = '/var/opt/gitlab/postgresql'
postgresql['home'] = '/var/opt/gitlab/postgresql'
postgresql['port'] = 5432
gitlab_rails['db_host'] = '/var/opt/gitlab/postgresql'
这里有一个非常关键的细节:
修改 postgresql['unix_socket_directory']****,并不意味着 Rails 的数据库 Host 会自动保持一致。
本次第一次执行 gitlab-ctl reconfigure 时,Rails 数据库迁移阶段出现:
ActiveRecord::ConnectionNotEstablished
could not connect to server:
No such file or directory
Unix domain socket:
"/disk/data/gitlab-postgresql/.s.PGSQL.5432"
而实际希望 PostgreSQL 使用的 Socket 是:
/var/opt/gitlab/postgresql
因此需要显式设置:
gitlab_rails['db_host'] = '/var/opt/gitlab/postgresql'
11.3 遇到启动脚本已更新但进程仍指向旧目录
本次还有一个特殊情况。
检查 Omnibus 启动脚本:
sudo cat /opt/gitlab/sv/postgresql/run
显示:
postgres -D /disk/data/gitlab-postgresql/data
但是检查实际进程:
sudo ps -ef | grep '[p]ostgres -D'
却显示:
postgres -D /var/opt/gitlab/postgresql/data
说明服务启动脚本虽然更新了,但旧 PostgreSQL 进程没有真正重启。
解决方法是有序停止服务:
sudo gitlab-ctl stop postgresql
确认旧进程已经退出,然后:
sudo gitlab-ctl start postgresql
再执行:
sudo -u gitlab-psql /opt/gitlab/embedded/bin/psql \
-h /var/opt/gitlab/postgresql \
-p 5432 \
-d gitlabhq_production \
-c "SHOW data_directory;"
最终成功得到:
/disk/data/gitlab-postgresql/data
这才证明 PostgreSQL 真正切换到了机械硬盘,而不是仅仅修改了配置文件。
11.4 重新配置 GitLab
执行:
sudo gitlab-ctl reconfigure
本次最终返回:
gitlab Reconfigured!
退出码为 0。
随后启动服务:
sudo gitlab-ctl start
sudo gitlab-ctl status
PostgreSQL、Redis、Gitaly、Puma、Sidekiq、GitLab Workhorse、Nginx 等组件全部显示 run。
十二、验证 GitLab Rails 恢复情况
执行:
sudo gitlab-rails runner '
puts "Projects: #{Project.count}"
puts "Statistics: #{ProjectStatistics.count}"
Project.order(:id).each do |p|
puts "#{p.id} #{p.name}: #{p.statistics&.commit_count.inspect}"
end
'
最终得到:
Projects: 6
Statistics: 6
1 Monitoring: 0
2 test1: 0
8 BUS_RPOJECTS: 0
9 Hardware_Project_data: 0
11 file_backup: 0
15 IPC Test: 0
可以确认:
-
GitLab Rails 能成功连接恢复后的 PostgreSQL。
-
六个项目均存在。
-
六个项目均具备可读取的统计记录。
-
原先
ProjectStatistics对象缺失的问题已得到修复。
但此时统计值为零,后续仍需重新计算实际数据。
12.1 还需要哪些最终测试?
建议进一步检查:
-
GitLab 登录是否成功。
-
六个项目首页是否仍报 HTTP 500。
-
项目文件列表是否显示正常。
-
Git Commit 历史是否完整。
-
Git Clone 和 Push 是否正常。
-
附件、LFS 和 CI/CD 数据是否可用。
上述测试应在恢复后实际执行,不能只凭 gitlab-ctl status 显示 run 就判断系统完全正常。
十三、这次故障的核心经验
13.1 GitLab 500 不一定是 Web 服务故障
GitLab 使用 Rails、PostgreSQL、Gitaly 等多个组件。
即使 Nginx、Puma、Redis 都启动成功,项目统计数据缺失仍然可能导致 HTTP 500。
因此,应该优先根据 Rails 异常定位数据关联问题,而不是反复重启服务。
13.2 数据库结构和 GitLab 版本必须一致
当数据库表结构与当前 GitLab 安装版本不匹配时,程序可能缺少需要的列、索引或约束。
手工修复只适用于确认问题范围、具备可靠备份、并且能够在测试环境验证的情况。
13.3 硬盘 I/O 错误必须优先处理
本次 NVMe 多次出现:
critical medium error
这属于实际的存储风险。
即使暂时修复数据库,如果仍然运行在发生介质读取错误的磁盘上,故障也可能再次出现。
13.4 数据目录改变不等于 PostgreSQL 进程已经切换
本次最容易误判的地方是:
配置文件指向新目录
实际进程仍使用旧目录
正确的验证方式是:
SHOW data_directory;
而不是仅查看 gitlab.rb 或 PostgreSQL 服务状态。
13.5 GitLab 备份必须验证数据库内容
本次曾检查到一份 GitLab 备份归档,其内部数据库 SQL 压缩文件只有约 20 字节,并不包含有效数据库内容。
因此,备份完成不等于备份有效。
建议至少检查备份日志、数据库归档内容,并定期在隔离环境进行恢复演练。
十四、后续如何提高 GitLab 服务器可靠性?
建议采用以下策略:
| 措施 | 建议 |
|---|---|
| 系统硬盘 | 更换出现 I/O 错误的 NVMe |
| 数据磁盘 | 使用健康 SSD 或有冗余能力的存储 |
| 数据库备份 | 每日创建可恢复的 PostgreSQL/GitLab 备份 |
| Git 仓库备份 | 与数据库一起进行一致性备份 |
| 配置和密钥 | 独立备份 /etc/gitlab |
| 本地副本 | 保存到不同物理磁盘 |
| 异机副本 | NAS、另一台服务器或云存储 |
| 恢复验证 | 定期执行完整恢复测试 |
特别提醒:RAID1 只能解决部分磁盘故障带来的可用性问题,不能替代备份。误删除、误操作和数据库逻辑损坏同样可能同步到镜像盘。
十五、总结
本次 GitLab 500 故障,并不是单纯的 Nginx、Puma 或 Rails 服务未启动,而是数据库层面的异常。
排查与恢复主要经历了以下过程:
GitLab 500 → Rails 日志定位 commit_count****空对象 → 发现 project_statistics****缺失记录与字段 → 发现 NVMe I/O 错误 → 抢救 PostgreSQL 逻辑备份 → 在机械硬盘重建 PostgreSQL 13.6 → 恢复项目统计表 → 修复 Peer 认证和 Socket 权限 → 修改 Omnibus 数据目录 → 修复进程路径及 Rails Socket 配置 → GitLab 服务启动、Rails 统计查询成功。
整个过程最重要的原则是:
先保护数据,再定位错误;先验证新数据库,再切换服务;最后通过实际业务功能验证恢复效果。
本文采用的命令和 SQL 基于 GitLab Omnibus 15.2.0、PostgreSQL 13.6 的特定环境,涉及数据库结构变更和服务停机,不建议在其他版本的生产服务器上未经验证直接执行。
关键词: GitLab 500、GitLab Omnibus、PostgreSQL、project_statistics、commit_count、GitLab 数据恢复、PostgreSQL 数据迁移、NVMe I/O Error、GitLab 故障排查