置保存500:從非法URL臟數(shù)據(jù)到完整排查修復(fù)指南)
GitLab版本升級這件事做得多了你會形成一種條件反射升級不可怕升級后的第一個午休才可怕。這不上個月我們這套GitLab實例剛從12.3.9一路升到14.4.5整個遷移過程順利得像教科書結(jié)果升級完不到半小時管理員在后臺修改通用設(shè)置頁面上直接彈了個“Something went wrong on our end.”通用設(shè)置一點就報錯而且誰都沒改過數(shù)據(jù)庫嚇得我午覺都沒睡。這篇文章就把這次故障的完整鏈路寫清楚從現(xiàn)象到日志定位從根因到修復(fù)再到順藤摸瓜發(fā)現(xiàn)的一批同類隱患。如果你也正準(zhǔn)備做GitLab大版本升級或者升完級發(fā)現(xiàn)后臺設(shè)置點不動、保存500這篇文章應(yīng)該能幫你省掉一整個下午的排查時間。1. 升級背景為什么從12.3.9到14.4.5需要格外謹(jǐn)慎1.1 跨三個大版本的升級路線GitLab的大版本升級和普通軟件不一樣不是把新版本包裝上去就完事。官方對升級路徑有明確要求跨大版本升級時必須先從當(dāng)前大版本升級到該大版本的最后一個小版本再進(jìn)入下一個大版本否則數(shù)據(jù)庫的schema遷移可能因為跳級而直接失敗輕則升級中斷重則數(shù)據(jù)不一致。我們這套實例的原始版本是12.3.9目標(biāo)版本是14.4.5中間隔了12、13、14三個大版本。按官方建議的路徑實際執(zhí)行順序是# 第一步先升到12.x的最后一個版本 # 12.3.9 - 12.10.14 # 第二步跨入13.x大版本并且升到13.x的最后一個版本 # 12.10.14 - 13.12.15 # 第三步進(jìn)入14.x直接升到14.4.5 # 13.12.15 - 14.4.5這里有個容易忽略的點很多人在12.3.9的時候直接下載14.4.5的安裝包執(zhí)行完gitlab-ctl upgrade就以為完事了。實際上GitLab的升級腳本會在數(shù)據(jù)庫遷移階段檢查當(dāng)前schema版本跨度過大會直接拒絕執(zhí)行遷移或者更糟遷移執(zhí)行到一半報錯整實例處于半升級狀態(tài)。我們當(dāng)時就是老老實實按三步走每步都做了備份每步升級完都跑一遍基礎(chǔ)驗證整個過程倒是沒出幺蛾子。1.2 升級前的準(zhǔn)備動作這次升級前我們做的準(zhǔn)備值得拿出來說一下。首先是把生產(chǎn)環(huán)境的數(shù)據(jù)備份恢復(fù)到一套staging環(huán)境里在staging上完完整整走了一遍升級流程。這一步很重要因為GitLab的升級腳本只負(fù)責(zé)遷移不負(fù)責(zé)驗證業(yè)務(wù)數(shù)據(jù)的兼容性有些故障在升級過程中不報錯要等升級后你真正去操作某些功能時才炸出來。備份命令這里也給一下注意不同版本命令可能不太一樣12.x時期還能用gitlab-rake gitlab:backup:create13.x之后新版本更建議直接用gitlab-backup# 升級前在每個版本節(jié)點都執(zhí)行一次 sudo gitlab-backup create sudo gitlab-ctl status # 確認(rèn)各服務(wù)狀態(tài)正常 sudo gitlab-ctl reconfigure sudo gitlab-rake gitlab:check升級完成之后我們按照慣例先驗證了登錄、工程列表、CI跑一次流水線這些核心鏈路當(dāng)時覺得一切正常就去后臺改設(shè)置。結(jié)果恰恰是“改設(shè)置”這個動作把藏了很久的存量數(shù)據(jù)問題給捅出來了。2. 報錯現(xiàn)場保存通用設(shè)置時頁面直接5002.1 操作路徑和第一反應(yīng)故障復(fù)現(xiàn)路徑非常清晰。管理員賬號登錄后進(jìn)入 Admin Area → Settings → General這是站點的通用設(shè)置頁。當(dāng)時只是想改一下站點描述順便檢查首頁URL字段結(jié)果點下 Save changes 按鈕之后頁面沒有出現(xiàn)熟悉的“保存成功”提示而是直接跳到了GitLab經(jīng)典的錯誤頁。頁面提示是Something went wrong on our end.翻譯成人話就是你發(fā)起的請求在后端拋了一個未處理的異常500了。這個提示本身沒有任何排查價值它只是GitLab對生產(chǎn)環(huán)境用戶屏蔽錯誤詳情的手段。真正有價值的信息在日志里。遇到這類問題我的第一反應(yīng)是別去猜先去翻日志。GitLab的日志路徑比較固定恰好能直接看到本次請求的完整調(diào)用鏈。2.2 從日志里找到真正的異常我們的GitLab使用的是omnibus安裝方式日志都在/var/log/gitlab/gitlab-rails/目錄下。這次故障對應(yīng)的請求記錄在production_json.log里用tail拉出來的關(guān)鍵信息是這樣的{ 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: ... } } }日志里的信息量已經(jīng)很大了我給幾個關(guān)鍵字段做個拆解字段含義這次故障中傳遞的信息methodHTTP方法PUT說明是更新操作path請求路徑/admin/application_settings/general就是通用設(shè)置保存接口controller/action控制器與方法Admin::ApplicationSettingsController#update走的是后臺設(shè)置更新邏輯statusHTTP狀態(tài)碼500服務(wù)端異常error異常類型與消息ActiveRecord::RecordInvalid模型校驗失敗具體是“Homepage URL is not a valid URL”params當(dāng)前提交的參數(shù)這里有個陷阱本次params里只提交了site_description壓根沒提交home_page_url看到ActiveRecord::RecordInvalid的時候我就知道問題出在數(shù)據(jù)校驗上而不是路由、權(quán)限或者別的雜七雜八的地方。但這里有個特別刁鉆的細(xì)節(jié)日志里params顯示這次請求只修改了site_description一個描述字段為什么會觸發(fā)home_page_url的校驗錯誤這就是本次故障最值得記錄的點你改的字段和報錯的字段完全不是同一個。3. 排查鏈路從URL校驗一路追到庫存臟數(shù)據(jù)3.1 在console里直接觸發(fā)全量校驗要復(fù)現(xiàn)這個問題不需要重新去頁面上點保存。在服務(wù)器上打開Rails控制臺直接對ApplicationSetting模型做一次保存操作sudo gitlab-rails consoles ApplicationSetting.current s.save!這就是最干凈的復(fù)現(xiàn)方式。上面這行代碼沒有做任何修改就是原封不動地把當(dāng)前設(shè)置記錄重新保存一遍。在正常狀態(tài)它會安靜地返回true在這次故障環(huán)境下它直接拋出了和頁面上一模一樣的異常ActiveRecord::RecordInvalid: Validation failed: Homepage URL is not a valid URL這個復(fù)現(xiàn)結(jié)果說明兩件事。第一問題不在頁面不在前端也不在你本次修改的內(nèi)容上而是整條application_settings記錄本身帶著一個非法值。第二GitLab在保存任何設(shè)置時都會對這條記錄做一次全字段校驗?zāi)呐履阒桓牧艘粋€描述字段它也會把這條記錄里的所有字段全部過一遍校驗器。3.2 為什么“沒改過的字段”會擋住保存這里要順帶解釋一個Rails的機制方便和我一樣半路出家搞運維的朋友理解。Rails的ActiveRecord模型在更新記錄時默認(rèn)會觸發(fā)valid?校驗而模型上所有的validates聲明都會被執(zhí)行。換句話說save!不僅僅是校驗?zāi)阌凶兓淖侄味切r炚麠l記錄在當(dāng)前代碼規(guī)則下是否合法。GitLab升級到14.4.5之后ApplicationSetting模型里針對URL類字段的校驗規(guī)則比12.3.9時嚴(yán)格得多。新版本的校驗器要求URL必須帶http://或https://協(xié)議頭而且host部分必須能正常解析出來。如果你在舊版本里存了一個“看起來能用但不完全合法”的URL升級后新代碼一校驗這個陳年老數(shù)據(jù)就會把整個保存操作給攔下來。3.3 從模型源碼和數(shù)據(jù)庫把舊值揪出來知道了原理排查就變得很簡單。先用Rails的控制臺把當(dāng)前庫里home_page_url的值打印出來s ApplicationSetting.current puts s.home_page_url.inspect輸出結(jié)果如下//intranet.example.com看到這串值的時候我心里就有數(shù)了。這是個沒有協(xié)議頭的裸地址瀏覽器可能有自己的容錯機制某些場景下能自動補全協(xié)議但GitLab后臺的URL校驗器不會慣著它。URI.parse解析這種值的時候scheme會被解析成nil直接不滿足“只允許http和https”的約束。為了交叉驗證我還做了一步直接去模型源碼里翻了校驗規(guī)則。omnibus安裝的GitLab源碼路徑在grep -rn home_page_url /opt/gitlab/embedded/service/gitlab-rails/app/models/相關(guān)的validates聲明寫得明明白白URL類型字段走的是GitLab的URL校驗器schemes白名單只有[http, https]。數(shù)據(jù)庫這一側(cè)也用SQL確認(rèn)了一遍SELECT home_page_url, support_url, terms FROM application_settings;果然不止home_page_url連support_url都存著同樣格式的裸地址。這說明當(dāng)時某位管理員在舊版本上手動填這些字段的時候壓根沒填協(xié)議頭而12.3.9時代估計對這類字段沒卡那么嚴(yán)于是臟數(shù)據(jù)就這么被放了進(jìn)來一直躺到這次升級。4. 修復(fù)方案先把非法值改合法再動后臺4.1 一次成功的console修復(fù)修復(fù)本身不復(fù)雜把非法值改成合法值就行。我們的場景里這些URL實際指向的是內(nèi)部辦公系統(tǒng)入口補上協(xié)議頭即可s ApplicationSetting.current s.home_page_url https://intranet.example.com s.support_url https://intranet.example.com/support s.save!保存成功后返回值是true不再拋校驗異常。然后回到后臺頁面重新把站點描述改好點保存成功提示正常出現(xiàn)問題解決。這里有一個建議遇到多個URL類字段都有問題時別一個字段一個字段地改直接在console里逐個檢查交給同一個保存事務(wù)最后一次性save!避免中間狀態(tài)反復(fù)觸發(fā)校驗失敗。4.2 為什么不建議用update_columns強行繞過很多人看到這種問題第一反應(yīng)是找個辦法繞過校驗。Rails確實提供了繞過校驗的接口比如update_columns它可以直接寫數(shù)據(jù)庫不觸發(fā)任何校驗和回調(diào)# 不推薦的做法 ApplicationSetting.current.update_columns(home_page_url: https://intranet.example.com)這條命令確實能把這次報錯按下去但我不推薦這么做。原因很實際這次的根因不是某個字段寫錯而是整條記錄攜帶了大量未經(jīng)驗證的舊值。你用update_columns只修正了一個字段其他同樣不合格的字段還在那里躺著下次保存設(shè)置的時候還會以另一個報錯形式跳出來。而且繞過校驗意味著繞過了模型里的回調(diào)邏輯GitLab在新版本里可能依賴某些字段的值來驅(qū)動其他功能比如出站請求限制、登錄頁定制等。你硬寫進(jìn)去的值如果不符合預(yù)期可能在更隱蔽的地方造成狀態(tài)不一致。正確的做法是像我們一樣把所有不合法字段一次性修正讓整條記錄能通過新規(guī)則的校驗這才是干凈的狀態(tài)。4.3 修復(fù)后的驗證和同類字段排查修復(fù)完成后我沒有直接關(guān)掉console而是順手做了一輪同類字段排查。所有像URL、協(xié)議、枚舉值這類強約束字段都可能存在類似的舊數(shù)據(jù)問題。建議在console里把設(shè)置記錄的所有字段過一遍特別是這些常見字段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確認(rèn)整體健康。這次檢查確實又發(fā)現(xiàn)了兩處不合法URL一并修掉之后后臺設(shè)置頁保存功能徹底恢復(fù)正常。5. 同一個坑循環(huán)上演以后升級要注意的幾類設(shè)置故障5.1 枚舉字段的合法值范圍變了URL校驗問題只是“升級后改設(shè)置報錯”的其中一個子類。我查了一圈發(fā)現(xiàn)GitLab跨大版本升級后通用設(shè)置相關(guān)的報錯里最常見的是枚舉字段值不在合法列表內(nèi)。典型的報錯長這樣ActiveRecord::RecordInvalid: Validation failed: Default project creation is not included in the list這種問題的根源是不同大版本對某個枚舉字段的合法值定義發(fā)生了變化。12.x時代允許存的某個舊值到了14.x版本的模型中已經(jīng)不在合法列表里了。而數(shù)據(jù)庫里存的還是老值一旦保存整個設(shè)置記錄校驗器就會直接拒絕。排查這類問題時可以直接在console里看模型對這個字段的校驗規(guī)則不用去翻源碼文件ApplicationSetting.validators_on(:default_project_creation)這會打印出當(dāng)前版本針對這個字段定義的validator包括合法的可選值列表。然后對比數(shù)據(jù)庫里的實際值就能判斷到底是哪個舊值失效了改成新版本認(rèn)可的值即可。5.2 配置項存儲方式調(diào)整讓舊值悄悄失效還有一類故障更隱蔽它不報錯但你的配置會“神秘消失”或者“改了不生效”。這類問題通常不是校驗規(guī)則變化而是配置項的存儲方式發(fā)生了變化。GitLab在多個大版本里對部分設(shè)置字段做過存儲結(jié)構(gòu)調(diào)整有些老字段從序列化數(shù)組改成了JSONB有些布爾值字段被合并拆分。升級后數(shù)據(jù)庫遷移按理應(yīng)該完成字段轉(zhuǎn)換但某些歷史數(shù)據(jù)在遷移前就不規(guī)范轉(zhuǎn)出來的結(jié)果就可能是空數(shù)組、空字符串或者一個詭異對象。表現(xiàn)到頁面上就是配置項明明還顯示著內(nèi)容但你一保存它就變成默認(rèn)值或者你改了一段配置怎么都不生效。遇到這類問題建議先去查這個字段在目標(biāo)版本里是怎么定義的。最直接的辦法是看GitLab源碼里對應(yīng)的遷移文件grep -rn jsonb\|serialize\|default_project_creation /opt/gitlab/embedded/service/gitlab-rails/db/migrate/ | head -20通過遷移文件確認(rèn)字段在新版本里的類型和默認(rèn)值再回數(shù)據(jù)庫里對比實際值。如果確實出現(xiàn)了轉(zhuǎn)換異常手工修正存量數(shù)據(jù)的格式就行了。5.3 把“設(shè)置體檢”并入升級流程這次故障之后我把“設(shè)置體檢”正式納入了升級后的驗證清單。所謂體檢就是在升級完成、業(yè)務(wù)驗證通過后先別急著去后臺改配置先在console里跑一次全量校驗# 升級后的第一道關(guān)卡 s ApplicationSetting.current s.save! puts settings model save OK這條命令會觸發(fā)整條設(shè)置記錄的全字段校驗?zāi)茉谧疃虝r間內(nèi)發(fā)現(xiàn)所有因新舊規(guī)則差異導(dǎo)致的臟數(shù)據(jù)問題。如果這條命令直接過了那后臺設(shè)置頁基本不會出類似的幺蛾子如果它報錯了那就按本次文章里的思路把報錯字段逐個修正直到能干凈地保存。除了設(shè)置模型本身的校驗我還會手動驗證幾個對版本升級最敏感的功能創(chuàng)建新工程、觸發(fā)一次CI流水線、驗證Webhook回調(diào)等。原因是GitLab 14.x對出站請求的本地網(wǎng)絡(luò)訪問限制變化也很大這些功能都可能因為舊配置而出現(xiàn)“升級前正常、升級后失效”的情況。這些驗證看著不起眼但都比“等到用戶報障再排查”省心得多。最后再分享一點這次的經(jīng)驗這次故障從頭到尾折騰了小半天但真正定位到根因也就花了不到半小時?;仡^復(fù)盤最值得記住的一句話是升級后修改設(shè)置報錯問題大概率不是設(shè)置本身而是這條設(shè)置記錄里的歷史數(shù)據(jù)無法通過新版本的規(guī)則校驗。我從這次經(jīng)歷里總結(jié)出兩條可復(fù)用的經(jīng)驗。第一GitLab升級前不要只備份數(shù)據(jù)和配置文件最好把application_settings表里的核心字段導(dǎo)出來和升級后在staging環(huán)境里的同字段做一次對比提前發(fā)現(xiàn)那些在新版本里已經(jīng)非法的值。第二升級完成后不要著急做配置變更先在console里主動對設(shè)置模型跑一次save!這一步花不了一分鐘卻能把你從“后臺設(shè)置一點就500”的尷尬中提前拉出來。如果你現(xiàn)在正好也卡在類似的報錯上別急著重建實例或回滾數(shù)據(jù)庫先把異常日志里的字段名找出來再在console里打印那條記錄的對應(yīng)值大概率一分鐘就能定位到問題所在。