山洪災(zāi)害后的 Ceph 慘案:PG incomplete 到 RBD 鏡像消失
背景
在一次山洪災(zāi)害后,機房的服務(wù)器全部斷電,等供電恢復(fù)后進入系統(tǒng)發(fā)現(xiàn)所有的虛擬機文件系統(tǒng)損壞了,并且查看ceph集群有個pg出現(xiàn)inactive和incomplete狀態(tài),上傳新的鏡像io也會卡住,以下是恢復(fù)過程。
圖片

排查過程
incomplete和inactive狀態(tài)含義解釋
inactive
- 含義:PG 處于不可用狀態(tài)。
- 表現(xiàn):客戶端對這個 PG 的讀寫請求都會被阻塞。
- 原因可能包括:
這個 PG 沒有足夠的 OSD 存活來提供服務(wù)。
PG 沒有被分配到合適的 OSD 上。
OSD 沒有完成 peering(對等協(xié)商)過程。
換句話說,inactive 就是 PG 不能對外提供正常的 IO 服務(wù)。
incomplete
- 含義:PG 在 peering 時發(fā)現(xiàn)缺少必需的數(shù)據(jù)副本,導(dǎo)致無法達到一致性。
- 表現(xiàn):PG 中的數(shù)據(jù)不完整,無法對外提供讀寫。
- 常見原因:
某些 OSD 宕機或丟失數(shù)據(jù),導(dǎo)致 PG 的對象副本無法湊齊。
新 OSD 加入或者數(shù)據(jù)遷移時丟失了必要的副本。
硬盤故障或誤刪導(dǎo)致數(shù)據(jù)確實丟失。
通常 incomplete 比 inactive 更嚴重, inactive只是PG 暫時不可用,但數(shù)據(jù)可能還在,只是沒有滿足對外服務(wù)條件。 incomplete出現(xiàn)時說明peering 過程中無法收集到足夠的、權(quán)威的一致數(shù)據(jù)副本,意味著有的數(shù)據(jù)副本確實不存在了,需要人工干預(yù)才能恢復(fù)。往往出現(xiàn)在peering的過程中服務(wù)器異常斷電, 在斷電前 PG 的日志還沒來得及落盤, 所有副本上的 PG log 都不完整,導(dǎo)致無法確定哪些對象是最新的 。
嘗試對pg進行修復(fù)
查看集群所有的osd,發(fā)現(xiàn)都是up的
圖片
嘗試常規(guī)修復(fù)發(fā)現(xiàn)沒什么用
ceph pg repair 2.1c后查看pg上的對象數(shù)和丟失的對象數(shù),發(fā)現(xiàn)pg上的對象數(shù)為0
ceph pg ls | grep 2.1c
ceph pg 2.1c list_unfound
圖片
嘗試回滾pg舊版本和重啟pg副本所在的osd服務(wù)后重新修復(fù)均無效
ceph pg 2.1c mark_unfound_lost revert
ceph pg repair 2.1c將pg副本的osd out再in后狀態(tài)仍沒有變化
ceph osd out <id>
ceph osd in <id>使用ceph-objectstore-tool操作pg副本
集群狀態(tài)一直無法恢復(fù),準備使用ceph-objectstore-tool工具操作pg副本,只保留一份pg的副本,將其他兩份的副本刪除,并基于剩余的pg副本做回填,最后將剩下的pg副本標記為complete狀態(tài)。
準備操作
#查看pg副本所在的osd
ceph pg map 2.1c
#防止副本操作期間觸發(fā)數(shù)據(jù)重新均衡
ceph osd set noout
# 臨時降低 min_size
ceph osd pool set libvirt-pool min_size 1備份導(dǎo)出pg副本
可以看到導(dǎo)出的副本大小都是十幾K,數(shù)據(jù)基本查看其他正常的pg,對象數(shù)量平均是在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節(jié)點上的故障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
圖片
從剩余節(jié)點導(dǎo)入pg副本到其他兩個osd節(jié)點
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
圖片
將剩余節(jié)點的pg副本標記為complete
執(zhí)行常規(guī)恢復(fù)操作發(fā)現(xiàn)集群還是處于incomplete狀態(tài)
ceph pg repair 2.1c將剩余節(jié)點的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之后集群顯示健康狀態(tài)正常了
圖片
但是發(fā)現(xiàn)rbd查看鏡像發(fā)現(xiàn)全沒了,存儲大小沒有變化
圖片
對rbd鏡像列表數(shù)據(jù)進行恢復(fù)
查看鏡像頭對象,發(fā)現(xiàn)還在,但是根據(jù)ID查詢存儲池中的鏡像已經(jīng)查不到了
rados -p libvirt-pool ls | grep '^rbd_header\.' | head
rbd -p libvirt-pool --image-id <id> info
圖片
檢查header對象的元數(shù)據(jù),發(fā)現(xiàn)還在,說明只是目錄對象丟失了
rados -p libvirt-pool listomapkeys rbd_header.d8b1996ee6b524 | head
圖片
使用rados查看鏡像發(fā)現(xiàn)能查詢到
rados -p libvirt-pool stat rbd_header.d8b1996ee6b524
圖片
搭建測試環(huán)境復(fù)現(xiàn)
搭建好測試環(huán)境后修復(fù)好后再在生產(chǎn)環(huán)境修復(fù),先設(shè)置osd暫?;謴?fù)/回填,且把存儲池 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讓另外兩個副本上線并恢復(fù)回填
systemctl start ceph-osd@4
systemctl start ceph-osd@5
ceph osd unset noout
ceph osd unset norecover
ceph osd unset nobackfill驗證復(fù)現(xiàn)結(jié)果,目錄對象確實丟了,但是數(shù)據(jù)還在
rados -p libvirt-pool stat rbd_directory
rbd ls -p libvirt-pool
rados -p libvirt-pool ls | grep '^rbd_header\.' | head
圖片
圖片
恢復(fù)rbd的目錄對象
從網(wǎng)上找了兩個腳本,一個腳本可以根據(jù)header的ID反查鏡像名,另一個是根據(jù)鏡像名和header的ID來恢復(fù)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 "發(fā)現(xiàn): $obj -> name = ${obj#rbd_id.}"
found=1
fi
done
if [ $found -eq 0 ]; then
echo "未找到 ID=$ID 對應(yīng)的鏡像"
exit 2
fi通過直接改寫 pool 里的 rbd_directory 對象的 OMAP 鍵值 來恢復(fù)
- 每個 RBD 鏡像都有一個頭對象:
rbd_header.<ID>,鏡像的各種元數(shù)據(jù)都掛在它的 omap 上。 rbd ls并不是去遍歷所有rbd_header.*,而是讀**rbd_directory**這個對象的 omap:
name_<NAME> → 值為 <ID>
id_<ID> → 值為 <NAME>
- 值的編碼不是裸字符串,而是:小端 4 字節(jié)長度(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) 備份一下目前這兩條(如果不存在會報錯但不影響繼續(xù))
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"
圖片
查看恢復(fù)后的鏡像列表發(fā)現(xiàn)已經(jīng)恢復(fù)
圖片


























