
GitLab版本升级这件事做得多了你会形成一种条件反射升级不可怕升级后的第一个午休才可怕。这不上个月我们这套GitLab实例刚从12.3.9一路升到14.4.5整个迁移过程顺利得像教科书结果升级完不到半小时管理员在后台修改通用设置页面上直接弹了个“Something went wrong on our end.”通用设置一点就报错而且谁都没改过数据库吓得我午觉都没睡。这篇文章就把这次故障的完整链路写清楚从现象到日志定位从根因到修复再到顺藤摸瓜发现的一批同类隐患。如果你也正准备做GitLab大版本升级或者升完级发现后台设置点不动、保存500这篇文章应该能帮你省掉一整个下午的排查时间。1. 升级背景为什么从12.3.9到14.4.5需要格外谨慎1.1 跨三个大版本的升级路线GitLab的大版本升级和普通软件不一样不是把新版本包装上去就完事。官方对升级路径有明确要求跨大版本升级时必须先从当前大版本升级到该大版本的最后一个小版本再进入下一个大版本否则数据库的schema迁移可能因为跳级而直接失败轻则升级中断重则数据不一致。我们这套实例的原始版本是12.3.9目标版本是14.4.5中间隔了12、13、14三个大版本。按官方建议的路径实际执行顺序是# 第一步先升到12.x的最后一个版本 # 12.3.9 - 12.10.14 # 第二步跨入13.x大版本并且升到13.x的最后一个版本 # 12.10.14 - 13.12.15 # 第三步进入14.x直接升到14.4.5 # 13.12.15 - 14.4.5这里有个容易忽略的点很多人在12.3.9的时候直接下载14.4.5的安装包执行完gitlab-ctl upgrade就以为完事了。实际上GitLab的升级脚本会在数据库迁移阶段检查当前schema版本跨度过大会直接拒绝执行迁移或者更糟迁移执行到一半报错整实例处于半升级状态。我们当时就是老老实实按三步走每步都做了备份每步升级完都跑一遍基础验证整个过程倒是没出幺蛾子。1.2 升级前的准备动作这次升级前我们做的准备值得拿出来说一下。首先是把生产环境的数据备份恢复到一套staging环境里在staging上完完整整走了一遍升级流程。这一步很重要因为GitLab的升级脚本只负责迁移不负责验证业务数据的兼容性有些故障在升级过程中不报错要等升级后你真正去操作某些功能时才炸出来。备份命令这里也给一下注意不同版本命令可能不太一样12.x时期还能用gitlab-rake gitlab:backup:create13.x之后新版本更建议直接用gitlab-backup# 升级前在每个版本节点都执行一次 sudo gitlab-backup create sudo gitlab-ctl status # 确认各服务状态正常 sudo gitlab-ctl reconfigure sudo gitlab-rake gitlab:check升级完成之后我们按照惯例先验证了登录、工程列表、CI跑一次流水线这些核心链路当时觉得一切正常就去后台改设置。结果恰恰是“改设置”这个动作把藏了很久的存量数据问题给捅出来了。2. 报错现场保存通用设置时页面直接5002.1 操作路径和第一反应故障复现路径非常清晰。管理员账号登录后进入 Admin Area → Settings → General这是站点的通用设置页。当时只是想改一下站点描述顺便检查首页URL字段结果点下 Save changes 按钮之后页面没有出现熟悉的“保存成功”提示而是直接跳到了GitLab经典的错误页。页面提示是Something went wrong on our end.翻译成人话就是你发起的请求在后端抛了一个未处理的异常500了。这个提示本身没有任何排查价值它只是GitLab对生产环境用户屏蔽错误详情的手段。真正有价值的信息在日志里。遇到这类问题我的第一反应是别去猜先去翻日志。GitLab的日志路径比较固定恰好能直接看到本次请求的完整调用链。2.2 从日志里找到真正的异常我们的GitLab使用的是omnibus安装方式日志都在/var/log/gitlab/gitlab-rails/目录下。这次故障对应的请求记录在production_json.log里用tail拉出来的关键信息是这样的{ method: PUT, path: /admin/application_settings/general, format: html, controller: Admin::ApplicationSettingsController, action: update, status: 500, error: ActiveRecord::RecordInvalid: Validation failed: Homepage URL is not a valid URL, duration: 1187, params: { application_setting: { site_description: ... } } }日志里的信息量已经很大了我给几个关键字段做个拆解字段含义这次故障中传递的信息methodHTTP方法PUT说明是更新操作path请求路径/admin/application_settings/general就是通用设置保存接口controller/action控制器与方法Admin::ApplicationSettingsController#update走的是后台设置更新逻辑statusHTTP状态码500服务端异常error异常类型与消息ActiveRecord::RecordInvalid模型校验失败具体是“Homepage URL is not a valid URL”params当前提交的参数这里有个陷阱本次params里只提交了site_description压根没提交home_page_url看到ActiveRecord::RecordInvalid的时候我就知道问题出在数据校验上而不是路由、权限或者别的杂七杂八的地方。但这里有个特别刁钻的细节日志里params显示这次请求只修改了site_description一个描述字段为什么会触发home_page_url的校验错误这就是本次故障最值得记录的点你改的字段和报错的字段完全不是同一个。3. 排查链路从URL校验一路追到库存脏数据3.1 在console里直接触发全量校验要复现这个问题不需要重新去页面上点保存。在服务器上打开Rails控制台直接对ApplicationSetting模型做一次保存操作sudo gitlab-rails consoles ApplicationSetting.current s.save!这就是最干净的复现方式。上面这行代码没有做任何修改就是原封不动地把当前设置记录重新保存一遍。在正常状态它会安静地返回true在这次故障环境下它直接抛出了和页面上一模一样的异常ActiveRecord::RecordInvalid: Validation failed: Homepage URL is not a valid URL这个复现结果说明两件事。第一问题不在页面不在前端也不在你本次修改的内容上而是整条application_settings记录本身带着一个非法值。第二GitLab在保存任何设置时都会对这条记录做一次全字段校验哪怕你只改了一个描述字段它也会把这条记录里的所有字段全部过一遍校验器。3.2 为什么“没改过的字段”会挡住保存这里要顺带解释一个Rails的机制方便和我一样半路出家搞运维的朋友理解。Rails的ActiveRecord模型在更新记录时默认会触发valid?校验而模型上所有的validates声明都会被执行。换句话说save!不仅仅是校验你有变化的字段而是校验整条记录在当前代码规则下是否合法。GitLab升级到14.4.5之后ApplicationSetting模型里针对URL类字段的校验规则比12.3.9时严格得多。新版本的校验器要求URL必须带http://或https://协议头而且host部分必须能正常解析出来。如果你在旧版本里存了一个“看起来能用但不完全合法”的URL升级后新代码一校验这个陈年老数据就会把整个保存操作给拦下来。3.3 从模型源码和数据库把旧值揪出来知道了原理排查就变得很简单。先用Rails的控制台把当前库里home_page_url的值打印出来s ApplicationSetting.current puts s.home_page_url.inspect输出结果如下//intranet.example.com看到这串值的时候我心里就有数了。这是个没有协议头的裸地址浏览器可能有自己的容错机制某些场景下能自动补全协议但GitLab后台的URL校验器不会惯着它。URI.parse解析这种值的时候scheme会被解析成nil直接不满足“只允许http和https”的约束。为了交叉验证我还做了一步直接去模型源码里翻了校验规则。omnibus安装的GitLab源码路径在grep -rn home_page_url /opt/gitlab/embedded/service/gitlab-rails/app/models/相关的validates声明写得明明白白URL类型字段走的是GitLab的URL校验器schemes白名单只有[http, https]。数据库这一侧也用SQL确认了一遍SELECT home_page_url, support_url, terms FROM application_settings;果然不止home_page_url连support_url都存着同样格式的裸地址。这说明当时某位管理员在旧版本上手动填这些字段的时候压根没填协议头而12.3.9时代估计对这类字段没卡那么严于是脏数据就这么被放了进来一直躺到这次升级。4. 修复方案先把非法值改合法再动后台4.1 一次成功的console修复修复本身不复杂把非法值改成合法值就行。我们的场景里这些URL实际指向的是内部办公系统入口补上协议头即可s ApplicationSetting.current s.home_page_url https://intranet.example.com s.support_url https://intranet.example.com/support s.save!保存成功后返回值是true不再抛校验异常。然后回到后台页面重新把站点描述改好点保存成功提示正常出现问题解决。这里有一个建议遇到多个URL类字段都有问题时别一个字段一个字段地改直接在console里逐个检查交给同一个保存事务最后一次性save!避免中间状态反复触发校验失败。4.2 为什么不建议用update_columns强行绕过很多人看到这种问题第一反应是找个办法绕过校验。Rails确实提供了绕过校验的接口比如update_columns它可以直接写数据库不触发任何校验和回调# 不推荐的做法 ApplicationSetting.current.update_columns(home_page_url: https://intranet.example.com)这条命令确实能把这次报错按下去但我不推荐这么做。原因很实际这次的根因不是某个字段写错而是整条记录携带了大量未经验证的旧值。你用update_columns只修正了一个字段其他同样不合格的字段还在那里躺着下次保存设置的时候还会以另一个报错形式跳出来。而且绕过校验意味着绕过了模型里的回调逻辑GitLab在新版本里可能依赖某些字段的值来驱动其他功能比如出站请求限制、登录页定制等。你硬写进去的值如果不符合预期可能在更隐蔽的地方造成状态不一致。正确的做法是像我们一样把所有不合法字段一次性修正让整条记录能通过新规则的校验这才是干净的状态。4.3 修复后的验证和同类字段排查修复完成后我没有直接关掉console而是顺手做了一轮同类字段排查。所有像URL、协议、枚举值这类强约束字段都可能存在类似的旧数据问题。建议在console里把设置记录的所有字段过一遍特别是这些常见字段s ApplicationSetting.current %i[ home_page_url support_url terms help_page_url after_sign_out_path redirect_http_to_https_host ].each do |field| value s.public_send(field) puts #{field}: #{value.inspect} if value.present? end检查完URL字段之后再完整跑一次gitlab-rails gitlab:check确认整体健康。这次检查确实又发现了两处不合法URL一并修掉之后后台设置页保存功能彻底恢复正常。5. 同一个坑循环上演以后升级要注意的几类设置故障5.1 枚举字段的合法值范围变了URL校验问题只是“升级后改设置报错”的其中一个子类。我查了一圈发现GitLab跨大版本升级后通用设置相关的报错里最常见的是枚举字段值不在合法列表内。典型的报错长这样ActiveRecord::RecordInvalid: Validation failed: Default project creation is not included in the list这种问题的根源是不同大版本对某个枚举字段的合法值定义发生了变化。12.x时代允许存的某个旧值到了14.x版本的模型中已经不在合法列表里了。而数据库里存的还是老值一旦保存整个设置记录校验器就会直接拒绝。排查这类问题时可以直接在console里看模型对这个字段的校验规则不用去翻源码文件ApplicationSetting.validators_on(:default_project_creation)这会打印出当前版本针对这个字段定义的validator包括合法的可选值列表。然后对比数据库里的实际值就能判断到底是哪个旧值失效了改成新版本认可的值即可。5.2 配置项存储方式调整让旧值悄悄失效还有一类故障更隐蔽它不报错但你的配置会“神秘消失”或者“改了不生效”。这类问题通常不是校验规则变化而是配置项的存储方式发生了变化。GitLab在多个大版本里对部分设置字段做过存储结构调整有些老字段从序列化数组改成了JSONB有些布尔值字段被合并拆分。升级后数据库迁移按理应该完成字段转换但某些历史数据在迁移前就不规范转出来的结果就可能是空数组、空字符串或者一个诡异对象。表现到页面上就是配置项明明还显示着内容但你一保存它就变成默认值或者你改了一段配置怎么都不生效。遇到这类问题建议先去查这个字段在目标版本里是怎么定义的。最直接的办法是看GitLab源码里对应的迁移文件grep -rn jsonb\|serialize\|default_project_creation /opt/gitlab/embedded/service/gitlab-rails/db/migrate/ | head -20通过迁移文件确认字段在新版本里的类型和默认值再回数据库里对比实际值。如果确实出现了转换异常手工修正存量数据的格式就行了。5.3 把“设置体检”并入升级流程这次故障之后我把“设置体检”正式纳入了升级后的验证清单。所谓体检就是在升级完成、业务验证通过后先别急着去后台改配置先在console里跑一次全量校验# 升级后的第一道关卡 s ApplicationSetting.current s.save! puts settings model save OK这条命令会触发整条设置记录的全字段校验能在最短时间内发现所有因新旧规则差异导致的脏数据问题。如果这条命令直接过了那后台设置页基本不会出类似的幺蛾子如果它报错了那就按本次文章里的思路把报错字段逐个修正直到能干净地保存。除了设置模型本身的校验我还会手动验证几个对版本升级最敏感的功能创建新工程、触发一次CI流水线、验证Webhook回调等。原因是GitLab 14.x对出站请求的本地网络访问限制变化也很大这些功能都可能因为旧配置而出现“升级前正常、升级后失效”的情况。这些验证看着不起眼但都比“等到用户报障再排查”省心得多。最后再分享一点这次的经验这次故障从头到尾折腾了小半天但真正定位到根因也就花了不到半小时。回头复盘最值得记住的一句话是升级后修改设置报错问题大概率不是设置本身而是这条设置记录里的历史数据无法通过新版本的规则校验。我从这次经历里总结出两条可复用的经验。第一GitLab升级前不要只备份数据和配置文件最好把application_settings表里的核心字段导出来和升级后在staging环境里的同字段做一次对比提前发现那些在新版本里已经非法的值。第二升级完成后不要着急做配置变更先在console里主动对设置模型跑一次save!这一步花不了一分钟却能把你从“后台设置一点就500”的尴尬中提前拉出来。如果你现在正好也卡在类似的报错上别急着重建实例或回滚数据库先把异常日志里的字段名找出来再在console里打印那条记录的对应值大概率一分钟就能定位到问题所在。