วิธีตั้งค่า Kernel บูตเริ่มต้นบน VPS Ubuntu อย่างปลอดภัย
แก้ไขปัญหา GRUB_DEFAULT ไม่ทำงานบน Ubuntu Cloud Image ด้วยการตรวจสอบรายการเมนูจริงและตั้งค่า Kernel ที่ต้องการอย่างแม่นยำ เพื่อหลีกเลี่ยงการติดค้างที่หน้า Rescue Console
สิ่งที่กำหนดว่า VPS ของคุณจะบูตด้วยเคอร์เนลใด
สิ่งที่กำหนดว่า VPS ของคุณจะบูตด้วยเคอร์เนลใดในครั้งถัดไปคือไฟล์ที่ถูกสร้างขึ้นมาหนึ่งไฟล์ นั่นคือ /boot/grub/grub.cfg คุณไม่ควรแก้ไขไฟล์ดังกล่าวโดยตรง แต่ให้แก้ไขไฟล์ที่เป็นอินพุตแล้วสร้างไฟล์นั้นขึ้นมาใหม่ ในอิมเมจระบบคลาวด์ของ Ubuntu อินพุตส่วนหนึ่งมาจากผู้ให้บริการอิมเมจ ซึ่งอาจทำให้การเลือกเมนูไม่มีผล นี่คือเหตุผลว่าทำไมการใช้ GRUB_DEFAULT=1 ตามด้วย update-grub จึงไม่เกิดการเปลี่ยนแปลงใดๆ บนเซิร์ฟเวอร์ที่เช่ามา ในขณะที่ขั้นตอนเดียวกันนี้สามารถใช้งานได้บนการติดตั้งบนแล็ปท็อป
ให้ดำเนินการตามลำดับนี้ ยืนยันก่อนว่าคุณมีสิทธิ์เลือกเคอร์เนลเอง อ่านไฟล์อินพุตทุกไฟล์ รวมถึงไฟล์ที่ผู้ให้บริการเพิ่มเข้ามา อ่านผลลัพธ์ที่ถูกสร้างขึ้นและนับจำนวนรายการที่มีอยู่จริง จากนั้นจึงค่อยเลือกวิธีการล็อกเวอร์ชัน (pinning) การทำขั้นตอนนี้ผิดพลาดบนเครื่องที่คุณเข้าถึงได้ผ่าน SSH เท่านั้น จะทำให้คุณต้องใช้ rescue console ดังนั้นคำตอบที่ปลอดภัยที่สุดจะอยู่ที่ท้ายหน้านี้ และมักจะเป็นวิธีที่ถูกต้องเสมอ
ตรวจสอบก่อนว่า kernel เป็นของคุณเพื่อทำการ pin
uname -r
systemd-detect-virt
ls -1 /boot/vmlinuz-*systemd-detect-virt การแสดงผล kvm, qemu หรือ xen หมายความว่าคุณรัน kernel ของคุณเอง และเนื้อหาทั้งหมดด้านล่างนี้จะมีผลกับคุณ การแสดงผล lxc หรือ openvz หมายความว่าเซิร์ฟเวอร์ของคุณใช้ kernel ร่วมกับโฮสต์ ดังนั้นคุณจึงไม่มี bootloader ของตัวเองและไม่มีสิ่งใดให้ pin ในกรณีนี้ uname -r จะรายงานเวอร์ชันที่ไม่ปรากฏอยู่ใน /boot/vmlinuz-* เลย เนื่องจาก kernel ที่กำลังทำงานอยู่เป็นของโฮสต์ และไม่มีการตั้งค่าใดบนดิสก์ของคุณที่จะเปลี่ยนแปลงมันได้
ls -1 /boot/vmlinuz-* คือรายการ kernel จริงที่คุณสามารถเลือกได้ หากในรายการมีเพียงบรรทัดเดียว แสดงว่า kernel ก่อนหน้านี้ถูกลบไปแล้ว และไม่มีการตั้งค่า bootloader ใดที่จะนำมันกลับมาได้ เหตุการณ์นี้มักเกิดขึ้นระหว่างการทำ autoremove ซึ่งเป็นเรื่องที่ควรทำความเข้าใจก่อนที่คุณจะ ล้าง kernel เก่าบน Ubuntu ในเครื่องที่คุณให้ความสำคัญ
ไฟล์ที่คุณแก้ไขไม่ใช่ไฟล์ที่ GRUB อ่าน
/etc/default/grub เก็บค่าตัวแปร shell แบบธรรมดาไว้ มันเป็นไฟล์ขาเข้า ส่วน /boot/grub/grub.cfg คือผลลัพธ์ที่ได้ และไฟล์นี้จะขึ้นต้นด้วย # DO NOT EDIT THIS FILE พร้อมระบุเหตุผล ทุกสิ่งที่คุณเขียนลงในไฟล์ผลลัพธ์จะหายไปในครั้งถัดไปที่มีการติดตั้งหรือลบแพ็กเกจ kernel เนื่องจากสคริปต์ของแพ็กเกจเหล่านั้นจะสร้างไฟล์ขึ้นมาใหม่
cat /usr/sbin/update-grubupdate-grub เป็น wrapper ตัวหนึ่ง มันจะรัน 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 cloud images จะมาพร้อมกับไฟล์ในไดเรกทอรีดังกล่าว ซึ่งจะกำหนดค่าต่างๆ เช่น timeout และ kernel command line หลังจากที่ไฟล์ของคุณถูกอ่านไปแล้ว GRUB_TIMEOUT=10 ของคุณใน /etc/default/grub จึงถูกเขียนทับในเวลาต่อมาโดยไฟล์ของผู้ผลิตที่ตั้งค่าไว้เป็น 0 คำสั่ง grep ด้านบนจะแสดงการกำหนดค่าที่แท้จริงบน image ของคุณ ดังนั้นให้อ่านค่าเหล่านั้นแทนที่จะเชื่อประโยคนี้เพียงอย่างเดียว
กฎในทางปฏิบัติที่ตามมาคือ: ให้ใส่การตั้งค่าของคุณเองลงในไฟล์ที่ถูกเรียงลำดับเป็นลำดับสุดท้าย เช่น /etc/default/grub.d/99-local.cfg แทนที่จะแก้ไข /etc/default/grub โดยตรง วิธีนี้จะทำให้ไม่มีไฟล์ใดที่มากับ image สามารถทำงานหลังจากคุณได้
เหตุใด 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 สั่งให้ตัวสร้าง (generator) ค้นหาระบบไฟล์ราก (root filesystem) ด้วย partition UUID โดยเขียนลงในบรรทัดคำสั่งของ kernel โดยตรงในรูปแบบ root=PARTUUID=... แทนที่จะค้นหาด้วย filesystem UUID ในระหว่างการบูต ผู้จำหน่ายอิมเมจกำหนดค่านี้ไว้เพื่อให้ disk image หนึ่งชุดสามารถบูตบนฮาร์ดแวร์ที่ไม่ได้ถูกสร้างมาเพื่อมันได้อย่างน่าเชื่อถือ คำสั่ง grep ที่สองจะแสดงโค้ดที่ทำงานตามตัวแปรนี้ใน /etc/grub.d/10_linux สคริปต์ดังกล่าวอยู่ในดิสก์ของคุณเองและเป็นตัวกำหนดว่าอิมเมจของคุณทำงานอย่างไร
ผลลัพธ์ที่ตามมาคือประเด็นสำคัญในที่นี้: ในเส้นทางดังกล่าว ตัวสร้างจะเขียนรายการบูตแบบระบุเจาะจงแทนที่จะเป็นรายการ 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 คือวิธีที่อิมเมจของผู้ให้บริการของคุณใช้ระบุตำแหน่งระบบไฟล์ราก และการลบออกจะทำให้เครื่องเปลี่ยนไปใช้เส้นทางการค้นหาแทน ให้ทำ snapshot ก่อนที่คุณจะรัน update-grub จริง
หากเป้าหมายเดียวของคุณคือการรับมือกับ kernel ที่มีปัญหาเพียงตัวเดียว ให้หยุดที่ขั้นตอนนี้และใช้ตัวเลือกที่ปลอดภัยกว่าซึ่งระบุไว้ด้านล่าง การสร้างเมนูบูตใหม่บนเซิร์ฟเวอร์ระยะไกลเพื่อหลีกเลี่ยงการอัปเกรดเพียงครั้งเดียวมีความเสี่ยงสูงเกินกว่าปัญหาที่เกิดขึ้นจริง
เหตุผลที่การระบุตำแหน่งด้วยหมายเลขลำดับเป็นวิธีที่ไม่ถูกต้อง
GRUB_DEFAULT ยอมรับทั้งหมายเลขลำดับ, ชื่อหัวข้อ หรือตัวระบุ (identifier) หมายเลขลำดับจะนับรายการระดับบนสุดโดยเริ่มจาก 0 สำหรับรายการที่อยู่ภายในเมนูย่อยจะใช้ > เป็นตัวคั่น ดังนั้น GRUB_DEFAULT="1>2" จึงหมายถึงรายการที่อยู่ในลำดับที่ 2 ภายในเมนูย่อยลำดับที่ 1
ลำดับของรายการสามารถเปลี่ยนแปลงได้ 10_linux จะแสดงรายการเคอร์เนลโดยเรียงจากเวอร์ชันใหม่ล่าสุด ดังนั้นการติดตั้งเคอร์เนลใหม่จะเลื่อนรายการเก่าทั้งหมดลงไปหนึ่งลำดับ และการลบเคอร์เนลออกจะเลื่อนรายการเหล่านั้นขึ้นมา ค่า 1>2 ที่คุณตั้งไว้อย่างระมัดระวังจะยังคงทำงานได้หลังจากเกิดการเปลี่ยนแปลงดังกล่าว แต่จะกลายเป็นการชี้ไปยังเคอร์เนลตัวอื่นแทน ระบบจะไม่แจ้งเตือนข้อผิดพลาดใดๆ และคุณจะทราบถึงปัญหานี้ก็ต่อเมื่อรีบูตเครื่องไปแล้วเท่านั้น
ตัวระบุ (identifier) จะไม่มีการเปลี่ยนแปลงตำแหน่งเนื่องจากแต่ละตัวระบุมีเวอร์ชันของเคอร์เนลรวมอยู่ด้วย คุณสามารถตรวจสอบตัวระบุของคุณได้ด้วยคำสั่ง:
sudo awk -F"'" '/menuentry_id_option/ {print $2, "==>", $4}' /boot/grub/grub.cfgให้ข้ามบรรทัดแรกๆ ของผลลัพธ์ซึ่งเป็นส่วนของการกำหนดตัวแปรในส่วนหัวไป หลังจากนั้น ด้านซ้ายมือคือชื่อหัวข้อที่ผู้ใช้เห็น และด้านขวามือคือตัวระบุที่คุณต้องใช้กับเครื่องมือต่างๆ สำหรับรายการที่อยู่ภายในเมนูย่อย ให้เชื่อมตัวระบุของเมนูย่อยและตัวระบุของรายการเข้าด้วยกันด้วย > ตามลำดับนั้น เช่นเดียวกับรูปแบบการใช้หมายเลขลำดับทุกประการ
การบูต kernel รุ่นก่อนหน้าหนึ่งครั้งด้วย grub-reboot
การเลือกบูตเพียงครั้งเดียวเป็นวิธีที่เหมาะสมสำหรับเซิร์ฟเวอร์ระยะไกล เพราะระบบจะคืนค่ากลับเป็นปกติเองโดยอัตโนมัติ คำสั่ง grub-reboot จะเขียนค่า next_entry ลงใน /boot/grub/grubenv ตัว GRUB จะอ่านตัวแปรนี้ ล้างค่าทิ้ง และบันทึกค่าที่ล้างแล้ว ก่อน ที่จะเริ่มบูตสิ่งใด ดังนั้นหาก kernel เกิดอาการ panic ระบบจะไม่พยายามบูตซ้ำด้วย kernel เดิมในการบูตครั้งถัดไป คุณจะมีโอกาสลองหนึ่งครั้ง จากนั้นเครื่องจะกลับไปใช้ค่าเริ่มต้นตามปกติด้วยตัวมันเอง
ก่อนอื่นให้ตรวจสอบว่าไฟล์ config ที่สร้างขึ้นมีการอ่านตัวแปรดังกล่าวหรือไม่:
sudo grep -n -B2 -A5 'next_entry' /boot/grub/grub.cfgคุณควรจะเห็นบรรทัด load_env และบล็อกที่กำหนดค่า default จาก next_entry หากคำสั่ง grep ไม่แสดงผลลัพธ์ใดๆ แสดงว่า image ของคุณไม่ได้อ่านค่า grubenv ในระหว่างการบูต ดังนั้นคำสั่ง grub-reboot จะถูกยอมรับใน shell แต่จะถูก bootloader เพิกเฉย ซึ่งเป็นปัญหาเดียวกับเส้นทางการบูตตรงที่ระบุไว้ในส่วนก่อนหน้า
sudo grub-reboot '<the identifier you copied>'
sudo grub-editenv listตอนนี้ grub-editenv list ควรแสดงบรรทัด next_entry= ที่มีค่าที่คุณระบุไว้พอดี ให้เปิดหน้าจอ console ของผู้ให้บริการผ่านเบราว์เซอร์ จากนั้นสั่ง reboot และตรวจสอบผลลัพธ์
sudo rebootuname -rหาก uname -r แสดงผลเป็นเวอร์ชันที่เก่ากว่า แสดงว่าการปักหมุด (pin) สำเร็จ หากแสดงผลเป็นเวอร์ชันใหม่ แสดงว่าตัวระบุ (identifier) ไม่สามารถแก้ไขค่าได้ หรือ grubenv ไม่ถูกอ่าน ซึ่งไม่ว่าอย่างไรเครื่องก็ยังสามารถบูตขึ้นมาได้ ซึ่งเป็นจุดประสงค์หลักของการใช้คำสั่งแบบครั้งเดียวทิ้ง (one shot)
ทำให้การตั้งค่าคงอยู่ถาวรด้วย GRUB_DEFAULT=saved
GRUB_DEFAULT=saved จะทำให้ค่าเริ่มต้นถูกดึงมาจาก saved_entry ในไฟล์ grubenv และคุณสามารถกำหนดค่าดังกล่าวได้ด้วยคำสั่ง grub-set-default การตั้งค่านี้จะยังคงอยู่แม้มีการติดตั้ง kernel ใหม่ เนื่องจาก 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 เป็นการตั้งค่าที่แตกต่างออกไปและมักสับสนกับค่านี้ได้ง่าย โดยมันจะบันทึกรายการที่คุณเพิ่งบูตเข้าใช้งานให้กลายเป็นค่าเริ่มต้นใหม่โดยอัตโนมัติ ซึ่งหมายความว่าค่าเริ่มต้นจะเปลี่ยนไปตามการบูตครั้งล่าสุดที่สำเร็จ สำหรับเซิร์ฟเวอร์แล้ว การตั้งค่านี้อาจทำให้การรีบูตโดยไม่มีผู้ดูแลเปลี่ยนค่าที่คุณล็อกไว้ไปโดยไม่รู้ตัว ควรปิดการตั้งค่านี้ไว้เว้นแต่คุณจะต้องการพฤติกรรมดังกล่าว
การล็อกรายการด้วย identifier ยังคงมีจุดอ่อนอยู่ประการหนึ่ง คือหากคุณลบ kernel ที่ระบุไว้ออก identifier นั้นจะหาปลายทางไม่พบ และระบบจะถอยกลับไปเลือกรายการแรกแทน ดังนั้นคุณควรทำ package hold ไว้ หรือตั้งค่าไม่ให้ kernel ดังกล่าวถูกลบออกโดยคำสั่ง autoremove
การนำเมนูเข้าสู่คอนโซลของผู้ให้บริการ
การเลือกแบบโต้ตอบจำเป็นต้องแสดงเมนูบนหน้าจอ แต่ cloud image มักจะซ่อนเมนูนี้ไว้ ให้ใส่ค่าเหล่านี้ลงในไฟล์ที่ถูกประมวลผลเป็นลำดับสุดท้าย จากนั้นจึงรัน sudo update-grub
GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu
GRUB_RECORDFAIL_TIMEOUT=10GRUB_TIMEOUT_STYLE=hidden เมื่อใช้ร่วมกับ GRUB_TIMEOUT=0 จะไม่แสดงผลใดๆ เลย ทำให้ผู้ที่เฝ้าดูคอนโซลเห็นข้อความของ kernel เริ่มทำงานทันทีและสรุปว่า bootloader ถูกข้ามไป GRUB_RECORDFAIL_TIMEOUT คือค่า timeout แยกต่างหากที่ใช้หลังจากบูตไม่สำเร็จ ซึ่ง cloud image มักตั้งค่าไว้ที่ 0 เช่นกัน นี่คือเหตุผลที่เซิร์ฟเวอร์ที่บูตล้มเหลวไม่หยุดรอให้คุณเข้าไปจัดการ
หากผู้ให้บริการของคุณให้คอนโซลแบบ serial แทนแบบกราฟิกแล้วคุณยังไม่เห็นอะไรเลย แสดงว่า GRUB กำลังเขียนข้อมูลไปยัง terminal ที่คุณมองไม่เห็น ให้เพิ่มทั้งสองบรรทัดนี้พร้อมกัน เพราะบรรทัดแรกใช้เลือก output และบรรทัดที่สองใช้กำหนดค่าพอร์ต:
GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"นับจากนี้ไปจะมีการเพิ่มเวลา 10 วินาทีในการบูตทุกครั้ง ให้ตั้งค่า timeout กลับเป็น 0 เมื่อคุณดำเนินการเสร็จสิ้น
ทางเลือกที่ปลอดภัยกว่าการแก้ไข bootloader
การเปลี่ยนค่า input ของ bootloader บนเครื่องที่คุณเข้าถึงได้ผ่าน SSH เท่านั้น เป็นทางเลือกที่มีความเสี่ยงสูงสุดในหน้านี้ ยังมีวิธีแก้ปัญหาที่ต้นทุนต่ำกว่าและมักจะแก้ปัญหาที่แท้จริงได้ดีกว่า
การตรึงเวอร์ชันแพ็กเกจ kernel (Hold the kernel packages): หากเป้าหมายคือ "ไม่ต้องการให้ระบบอัปเดต kernel ใหม่" ให้แจ้งความประสงค์นี้กับ package manager แทนการไปแก้ไขที่ 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ให้ใช้ชื่อแพ็กเกจตามที่คำสั่งแรกแสดงผล เนื่องจากอิมเมจบนคลาวด์มักจะติดตั้ง kernel รุ่น virtual หรือ kvm แทนที่จะเป็น generic หาก kernel รุ่นใหม่ปรากฏขึ้นในช่วงที่มีการรีเฟรชอิมเมจและคุณสงสัยว่าตัว release มีการเปลี่ยนแปลงไปจากเดิม ความจริงคือไม่มีการเปลี่ยนแปลง เพราะ point release คือการรวมอัปเดตที่คุณได้รับอยู่แล้วเข้าไว้ในสื่อติดตั้งใหม่ ซึ่งจะไม่ส่งผลกระทบใดๆ ต่อเซิร์ฟเวอร์ที่ได้รับการแพตช์มาแล้ว แพ็กเกจที่ถูกตรึงไว้ (held package) จะถูกข้ามโดยคำสั่ง apt upgrade ซึ่งจะแจ้งเตือนด้วยสถานะ The following packages have been kept back: และจะถูกข้ามโดย unattended upgrades บน Ubuntu เช่นกัน ต้นทุนที่ต้องแลกคือ kernel ที่ถูกตรึงไว้จะไม่ได้รับแพตช์ความปลอดภัย ดังนั้นให้ถือว่าเป็นการหยุดพักชั่วคราวและควรปลดล็อกด้วยคำสั่ง sudo apt-mark unhold หากคุณหลีกเลี่ยงการอัปเดต kernel เพียงเพราะการรีบูตทำให้เกิด downtime ไม่ใช่เพราะ kernel มีปัญหา การทำ live kernel patching บน VPS คือคำตอบสำหรับกรณีนี้
ทำ snapshot ก่อนการอัปเกรด: การทำ snapshot ช่วยให้กู้คืนระบบได้ภายในไม่กี่นาที โดยไม่ต้องพิมพ์คำสั่งผ่าน console และไม่มีความเสี่ยงจากการที่ bootloader ถูกแก้ไขไปเพียงครึ่งเดียว ให้ทำ snapshot, อัปเกรด, รีบูต และตรวจสอบ หาก kernel ใหม่ทำงานผิดปกติ ให้ roll back กลับมา ซึ่งจะทำให้ boot path กลับไปเป็นสถานะเดิมทุกประการ
ใช้ console หรือ rescue image สำหรับเครื่องที่ล่มไปแล้ว: เมื่อเซิร์ฟเวอร์ไม่สามารถบูตได้ การแก้ไขไฟล์ config ของ bootloader ไม่ใช่วิธีการแก้ไขที่ถูกต้อง และกระบวนการกู้คืนนั้นมีขั้นตอนเฉพาะของมัน: สิ่งที่ควรทำเมื่อ VPS บูตไม่ขึ้นหลังการอัปเดต kernel
สิ่งที่ผิดพลาดและข้อความที่คุณจะพบ
การแก้ไข /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 อ่าน และบน cloud image ช่องว่างระหว่างไฟล์ทั้งสองคือจุดที่ทำให้เกิดความสับสน ให้อ่านไฟล์ config ที่ถูกสร้างขึ้นก่อนเสมอ การตัดสินใจทุกอย่างในหน้านี้เป็นผลมาจากสิ่งที่ไฟล์นั้นระบุไว้จริง
FAQ
ทำไมการตั้งค่า GRUB_DEFAULT=1 ถึงไม่เปลี่ยน kernel ที่ VPS ของฉันใช้บูต?
เนื่องจากใน Ubuntu cloud image นั้น /boot/grub/grub.cfg ที่ถูกสร้างขึ้นมักจะมีรายการบูตเพียงรายการเดียว ดังนั้นดัชนีที่ 1 จึงไม่มีชื่อเรียกและ GRUB จะย้อนกลับไปใช้รายการแรกแทน ให้ตรวจสอบด้วย sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg ซึ่งคุณจะพบว่ามีจำนวนรายการเพียง 1 รายการ สาเหตุเกิดจาก GRUB_FORCE_PARTUUID ซึ่งถูกกำหนดโดยผู้ให้บริการอิมเมจไว้ในไฟล์ภายใต้ /etc/default/grub.d/ ทำให้ตัวสร้างรายการบูตเลือกใช้เส้นทางการบูตโดยตรงแทนที่จะสร้างรายการ kernel ทั้งหมดที่ติดตั้งไว้ คุณสามารถค้นหาไฟล์ดังกล่าวได้ด้วย grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/
ฉันจะบูตด้วย kernel รุ่นก่อนหน้าเพียงครั้งเดียวได้อย่างไร?
ให้รัน sudo grub-reboot '<identifier>' โดยใช้ตัวระบุ (identifier) ที่คัดลอกมาจาก grub.cfg ของคุณเอง จากนั้นให้รีบูตโดยเปิดหน้าต่าง console ของผู้ให้บริการค้างไว้ GRUB จะล้างค่า next_entry ทิ้งก่อนที่จะบูต ดังนั้นการเลือกนี้จะมีผลเพียงครั้งเดียวเท่านั้น และหาก kernel เกิดอาการ panic ระบบจะไม่พยายามบูตซ้ำด้วย kernel เดิม ให้ตรวจสอบว่าค่าถูกบันทึกไว้แล้วด้วย sudo grub-editenv list ก่อนที่จะใช้งานจริง ให้รัน sudo grep -n next_entry /boot/grub/grub.cfg เสียก่อน เพราะหากอิมเมจมีการตั้งค่าที่ไม่โหลด grubenv คำสั่งดังกล่าวจะถูกเพิกเฉยโดยไม่มีการแจ้งเตือนข้อผิดพลาดใดๆ
ฉันควรระบุค่าด้วยหมายเลขลำดับหรือตัวระบุ?
ควรใช้ตัวระบุ หมายเลขลำดับคือตำแหน่งในรายการที่ 10_linux จะเรียงลำดับใหม่โดยเอาเวอร์ชันล่าสุดขึ้นก่อน ดังนั้นการติดตั้งหรือลบ kernel ใดๆ จะทำให้ลำดับเปลี่ยนไป และค่า 1>2 ที่ล้าสมัยอาจยังคงชี้ไปยังรายการที่ถูกต้องในเชิงเทคนิคแต่ไม่ใช่รายการที่คุณต้องการ โดยที่ระบบจะไม่แสดงคำเตือนใดๆ ทั้งสิ้น ตัวระบุจะมีเวอร์ชันของ kernel รวมอยู่ด้วย ดังนั้นมันจะตรงกับ kernel ที่คุณต้องการหรือล้มเหลวในการค้นหาไปเลย ให้แสดงรายการตัวระบุด้วย sudo grep -n menuentry_id_option /boot/grub/grub.cfg และคัดลอกข้อความในเครื่องหมายคำพูดที่ปรากฏในแต่ละบรรทัดรายการ
การล็อก (hold) แพ็กเกจ kernel ปลอดภัยกว่าการแก้ไข bootloader หรือไม่?
สำหรับวัตถุประสงค์ทั่วไป คำตอบคือใช่ sudo apt-mark hold linux-image-virtual linux-headers-virtual จะป้องกันไม่ให้ kernel รุ่นใหม่ถูกติดตั้งเข้ามา ทำให้เส้นทางการบูตไม่เปลี่ยนแปลงและไม่มีโอกาสเกิดข้อผิดพลาดจาก console ที่คุณอาจเข้าถึงไม่ได้ ให้ตรวจสอบชื่อ flavour ของ kernel ที่ติดตั้งอยู่ในเครื่องของคุณก่อนด้วย apt list --installed และตรวจสอบสถานะการล็อกด้วย apt-mark showhold ข้อแลกเปลี่ยนคือ kernel ที่ถูกล็อกไว้จะไม่ได้รับแพตช์ความปลอดภัย ดังนั้นควรตัดสินใจให้ดีว่าจะรัน sudo apt-mark unhold เมื่อใดก่อนที่จะทำการล็อกแพ็กเกจ