Ubuntu VPS 如何固定下次開機的 kernel
Ubuntu cloud image 上,GRUB_DEFAULT 可能完全無效。查看實際 GRUB 選單項目與 vendor 設定,再安全固定 VPS 下次開機的 kernel,避免 SSH 伺服器失去救援主控台。
決定 VPS 下次開機所載入的 kernel
VPS 下次開機載入哪個 kernel,是由一個產生的檔案 /boot/grub/grub.cfg 決定。請勿直接編輯這個檔案。應編輯其輸入內容,再重新產生檔案。在 Ubuntu cloud image 上,其中一項輸入來自 image vendor,可能使選單選擇失效。因此,在租用的伺服器上執行 GRUB_DEFAULT=1 後接 update-grub 可能完全沒有作用,但在筆電安裝環境中執行相同兩個步驟卻能正常生效。
請依下列順序操作。先確認 kernel 是否由您選擇。讀取所有輸入檔案,包括 vendor 新增的檔案。再讀取產生的輸出,確認其中實際包含多少個項目。完成這些步驟後,才能選擇固定 kernel 的方法。在只能透過 SSH 連線的機器上操作錯誤,可能會失去救援主控台;因此,本頁末尾提供的做法最安全,且通常也是正確選擇。
先確認 kernel 可由你固定
uname -r
systemd-detect-virt
ls -1 /boot/vmlinuz-*systemd-detect-virt 顯示 kvm、qemu 或 xen,表示你執行的是自己的 kernel,以下內容均適用。顯示 lxc 或 openvz,表示伺服器共用主機的 kernel,因此你沒有自己的 bootloader,也沒有可固定的 kernel。在這種情況下,uname -r 顯示的版本完全不會出現在 /boot/vmlinuz-* 中,因為執行中的 kernel 屬於主機,磁碟上的任何設定都無法變更它。
ls -1 /boot/vmlinuz-* 是你實際可以選擇的 kernel 清單。如果其中只有一行,表示先前的 kernel 已經刪除,任何 bootloader 設定都無法將它還原。這通常發生在 autoremove 過程中;如果你要在重要的伺服器上清理 Ubuntu 上的舊 kernel,應先了解這個機制。
你編輯的檔案不是 GRUB 讀取的檔案
/etc/default/grub 僅包含 shell 變數指派,是輸入檔案。/boot/grub/grub.cfg 是輸出檔案,開頭為 # DO NOT EDIT THIS FILE 及其原因。你寫入輸出檔案的任何內容,都會在下次安裝或移除 kernel 套件時消失,因為這些套件的 script 會重新產生該檔案。
cat /usr/sbin/update-grubupdate-grub 是 wrapper。它會執行 grub-mkconfig -o /boot/grub/grub.cfg;grub-mkconfig -o /boot/grub/grub.cfg 讀取變數、執行 /etc/grub.d/ 中的所有 script,並寫入結果。兩個 command,只有一個方向:輸入寫入 grub.cfg。
/etc/default/grub.d 會覆寫你的設定
grep -rn '^[^#]' /etc/default/grub /etc/default/grub.d/第二個路徑才是最容易忽略的部分。grub-mkconfig 會先載入 /etc/default/grub,接著依 glob 順序載入 /etc/default/grub.d/ 中的每個 *.cfg 檔案。請查看執行這項工作的程式碼:
grep -n 'default/grub' /usr/sbin/grub-mkconfig載入動作使用一般 shell,因此最後一個指定值會生效。Ubuntu cloud image 會在該目錄中提供檔案,並在讀取你的檔案後,設定逾時時間與 kernel command line 等項目。你在 /etc/default/grub 中設定的 GRUB_TIMEOUT=10,稍後會被將其設為 0 的 vendor 檔案覆寫。上方的 grep 會列出你映像檔中的確切指定內容,因此請以那些內容為準,不要直接相信本句說明。
實務規則如下:請將自訂設定放在排序結果最後的檔案中,例如 /etc/default/grub.d/99-local.cfg,不要修改 /etc/default/grub。如此一來,映像檔提供的內容就無法排在你的設定之後。
為何 GRUB_FORCE_PARTUUID 會讓選單選項失去作用
grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/
grep -n GRUB_FORCE_PARTUUID /etc/grub.d/10_linux
sudo grep -n 'root=PARTUUID' /boot/grub/grub.cfgGRUB_FORCE_PARTUUID 會指示產生器依分割區 UUID 尋找 root 檔案系統,並直接將 root=PARTUUID=... 寫入 kernel command line,而不是在開機時搜尋檔案系統 UUID。映像檔供應商設定此變數,是因為這能讓同一個磁碟映像檔在未用來建置該映像檔的硬體上可靠開機。第二個 grep 會顯示 /etc/grub.d/10_linux 中處理此變數的程式碼。該 script 位於你自己的磁碟上,也是判定映像檔實際行為的依據。
這裡重要的是其結果:在這條路徑上,產生器會寫入直接開機項目,而不是完整列出已安裝的 kernel。請計算最後產生的項目數量。
sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg
sudo grep -nE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg如果數量是 1,就沒有第二個項目可供選取,因此 GRUB_DEFAULT=1 指向不存在的項目。GRUB 無法解析該項目,所以會開機進入第一個項目,也就是你原本想避開的新 kernel。grub-set-default 同樣沒有幫助,因為問題不在預設值。你要選取的選單根本沒有產生。
若要恢復完整選單,請先將供應商檔案移至其他位置,再預覽結果,確認後才套用。單獨執行 grub-mkconfig 且不帶 -o 時,會將結果寫到 standard output,不會修改磁碟上的任何內容。
sudo grub-mkconfig 2>/dev/null | grep -cE '^\s*(menuentry|submenu) '
sudo mkdir -p /root/grub-backup
sudo mv /etc/default/grub.d/<the file your grep named> /root/grub-backup/
sudo grub-mkconfig 2>/dev/null | grep -cE '^\s*(menuentry|submenu) '如果數量從 1 增加到數個,表示移除強制設定後,這些項目便會出現。目前尚未寫入任何內容。如果第二次計數結果不正確,請將檔案放回原處,因為強制使用 PARTUUID 是供應商映像檔定位 root 檔案系統的方式;移除後,機器會改用搜尋路徑。實際執行 update-grub 前,請先建立 snapshot。
如果你的唯一目標是避開一個有問題的 kernel,請在此停止,改用下方較安全的選項。在遠端伺服器上重建開機選單,只為避開一次升級,所承擔的風險高於問題本身的價值。
為什麼不應固定使用項目編號
GRUB_DEFAULT 接受編號、標題或識別碼。編號會從 0 開始計算頂層項目。巢狀項目使用 > 作為分隔符號,因此 GRUB_DEFAULT="1>2" 表示子選單索引 1 中索引 2 的項目。
索引會變動。10_linux 會將核心依新到舊列出,因此安裝核心會讓所有較舊的項目向下移動一位,移除核心則會讓它們向上移動。你原本仔細設定的 1>2 之後仍能解析成功,但現在指向的是另一個核心。不會出現錯誤,也不會顯示警告;你會在重新開機後才發現問題。
識別碼不會變動,因為每個識別碼都包含核心版本。請讀取你的識別碼:
sudo awk -F"'" '/menuentry_id_option/ {print $2, "==>", $4}' /boot/grub/grub.cfg請忽略輸出的前幾行,因為那些是標頭中定義的變數。之後,左側是讀者看到的標題,右側則是你傳給工具的識別碼。對於子選單中的項目,請依照數字形式的順序,使用 > 將子選單識別碼與項目識別碼連接起來。
使用 grub-reboot 一次性啟動先前的 kernel
在遠端伺服器上,一次性選擇是正確做法,因為它會自動還原。grub-reboot 會將 next_entry 寫入 /boot/grub/grubenv。GRUB 讀取該變數後會清除它,並在啟動任何項目之前儲存清除後的值,因此即使 kernel 發生 panic,下次開機也不會再次嘗試。伺服器只會嘗試一次,之後會自行回到正常的預設項目。
先確認產生的設定檔確實會讀取該變數:
sudo grep -n -B2 -A5 'next_entry' /boot/grub/grub.cfg你應該會看到 load_env 行,以及將 default 設為 next_entry 的區塊。如果 grep 沒有輸出任何內容,表示你的映像檔在開機時根本不會讀取 grubenv,因此 grub-reboot 雖然會在 shell 中被接受,卻會遭 bootloader 忽略。這與上一節的強制直接開機路徑相同,只是現在出現在另一個位置。
sudo grub-reboot '<the identifier you copied>'
sudo grub-editenv list現在 grub-editenv list 應該會輸出 next_entry= 行,其中包含你傳入的完整內容。在瀏覽器分頁中開啟供應商的主控台,然後重新開機並檢查結果。
sudo rebootuname -r如果 uname -r 顯示較舊的版本,表示指定項目已生效。如果顯示新版本,表示識別碼未解析成功,或未讀取 grubenv;無論哪種情況,伺服器都能正常啟動,這正是使用一次性形式的目的。
讓設定透過 GRUB_DEFAULT=saved 持續生效
GRUB_DEFAULT=saved 會從 grubenv 的 saved_entry 取得預設值,而您可以使用 grub-set-default 設定該值。安裝核心時仍會保留這項設定,因為 update-grub 會重寫 grub.cfg,但不會變更 grubenv。
echo 'GRUB_DEFAULT=saved' | sudo tee /etc/default/grub.d/99-local.cfg
sudo update-grub
sudo grub-set-default '<the identifier you copied>'
sudo grub-editenv list
sudo grep -n 'set default' /boot/grub/grub.cfg最後一個指令必須輸出 set default="${saved_entry}"。如果輸出 set default="0",表示某個在您檔案之後載入的檔案,將 GRUB_DEFAULT 設回字面值。因此請再次列出 /etc/default/grub.d/,並確認 99-local.cfg 的排序位置確實最後。
GRUB_SAVEDEFAULT=true 是另一項設定,很容易與本設定混淆。它會將您剛才啟動的項目儲存為新的預設值,因此預設值會跟隨最近一次成功啟動的項目。在伺服器上,這表示無人值守的重新開機可能會悄悄變更您固定的核心。除非您確實需要這種行為,否則請維持停用。
依識別碼固定核心仍有一種情況會失效。移除該識別碼所指向的核心後,系統便無法解析這個識別碼,並回到第一個項目。因此也應保留該套件,或避免讓 autoremove 移除該核心。
將選單顯示在供應商主控台
互動式選擇需要在畫面上顯示選單,但雲端映像檔會將其隱藏。請將以下內容放入排序最後的檔案中,然後執行 sudo update-grub。
GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu
GRUB_RECORDFAIL_TIMEOUT=10GRUB_TIMEOUT_STYLE=hidden 搭配 GRUB_TIMEOUT=0 時完全不會顯示任何內容,因此查看主控台的人會立即看到核心訊息開始出現,並以為 bootloader 被略過了。GRUB_RECORDFAIL_TIMEOUT 是開機未完成後使用的獨立逾時設定。雲端映像檔也會將它設為 0,所以伺服器剛開機失敗時,仍不會停止並等待你的操作。
如果供應商提供的是 serial console,而不是圖形主控台,但你仍然看不到任何內容,表示 GRUB 正在寫入你無法查看的終端機。請同時加入以下兩行,因為第一行選取輸出,第二行設定連接埠:
GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"從現在起,每次開機都會增加 10 秒。完成後,請將 timeout 設回 0。
編輯 bootloader 以外的較安全選項
在只能透過 SSH 連線的機器上修改 bootloader 輸入,是本頁風險最高的選項。還有成本更低的作法,而且通常能直接解決真正的問題。
保留 kernel 套件。 如果目標是「不要替我安裝較新的 kernel」,應該將這項要求交給套件管理器,而不是交給 bootloader。
apt list --installed 2>/dev/null | grep -E '^linux-(image|headers|generic|virtual|kvm|aws|azure|gcp|oracle)'
sudo apt-mark hold linux-image-virtual linux-headers-virtual
apt-mark showhold請使用第一個命令列出的名稱,因為 cloud image 通常會安裝 virtual 或 kvm flavour,而不是 generic。標記為 hold 的套件會被 apt upgrade 略過,並以 The following packages have been kept back: 顯示;在 Ubuntu 上也會被 unattended upgrades 略過。這麼做確實有代價:被 hold 的 kernel 不會再接收安全修正,因此應將它視為有期限的暫停,並使用 sudo apt-mark unhold 解除 hold。如果避免更新 kernel 的原因是重新開機會造成停機,而不是某個 kernel 有問題,請改看 VPS 的 live kernel patching。
在升級前建立 snapshot。 snapshot 可在數分鐘內還原,不需要透過 console 輸入,也不會有 bootloader 變更只套用一半的風險。建立 snapshot、升級、重新開機,再確認結果。如果新的 kernel 運作異常,直接 rollback,boot path 會完全恢復原狀。
已停止運作的主機應使用 console 或 rescue image。 伺服器無法開機後,bootloader 設定不是修復問題的地方;這條復原路徑有自己的處理程序:VPS 在 kernel 更新後無法開機時的處理方式。
會發生什麼問題,以及你會看到的訊息
你對 /boot/grub/grub.cfg 的修改消失了。 系統安裝或移除了 kernel 套件,其 maintainer script 執行了 update-grub,然後根據輸入檔重新產生該檔案。# DO NOT EDIT THIS FILE 標頭會列出兩個輸入位置。請修改這些位置。
grub-editenv: error: environment block too small。 /boot/grub/grubenv 遺失或內容不完整。使用 sudo grub-editenv /boot/grub/grubenv create 重新建立,然後再次設定你的值,並以 sudo grub-editenv list 確認。
固定的 kernel 以 VFS: Unable to mount root fs on unknown-block(0,0) 發生 kernel panic。 你固定的項目指向已不在磁碟上的 kernel 或 initrd。通常是因為套件遭移除,但識別碼仍留在 grubenv 中。復原方式是從主控台開機至可正常運作的項目,然後清除過時的值。
預期重新開機後會變更的 uname -r 沒有變更。 依序檢查三件事:grub-editenv list 是否仍顯示你的值,或該值已被使用;你設定的識別碼是否出現在目前的 grub.cfg 中;grub.cfg 是否包含會讀取你所設定變數的 set default 行。這三項其中之一每次都能解釋原因。
當機後,選單自行出現。 GRUB 會在 grubenv 中將開機失敗記錄為 recordfail=1,因此下一次開機時強制顯示選單,讓人員介入處理。確認機器恢復正常後,使用 sudo grub-editenv /boot/grub/grubenv unset recordfail 清除該值。
最值得記住的一句話是:你編輯的檔案不是 GRUB 讀取的檔案;在 cloud image 上,兩者之間的差異正是造成混淆的原因。請先讀取產生的設定檔。這一頁的所有判斷都以該檔案的實際內容為依據。
FAQ
為什麼 GRUB_DEFAULT=1 不會變更 VPS 啟動的 kernel?
因為在 Ubuntu cloud image 中,產生的 /boot/grub/grub.cfg 通常只包含單一啟動項目,因此索引 1 不對應任何項目,GRUB 會退回使用第一個項目。使用 sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg 確認。若計數為 1,原因就在這裡。根本原因是 GRUB_FORCE_PARTUUID;image vendor 在 /etc/default/grub.d/ 下的檔案中設定了這個值,使產生器採用直接啟動路徑,而不是建立所有已安裝 kernel 的完整清單。使用 grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/ 找出該檔案。
如何只啟動上一次使用的 kernel?
使用從自己的 grub.cfg 複製出的識別碼執行 sudo grub-reboot '<identifier>',然後在 provider console 已開啟的情況下重新開機。GRUB 會在啟動前清除 next_entry,因此這個選擇只會套用一次;發生 kernel panic 的 kernel 也不會再次嘗試。使用 sudo grub-editenv list 確認值是否已寫入。在依賴這項功能前,先執行 sudo grep -n next_entry /boot/grub/grub.cfg,因為設定未載入 grubenv 的 image 會無錯誤地忽略此命令。
應該依項目編號還是識別碼固定 kernel?
應依識別碼。項目編號是 10_linux 以最新項目優先重建的清單位置,因此安裝或移除任何 kernel 都會使編號改變;過時的 1>2 仍可能解析為實際存在但錯誤的項目,且不會顯示任何警告。識別碼包含 kernel 版本,因此只會對應到指定的 kernel,或解析失敗。使用 sudo grep -n menuentry_id_option /boot/grub/grub.cfg 列出識別碼,並複製每一行項目後方的引號字串。
暫停 kernel package 更新是否比變更 bootloader 更安全?
以通常的目標而言,是。sudo apt-mark hold linux-image-virtual linux-headers-virtual 會阻止較新的 kernel 安裝,因此啟動路徑不會變更,也不必依賴可能無法使用的 console 進行操作。先使用 apt list --installed 檢查自己的主機上已安裝的 flavour 名稱,再使用 apt-mark showhold 確認是否已暫停更新。代價是暫停更新的 kernel 不會收到安全性修補,因此請在執行暫停更新前,決定何時執行 sudo apt-mark unhold。