Cách ghim kernel khởi động trên VPS Ubuntu an toàn
GRUB_DEFAULT thường không hoạt động trên Ubuntu cloud image. Bài viết hướng dẫn cách kiểm tra menu entry thực tế và ghim kernel chính xác mà không cần dùng đến rescue console.
Điều gì quyết định kernel nào sẽ khởi động trên VPS của bạn
Kernel nào sẽ khởi động trên VPS của bạn được quyết định bởi một file được tạo tự động, /boot/grub/grub.cfg. Bạn không bao giờ được chỉnh sửa file này. Bạn chỉ chỉnh sửa các file đầu vào của nó rồi tạo lại nó. Trên các cloud image của Ubuntu, một trong những file đầu vào này do nhà cung cấp image đưa vào, và nó có thể khiến việc chọn menu trở nên vô nghĩa. Đó là lý do tại sao GRUB_DEFAULT=1 theo sau bởi update-grub không thay đổi được gì trên server thuê, trong khi hai bước này lại hoạt động trên bản cài đặt laptop.
Hãy làm theo thứ tự sau. Xác nhận rằng bạn có quyền chọn kernel. Đọc mọi file đầu vào, bao gồm cả những file do nhà cung cấp thêm vào. Đọc file đầu ra đã được tạo và đếm số lượng mục thực tế mà nó chứa. Chỉ sau đó mới chọn phương pháp ghim (pinning) kernel. Làm sai bước này trên một máy chủ mà bạn chỉ có thể truy cập qua SSH sẽ khiến bạn phải dùng đến rescue console. Vì vậy, các phương án an toàn nhất nằm ở cuối trang này và chúng thường là những lựa chọn đúng đắn.
Trước tiên hãy kiểm tra xem kernel có phải của bạn để thực hiện pin hay không
uname -r
systemd-detect-virt
ls -1 /boot/vmlinuz-*systemd-detect-virt việc in ra kvm, qemu hoặc xen nghĩa là bạn đang chạy kernel riêng và mọi hướng dẫn bên dưới đều áp dụng được. Việc in ra lxc hoặc openvz nghĩa là server của bạn dùng chung kernel với host, do đó không có bootloader riêng và không có gì để pin. Trong trường hợp đó, uname -r báo cáo một phiên bản không hề xuất hiện trong /boot/vmlinuz-*, vì kernel đang chạy thuộc về host và không có thiết lập nào trên ổ đĩa của bạn có thể thay đổi nó.
ls -1 /boot/vmlinuz-* là danh sách thực tế các kernel mà bạn có thể chọn. Nếu nó chỉ chứa một dòng, kernel trước đó đã bị xóa và không có thiết lập bootloader nào có thể khôi phục nó. Điều này thường xảy ra trong quá trình autoremove, một thao tác cần hiểu rõ trước khi bạn dọn dẹp các kernel cũ trên Ubuntu trên một hệ thống mà bạn quan tâm.
File bạn chỉnh sửa không phải là file GRUB đọc
/etc/default/grub chứa các gán biến shell thuần túy. Nó là file đầu vào. /boot/grub/grub.cfg là file đầu ra, và nó bắt đầu bằng # DO NOT EDIT THIS FILE cùng với lý do. Bất cứ thứ gì bạn viết vào file đầu ra sẽ bị mất vào lần tới khi một gói kernel được cài đặt hoặc gỡ bỏ, vì các script của gói đó sẽ tạo lại file này.
cat /usr/sbin/update-grubupdate-grub là một trình bao bọc (wrapper). Nó chạy grub-mkconfig -o /boot/grub/grub.cfg, trình này đọc các biến, chạy mọi script trong /etc/grub.d/, và ghi kết quả. Hai lệnh, một hướng: đầu vào đi vào, grub.cfg đi ra.
Điều gì ghi đè thiết lập của bạn: /etc/default/grub.d
grep -rn '^[^#]' /etc/default/grub /etc/default/grub.d/Đường dẫn thứ hai là phần mọi người thường bỏ sót. grub-mkconfig nạp /etc/default/grub trước, sau đó là mọi tệp *.cfg trong /etc/default/grub.d/ theo thứ tự glob. Hãy đọc đoạn mã thực hiện việc này:
grep -n 'default/grub' /usr/sbin/grub-mkconfigViệc nạp tệp (sourcing) là lệnh shell thuần túy, vì vậy phép gán cuối cùng sẽ có hiệu lực. Các cloud image của Ubuntu phát hành kèm các tệp trong thư mục đó, và chúng thiết lập các thông số như timeout và kernel command line sau khi tệp của bạn đã được đọc. GRUB_TIMEOUT=10 của bạn trong /etc/default/grub bị ghi đè ngay sau đó bởi một tệp của nhà cung cấp, tệp này thiết lập giá trị về 0. Lệnh grep ở trên in ra các phép gán chính xác trên image của bạn, vì vậy hãy đọc các giá trị đó thay vì tin vào câu này.
Quy tắc thực tế rút ra là: hãy đặt các thiết lập của riêng bạn vào một tệp được sắp xếp cuối cùng, ví dụ như /etc/default/grub.d/99-local.cfg, thay vì chỉnh sửa /etc/default/grub. Khi đó, không có tệp nào do image phát hành có thể được thực thi sau bạn.
Tại sao GRUB_FORCE_PARTUUID làm cho việc chọn menu trở nên vô nghĩa
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 ra lệnh cho trình tạo cấu hình tìm filesystem root bằng partition UUID, được ghi trực tiếp vào dòng lệnh kernel dưới dạng root=PARTUUID=..., thay vì tìm kiếm filesystem UUID trong quá trình boot. Nhà cung cấp image thiết lập tùy chọn này để một disk image có thể boot ổn định trên phần cứng mà nó không được tạo ra ban đầu. Lệnh grep thứ hai cho bạn thấy đoạn mã xử lý biến này trong /etc/grub.d/10_linux. Script đó nằm trên ổ đĩa của bạn và nó là cơ sở xác định cách image của bạn hoạt động.
Hệ quả là điều quan trọng ở đây: trên đường dẫn đó, trình tạo sẽ ghi một mục boot trực tiếp thay vì một danh sách đầy đủ các kernel đã cài đặt. Hãy đếm số lượng mục bạn nhận được.
sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg
sudo grep -nE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfgNếu số lượng là 1, sẽ không có mục thứ hai để chọn, vì vậy GRUB_DEFAULT=1 chỉ định một mục không tồn tại. GRUB không thể phân giải nó, nên nó sẽ boot mục đầu tiên, chính là kernel mới mà bạn đang cố tránh. grub-set-default cũng không giúp ích gì, vì giá trị mặc định không phải là phần bị lỗi. Menu mà bạn đang cố chọn chưa bao giờ được tạo ra.
Để lấy lại menu đầy đủ, hãy di chuyển file của nhà cung cấp sang chỗ khác và xem trước kết quả trước khi áp dụng. grub-mkconfig khi không có -o sẽ ghi ra standard output và không thay đổi bất cứ thứ gì trên ổ đĩa.
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) 'Số lượng mục tăng từ 1 lên nhiều hơn có nghĩa là các mục đã xuất hiện sau khi bỏ tùy chọn ép buộc. Chưa có thay đổi nào được ghi lại. Hãy trả lại file cũ nếu số lượng mục thứ hai không đúng như ý, vì PARTUUID bị ép buộc là cách image của nhà cung cấp xác định vị trí filesystem root, và việc xóa nó sẽ chuyển máy sang chế độ tìm kiếm. Hãy tạo snapshot trước khi chạy update-grub thực sự.
Nếu mục tiêu duy nhất của bạn là vượt qua một kernel lỗi, hãy dừng lại ở đây và sử dụng các tùy chọn an toàn hơn ở bên dưới. Việc xây dựng lại menu boot trên một máy chủ từ xa chỉ để tránh một bản cập nhật đơn lẻ mang lại nhiều rủi ro hơn là lợi ích.
Tại sao dùng số thứ tự để ghim entry lại là sai lầm
GRUB_DEFAULT chấp nhận một con số, tiêu đề hoặc định danh. Các con số đếm các entry cấp cao nhất bắt đầu từ 0. Một entry lồng nhau sử dụng > làm dấu phân cách, vì vậy GRUB_DEFAULT="1>2" có nghĩa là entry ở chỉ mục 2 bên trong menu con ở chỉ mục 1.
Các chỉ mục sẽ thay đổi. 10_linux liệt kê các kernel theo thứ tự mới nhất trước, vì vậy việc cài đặt một kernel mới sẽ đẩy mọi entry cũ xuống một vị trí, và việc gỡ bỏ một kernel sẽ kéo chúng lên. Cấu hình 1>2 mà bạn đã cẩn thận thiết lập vẫn sẽ được giải quyết sau đó. Nhưng giờ đây nó lại trỏ đến một kernel khác. Không có lỗi, không có cảnh báo, và bạn chỉ phát hiện ra điều đó sau khi reboot.
Các định danh không thay đổi vì mỗi định danh đều chứa phiên bản kernel. Hãy đọc định danh của bạn:
sudo awk -F"'" '/menuentry_id_option/ {print $2, "==>", $4}' /boot/grub/grub.cfgBỏ qua vài dòng đầu của kết quả, đó là các biến được định nghĩa trong phần header. Sau đó, phía bên trái là tiêu đề mà người dùng nhìn thấy và phía bên phải là định danh bạn truyền cho các công cụ. Đối với một entry bên trong menu con, hãy kết hợp định danh của menu con và định danh của entry bằng >, theo đúng thứ tự đó, giống hệt như cách dùng định dạng số.
Boot kernel cũ một lần bằng grub-reboot
Lựa chọn boot một lần là phương án an toàn trên server từ xa vì nó tự hủy sau khi thực hiện. grub-reboot ghi next_entry vào /boot/grub/grubenv. GRUB đọc biến này, xóa nó đi và lưu giá trị đã xóa trước khi boot bất kỳ thứ gì, vì vậy nếu kernel bị panic, nó sẽ không bị lặp lại ở lần boot sau. Bạn có một lần thử, sau đó máy sẽ tự quay về cấu hình mặc định.
Trước tiên, hãy xác nhận file cấu hình đã tạo của bạn có đọc biến này hay không:
sudo grep -n -B2 -A5 'next_entry' /boot/grub/grub.cfgBạn cần một dòng load_env và một block thiết lập default từ next_entry. Nếu lệnh grep không in ra gì, image của bạn không hề đọc grubenv khi boot, nên grub-reboot sẽ được chấp nhận ở shell nhưng bị bootloader bỏ qua. Đây chính là đường dẫn boot trực tiếp bị ép buộc từ phần trước xuất hiện ở một nơi khác.
sudo grub-reboot '<the identifier you copied>'
sudo grub-editenv listgrub-editenv list lúc này sẽ in ra một dòng next_entry= chứa chính xác những gì bạn đã truyền vào. Mở console của nhà cung cấp trong tab trình duyệt, sau đó reboot và kiểm tra kết quả.
sudo rebootuname -runame -r báo phiên bản cũ hơn nghĩa là lệnh pin đã hoạt động. Nếu báo phiên bản mới nghĩa là định danh không phân giải được hoặc grubenv không được đọc, và dù sao thì máy vẫn lên nguồn, đó chính là mục đích của việc sử dụng lệnh boot một lần.
Thiết lập lựa chọn cố định với GRUB_DEFAULT=saved
GRUB_DEFAULT=saved giúp giá trị mặc định được lấy từ saved_entry trong grubenv, và bạn thiết lập giá trị đó bằng grub-set-default. Cấu hình này vẫn tồn tại sau khi cài đặt kernel mới, vì update-grub ghi đè lên grub.cfg và không bao giờ can thiệp vào 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.cfgLệnh cuối cùng phải in ra set default="${saved_entry}". Nếu nó in ra set default="0", có một file nào đó được nạp sau file của bạn đã ghi đè GRUB_DEFAULT về một giá trị cố định. Hãy liệt kê lại /etc/default/grub.d/ và kiểm tra xem 99-local.cfg có thực sự nằm ở cuối danh sách hay không.
GRUB_SAVEDEFAULT=true là một thiết lập khác và rất dễ nhầm lẫn với thiết lập này. Nó lưu lại bất cứ thứ gì bạn vừa boot làm mặc định mới, vì vậy giá trị mặc định sẽ thay đổi theo lần boot thành công gần nhất. Trên server, điều này có nghĩa là một lần reboot tự động có thể âm thầm thay đổi kernel bạn đã ghim. Hãy tắt nó trừ khi đó là hành vi bạn muốn.
Việc ghim theo định danh vẫn có một điểm yếu. Nếu bạn xóa kernel được chỉ định, định danh đó sẽ không còn phân giải được nữa, khiến hệ thống quay trở lại mục đầu tiên trong danh sách. Vì vậy, hãy giữ lại gói đó (hold) hoặc loại trừ kernel đó khỏi danh sách autoremove.
Đưa menu lên console của nhà cung cấp
Việc chọn lựa tương tác yêu cầu menu phải hiển thị trên màn hình, nhưng các cloud image lại ẩn nó đi. Hãy thêm các dòng này vào file được sắp xếp cuối cùng, sau đó chạy sudo update-grub.
GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu
GRUB_RECORDFAIL_TIMEOUT=10GRUB_TIMEOUT_STYLE=hidden cùng với GRUB_TIMEOUT=0 khiến màn hình không hiển thị gì cả, nên người theo dõi console sẽ thấy các thông báo kernel bắt đầu ngay lập tức và kết luận rằng bootloader đã bị bỏ qua. GRUB_RECORDFAIL_TIMEOUT là timeout riêng biệt được sử dụng sau một lần boot không hoàn tất, và các cloud image cũng đặt nó về 0, đó là lý do tại sao một server vừa boot lỗi vẫn không dừng lại để chờ bạn.
Nếu nhà cung cấp của bạn cung cấp serial console thay vì console đồ họa mà bạn vẫn không thấy gì, nghĩa là GRUB đang ghi vào một terminal mà bạn không thể nhìn thấy. Hãy thêm cả hai dòng cùng lúc, vì dòng đầu tiên chọn đầu ra và dòng thứ hai cấu hình cổng:
GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"Mười giây sẽ được thêm vào mỗi lần boot kể từ bây giờ. Hãy đặt lại timeout về 0 sau khi bạn hoàn tất.
Các tùy chọn an toàn hơn việc chỉnh sửa bootloader
Thay đổi cấu hình bootloader trên một máy chủ mà bạn chỉ có thể truy cập qua SSH là phương án rủi ro nhất trong trang này. Có những giải pháp rẻ hơn và thường giải quyết được vấn đề thực sự.
Giữ nguyên (hold) các gói kernel. Nếu mục tiêu là "không cập nhật lên kernel mới hơn", hãy yêu cầu trình quản lý gói làm điều đó thay vì can thiệp vào 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 showholdHãy sử dụng bất kỳ tên nào mà lệnh đầu tiên hiển thị, vì các cloud image thường cài đặt phiên bản virtual hoặc kvm thay vì generic. Một gói đã được giữ (held) sẽ bị apt upgrade bỏ qua, trình quản lý gói sẽ thông báo việc này bằng The following packages have been kept back:, và nó cũng bị bỏ qua bởi tự động nâng cấp trên Ubuntu. Cái giá phải trả là có thật: một kernel bị giữ sẽ không nhận được các bản vá bảo mật, vì vậy hãy coi đây là một khoảng tạm dừng có thời hạn và giải phóng nó bằng sudo apt-mark unhold. Nếu bạn tránh cập nhật kernel vì việc reboot gây downtime thay vì do kernel đó bị lỗi, live kernel patching trên VPS là giải pháp cho vấn đề đó.
Snapshot trước khi nâng cấp. Một bản snapshot có thể khôi phục trong vài phút, không cần gõ lệnh qua console và không có rủi ro thay đổi bootloader bị áp dụng dở dang. Snapshot, nâng cấp, reboot, xác thực. Nếu kernel mới hoạt động không ổn định, hãy roll back và đường dẫn khởi động sẽ chính xác như trạng thái ban đầu.
Sử dụng console hoặc rescue image cho máy chủ đã bị down. Khi server không thể khởi động, cấu hình bootloader không phải là nơi để bạn sửa lỗi, và quy trình khôi phục đó là một thủ tục riêng biệt: cần làm gì khi VPS không khởi động được sau khi cập nhật kernel.
Những lỗi thường gặp và thông báo bạn sẽ thấy
Chỉnh sửa của bạn trong /boot/grub/grub.cfg đã biến mất. Một gói kernel đã được cài đặt hoặc gỡ bỏ, script của người duy trì gói đã chạy update-grub, và file này được tạo lại từ các đầu vào. Phần header # DO NOT EDIT THIS FILE liệt kê hai vị trí đầu vào. Hãy chỉnh sửa các file đó.
grub-editenv: error: environment block too small. /boot/grub/grubenv bị thiếu hoặc bị cắt cụt. Hãy tạo lại nó bằng sudo grub-editenv /boot/grub/grubenv create, sau đó thiết lập lại giá trị của bạn và xác nhận bằng sudo grub-editenv list.
Kernel được ghim (pinned) bị panic với VFS: Unable to mount root fs on unknown-block(0,0). Mục bạn ghim trỏ đến một kernel hoặc initrd không còn tồn tại trên đĩa, thường là do gói đã bị gỡ bỏ trong khi định danh vẫn còn trong grubenv. Cách khôi phục là boot từ console bằng một mục hoạt động bình thường, sau đó xóa giá trị cũ đi.
uname -r không thay đổi sau khi reboot dù bạn mong đợi nó thay đổi. Hãy kiểm tra ba thứ theo thứ tự: grub-editenv list còn hiển thị giá trị của bạn hay đã bị tiêu thụ; định danh bạn đặt có xuất hiện trong grub.cfg hiện tại không; grub.cfg có chứa dòng set default đọc biến bạn đã đặt không. Một trong ba điều này luôn là nguyên nhân.
Menu tự động xuất hiện sau một lần crash. GRUB ghi lại lần boot thất bại trong grubenv dưới dạng recordfail=1, điều này buộc menu phải hiện ra ở lần boot kế tiếp để người dùng can thiệp. Hãy xóa nó bằng sudo grub-editenv /boot/grub/grubenv unset recordfail sau khi máy đã ổn định.
Câu duy nhất cần ghi nhớ: file bạn chỉnh sửa không phải là file GRUB đọc, và trên các cloud image, khoảng cách giữa chúng chính là nơi gây ra nhầm lẫn. Hãy đọc file cấu hình đã được tạo trước. Mọi quyết định trên trang này đều dựa trên những gì file đó thực sự hiển thị.
FAQ
Tại sao GRUB_DEFAULT=1 không thay đổi kernel mà VPS của tôi khởi động?
Vì trên các cloud image của Ubuntu, /boot/grub/grub.cfg được tạo ra thường chỉ chứa một mục khởi động duy nhất, nên chỉ số 1 không trỏ đến đâu cả và GRUB sẽ quay về mục đầu tiên. Hãy xác nhận bằng sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg. Kết quả trả về là 1. Nguyên nhân là do GRUB_FORCE_PARTUUID, được nhà cung cấp image thiết lập trong một file nằm dưới /etc/default/grub.d/, khiến trình tạo cấu hình đi theo đường dẫn khởi động trực tiếp thay vì xây dựng danh sách đầy đủ các kernel đã cài đặt. Tìm file đó bằng grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/.
Làm thế nào để khởi động kernel trước đó chỉ một lần?
Chạy sudo grub-reboot '<identifier>' với định danh (identifier) được sao chép từ grub.cfg của chính bạn, sau đó reboot trong khi đã mở sẵn console của nhà cung cấp. GRUB sẽ xóa next_entry trước khi khởi động, vì vậy lựa chọn này chỉ áp dụng cho đúng một lần thử và nếu kernel bị panic thì nó sẽ không được thử lại. Xác nhận giá trị đã được ghi nhận bằng sudo grub-editenv list. Trước khi tin tưởng vào nó, hãy chạy sudo grep -n next_entry /boot/grub/grub.cfg, vì một image có cấu hình không load grubenv sẽ bỏ qua lệnh này mà không báo lỗi.
Nên ghim (pin) theo số thứ tự mục hay theo định danh?
Nên dùng định danh. Số thứ tự mục là vị trí trong danh sách mà 10_linux xây dựng lại với kernel mới nhất lên đầu, vì vậy việc cài đặt hoặc gỡ bỏ bất kỳ kernel nào cũng sẽ làm thay đổi thứ tự của chúng, và một 1>2 cũ vẫn có thể trỏ đến một mục thực tế nhưng không đúng ý bạn mà không có cảnh báo nào. Định danh chứa phiên bản kernel, nên chúng sẽ khớp với kernel bạn muốn hoặc không tìm thấy. Liệt kê chúng bằng sudo grep -n menuentry_id_option /boot/grub/grub.cfg và sao chép chuỗi nằm trong dấu ngoặc kép sau mỗi dòng mục.
Việc giữ (hold) gói kernel có an toàn hơn thay đổi bootloader không?
Với mục đích thông thường thì có. sudo apt-mark hold linux-image-virtual linux-headers-virtual ngăn chặn việc cập nhật kernel mới, vì vậy đường dẫn khởi động không bao giờ thay đổi và không có gì sai sót xảy ra từ một console mà có thể bạn không truy cập được. Trước tiên, hãy kiểm tra tên các phiên bản (flavour) đã cài đặt trên máy của bạn bằng apt list --installed, và xác minh trạng thái hold bằng apt-mark showhold. Đánh đổi ở đây là kernel bị hold sẽ không nhận được các bản vá bảo mật, vì vậy hãy quyết định thời điểm bạn sẽ chạy sudo apt-mark unhold trước khi thực hiện lệnh hold.