機(jī):A/B雙槽位OTA更新機(jī)制實(shí)戰(zhàn)解析)
做車(chē)載安卓開(kāi)發(fā)的朋友應(yīng)該都清楚給車(chē)機(jī)做 OTA 更新和給手機(jī)做 OTA 完全不是一個(gè)難度等級(jí)。手機(jī)更新掛了用戶頂多吐槽兩句進(jìn) recovery 再刷一次就能救回來(lái)車(chē)機(jī)要是更新到一半斷電、斷網(wǎng)或者引導(dǎo)程序被寫(xiě)壞用戶面對(duì)的是一塊黑屏的中控導(dǎo)航、倒車(chē)影像、空調(diào)控制全部失效。這種現(xiàn)場(chǎng)處理成本比手機(jī)高一個(gè)量級(jí)因?yàn)檐?chē)主不會(huì)自己拆機(jī)刷機(jī)也沒(méi)人愿意為了一個(gè)系統(tǒng)版本跑兩趟維修點(diǎn)。我前兩年接手一個(gè)基于安卓系統(tǒng)的車(chē)機(jī)項(xiàng)目在更新流程上反復(fù)栽跟頭最后把 Chrome OS 的更新機(jī)制完整過(guò)了一遍才算把思路徹底理順。Chrome OS 的更新流程和安卓系統(tǒng)在底層是同源的核心思想是“雙槽位 原子切換”永遠(yuǎn)不修改正在運(yùn)行的系統(tǒng)而是把新版本完整寫(xiě)到另一個(gè)槽位分區(qū)塊校驗(yàn)通過(guò)后再切換過(guò)去啟動(dòng)失敗還能自動(dòng)回退。這篇文章把這條流程從版本檢查到重啟回滾逐段拆開(kāi)講重點(diǎn)落在怎么把它落地到安卓系統(tǒng)的車(chē)載設(shè)備、手機(jī)這類(lèi)場(chǎng)景里。適合看這篇內(nèi)容的有三類(lèi)人正在設(shè)計(jì)車(chē)機(jī)或智能座艙 OTA 方案的系統(tǒng)工程師想讓安卓設(shè)備更新更可靠的 ROM 開(kāi)發(fā)者以及單純想弄明白“系統(tǒng)更新在后臺(tái)到底做了什么”的進(jìn)階用戶。我不畫(huà)那些漂亮的架構(gòu)圖只講實(shí)際跑通這條鏈路需要知道的事以及我踩過(guò)的坑。1. 車(chē)載更新為什么容易翻車(chē)三個(gè)繞不開(kāi)的現(xiàn)實(shí)約束1.1 電力不可控更新做到一半用戶可能直接熄火車(chē)機(jī)沒(méi)有“充電寶”這種后備方案。手機(jī)的更新策略可以簡(jiǎn)單粗暴地要求“電量大于 50%接上充電器再更新”車(chē)機(jī)不行它依靠整車(chē)電源。用戶可能在地庫(kù)里待著無(wú)聊點(diǎn)了一下“檢查更新”下載完剛進(jìn)入寫(xiě)入階段就關(guān)掉電源下車(chē)走了。整車(chē)斷電意味著 SoC 瞬間失去供電正在寫(xiě) eMMC 的進(jìn)程直接被拍死。更麻煩的是車(chē)機(jī)系統(tǒng)里往往還掛著儀表聯(lián)動(dòng)、電源管理、網(wǎng)絡(luò)喚醒等一堆域控制器邏輯。你很難用一個(gè)簡(jiǎn)單的“電量閾值”判斷能不能寫(xiě)分區(qū)因?yàn)槟悴恢老乱幻胗脩魰?huì)不會(huì)啟動(dòng)發(fā)動(dòng)機(jī)。我的做法是加一個(gè)“電源擋位 電壓兜底”雙條件整車(chē)電源處于 ON 擋且電瓶電壓高于某個(gè)閾值比如 12V才允許進(jìn)入寫(xiě)入階段下載階段則不受此限制。這樣至少把“熄火瞬間寫(xiě)入”的概率降到最低。1.2 網(wǎng)絡(luò)不可靠地庫(kù)、隧道、高速移動(dòng)下的下載體驗(yàn)車(chē)機(jī)的網(wǎng)絡(luò)環(huán)境比手機(jī)差得多。手機(jī)在地庫(kù)里沒(méi)信號(hào)可以等回家連 Wi-Fi車(chē)機(jī)經(jīng)常就是在地庫(kù)或者戶外停車(chē)場(chǎng)停著的時(shí)候才有人想起更新。再加上很多車(chē)機(jī)的蜂窩模塊硬件帶寬本身就有限一個(gè)幾 GB 的鏡像包下載幾個(gè)小時(shí)是常態(tài)下載過(guò)程中進(jìn)出隧道、跨基站、信號(hào)從滿格變成無(wú)服務(wù)都是家常便飯。所以車(chē)機(jī) OTA 的第一條鐵律就是下載必須支持?jǐn)帱c(diǎn)續(xù)傳而且進(jìn)度要持久化。Chrome OS 的更新引擎在這一點(diǎn)上做得很扎實(shí)它把已下載數(shù)據(jù)的偏移量記錄在持久分區(qū)里重啟后還能接著下。很多車(chē)機(jī)項(xiàng)目第一次做 OTA 時(shí)沒(méi)有意識(shí)到這一點(diǎn)把下載緩存放在容易被清掉的內(nèi)存文件系統(tǒng)里結(jié)果斷一次電80% 的進(jìn)度直接清零用戶心態(tài)直接爆炸。1.3 救援手段缺失車(chē)機(jī)沒(méi)有“刷機(jī)自由”手機(jī)用戶遇到系統(tǒng)壞了還能自己進(jìn) recovery 雙清、線刷、卡刷社區(qū)教程一抓一大把。車(chē)機(jī)呢大部分車(chē)主不知道什么是 recovery中控面板上也不存在“音量鍵 電源鍵”這種組合鍵想進(jìn)刷機(jī)模式得靠診斷儀或者特殊指令。就算工程師能遠(yuǎn)程進(jìn)入診斷模式車(chē)機(jī)本身黑屏的情況下用戶連車(chē)都開(kāi)不順體驗(yàn)極其糟糕。這些約束疊加起來(lái)指向同一個(gè)結(jié)論車(chē)機(jī)的更新方案必須默認(rèn)“更新過(guò)程中隨時(shí)可能被打斷”而且打斷之后系統(tǒng)還要能自己恢復(fù)。這正是 Chrome OS 更新機(jī)制的設(shè)計(jì)前提也是我把它作為參考藍(lán)本的原因。2. Chrome OS 更新機(jī)制的核心骨架雙槽位、狀態(tài)機(jī)與更新引擎2.1 雙槽位設(shè)計(jì)不是“覆蓋”而是“寫(xiě)完再切”Chrome OS 的存儲(chǔ)里同時(shí)保留兩份系統(tǒng)KERNEL-A/ROOT-A 和 KERNEL-B/ROOT-B這就是所謂的 A/B 雙槽位。系統(tǒng)當(dāng)前從 A 槽啟動(dòng)時(shí)B 槽是空閑的一次更新做的事情就是往 B 槽寫(xiě)一份完整的新系統(tǒng)而不是去改動(dòng) A 槽的任何字節(jié)。寫(xiě)完并校驗(yàn)通過(guò)之后才把“下次啟動(dòng)優(yōu)先用 B 槽”這個(gè)標(biāo)記寫(xiě)進(jìn)引導(dǎo)程序能讀到的位置。這個(gè)設(shè)計(jì)可以類(lèi)比成搬家你不是住在正在裝修的房子里而是在隔壁先裝好一套樣板間驗(yàn)收滿意之后再搬過(guò)去舊房子留著作為退路。對(duì)車(chē)機(jī)這種“不允許現(xiàn)場(chǎng)救磚”的場(chǎng)景來(lái)說(shuō)這個(gè)“退路”是剛需而不是錦上添花。2.2 更新引擎與狀態(tài)機(jī)整個(gè)流程的大腦Chrome OS 里的更新動(dòng)作由一個(gè)常駐后臺(tái)的守護(hù)進(jìn)程負(fù)責(zé)源碼里通常叫 update_engine。它負(fù)責(zé)跟更新服務(wù)器通信、下載鏡像、寫(xiě)分區(qū)、觸發(fā)重啟所有操作都有一個(gè)清晰的狀態(tài)機(jī)空閑、檢查更新、發(fā)現(xiàn)新版本、下載中、校驗(yàn)中、收尾、等待重啟。每個(gè)狀態(tài)都會(huì)落日志排查問(wèn)題的時(shí)候第一步永遠(yuǎn)是看它停在哪個(gè)狀態(tài)。這一點(diǎn)對(duì)移植到安卓非常重要。安卓系統(tǒng)從支持 A/B 無(wú)縫更新開(kāi)始也把同一個(gè)更新引擎接進(jìn)了自己的框架車(chē)機(jī) ROM 里能看到這個(gè)進(jìn)程再正常不過(guò)。它的狀態(tài)機(jī)和 Chrome OS 基本同源你只要理解了這套狀態(tài)機(jī)車(chē)機(jī) OTA 的調(diào)試工作就成功了一半。2.3 分區(qū)布局內(nèi)核、根文件系統(tǒng)、用戶數(shù)據(jù)各自有不同的待遇Chrome OS 的分區(qū)不是所有內(nèi)容都做雙份。內(nèi)核和根文件系統(tǒng)走 A/B 雙槽位因?yàn)樗鼈儧Q定了系統(tǒng)能不能起來(lái)用戶數(shù)據(jù)分區(qū)是單份的兩個(gè)槽位共用因?yàn)樗绨姹颈A?。這個(gè)分工很有講究系統(tǒng)代碼可以隨時(shí)替換用戶數(shù)據(jù)不能隨便丟。落到安卓車(chē)機(jī)上對(duì)應(yīng)關(guān)系大概是這樣內(nèi)核對(duì)應(yīng) boot 分區(qū)根文件系統(tǒng)對(duì)應(yīng) system/vendor 這些系統(tǒng)分區(qū)用戶數(shù)據(jù)對(duì)應(yīng) data 分區(qū)。你可以把 Chrome OS 的分區(qū)哲學(xué)理解為“把不可變的東西做成雙份把可變的東西保持單份”這個(gè)原則在移植時(shí)依然成立。3. 一次完整更新的五步流從版本協(xié)商到失敗回滾3.1 第一步設(shè)備“報(bào)家底”服務(wù)器決定給不給包更新開(kāi)始前設(shè)備要向更新服務(wù)器匯報(bào)自己的身份和現(xiàn)狀硬件標(biāo)識(shí)哪一款車(chē)機(jī)、哪個(gè)型號(hào)的設(shè)備、當(dāng)前系統(tǒng)版本號(hào)、所屬更新通道穩(wěn)定版還是測(cè)試版、固件版本等。服務(wù)器根據(jù)這些信息判斷這個(gè)設(shè)備當(dāng)前版本離最新版本差多遠(yuǎn)該推全量包還是增量包以及要不要限制灰度范圍。這一步經(jīng)常被車(chē)機(jī)團(tuán)隊(duì)忽略。很多項(xiàng)目直接把更新服務(wù)器做成“有包就發(fā)”完全沒(méi)有版本協(xié)商的概念。結(jié)果就是一臺(tái)長(zhǎng)期斷網(wǎng)的設(shè)備三個(gè)月后終于連上網(wǎng)服務(wù)器給它推了一個(gè)基于三個(gè)月前版本的增量包而它的當(dāng)前版本比增量包的基準(zhǔn)還老寫(xiě)入時(shí)校驗(yàn)直接失敗。所以版本協(xié)商不是可選項(xiàng)是整個(gè)流程的地基。3.2 第二步全量包還是增量包網(wǎng)絡(luò)決定策略更新包分兩種。全量包包含目標(biāo)版本的完整鏡像體積大好處是不依賴(lài)設(shè)備當(dāng)前版本寫(xiě)起來(lái)最穩(wěn)妥增量包只包含新舊版本之間的差異體積可能只有全量包的十分之一但要求設(shè)備當(dāng)前版本正好在基準(zhǔn)范圍內(nèi)。兩者的取舍在車(chē)機(jī)場(chǎng)景里尤其明顯。我給車(chē)機(jī)項(xiàng)目定的策略很簡(jiǎn)單默認(rèn)優(yōu)先增量包前提是設(shè)備當(dāng)前版本距離目標(biāo)版本不超過(guò)兩個(gè)版本超過(guò)兩代或找不到對(duì)應(yīng)基準(zhǔn)直接轉(zhuǎn)全量包。寧可下載慢一點(diǎn)也不讓用戶在“基準(zhǔn)版本不匹配”的報(bào)錯(cuò)里打轉(zhuǎn)。對(duì)比項(xiàng)全量包增量包體積大完整鏡像小版本間差異適用條件任意舊版本指定舊版本范圍生成復(fù)雜度低高需要基準(zhǔn)版本失敗風(fēng)險(xiǎn)低中基準(zhǔn)不匹配則失敗車(chē)機(jī)弱網(wǎng)場(chǎng)景后備方案首選3.3 第三步下載、斷點(diǎn)續(xù)傳與逐塊校驗(yàn)下載不是簡(jiǎn)單地把一個(gè)文件拉到本地。更新引擎會(huì)把鏡像包切成很多塊每一塊都有自己的哈希值下載過(guò)程中邊下邊驗(yàn)驗(yàn)不過(guò)的塊直接重下。整個(gè)鏡像包下載完成后還會(huì)對(duì)包簽名和整體哈希再做一次校驗(yàn)任何一步不通過(guò)更新都不會(huì)進(jìn)入寫(xiě)分區(qū)階段。斷點(diǎn)續(xù)傳的實(shí)現(xiàn)方式是把下載進(jìn)度記錄到持久分區(qū)的偏好文件里分塊信息和偏移量都留在那里。斷電重啟后引擎讀到原來(lái)的進(jìn)度從斷點(diǎn)繼續(xù)下而不是從頭再來(lái)。我在測(cè)試?yán)锬M過(guò)十幾次“下載中拔電源”只要暫存目錄和進(jìn)度文件放在持久分區(qū)恢復(fù)率是百分之百反過(guò)來(lái)說(shuō)只要這兩個(gè)東西放錯(cuò)地方一次斷電就前功盡棄。3.4 第四步寫(xiě)入非活動(dòng)槽位寫(xiě)完才算數(shù)下載校驗(yàn)通過(guò)后才是真正的“安裝”階段。更新引擎把鏡像內(nèi)容按塊寫(xiě)入非活動(dòng)槽位比如當(dāng)前從 A 槽啟動(dòng)就寫(xiě) B 槽的內(nèi)核和根文件系統(tǒng)分區(qū)。每一塊寫(xiě)完之后立刻做哈希比對(duì)不一致就中止并把這個(gè)槽位標(biāo)記為不可用。全部寫(xiě)完后還會(huì)對(duì)目標(biāo)鏡像做一次整體校驗(yàn)。這一步給車(chē)機(jī)方案提了個(gè)醒寫(xiě)入階段是最怕斷電的區(qū)間。雖然 A/B 雙槽位能保證“寫(xiě)一半斷電后 A 槽還能正常啟動(dòng)”但 B 槽會(huì)殘留一個(gè)半成品下次更新必須先清掉重寫(xiě)。所以我在車(chē)機(jī)上加了“寫(xiě)入階段禁止整車(chē)下電”的提示同時(shí)在策略上允許“下載可以斷寫(xiě)入必須一口氣完成”。3.5 第五步重啟切換、嘗試計(jì)數(shù)與自動(dòng)回退寫(xiě)完并校驗(yàn)通過(guò)后更新引擎把“優(yōu)先啟動(dòng) B 槽”的標(biāo)記寫(xiě)進(jìn)引導(dǎo)程序的數(shù)據(jù)區(qū)域然后請(qǐng)求系統(tǒng)重啟。引導(dǎo)程序啟動(dòng) B 槽時(shí)會(huì)維護(hù)一個(gè)嘗試計(jì)數(shù)每啟動(dòng)失敗一次計(jì)數(shù)減一減到零就自動(dòng)切回 A 槽只有 B 槽真正起來(lái)、關(guān)鍵服務(wù)跑通之后才把計(jì)數(shù)清零確定性地鎖定 B 槽。這個(gè)嘗試計(jì)數(shù)機(jī)制是整套回滾能否生效的關(guān)鍵。很多移植版在這個(gè)環(huán)節(jié)想當(dāng)然認(rèn)為“重啟后能進(jìn)桌面就算成功”于是提前把計(jì)數(shù)清掉。結(jié)果系統(tǒng)在進(jìn)桌面前其實(shí)已經(jīng)悄悄崩潰過(guò)兩次計(jì)數(shù)被人為清零后下次一崩就連回退的資格都沒(méi)有了。4. 把 Chrome OS 思路移植到安卓車(chē)機(jī)/手機(jī)四個(gè)必須處理的落差4.1 分區(qū)模型不同一個(gè) kernel/rootfs一個(gè) boot/system/vendorChrome OS 的雙槽位是“內(nèi)核 根文件系統(tǒng)”兩個(gè)大分區(qū)。安卓車(chē)機(jī)/手機(jī)要復(fù)雜得多至少包含 boot、system、vendor有些還有 init_boot、odm、product而且這些分區(qū)在較新的安卓里還可能不是物理分區(qū)而是被塞進(jìn)一個(gè)叫 super 的大分區(qū)里的邏輯分區(qū)。移植 A/B 時(shí)你不能簡(jiǎn)單地把“寫(xiě)一個(gè) rootfs 鏡像”翻譯成“寫(xiě)一個(gè) system 鏡像”而是要把構(gòu)成一套完整系統(tǒng)的所有目標(biāo)分區(qū)都納入寫(xiě)入計(jì)劃。好消息是不用自己從零實(shí)現(xiàn)。安卓本身已經(jīng)有 A/B 分區(qū)的標(biāo)準(zhǔn)布局boot_a/boot_b、system_a/system_b、vendor_a/vendor_b引導(dǎo)程序通過(guò) misc 分區(qū)里的 bootloader message 記錄當(dāng)前槽位和嘗試次數(shù)。你要做的主要是核對(duì)設(shè)備里的分區(qū)表確保這些“雙份”分區(qū)真實(shí)存在而不是名字有了、實(shí)際空間共用。分區(qū)角色Chrome OS安卓 A/B 對(duì)應(yīng)內(nèi)核KERNEL-A/KERNEL-Bboot_a/boot_b根文件系統(tǒng)ROOT-A/ROOT-Bsystem_a/system_b、vendor_a/vendor_b用戶數(shù)據(jù)單份 statefuldata 分區(qū)單份共享槽位標(biāo)記引導(dǎo)程序數(shù)據(jù)區(qū)misc 分區(qū)的 bootloader message4.2 驗(yàn)證鏈不同從簽名鏡像到完整 verified bootChrome OS 的更新安全性靠的是完整信任鏈引導(dǎo)程序驗(yàn)證內(nèi)核簽名內(nèi)核驗(yàn)證根文件系統(tǒng)的 dm-verity 哈希樹(shù)每一級(jí)都驗(yàn)過(guò)了才放行。安卓也有對(duì)應(yīng)的驗(yàn)證框架但在車(chē)機(jī)上很多方案只停留在“下載的鏡像校驗(yàn)一下哈?!边@個(gè)層面引導(dǎo)鏈完全沒(méi)接。我的建議是既然做了雙槽位就順手把驗(yàn)證鏈也補(bǔ)齊。系統(tǒng)簽名、dm-verity 表這些在車(chē)機(jī)上越來(lái)越重要。否則你辛苦保證的更新安全可能被一個(gè)沒(méi)上鎖的引導(dǎo)程序全部繞過(guò)去雙槽位再穩(wěn)也架不住入口是敞開(kāi)的。4.3 供電與網(wǎng)絡(luò)策略不能照抄要按使用場(chǎng)景重設(shè)Chrome OS 是筆記本和迷你主機(jī)場(chǎng)景它的電源約束是“插電 有電池”手機(jī)是“電池 偶爾連著充電器”車(chē)機(jī)是“整車(chē)電源 可能隨時(shí)被拔鑰匙”。三者的更新策略必須分別設(shè)置不能一套邏輯通吃。我列過(guò)一張對(duì)照表車(chē)機(jī)的幾個(gè)關(guān)鍵門(mén)檻和手機(jī)完全不同檢查更新頻率車(chē)機(jī)不需要像手機(jī)那樣一天查好幾次設(shè)置成 6 小時(shí)一次即可電量門(mén)檻手機(jī)看電池百分比車(chē)機(jī)看電源擋位和電瓶電壓網(wǎng)絡(luò)策略車(chē)機(jī)如果裝了蜂窩模塊必須區(qū)分計(jì)費(fèi)網(wǎng)絡(luò)和免費(fèi) Wi-Fi避免一個(gè)鏡像包把用戶的流量套餐刷穿。4.4 存儲(chǔ)空間預(yù)算雙槽位意味著鏡像空間翻倍A/B 最直白的代價(jià)是存儲(chǔ)開(kāi)銷(xiāo)系統(tǒng)所有關(guān)鍵分區(qū)要做兩份額外還要留出下載暫存空間。以一套中等規(guī)模的車(chē)機(jī)安卓系統(tǒng)為例boot 分區(qū)就算 64MBsystem 分區(qū) 4GBvendor 分區(qū) 1.5GB雙份就是約 11GB再加一個(gè)全量包暫存 5GB一共 16GB 起步。很多低成本車(chē)機(jī)的存儲(chǔ)只有 16GB 或 32GB預(yù)算一算就穿幫。應(yīng)對(duì)方案有幾條路優(yōu)先用增量包體積能壓到全量的三分之一甚至更小開(kāi)啟流式下載邊下邊寫(xiě)省掉完整暫存這一份存儲(chǔ)實(shí)在不夠的再考慮虛擬 A/B用快照方式減少完整副本。但虛擬 A/B 對(duì)系統(tǒng)分區(qū)布局和啟動(dòng)校驗(yàn)的要求更高屬于進(jìn)階方案團(tuán)隊(duì)沒(méi)有足夠測(cè)試資源時(shí)不要輕易上。5. 實(shí)測(cè)中最容易翻車(chē)的三個(gè)場(chǎng)景與完整排查思路5.1 斷電斷網(wǎng)后更新“丟失”一看暫存路徑二看進(jìn)度存儲(chǔ)位置現(xiàn)象下載到 80%斷了一次電重啟后更新引擎狀態(tài)回到空閑進(jìn)度清零。先別急著懷疑更新引擎有問(wèn)題多半是暫存目錄或進(jìn)度記錄放在了不該放的地方。排查鏈路如下第一步進(jìn)系統(tǒng)后查看下載緩存目錄掛在哪個(gè)文件系統(tǒng)上執(zhí)行 df 確認(rèn)它是持久分區(qū)還是內(nèi)存文件系統(tǒng)。如果掛在內(nèi)存文件系統(tǒng)重啟必丟先把路徑改到持久分區(qū)。第二步查看進(jìn)度文件的存儲(chǔ)目錄確認(rèn)它在持久分區(qū)并且斷電后沒(méi)有被清理腳本刪掉。第三步看更新引擎日志里有沒(méi)有明顯的續(xù)傳記錄字樣。有說(shuō)明續(xù)傳機(jī)制在工作沒(méi)有說(shuō)明前兩步有一處沒(méi)做到位。5.2 新槽位起不來(lái)但回滾失效問(wèn)題多半在嘗試計(jì)數(shù)處理現(xiàn)象B 槽寫(xiě)入成功重啟后黑屏再重啟還是黑屏本該回滾到 A 槽卻完全沒(méi)有動(dòng)靜。這類(lèi)問(wèn)題十有八九出在嘗試計(jì)數(shù)的處理順序上。排查鏈路先抓引導(dǎo)程序日志確認(rèn)每次啟動(dòng)實(shí)際選中的是哪個(gè)槽位、計(jì)數(shù)還剩幾次。如果引導(dǎo)程序每次都在試 B說(shuō)明“優(yōu)先啟動(dòng) B”的標(biāo)記沒(méi)被正確清掉但計(jì)數(shù)又沒(méi)在減少多見(jiàn)于計(jì)數(shù)被放在了易失存儲(chǔ)里。再查系統(tǒng)側(cè)負(fù)責(zé)“標(biāo)記成功啟動(dòng)”的代碼確認(rèn)它是在完全啟動(dòng)完成之后才執(zhí)行的。很多時(shí)候移植團(tuán)隊(duì)為圖省事把這個(gè)標(biāo)記提前到內(nèi)核起來(lái)就執(zhí)行結(jié)果系統(tǒng)明明沒(méi)起來(lái)標(biāo)記已經(jīng)糊上去了。5.3 寫(xiě)入中報(bào)空間不足預(yù)算時(shí)少算了一個(gè)臨時(shí)文件現(xiàn)象寫(xiě)入到 60% 時(shí)更新中止日志報(bào)空間不足。明明之前算過(guò)“A 槽用多少、B 槽用多少、預(yù)留多少”怎么會(huì)不夠原因往往出在沒(méi)算這幾項(xiàng)下載暫存文件本身可能還在占用空間如果沒(méi)開(kāi)流式下載日志文件在調(diào)試模式下會(huì)飛速膨脹還有系統(tǒng)分區(qū)的元數(shù)據(jù)、哈希樹(shù)、以及文件系統(tǒng)預(yù)留塊。我的建議是預(yù)算公式別太樂(lè)觀系統(tǒng)鏡像雙份 下載暫存 10% 到 15% 的余量余量按實(shí)際日志增長(zhǎng)和分區(qū)元數(shù)據(jù)估。上線前寫(xiě)一個(gè)自動(dòng)化腳本把每個(gè)分區(qū)的剩余空間打印出來(lái)和人肉預(yù)估對(duì)一遍。6. 落地車(chē)機(jī)項(xiàng)目時(shí)我自己用的參數(shù)配置與驗(yàn)收清單6.1 更新策略參數(shù)按“車(chē)機(jī)不是手機(jī)”來(lái)設(shè)以下是我在實(shí)際車(chē)機(jī)項(xiàng)目里驗(yàn)證過(guò)的參考參數(shù)可以直接抄作業(yè)但務(wù)必按你們自己的硬件調(diào)整更新檢查周期6 小時(shí)一次另外在整車(chē)下電前如果檢測(cè)到未完成的下載允許延長(zhǎng)工作 5 分鐘做一次斷點(diǎn)存檔。電量/電源門(mén)檻整車(chē)電源處于 ON 擋且電瓶電壓高于 12V 才允許寫(xiě)入下載不受此限。網(wǎng)絡(luò)策略免費(fèi) Wi-Fi 自動(dòng)下載蜂窩網(wǎng)絡(luò)只在彈窗確認(rèn)后下載且單次流量上限 500MB超出自動(dòng)暫停。包策略?xún)?yōu)先增量包版本差距超過(guò)兩代轉(zhuǎn)全量包。重試策略下載失敗重試 3 次間隔 5/15/30 分鐘指數(shù)退避?;叶炔呗?% → 5% → 20% → 50% → 100%每檔停留至少一天。6.2 上線前的驗(yàn)收清單把斷電測(cè)試做成自動(dòng)化我在項(xiàng)目里把下面這些場(chǎng)景寫(xiě)成了自動(dòng)化腳本每次打包后必須全綠才能發(fā)版下載過(guò)程中任意時(shí)刻斷電重啟后能從斷點(diǎn)續(xù)傳。寫(xiě)入過(guò)程中斷電重啟后舊槽位能正常啟動(dòng)下次更新能清掉半成品重新來(lái)。B 槽寫(xiě)入成功后人為破壞其內(nèi)核重啟應(yīng)觸發(fā)嘗試計(jì)數(shù)遞減并回退 A 槽。新槽位啟動(dòng)成功后嘗試計(jì)數(shù)被正確清零不會(huì)在若干次重啟后誤回退。電量低于閾值時(shí)系統(tǒng)拒絕進(jìn)入寫(xiě)入階段但允許完成下載??臻g不足時(shí)給出明確提示而不是悄悄失敗。雙槽位來(lái)回升級(jí)降級(jí)循環(huán)十次用戶數(shù)據(jù)不丟。這些用例看著瑣碎但它們才是車(chē)機(jī) OTA 真正值錢(qián)的部分。功能開(kāi)發(fā)可以靠代碼評(píng)審可靠性只能靠反復(fù)打斷電測(cè)出來(lái)。6.3 灰度發(fā)布與失敗決策別一口氣推給所有用戶最后一個(gè)建議永遠(yuǎn)不要一次性把更新推給全部設(shè)備。雙槽位機(jī)制再穩(wěn)也架不住硬件批次差異、外設(shè)兼容性、用戶使用習(xí)慣這些變量?;叶劝l(fā)布的時(shí)候要盯的不是“下載了多少”而是“成功啟動(dòng)新槽位的比例”和“回滾率”這兩個(gè)指標(biāo)。回滾率超過(guò)閾值立刻停發(fā)利用舊槽位還保存著的上一版本引導(dǎo)所有設(shè)備回退。我在項(xiàng)目里設(shè)過(guò)一個(gè)簡(jiǎn)單規(guī)則整體更新成功率低于 95%或者回滾率高于 3%立即凍結(jié)灰度并拉通排查。這個(gè)數(shù)字不是拍腦袋定的是先用幾十臺(tái)內(nèi)部測(cè)試車(chē)跑了兩周統(tǒng)計(jì)出正常波動(dòng)范圍之后再設(shè)的。最后再分享一點(diǎn)個(gè)人體會(huì)。做完這個(gè)車(chē)機(jī) OTA 項(xiàng)目我最大的感悟是更新流程的好壞不在平時(shí)跑得多順而在斷電、斷網(wǎng)、存儲(chǔ)寫(xiě)壞這些最?lèi)毫拥臈l件下能不能兜底。雙槽位、原子切換、嘗試計(jì)數(shù)這套東西聽(tīng)起來(lái)很簡(jiǎn)單但每一環(huán)都要靠反復(fù)的斷電和壞塊測(cè)試才能真正讓人放心。如果你們項(xiàng)目也在做安卓車(chē)機(jī)或手機(jī)的更新方案我建議第一步不是選服務(wù)器也不是寫(xiě)代碼而是先把這套流程在測(cè)試臺(tái)架上完整跑通再來(lái)談灰度。另外一個(gè)小技巧把更新引擎的日志級(jí)別調(diào)到最詳細(xì)并且保留最近三份日志。很多線上問(wèn)題靠日志就能直接定位別等到出事那天才發(fā)現(xiàn)日志已經(jīng)被沖掉了。