ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GitLab跳版本升级后通用设置保存报500:迁移与缓存排查实战

GitLab跳版本升级后通用设置保存报500:迁移与缓存排查实战

GitLab 跨大版本升级后,点开后台“通用设置”随便改一个字段再点保存,结果直接甩一个 500 页面出来,这种事在运维圈真不算少见。尤其你这边的路径是 12.3.9 直接跳到 14.4.5,中间隔了 12、13、14 三个大版本线,风险点不是一两个。这篇文章我会从问题定性开始,把排查思路、修复步骤和避坑经验一次讲清楚,给正在维护 GitLab 实例的运维和 DevOps 同学做参考。

先聊一个关键认知:你遇到的“修改通用设置报错”,大概率不是页面本身坏了,而是你的 GitLab 数据库结构和代码版本已经对不上了。先接受这个前提,后面很多事情就顺了。

1. 为什么 12.3.9 跳到 14.4.5 会踩“改设置”这颗雷

1.1 十几个小版本的跳跃,落下的全是迁移债

12.3.9 是 2020 年初的版本,14.4.5 是 2021 年底的版本,这段跨度里 GitLab 内部发生了非常多结构性变化,其中最要命的就是数据库迁移体系的演进。

GitLab 官方一直强调升级要“逐大版本走”,先升到当前大版本的最新补丁版,再往上切下一个大版本。为什么这么要求?不是因为官方强迫症,而是因为每个大版本都会引入一批数据库迁移脚本,有些迁移必须在上一版本的状态上逐步执行,跳过任何一个环节,后续版本的代码一旦读取旧表结构,就会产生不一致。

12.x 的时候,后台任务的迁移机制还比较传统,从 13.x 开始,batched background migration 成为主流,很多数据清洗是异步在后台跑的。你从 12.3.9 直接升到 14.4.5,相当于数据库还停留在 12.x 的思维模式,代码却已经是 14.x 的逻辑,两边对不上是非常正常的。

1.2 “通用设置”不是纯前端,它直接操作核心配置表

很多人觉得后台的“通用设置”就是改个页面上的输入框,提交了就完事。实际上,GitLab 的通用设置页面对应的是application_settings表,前端表单提交之后,Rails 后端会对这张表做一次完整的读写更新。

这张表在 12.x 和 14.x 之间的变化非常大,包括字段的拆分、约束条件的增加、字段改名、外键关联调整等。当你的数据库 schema 还是 12.x 时代的旧结构,而 Rails 代码已经按 14.4.5 的新结构去写 SQL 时,结果就是:

  • 字段不存在,直接报ActiveRecord::StatementInvalid
  • 非空约束被触发,因为新版本要求某些字段必须有值
  • 外键关联失败,因为关联表还没有补齐
  • 缓存里残留旧配置,写入时校验不通过

理解了这一层,你的排查思路就应该从“设置页面为什么坏”转向“数据库迁移为什么没到位”。

2. 升级后先别急着改设置,花 10 分钟做环境体检

接到这个问题,我一般不会直接去翻配置页面的代码,而是先对实例整体做一次体检,确认三件事:服务状态、数据库迁移状态、后台任务队列状态。

2.1 第一件事:确认 GitLab 整体服务状态

先跑一遍最基础的检查,把服务状态摸清楚:

gitlab-ctl status

重点关注postgresql、redis、sidekiq、puma这几个核心服务,状态都必须是run。如果某个服务是down或者反复重启,那问题性质就完全不一样了,先解决服务存活,再谈设置保存。

服务看起来都正常之后,再跑一次官方自检:

gitlab-rake gitlab:check

这个命令会检查 GitLab 自身的配置一致性、数据库连接、Redis 连接等。如果输出里有gitlab-shell或者database相关的FAIL,说明环境本身就有问题,后面排查方向就要调整。

2.2 第二件事:检查数据库迁移到底跑完没有

这是排查“修改设置报错”最关键的环节。GitLab 的所有版本升级都会带一批数据库迁移脚本,正常情况下升级程序会帮你执行,但如果升级过程被中断、超时、或者手动跳过了某些步骤,就会留下未执行的迁移。

用这个命令看迁移状态:

gitlab-rails db:migrate:status | grep 'down'

如果没有任何输出,说明迁移记录层面没有落后项。但注意,这里没有down不代表万事大吉,还有一类更隐蔽的问题是:迁移已经标记为执行过,但里面的数据清洗任务并没有真正完成,这种情况要看第 2.3 节。

如果确实有down的记录,那就是比较明确的问题了,说明当前数据库 schema 确实落后于代码版本,后面修复部分会讲怎么处理。

2.3 第三件事:看 Sidekiq 队列里有没有压着老任务

跨大版本升级最容易忽略的是异步任务。GitLab 很多数据清洗、后台迁移是通过 Sidekiq 异步执行的,升级之后如果 Sidekiq 队列积压了大量老任务,并且这些任务还在尝试操作旧数据结构,就会产生各种诡异报错。

用 Rails Runner 看一下队列情况:

gitlab-rails runner "puts Sidekiq::Queue.all.map { |q| [q.name, q.size] }"

重点关注database_batch_migration、background_migration这类队列,如果积压数量明显异常,而且队列里的任务长期执行失败,那“改设置报错”可能只是其中一个表象。

到这里,体检基本做完了。如果发现迁移有缺失或者队列有阻塞,不要急着去后台测试保存设置,先把状态修干净。

3. 从日志到代码,一点一点定位“通用设置报错”的根因

环境体检做完之后,如果服务正常、迁移记录没有明显 down、队列也没有严重积压,那问题就藏在更深的细节里,这时候需要走日志和代码路径。

3.1 先看 Rails 生产日志里的请求链路

修改通用设置保存失败,第一现场就是 GitLab Rails 的日志。在大多数安装方式下,日志路径是:

/var/log/gitlab/gitlab-rails/production.log

用时间点去过滤请求,比如你刚才在后台点保存的时间段:

grep "2023-08-01T14:30" /var/log/gitlab/gitlab-rails/production.log

正常请求会看到类似这样的记录:

Started PATCH "/admin/application_settings/general" Processing by Admin::ApplicationSettingsController#update Completed 200 OK

如果保存失败,你会看到的可能是:

Completed 500 Internal Server Error ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column application_settings.new_field does not exist

当你看到PG::UndefinedColumn、PG::NotNullViolation、PG::ForeignKeyViolation这类关键词时,问题就很清晰了——数据库表结构跟代码预期不一致。

如果看到的是NoMethodError,比如:

NoMethodError: undefined method `some_feature?` for an instance of ApplicationSetting

这就说明代码里访问了应用中不存在的属性,大概率也是因为迁移没跟上。

3.2 常见根因:表结构落后、缓存脏数据、参数被移除

不同类型的报错对应不同的根因,我梳理了三种高频场景:

一是表结构落后。GitLab 13.x 之后很多新版本都会往application_settings表加列,如果你库里的表没加这个列,代码却往这个列写值,就报UndefinedColumn。这种情况在“升级跳过中间大版本”后尤其常见。

二是缓存脏数据。GitLab 使用 Redis 做配置缓存,application_settings在写入前会先查缓存,如果缓存里残留了旧版本的数据结构,新代码在读出来做校验时就会出问题。这时候即使数据库是好的,保存动作也可能失败。

三是配置参数被移除或改结构。14.x 对很多通用的设置项做了规范化,有些设置从一个 JSON 字段拆成了独立列,有些设置被彻底移除。旧配置数据里如果还带着已经废弃的键,写入时同样会触发验证失败。

3.3 用 Rails Console 直接验证写入路径

日志定位到具体异常之后,我习惯用 Rails Console 直接做一次写入验证,把问题确认死,避免在页面上反复猜测。

gitlab-rails console

进入控制台后,先读取当前的设置记录:

s = ApplicationSetting.current s.singleton_methods s.attributes.keys

看一眼attributes.keys里是否有代码期望的字段。如果字段缺失,后面直接尝试修改一个已知字段:

s.site_title = "Test Title Change" s.save!

如果save!抛异常,异常信息会比页面上那行 500 更具体。比如:

ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column application_settings.new_field does not exist

到这一步,问题基本定性了。接下来就是修复环节。

4. 实操修复:把迁移补齐、缓存清掉、配置正常保存

修复不是简单改一行配置,而是把整个环境修回一致状态。我的思路分三步:补迁移、清缓存、回归验证。

4.1 第一步:补齐数据库迁移

如果db:migrate:status里有down的记录,直接补齐:

gitlab-rake db:migrate

这个命令会把所有未执行的迁移脚本跑一遍。迁移过程中如果报错,千万不要忽略,常见情况是某张表里已经有大量数据,迁移脚本要对这些数据做清洗,导致执行时间很长甚至超时。

迁移跑完之后,再确认一次状态:

gitlab-rails db:migrate:status | grep 'down'

没有down输出,说明迁移记录层已经干净了。

但如果db:migrate:status里没有down,而application_settings表确实缺列,那就要检查数据库 schema 本身是不是被手动改过,或者有迁移记录被异常标记为已执行。这种情况不要自己去手工ALTER TABLE加列,正确做法是找到对应的迁移文件,确认为什么没有实际执行,再决定修复方案。

进入 14.4.5 之后的版本,GitLab 还要求检查 batched background migrations 的状态:

gitlab-rails runner "puts Gitlab::Database::BackgroundMigration.batched_migrations.map { |m| [m.job_class_name, m.status] }"

如果是active状态,说明这个后台迁移还没结束。此时去改设置是有风险的,因为数据清洗还在进行,表结构会继续变化。等待它跑完,或者用gitlab-rake db:migrate:up针对特定迁移手动触发,都是可选路径。

4.2 第二步:清理 Redis 配置缓存

确认数据库迁移没问题之后,还有一个很容易让“改设置报错”不死不活的元凶——Redis 缓存。

GitLab 会把application_settings的对象缓存在 Redis,升级过程中如果缓存对象的序列化结构跟新版本不一致,读出来就是一份“带有旧字段的老对象”,保存时就会触发各种校验失败。

清理缓存的方式并不复杂,但要注意操作范围,不要在业务高峰期直接FLUSHALL把整实例的缓存清空,那会让所有请求都打到数据库。推荐只清配置相关的缓存:

gitlab-rails runner "Rails.cache.delete('application_settings:current')"

如果上面的 key 结构记不准,也可以直接让配置重新加载:

ApplicationSetting.expire_cache ApplicationSetting.current

这两行的作用就是先让旧缓存失效,再强制重新从数据库读一次配置并写回新缓存。操作之后,再到控制台里做一次写入测试。

4.3 第三步:在控制台完成写入回归验证

重新进入 Rails Console,做一次完整的写入-读取验证:

s = ApplicationSetting.current s.site_title = "Regression Test" s.save! s.reload puts s.site_title

如果这一步能正常保存并读回来,说明写入路径已经通了。这时候再回到后台页面,把站点名称等通用设置改一改,点保存,应该就能正常返回成功提示。

注意,这里做了写入回归验证之后,记得把测试值改回正式值,不要留着“Regression Test”作为站点标题。

4.4 还有一种容易忽略的情况:配置文件里的废弃参数

除了数据库和缓存,还有一类问题会挡住“修改通用设置”的保存请求——旧版本遗留的gitlab.rb配置项。

14.x 对很多gitlab.rb里的参数做了移出或重命名处理,比如某些跟监控、导入导出相关的配置项。旧配置里如果带着这些已经不被识别的参数,GitLab 在加载配置时报 warning 甚至 error,后台某些依赖配置状态的写入操作也会被卡住。

排查方式是跑一次配置检查:

gitlab-ctl reconfigure

如果 reconfigure 过程中输出里有Deprecated或者Obsolete字样,说明gitlab.rb里有应该清理的项。把废弃项注释掉或者按新格式改写,重新 reconfigure,再试保存设置。

这一步经常被忽略,但它确实是“升级后改设置报错”的常见来源之一。

5. 升级避坑经验与问题速查

5.1 这次维护之后,我总结的升级避坑清单

先说结论:跨大版本升级,慢就是快。

我见过太多人觉得“直接跑到最新版本省事”,结果就是在生产环境花一整个晚上排查迁移问题。如果你还没升级,或者已经在坑里想爬出来,以下几条经验值得记住:

第一,升级路径不要跳。比如从 12.3.9 到 14.4.5,官方要求的理想路径是先升到 12.3 的最新补丁,再升 13.x 的最新版,最后到 14.4.5。如果条件不允许,至少要确保目标版本可以支持从源版本直接升级,并且提前读官方升级文档。

第二,升级前备份要按大版本分别备份。不要只留一份“升级前备份”,升级失败想回滚时,那份备份可能已经是中间状态。

第三,升级后立刻检查迁移状态和后台队列,不要等用户反馈。很多问题在升级完成当天不会爆发,往往是在第一次写库操作时才冒出来。

第四,配置文件里的废弃项要主动清理。每次升级公告里都会列出 deprecated 配置,花十分钟提前清理,能避免后面一大堆“奇奇怪怪”的报错。

5.2 常见报错与对应排查方向速查表

报错表现可能根因优先排查项
保存后 500,日志见PG::UndefinedColumnapplication_settings表缺列,迁移未完成db:migrate:status是否有 down;后台迁移是否 active
保存后 500,日志见PG::NotNullViolation新版本新增了非空约束,旧数据不满足检查新增字段的默认值和旧数据分布
保存后 500,日志见NoMethodError缓存里残留旧对象结构清理application_settings相关缓存
保存时提示参数不可用gitlab.rb里有废弃配置项gitlab-ctl reconfigure查看 Deprecated 输出
升级后 Sidekiq 队列积压大量后台迁移任务升级过程过短,后台数据清洗没跑完检查 batched background migrations 状态
设置页面打开就有报错前端资产或配置接口异常gitlab-rake gitlab:check排除整体健康问题

后台迁移状态检查是整个排查环节里比较容易被忽略的一步。GitLab 的 background migration 机制决定了某些数据清洗可以在业务运行的同时慢慢执行,但这些任务在执行期间会占用数据库资源,也会让表结构处于“中间态”。你的升级如果发生在业务高峰期,后台任务可能会被压到很慢,表面上服务状态正常,内部数据结构却在慢慢变。这种情况下,直接改通用设置很容易撞到正在变化的列。

还有一个细节值得单独说,就是“修改通用设置报错”这个现象本身,不要把它当成单独事件去查。它往往只是整个升级残留问题的冰山一角,其他后台管理操作,比如管理项目、调整 CI/CD 变量、修改用户权限,可能也会跟着报错。你在日志里看到的可能是不同接口的异常,但根子都是同一个——升级后的环境一致性没有恢复。先把迁移和缓存处理干净,再逐个业务功能回归测试,这个顺序不要反过来。

我在几次 GitLab 升级维护中体会最深的一点是:升级失败的现场,几乎都不是因为“版本太新”或者“功能太复杂”,而是因为升级过程里的数据迁移没有老老实实走完。数据库表结构、后台任务、缓存状态、配置文件这四个维度,任何一个没对齐,都会在后面某个看似无关的操作里突然爆出来。你把这四个维度按顺序检查一遍,大多数“改设置报错”之类的问题都能定位到根因。

最后再说一个非常实用的小技巧:升级完成后,不要急着把所有服务恢复到正常流量,先留一个窗口期做一次完整的后台操作巡检,包括修改通用设置、创建项目、跑一次 CI 流水线、处理一个合并请求。这些操作覆盖了 GitLab 最常见的写路径,任何一个环节报错都能在窗口期提前暴露,而不是等用户上线后才发现。这个过程,比我遇到问题再翻日志要省力得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进