กลับไปที่บล็อก
7 ตุลาคม 2569Sergei Solod54 นาทีในการอ่าน

ผมกู้ไฟล์จากโคลน APFS ที่เสียหายได้อย่างไร หลัง GNU ddrescue คัดลอกไดรฟ์ 4 TB ไปแล้ว 99.999654%

GNU ddrescue คัดลอกข้อมูลจาก HDD ขนาด 4 TB ที่กำลังเสียได้ 99.999654% แต่โคลน APFS ยังไม่ผ่านการตรวจสอบระบบไฟล์ และ The Sleuth Kit แม้จะแสดงพาธได้ แต่กลับคืนไฟล์ขนาดศูนย์ไบต์ ผมกู้โครงสร้างไดเรกทอรีที่เลือกไว้ได้ โดยเก็บโคลนหลักแบบอ่านอย่างเดียวไว้ เปลี่ยนไปใช้ libfsapfs และสร้างกระบวนการดึงไฟล์ที่ทำต่อจากจุดเดิมได้

APFSGNU ddrescueการกู้คืนข้อมูลlibfsapfsmacOS

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 ที่เสียโดยอัตโนมัติทุกครั้งที่ผมต่ออุปกรณ์จัดเก็บข้อมูลกลับเข้าไป

ลำดับที่ปลอดภัยซึ่งผมใช้ในที่สุดคือ:

  1. เชื่อมต่อและเมานต์ปลายทางสำหรับการกู้ที่ปกติ
  2. ตรวจสอบตัวระบุของ volume และพื้นที่ว่าง
  3. หยุด diskarbitrationd ชั่วคราว
  4. ตรวจว่าไม่มี process mount_apfs ที่กำลังทำงาน
  5. ต่อโคลนหลัก
  6. ระบุโคลนหลักแบบไดนามิก จากขนาดเชิงกายภาพที่ทราบและ ตัวระบุ APFS
  7. ตรวจว่าโคลนหลักไม่ได้เมานต์อยู่
  8. ทำงานกู้ข้อมูลแบบอ่านอย่างเดียว
  9. เมื่อจะหยุด ให้บันทึกสถานะ, กลับมาให้ 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 ได้ แม้ดิสก์ข้างใต้จะเป็นลูกเดียวกัน

และสถาปัตยกรรมสองไดรฟ์ทำให้ผมมีอิสระจะลองผิดในทุกจุดอื่น โดยโคลนหลักยังคงไม่เปลี่ยนแปลง

ขั้นตอนการกู้ที่ผมจะใช้อีกครั้ง

  1. หยุดกิจกรรมระบบไฟล์ตามปกติบนสื่อจัดเก็บข้อมูลที่กำลังเสียเชิงกล ถ้าการอ่านค้าง อุปกรณ์หาย หรือมีเสียงคลิก ผมจะให้ความสำคัญกับโคลนระดับบล็อกที่ทำต่อจากจุดเดิมได้ก่อนการเปิดดูด้วย Finder
  2. ใช้ GNU ddrescue พร้อม mapfile ที่บันทึกสถานะถาวรและขอบเขตการกู้ที่ตรวจสอบแล้ว mapfile เก็บความคืบหน้า; ขนาดต้นทางที่ทราบช่วยไม่ให้ความสับสนเรื่องขนาดอุปกรณ์กลายเป็นส่วนหนึ่งของปัญหาการกู้
  3. เก็บโคลนหลักหนึ่งชุดเป็นแบบอ่านอย่างเดียว อย่าซ่อม ปรับขนาด แบ่งพาร์ทิชันใหม่ หรือใช้เป็นที่เก็บไฟล์ที่กู้ได้
  4. เขียนไฟล์ที่กู้ได้ลงดิสก์จริงลูกที่สอง การรักษาต้นทางกับการเก็บไฟล์ผลลัพธ์เป็นคนละงาน
  5. วินิจฉัยระบบไฟล์ในโคลนแบบอ่านอย่างเดียวก่อน โคลนเชิงกายภาพที่อยู่บนฮาร์ดแวร์ปกติแต่มี APFS metadata เสีย เป็นปัญหาการกู้เชิงตรรกะ ไม่ใช่ปัญหาเดียวกับดิสก์ต้นทางที่กำลังมีเสียงคลิก
  6. เลือกไฟล์ที่ล้มเหลวแบบทำซ้ำได้หนึ่งไฟล์เป็นการทดสอบ parser ไฟล์ที่รู้ว่าล้มเหลวขนาด 56 KB บอกผมเกี่ยวกับ parser ทางเลือกได้มากกว่าการดึงไฟล์จำนวนมากแบบสุ่มสี่สุ่มห้าเป็นชั่วโมง
  7. ตรวจสอบ parser เอง ถ้าเครื่องมือค้างก่อนจะอ่านต้นทางอย่างมีความหมาย อย่าตีความว่าเป็นหลักฐานว่าข้อมูลหายแล้ว
  8. อย่าคิดว่าตัวอ่าน APFS เพียงแบบเดียวเป็นผู้กำหนดว่าอะไร “กู้ได้” TSK และ libfsapfs ทำงานต่างกันมากบนไบต์ชุดเดียวกันที่โคลนมา
  9. ระบุดิสก์ด้วยคุณสมบัติที่คงที่ ไม่ใช่หมายเลขอุปกรณ์ชั่วคราว UUID ของระบบไฟล์กับขนาดเชิงกายภาพที่ทราบปลอดภัยกว่า /dev/diskN ของเมื่อวาน
  10. ทำให้การดึงไฟล์ระยะยาวทำต่อจากจุดเดิมได้ตั้งแต่ต้น สถานะถาวร ไฟล์ชั่วคราว การเปลี่ยนชื่อแบบ atomic งานที่พักไว้ การลองใหม่แบบจำกัด และการจัดการการปิดระบบอย่างเรียบร้อย ล้วนเป็นส่วนหนึ่งของความถูกต้องในระดับขนาดนี้
  11. แยกระดับการตรวจสอบ เปอร์เซ็นต์ของ ddrescue พาธที่มองเห็น การตรงกันของขนาดที่คาด และแฮชเนื้อหา พิสูจน์คนละเรื่องกัน

ตัวเลขที่ดูเหมือนเส้นชัย ที่จริงเป็นเพียงจุดจบของขั้นแรก

ตัวเลขที่ชวนให้เข้าใจผิดที่สุดตลอดการกู้ยังคงเป็น:

100.00%

มันดูเหมือนคำตอบของคำถามว่า “ผมช่วยดิสก์ไว้ได้หรือยัง?”

แต่สิ่งที่มันตอบจริง ๆ แคบกว่านั้นมาก:

ddrescue คัดลอกขอบเขตการกู้เชิงกายภาพสำเร็จไปมากเท่าไร?

มันไม่ได้ตอบว่า APFS สร้าง namespace กลับมาได้หรือไม่ ไม่ได้ตอบว่า parser ตัวหนึ่งไล่ผ่าน metadata ที่เสียซึ่ง parser อีกตัวปฏิเสธได้หรือไม่ ไม่ได้ตอบว่าไฟล์ที่กู้มามีความยาวตามที่คาดหรือไม่ และไม่ได้ตอบว่าการดึงไฟล์ที่มีไฟล์หลายล้านรายการจะรอดจากข้อผิดพลาดและการรีบูตโดยไม่ทำสถานะของตัวเองเสียหายได้หรือไม่

ผมต้องมีหลักฐานแยกสำหรับแต่ละระดับ

การกู้เชิงกายภาพสำเร็จก่อน ระบบไฟล์ยังเสีย parser ตัวแรกแสดงชื่อได้ แต่ล้มเหลวกับเนื้อหาไฟล์จำนวนมาก ตัวอ่าน APFS อีกแบบอ่านไฟล์ทดสอบเดียวกันได้สำเร็จ หลักฐานนั้นกลายเป็นตัวดึงไฟล์สำหรับไฟล์เดียว จากนั้นตัวดึงไฟล์กลายเป็นเอนจินกู้ข้อมูลที่ทำต่อจากจุดเดิมได้ และท้ายที่สุดเอนจินก็กู้โครงสร้างโฟลเดอร์ที่ผมเลือกไว้ไปยังดิสก์ลูกที่สอง ขณะที่โคลนหลักไม่เคยถูกแก้ไข

ฮาร์ดไดรฟ์ต้นฉบับไม่เคยกลับมาปกติ APFS ไม่ได้ซ่อมตัวเองอย่างมหัศจรรย์ สิ่งที่เปลี่ยนคือโมเดลการกู้

การกู้ระดับบล็อก การกู้ระบบไฟล์ และการตรวจสอบไฟล์เป็นขั้นตอนวิศวกรรมที่แยกจากกัน เมื่อมองเป็นปัญหาเดียว สถานการณ์แทบดูสิ้นหวัง แต่เมื่อแยกออกจากกัน ปัญหาก็จัดการได้