使用 lvm 內建的 raid 1 功能達成硬碟故障時的高可用性

硬碟是有壽命的消耗品。 如果不想讓某台電腦因為硬碟故障而遺失資料, 或需要重建一堆服務,應使用 raid 1 等磁碟陣列技術, 讓系統在一顆硬碟故障時仍可正常運作。 lvm 在 2.02 時加入了內建的 raid 功能, 可以直接使用 lvm 提供的工具建立 raid, 而無需另外使用 mdadm 等工具手動建立 raid 後再於上層建立 lvm。 在建立 raid 保證高可用性後,應在每顆硬碟上都安裝一次開機啟動程式, 保證預設的開機硬碟故障後,另一顆硬碟仍可在不需手動介入情況下正常開機。

raid 的作法

磁碟陣列 raid 有分為在軟體上實作和在硬體上實作。 身為土砲的自架家機當 server 流派, 我應該不會去碰到硬體 raid。 所以本文介紹的是 linux 的軟體 raid。 另外如果是 zfs 或 btrfs,這類高級檔案系統都是從底層管到頂層, 有自己內建的 raid 作法,不需要靠 mdadm 或 lvm raid。

很早就開始有高可用性的想法, 雖然實際上只壞掉過一顆沒在用的硬碟,所以也沒掉過資料。 一開始從鳥哥開始碰 raid,但鳥哥只介紹了 mdadm, 沒有介紹到 lvm 內建的 raid 功能。 後來動手測試時才發現 lvm 有內建, 試了沒什麼問題就轉用 lvm 內建的了。

lvm 概念

lvm 分為三層:

  1. 第一層物理卷 PV, 對應 linux 中的 塊裝置 , 一般就是整硬碟如 /dev/sda ,也可以只對應一個分割區 /dev/sda1

  2. 第二層邏輯群組 VG , 一個 vg 可以由一個以上的 pv 組成。 vg 的用途是把儲存的空間和物理上的硬碟解耦, 從此只需要關心某個 vg 還有多少容量, 是不是要加新 pv 了。

  3. 第三層邏輯卷 LV, lv 是屬於某個 vg,對外表現為一個正常的塊裝置, 有在建立時指定的容量,可以被 dd mkfs; 此外也可以做一些 lvm 的操作,如 快照

詳細可以去參考 鳥哥的 lvm 教學

建立 lvm

建立 lvm 的指令範例:

  1. 把第一顆硬碟和第二顆的第二個分割區格式化為 pv: pvcreate /dev/sda /dev/sdb2

  2. 建立一個 vg,包含這二個 pv: vgcreate my-vg /dev/sda /dev/sdb2

    在開機時,linux kernel 的 lvm 模組會掃描所有塊裝置, 檢查哪些塊裝置是 pv,然後他們上面有哪些 vg, vg 中有哪些 lv,然後建立出 lv 塊裝置介面。

  3. 在 vg 上建立 lv: lvcreate --size 10G --name my-root my-vglvcreate --size 10G --name my-home my-vg

  4. 之後就可以在 lv 上建立檔案系統,安裝作業系統了。 現在主流的 linux 使用 device mapper, 會把在 /dev 下自動建立和 vg 同名的資料夾, 然後資料夾內自然就是以 lv 名稱為名的塊裝置。

    mkfs -t ext4 /dev/my-vg/my-rootmkfs -t ext4 /dev/my-vg/my-home

在 debian 中 lvm 的指令放在 /sbin 下面, 一般 user 的 $PATH 不一定有包含 sbin,可能要自己加; 另外多數指令也都要在 root 權限下執行。

使用 lvm 指令

lvm 的指令都有對應的 man-page,寫得蠻詳細的, 但有些內容太多會不好讀。 lvm 的指令也都很制式的用對應的層級開頭, 例如想找 lv 相關的指令,就直接打 lv 後按二下 tab 自動補全, 就會列出多數 lv 相關的指令。

如果檢查這些 lvm 指令,會發現他們都是連結到 /sbin/lvm 的符號連結, 就像某些 busybox 環境中,各指令都只是符號連結到 busybox 的執行檔, busybox 會依據呼叫時的檔名來改變行為。

直接執行 lvm 本身,會進入一個 lvm 的互動式環境, 可以直接執行 lvcreate 等 lvm 指令。 或是把想執行的指令當作 lvm 的第一個參數, 如執行 lvm lvresize 就和直接執行 lvresize 等價, 類似的 busybox 也可以用這種方式呼叫。

有些比較受限的系統,例如開機失敗時進入的中繼系統 initramfs, 會只含有 lvm 一個執行檔而不包含那些符號連結, 也就是沒有 lvresize, 此時要執行各個 lvm 指令就得用 lvm lvresize 這種方式。

lvm 的神奇操作

我現在裝系統都會啟用 lvm,方便以後調整分割區容量或換硬碟。

調整分割區大小

我通常都是只有一顆硬碟,又會習慣分 home 和 root 二個分割區。 所會要用切分割的方式裝。 那如果之後發現 root 或 home 不夠大要調整就很麻煩。

如果是 lvm 可以在系統運作中直接調整大小, 當然因為 ext4 的限制,只能調大不能調小。 但如果是解除掛載的離線狀態,就可以調小了。

例如把 root 加大 1G,並自動調整檔案系統的大小: lvresize --size +1G my-vg/my-root --resizefs

不加 --resizefs 的話,就只會把塊裝置容量變大, 但其中的檔案系統仍只和建立時的塊裝置一樣大。 可以用 resize2fs 或對應的工具再調整, resize2fs 在不加參數的情況下, 就是直接把塊裝置中的檔案系統,調整到和塊裝置一樣大。

如果要調小,順序要反過來, 也就是先把檔案系統調小,然後再把塊裝置調小。 如果直接調小塊裝置的話,檔案系統就會有資料被不見, 造成檔案系統損毀。 怕麻煩可以直接用 lvresize 的 --resizefs 參數, lvresize 會自動處理好。

抽換硬碟

如果系統本來在一顆硬碟的 lvm 上,那要換硬碟也很簡單: 直接把新的硬碟做成 pv 加到 vg 裡, 然後把 vg 存在原本那顆硬碟 pv 的資料移到新硬碟, 就能把舊硬碟移出 vg,把硬碟拔走了, 完全不用碰到 dd 或 mkfs 之類的工具。 當然,如果原本的開機程式裝在舊硬碟, 記得得再裝在新硬碟一次。

  1. 把新硬碟做成 pv: pvcreate /dev/sdb1

  2. 把 pv 加到現有的 vg: vgextend my-vg /dev/sdb1

  3. 把 vg 中原本在舊硬碟的資料移走: pvmove /dev/sda1 。 pvmove 可以不指定目標, 總之就是叫 lvm 把存在該 pv 中的資料移到其它 pv。 在本例中,如果 vg 只有這顆和另一顆新硬碟, 那當然就是移到新硬碟上。

  4. 把 pv 從 vg 中移除: vgreduce my-vg /dev/sda1

  5. 如果要把舊硬碟上的 pv 標頭移除: pvremove /dev/sda1, 但不是必要,反正之後 mkfs 時也會直接蓋掉。

這些操作完全可以在開機中進行,在作業系統運作的狀態下, 你放系統的硬碟就從一顆換到另一顆了,很神奇吧。

lvm raid

lvm raid 則是把各種 raid 功能加進到 lvm 中。 早期 lvm 就有一個 mirror,是用來容錯的鏡像功能, 和 raid 1 類似,後來 lvm raid 推出後就取代了原本的 mirror 功能。 但我之前嘗試 lvm raid 1,讀取時似乎沒有加速到 n 倍, 所以想藉 raid 1 提速兼容錯的使用者可能要考慮。

要建立 lvm raid 0 和 1 簡單到不行, 直接在建立 lv 時把 lv 的類別指定為 raid 0 或 1 即可: lvcreate --type raid1 --size 10G --name my-raid1 my-vg 。 lvm 會自行找到 vg 中的二個 pv,各吃掉其 10G 來建立一個 10G 的 raid 1, 可以用 --mirrors $n 來指定除了原本的一份資料外,要備份幾份。

raid0 0 也相同。 其實 lvm 原本就有一個 stripe 功能了, 我不確定二個差在哪,都是把資料分別存到不同的 pv 上。 如果說只是想要 raid 0 的合併容量成一個大硬碟的功能, 其實 lvm 基本的 lv 就是了,raid 0 只是多了讀寫提速的功能。

另外一種作法是把現有的 lv 轉成 raid 1 的 lv: lvconvert --type raid1 my-vg/my-home 。 lvconvert 是另一個用來轉換 lv 類型的工具, 他指定 lv 的方式是用 $vg_name/$lv_name 的格式, 很多 lvm 工具都會用這類格式。 在 lvconvert 轉換後,會開始把資料同步到新 mirror 上, 可以用 lvs 看同步進度。

改成 raid 後,要更新 grub 和 initramfs, 但其實我不確定是不是必要。 grub 就 update-grub 再 grub-install 就好, 但可能要 注意開機磁區的空間夠不夠

之前碰過開機時的 initramfs 系統不能讀取 lvm raid 的問題。 更新 initramfs 就 update-initramfs -u -k all , 但如果更新後仍不能開機,可能是 raid 模組沒有加進去,

initramfs 我也不確定需不需要手動加入 raid 1 的模組。 之前裝的使用 lvm raid 1 的系統有出現過一個奇怪的問題 , 開機後卡在 initramfs 階段,無法進入正常系統。 後來發現是 initramfs 中沒有包含到 raid 1 需要的模組, 手動在 /etc/initramfs-tools/modules 中加入需要的模組即可。 因為不確定需要哪些,我後來就全加:

printf '%s\n' xor async_tx raid6_pq \
        async_xor async_pq async_memcpy async_raid6_recov \
        md_mod raid1 raid456 dm_raid dm_log dm_region_hash dm_mirror lvm \
        >> /etc/initramfs-tools/modules
update-initramfs -k all -u

raid1 類型的 lv,可以試著關機把其中一顆 pv 硬碟拔掉, 再開機仍可以正常讀取;此時就只剩一顆硬碟。 我不確定能不能直接移除仍在使用中的 pv 來測試容錯能力, 像 mdadm 可以用 --fail 來手動標記一顆硬碟為不使用, 但有看到 pvremove 一個 -ff 選項,似乎可以用來強制移除仍在使用的 pv。

如果要再加新硬碟: lvconvert --mirror +1 my-vg/my-home

grub

另外一項很重要的就是開機硬碟設定, raid 可以容錯後,還要可以正常開機, 因為現代電腦的開機是由硬碟起始位置資料去引導的, 而這部份 lvm 管不到,除非用硬體 raid 不然應該都不行。 所以除了讓硬碟容錯,系統資料不會損壞, 還要讓各硬碟都可以用來開機。

grub 開機流程

現代的 linux 可以不需要設置 boot 分區, 就算 root 在 lvm 中也可以直接開機, 因為現在的 grub 有 lvm 模組,可以讀取 lvm 的內容, 但如果是 legacy bios 的話,硬碟開頭要預留足夠的空間塞 grub。

legacy bios 下 grub 的載入分二步, 第一步從硬碟開頭的開機引導程式開始執行, 這部份要找到 linux 中的 grub 資料夾如 /boot/grub , 載入 grub.cfg 等檔案。 第二部就是依 grub.cfg 執行,載入各種模組,然後開機。

所以第一步中,開機引導程式要有掛載該檔案系統的能力。 如果 boot 分割或 boot 所在分割像 root 是 ext2 系列, 那難度要求不高,硬碟在第一個分割區前只要留 512 * 64 = 32Ki 就夠了。 但如果 boot 所在的分割是 lvm,就需要把 lvm 模組也塞在引導磁區, 那 64 sector 就會不夠大,所以開頭要留 512 * 2048 = 1024Ki

印象中在網路上看到,早期 grub 會將壓縮後的 grub 尺寸壓在 32Ki 以下, 才能塞進常見的 64 個磁區的預留空間。 但我用 ubuntu 18 和 debian buster 後的版本,64 磁區好像會不夠。 總之就這樣,細節我也不是很了解。

如果是 uefi 的話,問題就變成 efi 分區要夠大, 但通常是夠的,應該不會有 efi 分區不到 1M。

分割硬碟

以前我們習慣用 mbr 或 gpt 把硬碟切成不同塊裝置使用。 但上面的 lvm 可以發現, 其實可以不用切直接把整顆硬碟都劃給 lvm 當 pv 用。 但把整顆硬碟做成 pv 會發現,沒地方可以放 grub 或其它引導磁區了!

所以除非你不打算用這顆硬碟開機,不然無論是 legacy bios, 或是 efi 都要有分割表的存在。 保險的做法是一樣做分割表,然後只切一個分割區,把該分割區劃成 pv。 如果是 legacy bios,如上一節所述, 就得確定第一個分割前要留 2048 個磁區, efi 則是要多劃一個 efi 分區。

在各硬碟都安裝 grub

如果是 legacy bios,就確保各硬碟開頭的留空都有 2048 磁區就好了, 然後 grub-install --target i386-pc /dev/sda ,sdb 也一樣做一次。 開機順序則是在 bios 裡設定。

如果是 uefi,則要在二顆硬碟都做一個 efi 分區, 總之就是一個 fat 格式的檔案系統,幾百 M 就夠了。 然後分割區標籤設成 EF,如果是 fdisk 就是用 t 命令,編碼是 ef。 如果硬碟的分割表是 mbr,那 efi 分割要是主要分割區,gpt 就沒差。

然後把 efi 掛載到隨便一個地方,例如 mnt 好了: grub-install --target x86_64-efi --efi-directory /mnt , 如果有二顆硬碟,就得在二顆硬碟的 efi 分區都做一次。 efi 的安裝,說實在就是把 efi 格式的開機程式, 放到 efi 分區中的對應目錄下。

至於安裝好後,真正決定開機順序是由主機板上的 efi 儲存的,和硬碟無關。 grub-install 會直接把裝上去的 grub 設為預設開機項目, 如果要手動調整二顆硬碟要誰先誰後, 可以用 linux 中的 efibootmgr 調整, 或是電腦的 efi shell 也有可能提供介面供使用者調整。