Как зафиксировать версию ядра на VPS с Ubuntu
Параметр GRUB_DEFAULT часто игнорируется в облачных образах Ubuntu. Узнайте, как правильно прочитать список меню и закрепить нужное ядро без риска потери доступа по SSH.
Что определяет, какое ядро загружает ваш VPS
То, какое ядро загрузит ваш VPS при следующей перезагрузке, определяется одним сгенерированным файлом — /boot/grub/grub.cfg. Никогда не редактируйте этот файл напрямую. Изменяйте исходные файлы, на основе которых он создаётся, и перегенерируйте его. В облачных образах Ubuntu один из таких исходных файлов поставляется вендором; он может сделать выбор в меню неактуальным. Именно поэтому на арендованном сервере команды GRUB_DEFAULT=1 и update-grub не приносят результата, хотя на обычном ноутбуке эти же действия работают корректно.
Действуйте в следующем порядке. Сначала убедитесь, что вы имеете право выбирать ядро. Прочитайте все входные файлы, включая те, что добавил вендор. Прочитайте сгенерированный выходной файл и подсчитайте количество записей, которые он содержит. Только после этого выбирайте метод фиксации версии ядра. Ошибка в этом процессе на сервере, доступном только по SSH, потребует использования rescue console. Поэтому самые безопасные решения приведены в конце этой страницы, и зачастую они являются наиболее верными.
Сначала убедитесь, что вы можете закрепить свое ядро
uname -r
systemd-detect-virt
ls -1 /boot/vmlinuz-*systemd-detect-virt вывод kvm, qemu или xen означает, что вы используете собственное ядро, и все нижеприведенное применимо. Вывод lxc или openvz означает, что ваш сервер использует ядро хоста, поэтому у вас нет собственного загрузчика и нечего закреплять. В этом случае uname -r сообщает версию, которой вообще нет в /boot/vmlinuz-*, так как запущенное ядро принадлежит хосту, и никакие настройки на вашем диске не могут его изменить.
ls -1 /boot/vmlinuz-* — это актуальный список ядер, между которыми можно выбирать. Если в нем осталась только одна строка, значит, предыдущее ядро уже удалено, и никакие настройки загрузчика не помогут его вернуть. Обычно это происходит во время выполнения autoremove, что стоит изучить, прежде чем удалять старые ядра в Ubuntu на сервере, который вам дорог.
Файл, который вы редактируете, не является файлом, который читает GRUB
/etc/default/grub содержит простые присваивания переменных оболочки. Это входной файл. /boot/grub/grub.cfg — это выходной файл, и он начинается с # DO NOT EDIT THIS FILE и пояснения. Всё, что вы запишете в выходной файл, будет удалено при следующей установке или удалении пакета ядра, так как скрипты пакетов пересоздают его.
cat /usr/sbin/update-grubupdate-grub — это обертка. Она запускает grub-mkconfig -o /boot/grub/grub.cfg, который считывает переменные, выполняет каждый скрипт в /etc/grub.d/ и записывает результат. Две команды, одно направление: входные данные поступают на вход, а на выходе получается grub.cfg.
Что переопределяет ваши настройки: /etc/default/grub.d
grep -rn '^[^#]' /etc/default/grub /etc/default/grub.d/Второй путь — это то, что часто упускают из виду. grub-mkconfig сначала считывает /etc/default/grub, а затем каждый файл *.cfg в /etc/default/grub.d/ в порядке glob-расширения. Изучите код, который это выполняет:
grep -n 'default/grub' /usr/sbin/grub-mkconfigПодключение файлов (sourcing) выполняется как обычный shell-скрипт, поэтому последнее присваивание имеет приоритет. В облачных образах Ubuntu в этой директории содержатся файлы, которые задают параметры, такие как таймаут и командная строка ядра, уже после того, как ваш файл был прочитан. Ваш GRUB_TIMEOUT=10 в /etc/default/grub перезаписывается через мгновение файлом поставщика, который устанавливает значение 0. Команда 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=PARTUUID=..., вместо поиска UUID файловой системы во время загрузки. Поставщик образа устанавливает этот параметр, так как он позволяет одному образу диска надежно загружаться на оборудовании, для которого он не был создан. Второй вызов grep показывает код, который обрабатывает эту переменную в /etc/grub.d/10_linux. Этот скрипт находится на вашем диске, и именно он определяет поведение вашего образа.
Последствие здесь важнее всего: по этому пути генератор записывает прямую запись загрузки вместо полного списка установленных ядер. Посчитайте, что у вас получилось в итоге.
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 не может её разрешить, поэтому он загружает первую запись, которая является тем самым новым ядром, которого вы пытались избежать. grub-set-default также не помогает, так как проблема не в значении по умолчанию. Меню, из которого вы пытаетесь сделать выбор, так и не было создано.
Чтобы вернуть полное меню, переместите файл поставщика в другое место и просмотрите результат перед его применением. grub-mkconfig без -o выводит результат в стандартный поток вывода и ничего не меняет на диске.
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 — это способ, которым образ вашего провайдера находит корневую файловую систему, и его удаление переводит систему на путь поиска. Сделайте снимок состояния (snapshot) перед тем, как запускать update-grub по-настоящему.
Если ваша единственная цель — пережить одно неудачное ядро, остановитесь здесь и используйте более безопасные варианты, описанные ниже. Пересборка загрузочного меню на удаленном сервере ради одного обновления несет больше рисков, чем сама проблема.
Почему привязка к порядковым номерам записей — это ошибка
GRUB_DEFAULT принимает номер, заголовок или идентификатор. Номера отсчитывают записи верхнего уровня, начиная с 0. Вложенная запись использует > в качестве разделителя, поэтому GRUB_DEFAULT="1>2" означает запись с индексом 2 внутри подменю с индексом 1.
Индексы меняются. 10_linux выводит список ядер, начиная с самого нового, поэтому установка ядра сдвигает все старые записи на одну позицию вниз, а удаление — вверх. Ваша тщательно настроенная 1>2 по-прежнему будет разрешаться после таких изменений. Теперь она указывает на другое ядро. Ошибок не возникает, предупреждений нет, и вы узнаете об этом только после перезагрузки.
Идентификаторы не меняются, так как каждый из них содержит версию ядра. Прочитайте свои:
sudo awk -F"'" '/menuentry_id_option/ {print $2, "==>", $4}' /boot/grub/grub.cfgИгнорируйте первые несколько строк вывода, где определяется переменная в заголовке. После этого слева будет заголовок, который видит пользователь, а справа — идентификатор, который нужно передавать инструментам. Для записи внутри подменю объедините идентификатор подменю и идентификатор записи через > именно в таком порядке, как это делается в числовой форме.
Загрузка предыдущего ядра один раз с помощью grub-reboot
Выбор загрузки на один раз — верное решение для удаленного сервера, так как настройка сбрасывается автоматически. 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 будет принята в командной строке, но проигнорирована загрузчиком. Это тот же путь принудительной прямой загрузки из предыдущего раздела, который проявляется здесь во второй раз.
sudo grub-reboot '<the identifier you copied>'
sudo grub-editenv listgrub-editenv list теперь должен вывести строку next_entry=, содержащую именно то, что вы передали. Откройте консоль вашего провайдера в браузере, затем перезагрузитесь и проверьте результат.
sudo rebootuname -rЕсли uname -r показывает старую версию, значит, фиксация сработала. Если отображается новая версия, это означает, что либо идентификатор не разрешился, либо grubenv не считывается. В любом случае машина загрузилась, что и является целью использования однократного режима.
Закрепите выбор с помощью GRUB_DEFAULT=saved
GRUB_DEFAULT=saved заставляет систему брать значение по умолчанию из saved_entry в файле grubenv, а вы задаете это значение с помощью 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 — это другой параметр, который легко спутать с текущим. Он сохраняет последнюю загруженную систему как новую по умолчанию, поэтому выбор следует за последней успешной загрузкой. На сервере это означает, что автоматическая перезагрузка может незаметно изменить вашу привязку. Оставьте этот параметр выключенным, если вам не нужно именно такое поведение.
Привязка по идентификатору все равно может подвести в одном случае. Если удалить ядро, которое он называет, идентификатор перестанет разрешаться, и система вернется к первому пункту меню. Поэтому фиксируйте версию пакета (hold) или исключите это ядро из процесса autoremove.
Вывод меню на консоль провайдера
Для интерактивного выбора меню должно отображаться на экране, однако в облачных образах оно скрыто. Добавьте следующие параметры в файл, который считывается последним, а затем выполните sudo update-grub.
GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu
GRUB_RECORDFAIL_TIMEOUT=10Использование GRUB_TIMEOUT_STYLE=hidden вместе с GRUB_TIMEOUT=0 приводит к тому, что на экране ничего не отображается. В результате администратор, наблюдающий за консолью, видит сразу сообщения ядра и делает вывод, что загрузчик был пропущен. 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 секунд. После завершения работ верните значение тайм-аута на 0.
Более безопасные варианты, чем редактирование загрузчика
Изменение параметров загрузчика на машине, доступной только по SSH, — это самый рискованный вариант из всех представленных. Существуют более простые решения, которые обычно устраняют саму причину проблемы.
Зафиксируйте версии пакетов ядра. Если ваша цель — «не устанавливать более новое ядро», сообщите об этом менеджеру пакетов, а не загрузчику.
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Используйте те имена, которые вывела первая команда, так как в облачных образах часто устанавливается версия virtual или kvm, а не generic. Зафиксированный пакет игнорируется командой apt upgrade, которая сообщает об этом через The following packages have been kept back:, а также пропускается при автоматическом обновлении в Ubuntu. У этого подхода есть цена: зафиксированное ядро перестает получать обновления безопасности, поэтому рассматривайте это как временную меру с установленным сроком и разблокируйте его с помощью sudo apt-mark unhold. Если вы избегаете обновлений ядра из-за того, что перезагрузки вызывают простой, а не из-за проблем с конкретной версией, то live kernel patching на VPS решит эту задачу.
Делайте снапшот перед обновлением. Снапшот позволяет восстановить систему за несколько минут без ввода команд в консоли и риска некорректного изменения загрузчика. Сделайте снапшот, обновитесь, перезагрузитесь, проверьте работу. Если новое ядро работает некорректно, выполните откат — путь загрузки вернется в исходное состояние.
Используйте консоль или rescue-образ для сервера, который уже не загружается. Если сервер перестал загружаться, конфигурация загрузчика — это не то место, где нужно искать решение. Процедура восстановления в таком случае описывается отдельно: что делать, если VPS не загружается после обновления ядра.
Что может пойти не так и какие сообщения вы увидите
Ваши изменения в /boot/grub/grub.cfg исчезли. Был установлен или удален пакет ядра, его скрипт сопровождения выполнил 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 panic с ошибкой VFS: Unable to mount root fs on unknown-block(0,0). Запись, которую вы закрепили, указывает на ядро или 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, и в облачных образах именно этот разрыв является источником путаницы. Сначала читайте сгенерированную конфигурацию. Каждое решение на этой странице основывается на том, что написано в файле на самом деле.
FAQ
Почему параметр GRUB_DEFAULT=1 не меняет ядро, с которым загружается мой VPS?
Потому что в облачных образах Ubuntu сгенерированный /boot/grub/grub.cfg часто содержит только одну запись для загрузки, поэтому индекс 1 ни на что не указывает, и GRUB возвращается к первой записи. Проверьте это с помощью sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg. Ответ — количество записей, равное 1. Причина кроется в GRUB_FORCE_PARTUUID, установленном поставщиком образа в файле в директории /etc/default/grub.d/, что переводит генератор на путь прямой загрузки вместо построения полного списка установленных ядер. Найдите этот файл с помощью grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/.
Как загрузиться с предыдущим ядром только один раз?
Выполните sudo grub-reboot '<identifier>' с идентификатором, скопированным из вашего собственного grub.cfg, а затем перезагрузитесь, предварительно открыв консоль провайдера. GRUB очищает next_entry перед загрузкой, поэтому выбор применяется ровно к одной попытке, и ядро, вызвавшее kernel panic, не будет выбрано повторно. Убедитесь, что значение было применено, с помощью sudo grub-editenv list. Прежде чем полагаться на этот метод, выполните sudo grep -n next_entry /boot/grub/grub.cfg, так как образ, конфигурация которого не загружает grubenv, проигнорирует команду без вывода ошибки.
Что лучше использовать для фиксации: номер записи или идентификатор?
Идентификатор. Номера записей — это позиции в списке, который 10_linux перестраивает, помещая новые версии в начало, поэтому установка или удаление любого ядра сдвигает их, а устаревший 1>2 будет указывать на существующую, но неверную запись без каких-либо предупреждений. Идентификаторы содержат версию ядра, поэтому они либо соответствуют нужному ядру, либо не находят совпадений. Выведите их список с помощью sudo grep -n menuentry_id_option /boot/grub/grub.cfg и скопируйте строку в кавычках, которая следует за каждой записью.
Безопаснее ли удерживать пакет ядра, чем менять загрузчик?
Для достижения стандартной цели — да. sudo apt-mark hold linux-image-virtual linux-headers-virtual предотвращает установку более нового ядра, поэтому путь загрузки не меняется, и риск совершить ошибку при работе через консоль, к которой у вас может не быть доступа, отсутствует. Сначала проверьте названия установленных пакетов ядер на вашей системе с помощью apt list --installed, а затем подтвердите статус удержания с помощью apt-mark showhold. Обратная сторона заключается в том, что удерживаемое ядро не получает исправлений безопасности, поэтому заранее определите, когда вы выполните sudo apt-mark unhold, прежде чем устанавливать удержание.