山洪災害后的 Ceph 慘案:PG incomplete 到 RBD 鏡像消失
背景
在一次山洪災害后,機房的服務器全部斷電,等供電恢復后進入系統發現所有的虛擬機文件系統損壞了,并且查看ceph集群有個pg出現inactive和incomplete狀態,上傳新的鏡像io也會卡住,以下是恢復過程。
圖片
排查過程
incomplete和inactive狀態含義解釋
inactive
- 含義:PG 處于不可用狀態。
- 表現:客戶端對這個 PG 的讀寫請求都會被阻塞。
- 原因可能包括:
這個 PG 沒有足夠的 OSD 存活來提供服務。
PG 沒有被分配到合適的 OSD 上。
OSD 沒有完成 peering(對等協商)過程。
換句話說,inactive 就是 PG 不能對外提供正常的 IO 服務。
incomplete
- 含義:PG 在 peering 時發現缺少必需的數據副本,導致無法達到一致性。
- 表現:PG 中的數據不完整,無法對外提供讀寫。
- 常見原因:
某些 OSD 宕機或丟失數據,導致 PG 的對象副本無法湊齊。
新 OSD 加入或者數據遷移時丟失了必要的副本。
硬盤故障或誤刪導致數據確實丟失。
通常 incomplete 比 inactive 更嚴重, inactive只是PG 暫時不可用,但數據可能還在,只是沒有滿足對外服務條件。 incomplete出現時說明peering 過程中無法收集到足夠的、權威的一致數據副本,意味著有的數據副本確實不存在了,需要人工干預才能恢復。往往出現在peering的過程中服務器異常斷電, 在斷電前 PG 的日志還沒來得及落盤, 所有副本上的 PG log 都不完整,導致無法確定哪些對象是最新的 。
嘗試對pg進行修復
查看集群所有的osd,發現都是up的
圖片
嘗試常規修復發現沒什么用
ceph pg repair 2.1c
后查看pg上的對象數和丟失的對象數,發現pg上的對象數為0
ceph pg ls | grep 2.1c
ceph pg 2.1c list_unfound
圖片
嘗試回滾pg舊版本和重啟pg副本所在的osd服務后重新修復均無效
ceph pg 2.1c mark_unfound_lost revert
ceph pg repair 2.1c
將pg副本的osd out再in后狀態仍沒有變化
ceph osd out <id>
ceph osd in <id>
使用ceph-objectstore-tool操作pg副本
集群狀態一直無法恢復,準備使用ceph-objectstore-tool工具操作pg副本,只保留一份pg的副本,將其他兩份的副本刪除,并基于剩余的pg副本做回填,最后將剩下的pg副本標記為complete狀態。
準備操作
#查看pg副本所在的osd
ceph pg map 2.1c
#防止副本操作期間觸發數據重新均衡
ceph osd set noout
# 臨時降低 min_size
ceph osd pool set libvirt-pool min_size 1
備份導出pg副本
可以看到導出的副本大小都是十幾K,數據基本查看其他正常的pg,對象數量平均是在6000多個
systemctl stop ceph-osd@8
ceph-objectstore-tool --data-path /var/lib/ceph/osd/ceph-8 --type bluestore --pgid 2.1c --op export --file /opt/2.1c.obj_osd_8
systemctl stop ceph-osd@14
ceph-objectstore-tool --data-path /var/lib/ceph/osd/ceph-14 --type bluestore --pgid 2.1c --op export --file /opt/2.1c.obj_osd_14
systemctl stop ceph-osd@11
ceph-objectstore-tool --data-path /var/lib/ceph/osd/ceph-11 --type bluestore --pgid 2.1c --op export --file /opt/2.1c.obj_osd_11
圖片
圖片
刪除兩個osd節點上的故障pg副本
systemctl stop ceph-osd@8
ceph-objectstore-tool --data-path /var/lib/ceph/osd/ceph-8/ --type bluestore --pgid 2.1c --op remove --force
systemctl stop ceph-osd@11
ceph-objectstore-tool --data-path /var/lib/ceph/osd/ceph-11/ --type bluestore --pgid 2.1c --op remove --force
圖片
從剩余節點導入pg副本到其他兩個osd節點
ceph-objectstore-tool --data-path /var/lib/ceph/osd/ceph-11/ --type bluestore --pgid 2.1c --op import --file /opt/2.1c.obj_osd.14
ceph-objectstore-tool --data-path /var/lib/ceph/osd/ceph-8/ --type bluestore --pgid 2.1c --op import --file /opt/2.1c.obj_osd.14
systemctl start ceph-osd@11
systemctl start ceph-osd@8
圖片
將剩余節點的pg副本標記為complete
執行常規恢復操作發現集群還是處于incomplete狀態
ceph pg repair 2.1c
將剩余節點的pg副本標記為complete
systemctl stop ceph-osd@14
# 標記 complete
ceph-objectstore-tool --type bluestore --data-path /var/lib/ceph/osd/ceph-14 --pgid 2.1c --op mark-complete
ceph osd pool set libvirt-pool min_size 2
ceph osd unset noout
systemctl start ceph-osd@14
之后集群顯示健康狀態正常了
圖片
但是發現rbd查看鏡像發現全沒了,存儲大小沒有變化
圖片
對rbd鏡像列表數據進行恢復
查看鏡像頭對象,發現還在,但是根據ID查詢存儲池中的鏡像已經查不到了
rados -p libvirt-pool ls | grep '^rbd_header\.' | head
rbd -p libvirt-pool --image-id <id> info
圖片
檢查header對象的元數據,發現還在,說明只是目錄對象丟失了
rados -p libvirt-pool listomapkeys rbd_header.d8b1996ee6b524 | head
圖片
使用rados查看鏡像發現能查詢到
rados -p libvirt-pool stat rbd_header.d8b1996ee6b524
圖片
搭建測試環境復現
搭建好測試環境后修復好后再在生產環境修復,先設置osd暫停恢復/回填,且把存儲池 min_size 暫時降到 1
ceph osd set noout
ceph osd set norecover
ceph osd set nobackfill
ceph osd pool set libvirt-pool min_size 1
只保留 主副本 的osd在線,停掉另外兩個副本
systemctl stop ceph-osd@4
systemctl stop ceph-osd@5
刪除總目錄對象,再刪每個鏡像的 id 映射條目
rados -p libvirt-pool rm rbd_directory
for img in $(rbd ls -p libvirt-pool 2>/dev/null); do
rados -p libvirt-pool rm rbd_id.$img || true
done
讓另外兩個副本上線并恢復回填
systemctl start ceph-osd@4
systemctl start ceph-osd@5
ceph osd unset noout
ceph osd unset norecover
ceph osd unset nobackfill
驗證復現結果,目錄對象確實丟了,但是數據還在
rados -p libvirt-pool stat rbd_directory
rbd ls -p libvirt-pool
rados -p libvirt-pool ls | grep '^rbd_header\.' | head
圖片
圖片
恢復rbd的目錄對象
從網上找了兩個腳本,一個腳本可以根據header的ID反查鏡像名,另一個是根據鏡像名和header的ID來恢復rbd鏡像目錄
#!/bin/bash
# 用法: ./find_rbd_name.sh <IMAGE_ID>
# 例子: ./find_rbd_name.sh 3c456b8b4567
set -euo pipefail
POOL="libvirt-pool"
if [ $# -ne 1 ]; then
echo "用法: $0 <IMAGE_ID>"
exit 1
fi
ID="$1"
found=0
for obj in $(rados -p "$POOL" ls | grep '^rbd_id\.'); do
got=$(rados -p "$POOL" get "$obj" - 2>/dev/null | tr -d '\n\r')
if [ "$got" = "$ID" ]; then
echo "發現: $obj -> name = ${obj#rbd_id.}"
found=1
fi
done
if [ $found -eq 0 ]; then
echo "未找到 ID=$ID 對應的鏡像"
exit 2
fi
通過直接改寫 pool 里的 rbd_directory
對象的 OMAP 鍵值 來恢復
- 每個 RBD 鏡像都有一個頭對象:
rbd_header.<ID>
,鏡像的各種元數據都掛在它的 omap 上。 rbd ls
并不是去遍歷所有rbd_header.*
,而是讀**rbd_directory**
這個對象的 omap:
name_<NAME>
→ 值為 <ID>
id_<ID>
→ 值為 <NAME>
- 值的編碼不是裸字符串,而是:小端 4 字節長度(LE uint32) + 字符串本體。
例如name_foo
的值若為"abc123"
,實際二進制是:06 00 00 00 61 62 63 31 32 33
。 - 只要把這兩條映射補上,
rbd ls
就能重新列出<NAME>
;rbd info <NAME>
也能通過目錄映射定位到頭對象<ID>
。
#!/usr/bin/env bash
# 用法: ./fix_rbd_mapping.sh <NAME> <ID>
# 例子: ./fix_rbd_mapping.sh windows_7sp1_x86_dvd677486.img 2557396b8b4567
set -euo pipefail
POOL="libvirt-pool"
if [ $# -ne 2 ]; then
echo "用法: $0 <NAME> <ID>"
exit 1
fi
NAME="$1"
ID="$2"
# 0)(可選)先確保池已初始化過 RBD 目錄對象;冪等,安全
rbd pool init "$POOL"
# 1) 備份一下目前這兩條(如果不存在會報錯但不影響繼續)
rados -p "$POOL" getomapval rbd_directory "name_$NAME" /tmp/old_name_val.bin 2>/dev/null || true
rados -p "$POOL" getomapval rbd_directory "id_$ID" /tmp/old_id_val.bin 2>/dev/null || true
# 2) name_<name> -> <id> (值為:LE4長度 + 字符串ID)
python3 - <<'PY' | rados -p "$POOL" setomapval rbd_directory "name_$NAME"
import sys,struct
img_id="$ID"
sys.stdout.buffer.write(struct.pack("<I", len(img_id)))
sys.stdout.buffer.write(img_id.encode())
PY
# 3) id_<id> -> <name> (值為:LE4長度 + 字符串NAME)
python3 - <<'PY' | rados -p "$POOL" setomapval rbd_directory "id_$ID"
import sys,struct
name="$NAME"
sys.stdout.buffer.write(struct.pack("<I", len(name)))
sys.stdout.buffer.write(name.encode())
PY
# 4) 校驗兩條鍵寫好了
rados -p "$POOL" listomapvals rbd_directory | egrep 'name_$NAME|id_$ID' -n
# 5) 看列表與信息
rbd -p "$POOL" ls | grep -F -- "$NAME" || true
rbd -p "$POOL" info "$NAME"
圖片
查看恢復后的鏡像列表發現已經恢復
圖片