Btrfs 只讀事件
概述
2023年6月30日中午,編譯機上的 Grafana 發出告警郵件,稱某個監控項無數據了。晚些時候,由於收到多封告警郵件,開始檢查情況。在退出 zsh 寫命令歷史時出現「read-only filesystem」的字樣,lilydjwg 意識到它的 btrfs 文件系統成為強制只讀狀態。隨即造成無法正常運行服務。[1]
2023年7月1日晚間22:00左右,在新購的機器上恢復了老編譯機上傳來的快照的方式,編譯機復活,恢復服務。
2023年7月7日凌晨04:00左右,老編譯機上嘗試死馬當活馬醫的方案,恢復了 btrfs 。
時間線
時刻 | 誰 | 事件・操作 | 參考 |
---|---|---|---|
2023-06-30 午間 | Grafana | 報警稱某個監控項無數據,但面板顯示有正常數據 | |
2023-06-30 晚上 | lilydjwg | 更多 Grafana 告警郵件。退出 zsh 時發現 read-only filesystem ,察覺到 btrfs 被強迫只讀了 | |
2023-06-30 19:17 | lilydjwg | 請求 phoenixlzx 進入 archiso | |
2023-06-30 19:38 | phoenixlzx | 登入系統確認到 tty 大量只讀導致的報錯 | |
2023-06-30 19:44 | phoenixlzx | 從 iDRAC 嘗試掛載虛擬光碟重啟進入 Archiso ,未識別到引導 | |
2023-06-30 19:44 | phoenixlzx | archiso 的引導失敗導致引導進入正常系統,依舊 ro 。此前或此時可能有一陣可寫,快照腳本產生了額外快照並在 ro 前試圖刪除快照 | |
2023-06-30 19:46 | phoenixlzx | 從荷蘭再次嘗試進入 archiso ,引導 copytoram | |
2023-06-30 20:49 | lilydjwg | 成功登入 archiso | |
2023-06-30 20:53 | lilydjwg | 初步嘗試修復未果, farseerfc 建議嘗試 clear_cache 和 zero-log 騰出一部分 metadata 空間,也未成功 | 見詳述 |
2023-06-30 21:28 | farseerfc | 開始深入調查和更多嘗試 | 見詳述 |
2023-07-01 00:17 | farseerfc | 因為窮盡了比較穩妥的恢復方案,暫停嘗試恢復,優先備份,開始發送最新快照(2023-06-30-18)到 vultr.farseerfc.me | |
眾人 | 檢討更換機器 | ||
2023-07-01 01:35 | phoenixlzx | 下單 hetzner 新機 | |
2023-07-01 02:14 | farseerfc | 發現新機的 hetzner 的 rescue system 裡面自動 installimage 裝了 archlinux minimal ,需要重新進入 rescue system ,暫時擱置 | |
2023-07-01 14:33 | phoenixlzx | 以不自動裝系統的模式重新進入 rescue system | |
2023-07-01 15:03 | farseerfc | 在新機上通過 installimage 重新安裝 archlinux | |
2023-07-01 15:38 | farseerfc | 啟動進入新機上的 archlinux 中,開始發送快照到新機上 | |
2023-07-01 18:09 | farseerfc | 新機上的接收進度超過 vultr.farseerfc.me 上接收的量,所以停下了 vultr.farseerfc.me 上的發送 | |
2023-07-01 20:47 | farseerfc | 新機上接收完畢,在快照基礎上配置網絡、微碼、 grub 、 fstab | |
2023-07-01 21:34 | farseerfc | 新機上重啟到新的根,新編譯機復活 | |
2023-07-01 21:48 | lilydjwg | 配置自動快照腳本,lilac 開始加班 | |
2023-07-01 21:57 | felixonmars | 新機上開啟 bees , build.archlinuxcn.org DNS cname 指向新機。TTL更新後編譯機開始正常服務 | |
2023-07-02 11:34 | farseerfc | 老機上繼續嘗試死馬當活馬醫的方案,試圖保留數據挪動分區,可惜挪動分區後狀態依舊 | 見詳述 |
2023-07-02 22:37 | lilydjwg | 發現新機失聯 | |
2023-07-02 23:57 | phoenixlzx | 向 hetzner 要求 KVM Console | |
2023-07-03 00:15 | phoenixlzx | 獲得新機的 KVM Console 登入信息 | |
2023-07-03 00:22 | farseerfc | 發現新機 KVM Console 沒有視頻信號,也沒有重啟權限,詢問 phoenixlzx 有沒有辦法重啟 | |
2023-07-03 00:25 | phoenixlzx | 重啟新機 | |
2023-07-03 00:26 | farseerfc | 視頻信號顯示處於 BIOS Setup 界面,實施正常啟動 | |
眾人 | 調查失聯原因,沒有明顯證據解釋失聯,懷疑過熱 | ||
2023-07-04 13:46 | lilydjwg | 觀測到老機發送到新機上的快照有包資料庫損壞的現象,猜測老機故障重啟後有一段時間可寫,並創建了快照。在新機上手動修復 | |
2023-07-06 23:22 | farseerfc | 在老機上繼續嘗試死馬當活馬醫 | 見詳述 |
2023-07-07 04:05 | farseerfc | 成功恢復老機上的 btrfs,能夠正常 balance 和正常寫入 | 見詳述 |
事發時現象
編譯機一直以來有個問題:硬碟有多少用多少。前一任編譯機用的 1T 硬碟,是剛剛夠用,現在換 2T 了,結果 btrfs filesystem usage 一看就發現,空閒200多G數據是真的(因此沒有觸發相關報警),但是元數據滿了,也沒有未分配空間了。
編譯機的 btrfs 配置是由兩塊 NVMe SSD 構成,data 使用 RAID0 ,metadata / system 使用 RAID1 ,事發時的占用情況如下:
# btrfs filesystem usage /mnt
Overall:
Device size: 1.72TiB
Device allocated: 1.72TiB
Device unallocated: 2.09MiB
Device missing: 0.00B
Device slack: 20.00GiB
Used: 1.49TiB
Free (estimated): 238.78GiB (min: 238.78GiB)
Free (statfs, df): 238.78GiB
Data ratio: 1.00
Metadata ratio: 2.00
Global reserve: 512.00MiB (used: 0.00B)
Multiple profiles: no
Data,RAID0: Size:1.60TiB, Used:1.36TiB (85.40%)
/dev/nvme0n1p3 817.50GiB
/dev/nvme1n1p2 817.50GiB
Metadata,RAID1: Size:64.00GiB, Used:63.30GiB (98.91%)
/dev/nvme0n1p3 64.00GiB
/dev/nvme1n1p2 64.00GiB
System,RAID1: Size:8.00MiB, Used:144.00KiB (1.76%)
/dev/nvme0n1p3 8.00MiB
/dev/nvme1n1p2 8.00MiB
Unallocated:
/dev/nvme0n1p3 1.05MiB
/dev/nvme1n1p2 1.05MiB
可見 data profile 尚有 245G 可用空間,而 metadata profile 僅剩約 716M 可用,不再有 Unallocated 導致不能再分配新的 metadata 塊組。
此時可以 ro 方式掛載,在 ro 方式掛載時讀取數據一切正常,然而一旦嘗試 mount -oremount,rw 就會在一兩分鐘內變為只讀,同時內核 dmesg 中報錯說由於 metadata 空間不足導致 transaction 提交失敗。
在幾次 umount 和 mount 的過程中, farseerfc 發現報錯強制變為 ro 後,metadata 的空間並沒有完全用滿,反而是 Global Reserve 空間接近滿了:
# btrfs filesystem usage /mnt
Overall:
Device size: 1.72TiB
Device allocated: 1.72TiB
Device unallocated: 2.09MiB
Device missing: 0.00B
Device slack: 20.00GiB
Used: 1.49TiB
Free (estimated): 238.78GiB (min: 238.78GiB)
Free (statfs, df): 238.78GiB
Data ratio: 1.00
Metadata ratio: 2.00
Global reserve: 512.00MiB (used: 511.48MiB)
Multiple profiles: no
Data,RAID0: Size:1.60TiB, Used:1.36TiB (85.40%)
/dev/nvme0n1p3 817.50GiB
/dev/nvme1n1p2 817.50GiB
Metadata,RAID1: Size:64.00GiB, Used:63.30GiB (98.91%)
/dev/nvme0n1p3 64.00GiB
/dev/nvme1n1p2 64.00GiB
System,RAID1: Size:8.00MiB, Used:144.00KiB (1.76%)
/dev/nvme0n1p3 8.00MiB
/dev/nvme1n1p2 8.00MiB
Unallocated:
/dev/nvme0n1p3 1.05MiB
/dev/nvme1n1p2 1.05MiB
Global reserve 用於安排一次 transaction 中寫入的 metadata profile 的量,Global reserve 用滿導致 transaction 不能正常提交,從而被強制轉入 ro 。
本次問題解決難點在於 data 和 metadata 分別為 RAID0 和 RAID1 profile ,每次塊組分配時需要在不同設備上同時分配各一塊連續空間,所以需要在一個 transaction 內同時增加兩個額外設備解決空間不足的問題。
恢復過程詳述
初步嘗試
首先嘗試加個設備再 balance 一下,結果由於文件系統只讀,添加設備失敗。嘗試 remount rw,btrfs 說文件系統的只讀原因是因為報錯,所以不支持 remount rw。
由於這個 btrfs 文件系統掛載為根目錄,並不能在線卸載,只能嘗試離線修理。重啟後通過 iDRAC 進入 archiso 並嘗試搶在報錯之前添加兩個設備。btrfs 占用的兩個分區前各有一個 40G 左右的 swap 分區,此時沒有被使用,可以用來添加設備。但是添加設備並沒有成功。會卡住一會兒,然後報錯「No space left on device」。按 farseerfc 的建議,clear_cache 和 zero-log 都試過了,但並沒有解決問題。有人建議把大文件 truncate 一下,看看能不能剛好釋放出 1G 的連續空間出來,但是編譯機上有定時快照,truncate 了也不會立即釋放空間。
深入調查
如上述事發時的現象所述,故障的直接原因是
- metadata 空間不足,沒有 unallocated 可以分配新的 metadata
- global reserve 空間不足以安排一次 transaction 的提交
為緩解 1. metadata 空間不足,解決故障的一般性方案是通過 device add 增加新的設備,或者 filesystem resize 擴展現有設備的大小。而直接原因 2. global reserve 導致了 device add 添加新設備的嘗試不成功。
這種情況下,為了壓榨出一些額外的 metadata 空間,做過如下嘗試:
1. zero-log
通過 btrfs rescue zero-log 扔掉日誌樹(tree log tree),可空出日誌樹所占用的一點空間。日誌樹記錄著最新一次 transaction 之後,對文件系統進行的寫入和 fsync 操作日誌,扔掉這部分內容會導致文件系統狀態回到最近一次 transaction 提交時的狀態。
後續掛載時通過掛載選項 notreelog 可避免在掛載時重建 tree log tree 。
2. clear_cache,nospace_cache
通過掛載選項 clear_cache (或者 btrfs check --clear-space-cache v1|v2 命令),可以清理掉 free space cache (v1) 或者 free space tree (v2) 。free space cache (v1) 是 root tree 中的幾個特殊 inode ,可能占用不了多少空間,而 free space tree (v2) 是獨立的 btree ,理論上最多可以占用 1bit/4KiB 容量的空間。
後續掛載時通過掛載選項 nospace_cache 可避免在掛載時重建 free space cache / free space tree 。
3. nossd
SSD 優化試圖先(在內存中)分配連續的一塊地址區間,然後在這連續地址內進行小塊分配。關閉 ssd 優化雖然會犧牲性能,不過潛在允許分配更零碎,見縫插針地使用 metadata profile 中所剩無幾的空間。
結合如上掛載選項,仍未能避免問題。在嘗試 device add 時可以看到內核中的類似如下的輸出:
[ +0.000008] BTRFS info (device nvme0n1p3: state A): dumping space info:
[ +0.000007] BTRFS info (device nvme0n1p3: state A): space_info DATA
has 256392597504 free, is not full
[ +0.000007] BTRFS info (device nvme0n1p3: state A): space_info
total=1755577581568, used=1499184852992, pinned=0, reserved=0,
may_use=0, readonly=131072 zone_unusable=0
[ +0.000010] BTRFS info (device nvme0n1p3: state A): space_info METADATA has -540672 free, is full
[ +0.000006] BTRFS info (device nvme0n1p3: state A): space_info total=68719476736, used=67883679744, pinned=355762176, reserved=479903744, may_use=540672, readonly=131072 zone_unusable=0
[ +0.000009] BTRFS info (device nvme0n1p3: state A): space_info SYSTEM has 8208384 free, is not full
[ +0.000006] BTRFS info (device nvme0n1p3: state A): space_info total=8388608, used=147456, pinned=32768, reserved=0, may_use=0, readonly=0 zone_unusable=0
[ +0.000008] BTRFS info (device nvme0n1p3: state A): global_block_rsv: size 536870912 reserved 540672
[ +0.000006] BTRFS info (device nvme0n1p3: state A): trans_block_rsv: size 0 reserved 0
[ +0.000005] BTRFS info (device nvme0n1p3: state A): chunk_block_rsv: size 0 reserved 0
[ +0.000005] BTRFS info (device nvme0n1p3: state A): delayed_block_rsv: size 0 reserved 0
[ +0.000004] BTRFS info (device nvme0n1p3: state A): delayed_refs_rsv: size 14152892416 reserved 0
[ +0.000010] BTRFS: error (device nvme0n1p3: state A) in __btrfs_free_extent:3053: errno=-28 No space left
[ +0.001688] BTRFS info (device nvme0n1p3: state EA): forced readonly
最為關鍵的是 space_info METADATA has -540672 free, is full 這裡指出,出現問題時 metadata 的缺口可能僅有 528K,33 個 metadata leafnode 大小。
另外如果嘗試 rw 掛載並不做任何事情,等待內核自動提交事務,類似的輸出中可發現 space_info METADATA has -32768 free, is full ,缺口僅有 32K , 2 個 leafnode 。
每次經過上述嘗試之後,都需要 umount 卸載文件系統。好在 btrfs 使用的事務更新機制,每次卸載 btrfs 之後,狀態都恢復到了出事時那一刻,在 btrfs 層面無論做什麼嘗試都不會再進一步毀壞數據。
以及為了打時間差,上述嘗試使用以下這樣的操作:
$ umount /mnt
$ mount -oro,nologreplay,... /dev/nvme0n1p3 /mnt # 首先进行 ro 挂载,加上上述选项
$ mount -oremount,rw /mnt ; btrfs device add /dev/nvme0n1p2 /dev/nvme1n1p1 /mnt # 转为 rw 挂载后立刻实施操作
可惜都未能趕在 global reserve 用完 512M 前添加上設備。
遷移
因短時間內無法原地修復 btrfs,直接下單了位於 Hetzner 的新機器,並使用 btrfs send 遷移。7月1日晚22:00左右,服務恢復。
死馬當活馬醫:保留數據挪動分區
前述恢復方案中使用 btrfs device add 而非 btrfs filesystem resize 是因為可用的空間(原本的 SWAP 分區)位於 btrfs 分區之前。
由於內核日誌中顯示的 metadata 缺口只有約 528K ,考慮 filesystem resize 所需額外 metadata 是否可能小於 device add 所需 metadata 空間,於是嘗試挪動分區。
# cfdisk /dev/nvme0n1 # 缩小 nvme0n1p2 ,划出 10G 空间
# cfdisk /dev/nvme1n1 # 缩小 nvme1n1p1 ,划出 10G 空间
# echo "+,+" | sfdisk --move-data -N 3 /dev/nvme0n1 # 将 nvme0n1p3 向前扩展到填满空闲空间,并移动数据
# echo "+,+" | sfdisk --move-data -N 2 /dev/nvme1n1 # 将 nvme1n1p2 向前扩展到填满空闲空间,并移动数据
挪動分區後,正常掛載時,如前依舊會在一兩分鐘內轉入 ro 。
嘗試 btrfs rescue fix-device-size ,未能通過修復的方式改寫設備大小。
然後嘗試 btrfs filesystem resize max:
$ mount -oro,nologreplay,nossd,skip_balance,nospace_cache,notreelog /dev/nvme0n1p3 /mnt
$ mount -oremount,rw /mnt ; btrfs filesystem resize max /mnt
這次嘗試後,能成功 resize 一個設備,在嘗試 resize 第二個設備前 transaction 提交失敗,轉入 ro 狀態。此後 btrfs filesystem usage 顯示的利用率如下:
Overall:
Device size: 1.73TiB
Device allocated: 1.72TiB
Device unallocated: 10.00GiB
Device missing: 0.00B
Device slack: 10.00GiB
Used: 1.49TiB
Free (estimated): 248.79GiB (min: 243.79GiB)
Free (statfs, df): 248.78GiB
Data ratio: 1.00
Metadata ratio: 2.00
Global reserve: 512.00MiB (used: 511.48MiB)
Multiple profiles: no
Data,RAID0: Size:1.60TiB, Used:1.36TiB (85.40%)
/dev/nvme0n1p3 817.50GiB
/dev/nvme1n1p2 817.50GiB
Metadata,RAID1: Size:64.00GiB, Used:63.20GiB (98.76%)
/dev/nvme0n1p3 64.00GiB
/dev/nvme1n1p2 64.00GiB
System,RAID1: Size:8.00MiB, Used:144.00KiB (1.76%)
/dev/nvme0n1p3 8.00MiB
/dev/nvme1n1p2 8.00MiB
Unallocated:
/dev/nvme0n1p3 10.00GiB
/dev/nvme1n1p2 1.05MiB
根源問題依舊是 global resize 用滿導致 transaction 提交無法繼續。
死馬當活馬醫:嘗試 6.4.1 內核版本
6.4.1 內核相比 6.3.x 內核系列,在 global resize 計算方面多了兩個修正:
- 修復 global reserve 空間預估時,unlink 所需開銷的計算方式:unlink 所需開銷從 5.19 開始變大了 10 -> 12 ,使得 global reserve 空間預估可能不準確。
- btrfs: remove check for NULL block reserve at btrfs_block_rsv_check()
- btrfs: simplify variables in btrfs_block_rsv_refill()
- btrfs: fix calculation of the global block reserve's size
- btrfs: use a constant for the number of metadata units needed for an unlink
- btrfs: calculate the right space for delayed refs when updating global reserve
- 修復 global reserve 空間預估中可能的 qgroup 空間洩漏(leak)
是否是因為 global reserve 空間預估不準確,導致內核在一個事務中過度自信地過度提交(overcommit)了太多 metadata 寫入,導致 global reserve 空間用超?
由於 20230701 的 archiso 中仍未帶入 6.4.1 內核,所以構建了新 archiso ,從 iDRAC 引導進新的 archiso 中,重複嘗試上述 filesystem resize max 。然而情況並未改善。
死馬當活馬醫:改大 global reserve
搜到 linux-btrfs 郵件列表中 5.0.9 上一個非常類似的問題報告:Global reserve and ENOSPC while deleting snapshots on 5.0.9
其中提到 global reserve 用儘可能的原因是, btrfs 最近的 transaction 中記錄下了未完成的 subvolume delete 操作,掛載後 btrfs-cleaner 在後台試圖繼續刪除 subvolume ,導致 global reserve 用盡從而無法正常 device add 。
確認到老編譯機上確實有未完成的 orphan subvolume :
# btrfs-orphan-cleaner-progress /mnt
1 orphans left to clean
dropping root 36480 for at least 0 sec drop_progress (439534 EXTENT_DATA 0)
解決方案似乎只有 patch btrfs 代碼,直接改大 global reserve 的常數大小限制:
diff --git a/fs/btrfs/block-rsv.c b/fs/btrfs/block-rsv.c
index ac18c43fadad..b82b954fcbf4 100644
--- a/fs/btrfs/block-rsv.c
+++ b/fs/btrfs/block-rsv.c
@@ -368,7 +368,7 @@ void btrfs_update_global_block_rsv(struct btrfs_fs_info *fs_info)
spin_lock(&sinfo->lock);
spin_lock(&block_rsv->lock);
- block_rsv->size = min_t(u64, num_bytes, SZ_512M);
+ block_rsv->size = min_t(u64, num_bytes, SZ_2G);
if (block_rsv->reserved < block_rsv->size) {
num_bytes = block_rsv->size - block_rsv->reserved;
改到 2G 的 global reserve ,重新編譯內核,重新製作 Archiso 使用新內核,然後讓 iDRAC 使用新 Archiso 啟動。
這次,成功進行了 filesystem resize max 和 device add 。正常 btrfs balance -duage=10 之後 device delete 也如期完成。
btrfs 恢復正常。
經驗感想
獨立伺服器的帶外遠程控制,感覺都挺難用的
老編譯機提供了 Dell 的 iDRAC 管理,可惜獲得的帳戶權限很低,很多 iDRAC 應有的操作(比如安排下一次啟動時的虛擬光碟,比如遠程重啟)都不能在給予的用戶權限下進行,並且給予的用戶訪問時間有限。
另外還有個挺嚴重的問題是原本應該能從虛擬光碟啟動,但是不知為何,phoenixlzx 和 farseerfc (皆位於日本)的網絡環境下,從虛擬光碟啟動都會失敗。解決方案是找一台距離老編譯機網絡環境較近的 VPS ,從 VPS 開啟 iDRAC 並且掛載虛擬光碟。這樣能正常啟動,只不過 archiso 的 copytoram 仍然耗時良久(半小時)。花在啟動 archiso 的時間上很多,並且可能因為沒能啟動 archiso 導致系統重啟到正常系統了一次,產生了刪除快照和創建快照的寫入後讓故障更難恢復了。
新編譯機沒有 iDRAC ,需要申請 KVM Console 訪問,獲得的 KVM Console 也不能操作電源狀態。hetzner 提供的安裝系統的方式是 rescue system 基於 debian ,默認的 archlinux minimal 模板不太能符合需求,好在 installimage https://github.com/hetzneronline/installimage 開源並且還算好用。
btrfs 的 global reserve 機制,比我之前的理解更為複雜
global reserve 存在的目的,似乎是為了節流限制單次 metadata 寫入的量,所以默認 global reserve 不能太大。
目前代碼中上限被硬編碼到 512M ,想要提高限制的唯一方案是重新編譯內核,提高了修改它的門檻。
雖然有 /sys/fs/btrfs/<FSUUID>/allocation/global_rsv_reserved 但是似乎只能讀不能寫。
就在最近 6.4 內核中,這方面還有挺大的改動和修復。
不過這次遇到的情況是 delayed ref 太多導致 global reserve 用完, delayed ref 是在對 btree 子節點做 CoW 時,從葉子節點遞歸往上產生的,屬於沒法預知會產生多少那一類的,所以也很難說這裡的問題是計算上的 bug 還是設計如此沒有辦法避免。
一個可能的設計上的改進方式是計劃中的 extent tree v2 ,準備在 extent tree 中只記錄 data extent 而跳過 metadata extent ,從而能切斷很多 CoW 導致的遞歸寫入。 extent tree v2 將會是未來一兩年 btrfs 開發的大工程,希望同時能改善這次這種極端問題。
btrfs 的 auto bg reclaim 還沒正式投入使用
5.12 內核為 zoned 設備引入了 auto blockgroup reclaim ,在後台默默檢查 bg 的使用率,低於一定閾值之後自動觸發 balance 機制。
5.13~5.18 在為 auto bg reclaim 實施微調。
5.19 內核將 auto bg reclaim 公開給非 zoned 的一般設備。記得當初確認時有 50% 的閾值,不過印象中好像觸發機制有些不符合期待。
事發的 6.3 內核中 /sys/fs/btrfs/<FSUUID>/allocation/data/bg_reclaim_threshold 默認閾值為 0 ,沒有開啟 auto bg reclaim ,在 6.4 內核上依舊是 0 ,默認還未啟用 auto bg reclaim 特性。
值得嘗試一下啟用這個會有什麼效果,以及是否會產生大量寫入放大,平白損耗 SSD 壽命。
還是需要監控 metadata 大小
監控的對象應該是 min(unallocated by device) + metadata.unused ,當它小於 1G 的時候(足夠大的設備上一個 blockgroup 的大小)就需要人工處理了。當它小於 512M (global reserve) 的時候就非常危險,需要立刻處理。
處理的方式還是 btrfs balance -dusage=xx ,這裡需要注意目的是整理 data profile 騰出 unallocated ,所以不要加 -m 參數。
另外 btrfsmaintenance 項目中有 btrfs-balance.timer 也可嘗試。
參考資料
腳註
- ↑ 後檢查日誌發現下午兩點(UTC+8)多開始 vnstat 零星報告寫入耗時達數秒,Grafana 偶有 SQLite 資料庫被鎖定的報錯,journald 服務經常因無響應而觸發 watchdog 重啟服務。在更晚些的時候,journald 停止服務超時,此時日誌變得異常混亂。