GNU ddrescue แสดง 100.00% แต่การกู้ไฟล์แบบมีโครงสร้างรอบแรกของผมกลับได้ผลเป็น 0 useful recovered user files
ผลลัพธ์สองอย่างนี้มาจากเหตุขัดข้องของฮาร์ดไดรฟ์ 4 TB ลูกเดียวกัน และความขัดแย้งนี้เองที่ทำให้การกู้ครั้งนี้น่าสนใจกว่าแค่ “โคลนดิสก์ที่เสียแล้วคัดลอกไฟล์ออกมา” มาก ddrescue ทำหน้าที่ของมันได้ดีอย่างน่าทึ่ง โดยคัดลอกข้อมูลจากต้นทางเชิงกายภาพไปได้ประมาณ 99.999654% ตัวโคลนอยู่บนฮาร์ดแวร์ที่ปกติ แต่ APFS ยังเสียหาย macOS ไม่สามารถให้ผมเข้าถึงระบบไฟล์ได้ตามปกติ และชุดเครื่องมือกู้ APFS ชุดแรกสามารถไล่รายการโครงสร้างไดเรกทอรีได้เป็นส่วนใหญ่ แต่กลับอ่านเนื้อหาของไฟล์ทั่วไปไม่ได้
ท้ายที่สุดผมกู้โครงสร้างโฟลเดอร์ที่เลือกไว้และยังไล่รายการได้ครบทุกส่วนที่ต้องการ แต่ทำได้ก็ต่อเมื่อแยกเหตุการณ์นี้ออกเป็นสามปัญหา: การกู้ระดับบล็อกเชิงกายภาพ การตีความระบบไฟล์ที่เสียหาย และการดึงไฟล์จำนวนมากพร้อมการตรวจสอบและรองรับการทำต่อจากจุดเดิม
ก่อนอื่น ปัญหานี้ไม่ใช่แค่ปัญหาระบบไฟล์อีกต่อไป
Toshiba ขนาด 4 TB ลูกเดิมมาถึงจุดที่ผมไม่ไว้ใจให้ทำงานกับระบบไฟล์ตามปกติอีกแล้ว การอ่านบางครั้งใช้เวลา 60–75 วินาที การทำงานอาจค้าง ไดรฟ์หายไปจาก macOS เป็นช่วง ๆ มีเสียงคลิกชัดเจน และบางครั้งก็ดับไปเอง
ไม่นานก่อนเกิดปัญหา ผมเพิ่งเขียนข้อมูลเพิ่มไปราว 300 GB และทำการเปลี่ยนชื่อจำนวนมากที่กระทบไฟล์กับไดเรกทอรีประมาณ 500,000 รายการ จังหวะเวลาทำให้ ภาระงานที่หนักด้าน metadata นี้ดูน่าสงสัย แต่ผมพิสูจน์ไม่ได้ว่ามันเป็นสาเหตุของความเสียหายทางฮาร์ดแวร์ เป็นไปได้ว่ามันเพียงเพิ่มภาระให้ไดรฟ์ที่ไม่ปกติอยู่แล้วมากพอจนปัญหาแสดงตัวออกมา
สิ่งที่ผมยืนยันได้คือพฤติกรรมของไดรฟ์ เมื่อสื่อบันทึกแบบกลไกเริ่มค้าง หายจากระบบ และมีเสียงคลิก การเปิดไล่ดูไดเรกทอรีซ้ำ ๆ ก็ไม่ใช่วิธีคิดที่เหมาะสมอีกต่อไป การไล่ผ่านไดเรกทอรีอาจกระตุ้นการอ่านและการ seek เพิ่มขึ้น การเมานต์ระบบไฟล์อาจกระตุ้นงานด้าน metadata ทุกการทดลองกินเวลาจากอุปกรณ์เพียงชิ้นเดียวที่เราไม่รู้ว่าอายุการใช้งานที่เหลืออยู่มีเท่าไร
ดังนั้นผมจึงเปลี่ยนเป้าหมายจาก:
กู้ไฟล์ของผม
เป็น:
กู้ sector ที่ยังอ่านได้ให้มากที่สุด
ผมใช้ GNU ddrescue 1.30 ร่วมกับ mapfile ที่บันทึกสถานะถาวร ขนาดเชิงกายภาพที่แน่นอนของไดรฟ์ต้นทางคือ:
4,000,787,027,968 bytes
mapfile สำคัญมาก เพราะต้นทางไม่เสถียรพอสำหรับการคัดลอกให้จบในครั้งเดียว มันทำให้การกู้ทนต่ออาการค้าง การหลุด การรีสตาร์ต และการอ่านรอบถัด ๆ ไปได้ โดยไม่ลืมว่าบริเวณใดกู้มาแล้ว
ผมยังเจอปัญหาเชิงปฏิบัติกับพาธอุปกรณ์แบบ raw ของ macOS: ขอบเขตการกู้ที่เห็นอาจกลายเป็นค่าที่ไม่มีเหตุผล แทนที่จะจบตรงขอบเขตจริงของอุปกรณ์ ผมจึงจำกัดขอบเขตการกู้ให้เท่ากับขนาดเชิงกายภาพที่ทราบแน่นอนด้านบน ในกรณีนี้ การกำหนดขอบเขตอินพุตอย่างชัดเจนเป็นมาตรการด้านความถูกต้อง ไม่ใช่การปรับเพื่อเพิ่มความเร็ว
ทำไม ddrescue แสดง 100.00% แต่ไม่ได้แปลว่าไฟล์ปลอดภัยแล้ว
ช่วงใกล้จบการกู้เชิงกายภาพ ddrescue รายงานโดยประมาณว่า:
domain size: 4000 GB
rescued: 4000 GB
non-tried: 13481 kB
non-trimmed: 327680 B
non-scraped: 0 B
bad-sector: 27136 B
เปอร์เซ็นต์ที่เด่นที่สุดคือ:
100.00%
แต่สถานะที่ยังไม่ถูกแก้รวมกันยังมี:
13,835,816 bytes
หรือประมาณ:
13.84 MB
เมื่อเทียบกับต้นทางขนาด 4,000,787,027,968 ไบต์ สัดส่วนที่กู้ได้จริงอยู่ที่ประมาณ:
99.999654%
นี่เป็นผลลัพธ์การกู้ระดับบล็อกที่ยอดเยี่ยม แต่ไม่ใช่ผลลัพธ์ที่ยืนยันความสมบูรณ์ของไฟล์
ตำแหน่งของไบต์ที่หายสำคัญกว่าปริมาณรวมที่เห็นเด่น ๆ การเสียข้อมูลหลาย MB ในพื้นที่ที่ไม่ได้ใช้อาจไม่เกิดผลที่มองเห็นได้เลย บริเวณเล็ก ๆ ที่อ่านไม่ได้ภายในวิดีโออาจทำให้เสียเพียงไฟล์เดียว แต่การสูญเสียที่เล็กกว่านั้นมากใน metadata ของระบบไฟล์ อาจทำให้หา data extent ของไฟล์จำนวนมากที่จริง ๆ แล้วยังสมบูรณ์อยู่ได้ยาก
นี่กลายเป็นกรอบความคิดหลักสำหรับการกู้ส่วนที่เหลือ:
| ระดับ | คำถามที่ระดับนี้ตอบ | ความสำเร็จไม่ได้พิสูจน์ว่า |
|---|---|---|
| การกู้ระดับบล็อก | คัดลอกเซกเตอร์เชิงกายภาพได้หรือไม่? | APFS สามารถสร้างไฟล์ทุกไฟล์กลับมาได้ |
| การกู้ระบบไฟล์ | สามารถระบุพาธ metadata และ extent ได้หรือไม่? | ทุกไบต์ที่ดึงออกมาถูกต้อง |
| การตรวจสอบไฟล์ | ไฟล์มีขนาดหรือแฮชตามที่คาดหรือไม่? | ไม่เคยมีไฟล์ที่ตอนนี้ค้นหาไม่เจออยู่มาก่อน |
ผมลองกับบริเวณที่ยังอ่านไม่ได้อีกสองสามครั้งเป็นครั้งสุดท้าย ในที่สุดมันก็หยุดให้ข้อมูลใหม่ที่มีประโยชน์ ขณะที่ Toshiba ส่งเสียงคลิกหนักมาก ตรงนั้นคือจุดที่ผมหยุดใช้ไดรฟ์ต้นฉบับเป็นแหล่งกู้ที่ยังทำงานอยู่
ทำไมผมซื้อไดรฟ์ 5 TB สองลูกเพื่อกู้ดิสก์ 4 TB หนึ่งลูก
ไดรฟ์ใหม่ลูกแรกคือ Seagate Expansion ขนาด 5 TB ซึ่งมีความจุเชิงกายภาพที่แน่นอน:
5,000,981,077,504 bytes
ผมเขียนโคลนระดับบล็อกของ Toshiba ลงไป โครงร่างจากต้นทางกินพื้นที่ประมาณ 4 TB แรก จึงเหลืออีกราว 1 TB ถัดจากโครงร่างที่คัดลอกมา ผมตั้งใจปล่อยพื้นที่ส่วนเกินนั้นไว้โดยไม่แตะต้อง
ผมไม่ได้ขยาย APFS container ไม่ได้แบ่งพาร์ทิชันของโคลนใหม่เพื่อความสะดวก และไม่ได้รันการซ่อมระบบไฟล์บนมัน ดิสก์ลูกนี้จึงกลายเป็น โคลนหลัก
จากนั้นผมซื้อไดรฟ์ 5 TB ลูกที่สอง ซึ่งเพิ่งฟอร์แมตใหม่ เขียนได้ ผ่านการทดสอบแยกต่างหาก และใช้เฉพาะเป็นปลายทางของไฟล์ที่กู้ได้
HDD 4 TB ที่กำลังเสีย
│
│ GNU ddrescue
▼
ไดรฟ์ 5 TB #1
โคลนหลักระดับบล็อก
อ่านอย่างเดียว
│
│ การแยกวิเคราะห์และดึงข้อมูล APFS
▼
ไดรฟ์ 5 TB #2
ไฟล์ที่กู้ได้
เขียนได้
นั่นหมายถึงต้องซื้อพื้นที่จัดเก็บข้อมูลใหม่ที่มีความจุตามป้ายรวมกันราว 10 TB เพื่อกู้ volume ที่มีข้อมูลใช้อยู่ประมาณ 3.26 TB ไดรฟ์เพิ่มไม่ได้มีไว้เพื่อความจุ แต่เพื่อรักษา invariant ข้อนี้:
ถ้าการทดลองหนึ่งผิด ผมยังกลับไปหาโคลนหลักเดิมที่ไม่เคยถูกแก้ไขได้
การซ่อม การปรับขนาด การแบ่งพาร์ทิชันใหม่ หรือการเขียนผลลัพธ์ที่กู้ได้กลับลงโคลนหลัก จะทำให้การเก็บรักษาต้นฉบับปะปนกับการทดลอง การแยกต้นทางกับปลายทางไว้คนละไดรฟ์จริง ทำให้ความผิดพลาดยังแก้ย้อนกลับได้
ก่อนจะไว้ใจปลายทาง ผมรันทดสอบเขียน/อ่านประมาณ 10 GB ในการทดสอบนั้นมันรักษาความเร็วได้ราว 144.4 MB/s ทั้งสองทิศทาง ส่วนตัวอย่างการอ่านแบบ raw จากโคลนหลักอยู่ประมาณ 28–49 MB/s และระหว่างการตรวจเหล่านั้นไม่เกิดรูปแบบความล้มเหลวของ I/O เชิงกายภาพแบบเดียวกับไดรฟ์ต้นฉบับ
ถึงจุดนั้น ปัญหาเปลี่ยนไปแล้ว ผมไม่ได้กำลังดีบักฮาร์ดแวร์ที่กำลังเสียอีกต่อไป แต่กำลังดีบัก metadata ของ APFS ที่เสียหาย ซึ่งถูกเก็บอยู่บนฮาร์ดแวร์ที่ทำงานปกติในการทดสอบของผม
โคลน APFS อ่านได้ในฐานะอุปกรณ์ แต่ใช้ไม่ได้ในฐานะระบบไฟล์
พื้นที่จัดเก็บเชิงกายภาพของ APFS ที่โคลนมามีขนาด:
4,000,650,887,168 bytes
พาร์ทิชันที่เกี่ยวข้องเริ่มต้นที่เซกเตอร์:
264192
เมื่อแต่ละเซกเตอร์มีขนาด 512 ไบต์ จะได้ออฟเซ็ตเป็นไบต์:
135,266,304 bytes
APFS volume รายงานว่ามีพื้นที่ถูกใช้งานประมาณ:
3,260,976,717,824 bytes
การตรวจ APFS แบบอ่านอย่างเดียว ไปถึง fsroot tree ในที่สุด และรายงานว่า:
Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.
นี่คือจุดที่คำว่า “ddrescue คัดลอกมาได้เกือบทั้งดิสก์” ไม่เพียงพอสำหรับการวินิจฉัยทั้งหมดอีกต่อไป โคลนแบบ raw มีอยู่จริง แต่โครงสร้างระบบไฟล์ข้างในยังไม่สอดคล้องกัน
ผมตั้งใจให้การตรวจระบบไฟล์เป็นแบบไม่แก้ไขข้อมูล ผมไม่ได้เปลี่ยน fsck_apfs -n ให้กลายเป็นการซ่อมโคลนหลักคุณภาพสูงเพียงชุดเดียวที่มีอยู่ การซ่อมอาจเหมาะกับสื่อจัดเก็บข้อมูลปกติ แต่ในกรณีนี้มันจะเปลี่ยนหลักฐานที่ผมยังพยายามทำความเข้าใจอยู่
The Sleuth Kit แสดง APFS namespace ได้ แต่ล้มเหลวเมื่อต้องอ่านเนื้อหาไฟล์
The Sleuth Kit 4.15.0 เป็นชุดเครื่องมือกู้ชุดแรกที่ทำให้โคลนดูมีความหวัง เมื่อใช้ทั้งอุปกรณ์ที่โคลนมา offset ของพาร์ทิชันที่ทราบ และ APFS superblock ที่ผมหาเจอ ผมสามารถใช้ fls แสดงชื่อไดเรกทอรีจริงได้:
fls \\\\
-o 264192 \\\\
-B 4594668 \\\\
-p \\\\
/dev/rdiskN
ผมตั้งใจใช้ N เพราะหมายเลขดิสก์ของ macOS เปลี่ยนหลังถอดต่อใหม่และรีบูต ผมจึงไม่ถือ assignment ก่อนหน้าอย่าง /dev/disk6 ว่าเป็นตัวตนของอุปกรณ์
fls ไล่ผ่าน namespace ได้เป็นส่วนใหญ่ แค่ tree ขนาดใหญ่ชุดเดียวก็เผยไดเรกทอรีประมาณ 8,900 รายการ ชั่วขณะหนึ่งดูเหมือนว่าส่วนที่ยากที่สุดจะถูกแก้แล้ว
จากนั้นผมลองดึงเนื้อหาไฟล์
ตัวอย่างความล้มเหลวที่แทนปัญหาได้ดีคือ:
libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlock
tsk_recover เริ่มดึงไฟล์ได้แล้วไปล้มเหลวที่บล็อก APFS ส่วนการลอง icat ทีละไฟล์ก็พบปัญหาประเภทเดียวกัน
ความแตกต่างสำคัญคือ:
การไล่ดูไดเรกทอรีทำงานได้
ไม่ได้หมายความว่า:
การอ่านเนื้อหาไฟล์ทำงานได้
parser อาจมี metadata ที่ยังเหลืออยู่มากพอให้ค้นพบพาธ แต่ยังล้มเหลวในภายหลังตอนระบุ file object, extent metadata หรือ content block ที่จำเป็นสำหรับคืนสตรีมไบต์
ผมทำให้เอนจินกู้ข้อมูลจำนวนมากรุ่นแรกทนต่อความผิดพลาดได้ แต่ parser ยังไม่เหมาะกับความเสียหายแบบนี้
ปฏิกิริยาแรกของผมคือทำให้การดึงไฟล์ด้วย TSK ทนทานขึ้น แทนที่จะเปลี่ยน parser ทันที
ผมสร้าง wrapper สำหรับกู้ข้อมูลด้วย Python ครอบ fls และ icat มันเก็บสถานะถาวรไว้ใน SQLite บันทึกความล้มเหลว รองรับการทำต่อจากจุดเดิม เขียนผลลัพธ์บางส่วนแยกไว้ และลดความสำคัญของข้อมูลดูแลระบบของ macOS ที่มีค่าน้อยในรอบแรก
ผมเพิ่มกฎ “hotspot” ด้วย: ถ้าไฟล์สี่ไฟล์ติดกันในไดเรกทอรีเดียวล้มเหลวด้วย pattern APFSBlock แบบศูนย์ไบต์เหมือนกัน สคริปต์จะหยุดเสียเวลากับส่วนที่เหลือของ branch นั้น เลื่อนไปทำทีหลัง แล้วไปทำส่วนอื่นต่อ สมมติฐานในการทำงานคือกลุ่มความล้มเหลวที่เหมือนกันอาจใช้ metadata dependency ที่เสียตัวเดียวร่วมกัน แทนที่จะหมายถึง payload หลายร้อยชุดถูกทำลายแยกจากกัน
การควบคุมลำดับงานมีประโยชน์ แต่ตัวอ่าน APFS ที่อยู่ข้างใต้กลับไม่เหมาะกับความเสียหายแบบนี้
ณ จุดหนึ่งที่บันทึกไว้ ฐานข้อมูลการกู้มี:
DEFERRED_HOTSPOT: 30,356 files
FAILED: 486 files
ความล้มเหลว 486 รายการแบ่งเป็น:
409 APFSBlock crashes
75 rc=0 but output-size mismatch
2 other rc=1 failures
และรอบประมวลผลแบบมีโครงสร้างได้ผลเป็น:
0 useful recovered user files
ขนาดไม่ตรงกัน 75 รายการเปิดเผยบั๊กใน wrapper ของผมเอง: ไฟล์ metadata ของระบบบางไฟล์คืนข้อมูลมาแล้ว แต่ parser ของผมบันทึกขนาดที่คาดเป็นศูนย์ การแก้การตีความนี้สำคัญ แต่ไม่ได้เปลี่ยนผลลัพธ์หลัก ไฟล์ผู้ใช้ทั่วไปยังจบที่ศูนย์ไบต์พร้อม could not read APFSBlock
ผมเลือกไฟล์ AVIF ขนาดเล็กหนึ่งไฟล์เป็นกรณีทดสอบที่ทำซ้ำได้ TSK รู้ขนาดที่คาดของมัน:
expected size: 56,309 bytes
recovered: 0 bytes
ไฟล์นี้มีค่ามากกว่าการรันการกู้รอบใหญ่อีกหลายชั่วโมง ถ้าวิธีใหม่ยังไม่สามารถกู้ไฟล์ทดสอบขนาด 56 KB ที่ล้มเหลวแบบเดิมทุกครั้งได้ วิธีนั้นก็ยังไม่สมควรได้ลองกับข้อมูลที่เหลืออีกหลาย TB
บล็อกของดิสก์กับเส้นทาง metadata ของ APFS เป็นขอบเขตความล้มเหลวคนละส่วน
ถึงจุดนี้ผมมีข้อสังเกตสามอย่าง:
GNU ddrescue:
คัดลอกต้นทางเชิงกายภาพได้เกือบทั้งหมด
TSK fls:
ค้นพบพาธจริงได้จำนวนมาก
TSK icat:
เนื้อหาของไฟล์ทั่วไปจำนวนมากยังอ่านไม่สำเร็จ
ข้อสังเกตเหล่านี้อยู่ร่วมกันได้ เมื่อแยก lookup chain ในเชิงแนวคิด:
ชื่อพาธ
↓
ระเบียนไดเรกทอรี
↓
ออบเจ็กต์ไฟล์ / metadata ของ inode
↓
การแมป extent
↓
บล็อกข้อมูลเชิงกายภาพ
บล็อกข้อมูลที่อยู่ใกล้ด้านล่างอาจยังอยู่รอด แม้ลิงก์ที่สูงขึ้นใน metadata chain จะเสียหาย อีกความเป็นไปได้คือตัวอ่าน APFS สองแบบไล่ผ่านโครงสร้างที่เสียหายชุดเดียวกันด้วยวิธีต่างกัน
ผมไม่ได้แยกพบออบเจ็กต์ APFS ที่เสียหายเพียงตัวเดียวซึ่งอธิบายความล้มเหลวทั้งหมดได้ จึงไม่อ้างว่านั่นเป็นสาเหตุรากที่ยืนยันแล้ว สิ่งที่หลักฐานรองรับคือการทดลองที่มีประโยชน์กว่ามาก: คงไบต์ในโคลนไว้เหมือนเดิม แล้วเปลี่ยน parser
APFS checkpoint 142 จุดไม่ได้กลายเป็นทางย้อนกลับทันที
การสแกนพื้นที่ APFS checkpoint descriptor แบบ raw และอ่านอย่างเดียว พบ NXSB checkpoint superblock ที่เป็นไปได้ 142 รายการ โดย transaction ID ไล่จาก:
223133
ลงไปถึง:
222992
คำถามที่ชัดเจนคือ checkpoint ที่เก่ากว่าจะอ้างถึง metadata tree ที่สมบูรณ์กว่าหรือไม่
ผมคอมไพล์ apfs-fuse แล้วลอง checkpoint transaction ID ต่าง ๆ ผ่าน fuse-t checkpoint ล่าสุดค้าง XID ที่เก่ากว่าก็ค้างเหมือนกัน ผมกำหนด timeout แบบตายตัวต่อ checkpoint และมากกว่า 50 ครั้งติดกันก็ยังไม่สร้างการเมานต์ที่ใช้งานได้ ผมลองทั้ง backend NFS และ SMB ของ fuse-t ด้วย
นั่นไม่ได้พิสูจน์ว่า checkpoint ทั้งหมดเสีย การทดลองนี้ขึ้นอยู่กับ checkpoint, apfs-fuse, fuse-t, พฤติกรรมอุปกรณ์ของ macOS และแบ็กเอนด์การเมานต์ ความล้มเหลวที่จุดใดจุดหนึ่งในสแตกนี้ก็ให้ผลที่มองเห็นเหมือนกันได้
parser ที่ลองในภายหลังรายงานว่า APFS snapshot มีจำนวนศูนย์ ซึ่งยิ่งตอกย้ำความแตกต่างที่ผมต้องแยกให้ชัด: checkpoint transaction state เหล่านั้นไม่ใช่สิ่งเดียวกับ APFS snapshot ที่ผู้ใช้มองเห็น
parser ที่ค้างแม้แต่ตอน --help ใช้วินิจฉัยดิสก์ของผมไม่ได้
ผมคอมไพล์ go-apfs-v2 ด้วย การคอมไพล์สำเร็จ แต่แม้แต่:
apfs --help
ก็ยังค้างและต้องถูกยุติเมื่อหมดเวลาที่กำหนด
การตรวจบล็อกแบบขั้นต่ำผ่านทั้งพาธอุปกรณ์แบบ raw และพาธอุปกรณ์แบบ buffered ก็ค้างเช่นกัน ผมทำการทดสอบแบบจำกัดเวลาในลักษณะเดียวกันกับ apfsutil; รูปแบบอุปกรณ์ทั้งสอง timeout หลังประมาณ 15 วินาที
ความล้มเหลวเหล่านี้มีประโยชน์ เพราะช่วยไม่ให้ผมสรุปผิด เครื่องมือที่แม้แต่เส้นทาง help ของตัวเองยังทำให้จบอย่างเชื่อถือไม่ได้ เป็นหลักฐานที่อ่อนมากว่าจริง ๆ แล้วไฟล์ที่เสียหายยังกู้ได้หรือไม่
กฎของผมจึงกลายเป็น:
ตรวจสอบเครื่องมือกู้ก่อนจะถือว่าความล้มเหลวของเครื่องมือกู้เป็นหลักฐานเกี่ยวกับข้อมูล
การป้องกันไม่ให้ macOS เมานต์โคลนโดยอัตโนมัติ ตัดแหล่งความเสี่ยงอีกอย่างออกไป
ตัว macOS เองก็เป็นอีกส่วนที่เคลื่อนไหว ผมต้องการให้ปลายทางที่ปกติเมานต์ตามปกติ แต่ไม่ต้องการให้ Disk Arbitration พยายามเมานต์โคลน APFS ที่เสียโดยอัตโนมัติทุกครั้งที่ผมต่ออุปกรณ์จัดเก็บข้อมูลกลับเข้าไป
ลำดับที่ปลอดภัยซึ่งผมใช้ในที่สุดคือ:
- เชื่อมต่อและเมานต์ปลายทางสำหรับการกู้ที่ปกติ
- ตรวจสอบตัวระบุของ volume และพื้นที่ว่าง
- หยุด
diskarbitrationdชั่วคราว - ตรวจว่าไม่มี process
mount_apfsที่กำลังทำงาน - ต่อโคลนหลัก
- ระบุโคลนหลักแบบไดนามิก จากขนาดเชิงกายภาพที่ทราบและ ตัวระบุ APFS
- ตรวจว่าโคลนหลักไม่ได้เมานต์อยู่
- ทำงานกู้ข้อมูลแบบอ่านอย่างเดียว
- เมื่อจะหยุด ให้บันทึกสถานะ, กลับมาให้ Disk Arbitration ทำงาน แล้วค่อยปิดเครื่องหรือถอดอุปกรณ์ตามปกติ
คำสั่งที่ผมใช้จริงเพื่อหยุด Disk Arbitration ชั่วคราวคือ:
sudo kill -STOP "$(pgrep -x diskarbitrationd)"
ผมตรวจว่าสถานะโปรเซสมี T และเช็กโปรเซส mount_apfs ที่อาจค้างอยู่ก่อนดำเนินการต่อ
บทเรียนด้านความปลอดภัยที่สำคัญไม่ใช่หมายเลขดิสก์ใดหมายเลขหนึ่ง แต่ตรงกันข้าม: อย่าเชื่อหมายเลขดิสก์ของเมื่อวาน หลังรีบูต /dev/disk6 ที่เคยใช้ก่อนหน้าอาจชี้ไปยังอุปกรณ์จริงคนละตัว ผมใช้ APFS UUID กับขนาดเชิงกายภาพที่ทราบเป็น identity แล้วค่อยหาพาธอุปกรณ์ปัจจุบันจากสิ่งนั้น
libfsapfs กู้ไฟล์เดียวกับที่ TSK คืนมาเป็นศูนย์ไบต์ได้
จุดเปลี่ยนมาจาก libfsapfs ซึ่งเป็นตัวอ่าน APFS อีกแบบ
ผมคอมไพล์จากซอร์สโค้ดบน macOS ไบนารี fsapfsinfo ที่ได้ระบุตัวเองว่า:
fsapfsinfo 20260923
จากนั้นก็พบความแตกต่างสำคัญของอินเทอร์เฟซอุปกรณ์บน macOS
พาร์ทิชันแบบ raw character device:
/dev/rdiskNs2
ล้มเหลวอย่างรวดเร็วด้วยข้อผิดพลาด invalid argument ระหว่างการอ่าน ใกล้ออฟเซ็ต 4096
แต่รูปแบบ block device แบบ buffered:
/dev/diskNs2
ทำงานได้
โคลนเชิงกายภาพเดียวกัน พาร์ทิชัน APFS เดียวกัน แต่อินเทอร์เฟซอุปกรณ์ของ macOS ต่างกัน
fsapfsinfo เปิด container ได้และพบหนึ่ง volume จากนั้นผมทดสอบไฟล์ขนาด 56,309 ไบต์ตัวเดิมที่ TSK ดึงออกมาไม่สำเร็จ
TSK ให้ผล:
expected: 56,309
recovered: 0
APFSBlock failure
เมื่อผ่าน libfsapfs รายการไฟล์ให้ผล:
size: 56,309
MD5: c6f56db33eafc1de0f52a035bc255dc7
RC: 0
นี่เป็นผลแรกที่เปลี่ยนการวินิจฉัยอย่างมีนัยสำคัญ ไบต์ในโคลนไม่ได้เปลี่ยน ไฟล์ทดสอบไม่ได้เปลี่ยน และ APFS ที่เสียก็ไม่ได้ถูกซ่อม
สิ่งที่เปลี่ยนคือตัวอ่าน APFS
อย่างน้อยตอนนี้ผมรู้แล้วว่าไฟล์ผู้ใช้จริงที่ดูเหมือนกู้ไม่ได้ผ่าน TSK ยังเข้าถึงผ่าน libfsapfs ได้ลึกพอที่จะอ่านเนื้อหาได้ครบและคำนวณ digest
ผมดึงไฟล์ที่รู้ว่าเสียหนึ่งไฟล์ก่อนจะไว้ใจ libfsapfs กับข้อมูลหลาย TB
digest ที่สำเร็จเพียงครั้งเดียวยังไม่พอให้ผมเริ่มการกู้ข้อมูลหลาย TB ผมต้องการไบต์จริงบนดิสก์ปลายทาง
แทนที่จะเดา C API ของ libfsapfs ผมอ่านซอร์สโค้ดของไลบรารี และตามเส้นทางการอ่านที่ fsapfsinfo ใช้อยู่แล้วตอนคำนวณ digest จากนั้นจึงสร้างตัวดึงไฟล์ขนาดเล็กแบบอ่านอย่างเดียวสำหรับไฟล์ทดสอบนี้
ตัวดึงไฟล์เขียนออกมาได้ตรง:
56,309 bytes
ไปยังดิสก์สำหรับกู้ข้อมูลแยกต่างหาก MD5 ของไฟล์ที่ดึงออกมาได้คือ:
c6f56db33eafc1de0f52a035bc255dc7
ซึ่งตรงกับ digest ก่อนหน้า
หลังจากนั้นเท่านั้นผมจึงขยายวิธีนี้ การทดสอบด้วยไฟล์เดียวพิสูจน์สามอย่างแยกกัน: ระบุพาธได้ ดึงข้อมูลได้ครบตามจำนวนไบต์ที่คาดไว้ และไบต์ที่ดึงออกมาให้ digest เดียวกับการอ่านเนื้อหาเต็มก่อนหน้า
ปัญหาหลักของการกู้ข้อมูลจำนวนมากคือการจำกัดผลกระทบของความผิดพลาดและการทำต่อจากจุดเดิม
เมื่อ libfsapfs กู้ไฟล์ที่ TSK กู้ไม่ได้ ปัญหายากก็เปลี่ยนอีกครั้ง ผมต้องการระบบที่ประมวลผลโครงสร้างไดเรกทอรีขนาดใหญ่มากได้ โดยไม่ให้ branch ที่เสียเพียง branch เดียว การรีบูตหนึ่งครั้ง หรือ Ctrl+C หนึ่งครั้ง ทำให้งานทั้งหมดต้องเริ่มใหม่จากศูนย์
ดังนั้นกระบวนการกู้ข้อมูลจำนวนมากจึงใช้ invariant ที่เข้มงวดหลายข้อ:
- เปิดโคลนหลักแบบอ่านอย่างเดียว
- เขียนผลลัพธ์ที่กู้ได้ลงไดรฟ์ 5 TB ลูกที่สองเท่านั้น
- คงชื่อไดเรกทอรีและชื่อไฟล์ไว้
- เก็บความคืบหน้าใน SQLite เพื่อให้อยู่รอดหลังโปรเซสหยุดหรือมีการรีบูต
- เขียนแต่ละไฟล์ไปยังพาธชั่วคราวก่อน
- เปลี่ยนชื่อไฟล์ชั่วคราวเป็นพาธสุดท้ายหลังเขียนครบตามขนาดที่คาดเท่านั้น
- ระหว่างการทำต่อจากจุดเดิม สามารถตรวจพบไฟล์ที่มีอยู่แล้วและมีขนาดตามที่คาด
- แยกงานที่ล้มเหลวหรือมีปัญหาออกจากงานที่เสร็จแล้ว
- ตรวจพื้นที่ว่างและรักษาพื้นที่สำรองเพื่อความปลอดภัยไว้
- artifact จากงานพัฒนาที่สร้างใหม่ได้และ metadata ของระบบที่มีค่าน้อยสามารถข้ามหรือลดลำดับความสำคัญได้
การเปลี่ยนด้านประสิทธิภาพอย่างหนึ่งเห็นผลทันที: ผมหยุดเปิด APFS container ใหม่แยกสำหรับทุกไฟล์
เส้นทางเร็วประมวลผลทีละไดเรกทอรี worker เปิดต้นทาง ไล่รายการไดเรกทอรีนั้น กู้ไฟล์ปกติที่อยู่ตรงนั้น แล้วส่งไดเรกทอรีย่อยกลับเข้าคิว หากไดเรกทอรีล้มเหลวหรือ timeout controller จะทำเครื่องหมายเป็น DEFERRED แล้วไปต่อ แทนที่จะบล็อกรอบทั้งหมด
เมื่อไม่มีงานปกติที่รอดำเนินการเหลือ ไดเรกทอรีที่พักไว้จะถูกนำกลับมาทำผ่านเส้นทางสำรองที่ช้ากว่าและแยกงานระดับไฟล์ออกจากกันมากขึ้น จากนั้นความล้มเหลวที่ยังเหลือสามารถลองใหม่แยกเป็นรายกรณีได้
ค้นพบไดเรกทอรี
↓
กู้ไฟล์ที่อยู่โดยตรง
↓
ตรวจสอบขนาดที่คาด
↓
บันทึกสถานะถาวร
↓
เพิ่มไดเรกทอรีย่อยเข้าคิว
↓
พักงานที่ล้มเหลวเฉพาะจุด
↓
ทำงานส่วนอื่นต่อ
↓
กลับมาใช้เส้นทางสำรองและลองใหม่ภายหลัง
สถาปัตยกรรมนี้ตรงกับรูปแบบความเสียหายจริงมากกว่าคำสั่งแบบ recursive ขนาดใหญ่เพียงคำสั่งเดียว ความเสียหายไม่ได้กระจายสม่ำเสมอ ระบบกู้จึงไม่ควรบังคับให้ความคืบหน้าสม่ำเสมอเช่นกัน
ทำไมผมตรวจขนาดที่คาดและใช้การเปลี่ยนชื่อแบบ atomic แทนการคำนวณแฮชไฟล์นับล้าน
MD5 มีประโยชน์ในขั้นพิสูจน์ด้วยไฟล์เดียว เพราะผมต้องการหลักฐานที่แข็งแรงว่า parser กำลังอ่านเนื้อหาของไฟล์ที่ TSK อ่านไม่ได้ครบจริง
ถ้าต้องอ่านไบต์ที่กู้ได้ทั้งหมดซ้ำอีกรอบเพียงเพื่อคำนวณแฮชไฟล์นับล้าน จะเพิ่ม I/O จำนวนมาก สำหรับรอบการดึงไฟล์หลัก ผมจึงใช้ invariant อีกแบบ
สำหรับไฟล์ปกติแต่ละไฟล์ metadata ของ APFS ให้ขนาดที่คาดมา worker เขียนลงไฟล์ชั่วคราว และเลื่อนเป็นพาธสุดท้ายก็ต่อเมื่ออ่านครบแล้วตรงกับขนาดที่คาดนั้น
นั่นหมายความว่าการถูกขัดจังหวะไม่ควรทิ้งไฟล์สั้น ๆ ไว้ใต้ชื่อสุดท้าย จนดูเหมือนว่าไฟล์สมบูรณ์
ขนาดที่ตรงกันไม่ใช่หลักฐานความสมบูรณ์แบบเชิงคริปโตกราฟี และผมก็ไม่ถือว่าเป็นเช่นนั้น แต่ การตรวจสอบขนาดที่คาดร่วมกับการเปลี่ยนชื่อแบบ atomic เป็นขอบเขตความถูกต้องที่ใช้งานได้จริงสำหรับรอบประมวลผลปริมาณสูง ส่วนแฮชแบบเจาะจงยังมีประโยชน์กับตัวอย่างและกรณีล้มเหลวที่รู้จัก
SQLite ทำให้การรีบูตเป็นเรื่องธรรมดาแทนที่จะเป็นหายนะ
การกู้ใช้เวลานานพอที่ผมต้องปิดคอมพิวเตอร์แล้วกลับมาทำต่อภายหลัง ความต้องการนี้เปลี่ยนการออกแบบจาก “สคริปต์” เป็น “กระบวนการที่ทำต่อได้”
Ctrl+C ไม่ได้แค่ทิ้งโปรเซสลูกที่กำลังทำงาน controller จับการขัดจังหวะ หยุด worker คืนไดเรกทอรีที่กำลังทำให้กลับไปอยู่ในสถานะที่ทำต่อได้ บันทึกสถานะลง SQLite แล้วออก
การหยุดแบบสะอาดในเชิงแนวคิดมีหน้าตาประมาณนี้:
ไดเรกทอรีปัจจุบัน -> PENDING
CTRL+C: หยุดการกู้ข้อมูลอย่างปลอดภัยแล้ว
บันทึกสถานะแล้ว รันคำสั่งเดิมเพื่อทำต่อ
หลังรีบูต ผมตรวจตัวตนของดิสก์ซ้ำ แล้วเริ่มคำสั่งกู้ข้อมูลเดิม ฐานข้อมูลสถานะทำต่อจากคิวที่มีอยู่
การเริ่มใหม่ครั้งหนึ่งในภายหลังเริ่มด้วย:
DEFERRED: 1
DONE: 73,015
PENDING: 7,254
นี่มีความหมายมากกว่าแถบความคืบหน้าทั่วไปมาก เพราะแสดงว่าหน่วยไดเรกทอรีหลายหมื่นรายการที่ทำเสร็จแล้วรอดผ่านการรีบูตมาได้ และคิวที่เหลือถูกระบุไว้อย่างชัดเจน
ในการทำต่อจากจุดเดิมก่อนหน้านั้น ไดเรกทอรีที่ถูกขัดจังหวะในเซสชันก่อนถูกหยิบกลับมาทำอีกครั้ง ไฟล์ที่มีอยู่แล้วและมีขนาดที่คาดถูกตรวจพบ จึงต้องเขียนเฉพาะงานที่ยังขาด นี่คือพฤติกรรมที่ผมต้องการ: การเริ่มกู้ข้อมูลใหม่ควรเป็นเรื่องปกติ ไม่ใช่เรื่องน่ากลัว
326,799 ไฟล์คือหลักฐานแรกว่าวิธีนี้ขยายขนาดได้
ก่อนจะขยายตัวดึงไฟล์ใหม่ไปยังข้อมูลที่เลือกไว้ทั้งหมด ผมใช้โครงสร้างที่ให้ความสำคัญก่อนขนาดใหญ่หนึ่งชุดเป็นเป้าหมายสำหรับการตรวจสอบขนาดใหญ่
สถานะเมื่อเสร็จรายงานว่า:
directories completed: 8,940
new files written: 302,541
existing/resumed files: 24,258
recorded failed files: 0
ปลายทางมี:
326,799 files
145,039,215,948 bytes
หรือประมาณ:
135.08 GiB
จำนวนไฟล์ตรงกันพอดี:
302,541 + 24,258 = 326,799
ไม่มีไฟล์ .partial.* เหลืออยู่ในโครงสร้างที่ทำเสร็จแล้วนั้น
ไฟล์ทดสอบขนาด 56,309 ไบต์พิสูจน์ว่า parser ทำสำเร็จได้ในจุดที่ TSK ล้มเหลว ส่วนการกู้ 326,799 ไฟล์พิสูจน์ว่าวิธีเดียวกันรับมือโครงสร้างลำดับชั้นจริงขนาดใหญ่ได้ พร้อมตรรกะสำหรับการทำต่อจากจุดเดิม การตรวจไฟล์ที่มีอยู่แล้ว และไม่มีความล้มเหลวของไฟล์ที่ถูกบันทึกในรอบที่เสร็จสมบูรณ์นั้น
การกู้ชุดใหญ่เกิน 1.6 ล้านไฟล์ใหม่ก่อนจะเสร็จ
หลังโครงสร้างที่ให้ความสำคัญก่อนเสร็จอย่างสะอาด ผมขยายการกู้ไปยังข้อมูลระดับบนสุดที่เลือกไว้ส่วนที่เหลือ
ณ จุดหยุดที่ตั้งใจให้ปลอดภัยครั้งหนึ่ง SQLite รายงาน:
DONE directories: 39,015
PENDING directories: 17,017
DEFERRED directories: 1
new files written: 1,636,305
new bytes written: 902,715,716,335
คิดเป็นประมาณ:
840.72 GiB
ของข้อมูลใหม่ที่รอบประมวลผลไดเรกทอรีบันทึกว่าเขียนไปแล้ว ณ ตอนนั้น
ไดเรกทอรี DEFERRED เพียงรายการเดียวไม่ได้หมายถึงข้อมูลหาย แต่หมายความว่าเส้นทางเร็วตั้งใจหยุดไม่ให้ปัญหาเฉพาะจุดนั้นถ่วงงานที่ไม่เกี่ยวข้อง ช่วงเส้นทางสำรองมีไว้เพื่อกลับมาจัดการกรณีแบบนี้ภายหลังโดยเฉพาะ
เซสชันต่อ ๆ มาทำต่อจากฐานข้อมูลเดิม จำนวนที่เสร็จเพิ่มขึ้นและคิวที่รอดำเนินการลดลง สุดท้ายผมกู้โครงสร้างโฟลเดอร์ที่เลือกไว้และไล่รายการได้ครบทุกส่วนที่ต้องการ
สิ่งที่ผมพูดได้อย่างตรงไปตรงมาเกี่ยวกับผลการกู้สุดท้าย
ผมจะไม่อธิบายผลลัพธ์ว่า “กู้ได้ทุกไบต์” เพราะหลักฐานไม่รองรับคำกล่าวนั้น
map ของ ddrescue ต้นฉบับยังมีประมาณ 13.84 MB ที่ไม่ได้รับการยืนยันว่าคัดลอกสำเร็จ ผมยังพิสูจน์ไม่ได้ด้วยว่าไม่มีออบเจ็กต์ระบบไฟล์ใดหายจนค้นหาไม่ได้โดยสมบูรณ์ เพราะ metadata ที่จำเป็นสำหรับไล่รายการออบเจ็กต์นั้นอาจอยู่ในบริเวณที่เสียหาย
ข้อจำกัดเหล่านี้สำคัญ เพราะการกู้แบบมีโครงสร้างพิสูจน์ได้ว่าออบเจ็กต์ที่ไล่รายการได้ถูกดึงออกมา แต่พิสูจน์ไม่ได้ว่าในอดีตไม่เคยมีออบเจ็กต์ที่ namespace ซึ่งเสียหายจนตอนนี้ไม่สามารถแสดงออกมาได้
ข้อความสรุปที่แข็งแรงที่สุดจึงแคบกว่านั้น:
โครงสร้างโฟลเดอร์ที่ผมเลือกไว้และยังไล่รายการได้ทุกส่วนที่ต้องการ ถูกกู้สำเร็จผ่านกระบวนการกู้แบบมีโครงสร้าง
ผมไม่ต้องซ่อมโคลนหลักแบบ in-place ไม่ต้องนำ Toshiba ลูกเดิมที่ยังมีเสียงคลิกกลับมาใช้สำหรับการดึงไฟล์จำนวนมาก และไม่ต้องทำการ carve ทั้งดิสก์แบบ PhotoRec ที่จะเสียโครงสร้างไดเรกทอรีและชื่อไฟล์
เครื่องมือที่ล้มเหลวยังเป็นหลักฐานที่มีประโยชน์
มองย้อนกลับไป เส้นทางที่สำเร็จฟังดูเรียบง่าย:
โคลนด้วย ddrescue
↓
libfsapfs
↓
ตัวดึงไฟล์ที่ทำต่อจากจุดเดิมได้
↓
ไฟล์ที่กู้ได้
แต่นั่นไม่ใช่ความรู้สึกระหว่างที่กำลังสืบสวน และถ้าตัดวิธีที่ล้มเหลวออก ก็จะตัดบทเรียนด้านวิศวกรรมที่มีประโยชน์ไปมากด้วย
TSK สอนผมว่าการไล่ผ่าน namespace ของ APFS กับการอ่านเนื้อหาเป็นรูปแบบความล้มเหลวคนละแบบ
บั๊กใน wrapper ตัวแรกสอนผมว่าการจัดประเภทข้อผิดพลาดของสคริปต์กู้ข้อมูลเองไม่ใช่ ground truth
กลไก hotspot สอนให้ผมแยกความล้มเหลวเฉพาะจุดออก แทนที่จะปล่อยให้มันหยุดความคืบหน้าทั้งระบบ
การทดลอง checkpoint สอนผมว่าอย่าสับสน APFS transaction checkpoint กับ snapshot
การลอง FUSE สอนผมว่าการเมานต์ที่ล้มเหลวอาจเกี่ยวข้องกับหลายชั้นนอกเหนือจากข้อมูลระบบไฟล์เอง
ตัวอ่าน APFS ที่ค้างบน --help สอนให้ผมตรวจสอบเครื่องมือก่อนตีความข้อมูลวินิจฉัยของมัน
พฤติกรรมที่ต่างกันระหว่าง /dev/rdiskNs2 กับ /dev/diskNs2 สอนผมว่าเส้นทาง I/O ของระบบปฏิบัติการเปลี่ยนพฤติกรรมของ parser ได้ แม้ดิสก์ข้างใต้จะเป็นลูกเดียวกัน
และสถาปัตยกรรมสองไดรฟ์ทำให้ผมมีอิสระจะลองผิดในทุกจุดอื่น โดยโคลนหลักยังคงไม่เปลี่ยนแปลง
ขั้นตอนการกู้ที่ผมจะใช้อีกครั้ง
- หยุดกิจกรรมระบบไฟล์ตามปกติบนสื่อจัดเก็บข้อมูลที่กำลังเสียเชิงกล ถ้าการอ่านค้าง อุปกรณ์หาย หรือมีเสียงคลิก ผมจะให้ความสำคัญกับโคลนระดับบล็อกที่ทำต่อจากจุดเดิมได้ก่อนการเปิดดูด้วย Finder
- ใช้ GNU ddrescue พร้อม mapfile ที่บันทึกสถานะถาวรและขอบเขตการกู้ที่ตรวจสอบแล้ว mapfile เก็บความคืบหน้า; ขนาดต้นทางที่ทราบช่วยไม่ให้ความสับสนเรื่องขนาดอุปกรณ์กลายเป็นส่วนหนึ่งของปัญหาการกู้
- เก็บโคลนหลักหนึ่งชุดเป็นแบบอ่านอย่างเดียว อย่าซ่อม ปรับขนาด แบ่งพาร์ทิชันใหม่ หรือใช้เป็นที่เก็บไฟล์ที่กู้ได้
- เขียนไฟล์ที่กู้ได้ลงดิสก์จริงลูกที่สอง การรักษาต้นทางกับการเก็บไฟล์ผลลัพธ์เป็นคนละงาน
- วินิจฉัยระบบไฟล์ในโคลนแบบอ่านอย่างเดียวก่อน โคลนเชิงกายภาพที่อยู่บนฮาร์ดแวร์ปกติแต่มี APFS metadata เสีย เป็นปัญหาการกู้เชิงตรรกะ ไม่ใช่ปัญหาเดียวกับดิสก์ต้นทางที่กำลังมีเสียงคลิก
- เลือกไฟล์ที่ล้มเหลวแบบทำซ้ำได้หนึ่งไฟล์เป็นการทดสอบ parser ไฟล์ที่รู้ว่าล้มเหลวขนาด 56 KB บอกผมเกี่ยวกับ parser ทางเลือกได้มากกว่าการดึงไฟล์จำนวนมากแบบสุ่มสี่สุ่มห้าเป็นชั่วโมง
- ตรวจสอบ parser เอง ถ้าเครื่องมือค้างก่อนจะอ่านต้นทางอย่างมีความหมาย อย่าตีความว่าเป็นหลักฐานว่าข้อมูลหายแล้ว
- อย่าคิดว่าตัวอ่าน APFS เพียงแบบเดียวเป็นผู้กำหนดว่าอะไร “กู้ได้” TSK และ
libfsapfsทำงานต่างกันมากบนไบต์ชุดเดียวกันที่โคลนมา - ระบุดิสก์ด้วยคุณสมบัติที่คงที่ ไม่ใช่หมายเลขอุปกรณ์ชั่วคราว UUID ของระบบไฟล์กับขนาดเชิงกายภาพที่ทราบปลอดภัยกว่า
/dev/diskNของเมื่อวาน - ทำให้การดึงไฟล์ระยะยาวทำต่อจากจุดเดิมได้ตั้งแต่ต้น สถานะถาวร ไฟล์ชั่วคราว การเปลี่ยนชื่อแบบ atomic งานที่พักไว้ การลองใหม่แบบจำกัด และการจัดการการปิดระบบอย่างเรียบร้อย ล้วนเป็นส่วนหนึ่งของความถูกต้องในระดับขนาดนี้
- แยกระดับการตรวจสอบ เปอร์เซ็นต์ของ ddrescue พาธที่มองเห็น การตรงกันของขนาดที่คาด และแฮชเนื้อหา พิสูจน์คนละเรื่องกัน
ตัวเลขที่ดูเหมือนเส้นชัย ที่จริงเป็นเพียงจุดจบของขั้นแรก
ตัวเลขที่ชวนให้เข้าใจผิดที่สุดตลอดการกู้ยังคงเป็น:
100.00%
มันดูเหมือนคำตอบของคำถามว่า “ผมช่วยดิสก์ไว้ได้หรือยัง?”
แต่สิ่งที่มันตอบจริง ๆ แคบกว่านั้นมาก:
ddrescue คัดลอกขอบเขตการกู้เชิงกายภาพสำเร็จไปมากเท่าไร?
มันไม่ได้ตอบว่า APFS สร้าง namespace กลับมาได้หรือไม่ ไม่ได้ตอบว่า parser ตัวหนึ่งไล่ผ่าน metadata ที่เสียซึ่ง parser อีกตัวปฏิเสธได้หรือไม่ ไม่ได้ตอบว่าไฟล์ที่กู้มามีความยาวตามที่คาดหรือไม่ และไม่ได้ตอบว่าการดึงไฟล์ที่มีไฟล์หลายล้านรายการจะรอดจากข้อผิดพลาดและการรีบูตโดยไม่ทำสถานะของตัวเองเสียหายได้หรือไม่
ผมต้องมีหลักฐานแยกสำหรับแต่ละระดับ
การกู้เชิงกายภาพสำเร็จก่อน ระบบไฟล์ยังเสีย parser ตัวแรกแสดงชื่อได้ แต่ล้มเหลวกับเนื้อหาไฟล์จำนวนมาก ตัวอ่าน APFS อีกแบบอ่านไฟล์ทดสอบเดียวกันได้สำเร็จ หลักฐานนั้นกลายเป็นตัวดึงไฟล์สำหรับไฟล์เดียว จากนั้นตัวดึงไฟล์กลายเป็นเอนจินกู้ข้อมูลที่ทำต่อจากจุดเดิมได้ และท้ายที่สุดเอนจินก็กู้โครงสร้างโฟลเดอร์ที่ผมเลือกไว้ไปยังดิสก์ลูกที่สอง ขณะที่โคลนหลักไม่เคยถูกแก้ไข
ฮาร์ดไดรฟ์ต้นฉบับไม่เคยกลับมาปกติ APFS ไม่ได้ซ่อมตัวเองอย่างมหัศจรรย์ สิ่งที่เปลี่ยนคือโมเดลการกู้
การกู้ระดับบล็อก การกู้ระบบไฟล์ และการตรวจสอบไฟล์เป็นขั้นตอนวิศวกรรมที่แยกจากกัน เมื่อมองเป็นปัญหาเดียว สถานการณ์แทบดูสิ้นหวัง แต่เมื่อแยกออกจากกัน ปัญหาก็จัดการได้