GNU ddrescue pokazał 100.00%. Mój pierwszy uporządkowany przebieg odzyskiwania plików dał 0 useful recovered user files.
Te dwa wyniki pochodziły z tej samej awarii dysku twardego 4 TB, a właśnie ta sprzeczność sprawiła, że odzyskiwanie stało się znacznie ciekawsze niż „sklonuj uszkodzony dysk i skopiuj pliki”. ddrescue wykonał swoją pracę wyjątkowo dobrze: skopiował około 99,999654% fizycznego źródła. Klon znajdował się na sprawnym sprzęcie. Mimo to APFS nadal był uszkodzony, macOS nie udostępniał mi systemu plików w normalny sposób, a pierwszy zestaw narzędzi do odzyskiwania APFS potrafił wyliczyć duże części drzewa katalogów, jednocześnie nie odczytując zawartości zwykłych plików.
Ostatecznie odzyskałem każde wybrane, wyliczone drzewo katalogów, którego potrzebowałem, ale dopiero po potraktowaniu incydentu jako trzech osobnych problemów: fizycznego odzyskiwania bloków, interpretacji uszkodzonego systemu plików oraz masowej ekstrakcji plików z walidacją i możliwością wznowienia.
Najpierw przestał to być problem systemu plików
Oryginalny dysk Toshiba 4 TB doszedł do stanu, w którym nie ufałem mu już na tyle, by wykonywać zwykłe operacje systemu plików. Niektóre odczyty trwały 60–75 sekund. Operacje potrafiły się zawieszać. Dysk okresowo znikał z macOS, wyraźnie klikał, a czasem się wyłączał.
Krótko przed awarią zapisałem około 300 GB dodatkowych danych i wykonałem masową zmianę nazw obejmującą około 500 000 plików i katalogów. Zbieżność czasowa sprawiała, że to obciążenie intensywnie modyfikujące metadane wyglądało podejrzanie, ale nie mogę udowodnić, że spowodowało awarię sprzętu. Równie dobrze mogło po prostu wystarczająco mocno obciążyć już niesprawny dysk, by ujawnić problem.
Mogłem natomiast ustalić, jak zachowywał się dysk. Gdy mechaniczny nośnik zaczyna się zacinać, znikać i klikać, wielokrotne przeglądanie katalogów jest niewłaściwym poziomem abstrakcji. Przechodzenie po katalogach może wywoływać kolejne odczyty i ruchy głowicy. Montowanie systemu plików może uruchamiać pracę na metadanych. Każdy eksperyment zużywa czas jedynego komponentu, którego pozostałej użytecznej żywotności nie znamy.
Dlatego zmieniłem cel z:
recover my filesna:
recover as many readable sectors as possibleUżyłem GNU ddrescue 1.30 z trwałym mapfile. Dokładny fizyczny rozmiar dysku źródłowego wynosił:
4,000,787,027,968 bytesMapfile był niezbędny, ponieważ źródło nie było wystarczająco stabilne do jednorazowego kopiowania. Pozwalał przetrwać zacięcia, odłączenia, restarty i późniejsze przebiegi bez utraty informacji o tym, które obszary zostały już odzyskane.
Natrafiłem też na praktyczny problem z surową ścieżką urządzenia w macOS: pozorny zakres odzyskiwania mógł stać się absurdalny zamiast kończyć się na rzeczywistej granicy urządzenia. Dlatego ograniczyłem domenę odzyskiwania do znanego fizycznego rozmiaru podanego wyżej. W tym przypadku jawne ograniczenie wejścia było środkiem zapewniającym poprawność, a nie optymalizacją szybkości.
Dlaczego 100.00% w ddrescue nie oznaczało, że pliki są bezpieczne
Pod koniec fizycznego odzyskiwania ddrescue raportował w przybliżeniu:
domain size: 4000 GB
rescued: 4000 GB
non-tried: 13481 kB
non-trimmed: 327680 B
non-scraped: 0 B
bad-sector: 27136 BGłówny procent wynosił:
100.00%Ale nierozwiązane stany nadal dawały łącznie:
13,835,816 bytesczyli około:
13.84 MBW odniesieniu do źródła o rozmiarze 4 000 787 027 968 bajtów odzyskany udział wynosił około:
99.999654%To znakomity wynik odzyskiwania bloków. Nie jest to wynik integralności plików.
Położenie brakujących bajtów ma większe znaczenie niż sama efektowna liczba. Utrata kilku megabajtów z nieużywanego miejsca może nie mieć żadnego widocznego skutku. Mały nieczytelny obszar wewnątrz filmu może uszkodzić jeden plik. Znacznie mniejsza utrata w metadanych systemu plików może sprawić, że trudno będzie zlokalizować wiele poza tym nienaruszonych ekstentów danych.
To stało się głównym modelem myślowym na resztę odzyskiwania:
| Warstwa | Na jakie pytanie odpowiada | Czego sukces nie dowodzi |
|---|---|---|
| Odzyskiwanie bloków | Czy fizyczne sektory zostały skopiowane? | Że APFS potrafi odtworzyć każdy plik |
| Odzyskiwanie systemu plików | Czy da się rozwiązać ścieżki, metadane i ekstenty? | Że każdy wyodrębniony bajt jest prawidłowy |
| Walidacja plików | Czy plik ma oczekiwany rozmiar lub hash? | Że nigdy nie istniał żaden plik, którego nie da się już odnaleźć |
Podjąłem jeszcze kilka ostatnich prób odczytu pozostałych nieczytelnych obszarów. W końcu przestały przynosić użyteczne nowe dane, podczas gdy Toshiba intensywnie klikała. W tym momencie przestałem używać oryginalnego dysku jako aktywnego źródła odzyskiwania.
Dlaczego kupiłem dwa dyski 5 TB, żeby odzyskać jeden dysk 4 TB
Pierwszym nowym dyskiem był Seagate Expansion 5 TB o dokładnej fizycznej pojemności:
5,000,981,077,504 bytesZapisałem na nim blokowy klon Toshiby. Układ źródła zajmował mniej więcej pierwsze 4 TB, pozostawiając około 1 TB poza skopiowanym układem. Celowo nie ruszałem tej dodatkowej przestrzeni.
Nie powiększałem kontenera APFS. Nie przepartycjonowałem klonu dla wygody. Nie uruchamiałem na nim naprawy systemu plików. Ten dysk stał się klonem wzorcowym.
Następnie kupiłem drugi dysk 5 TB. Został świeżo sformatowany, był zapisywalny, niezależnie przetestowany i służył wyłącznie jako miejsce docelowe dla odzyskanych danych.
failing 4 TB HDD
│
│ GNU ddrescue
▼
5 TB drive #1
master block-level clone
READ-ONLY
│
│ APFS parsing and extraction
▼
5 TB drive #2
recovered files
WRITABLEOznaczało to zakup około 10 TB nominalnie nowej przestrzeni, aby odzyskać wolumin zawierający około 3,26 TB używanych danych. Dodatkowy dysk nie był potrzebny ze względu na pojemność. Chodziło o zachowanie niezmiennika:
If an experiment is wrong, I can return to the same untouched master clone.Naprawianie, zmiana rozmiaru, przepartycjonowanie albo zapisywanie odzyskanych danych na klonie wzorcowym mieszałoby zabezpieczenie źródła z eksperymentowaniem. Trzymanie źródła i celu na osobnych fizycznych dyskach sprawiało, że błędy dało się odwrócić.
Zanim zaufałem dyskowi docelowemu, wykonałem test zapisu i odczytu około 10 GB. W tym teście utrzymywał mniej więcej 144,4 MB/s w obu kierunkach. Przykładowe surowe odczyty z klonu wzorcowego osiągały około 28–49 MB/s i podczas tych kontroli nie odtwarzały wzorca fizycznych błędów I/O z oryginalnego dysku.
W tym momencie problem się zmienił. Nie diagnozowałem już psującego się sprzętu. Diagnozowałem uszkodzone metadane APFS zachowane na sprzęcie, który w moich testach zachowywał się normalnie.
Klon APFS był czytelny jako urządzenie, ale nieprawidłowy jako system plików
Sklonowany fizyczny magazyn APFS miał rozmiar:
4,000,650,887,168 bytesOdpowiednia partycja zaczynała się w sektorze:
264192Przy sektorach 512-bajtowych daje to przesunięcie w bajtach:
135,266,304 bytesWolumin APFS zgłaszał zużycie około:
3,260,976,717,824 byteszajętych.
Kontrola APFS tylko do odczytu w końcu dotarła do drzewa fsroot i zgłosiła:
Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.W tym miejscu stwierdzenie „ddrescue skopiował prawie cały dysk” przestało być użyteczne jako pełna diagnoza. Surowy klon istniał. Struktura systemu plików wewnątrz niego nadal była niespójna.
Celowo pozostawiłem kontrolę systemu plików w trybie niemodyfikującym. Nie zamieniłem fsck_apfs -n w operację naprawy na jedynym wysokiej jakości klonie wzorcowym, jaki miałem. Naprawa może być właściwa dla zwykłego nośnika, ale tutaj zmieniłaby materiał, który nadal próbowałem zrozumieć.
The Sleuth Kit potrafił wyświetlić przestrzeń nazw APFS, ale nie radził sobie z zawartością plików
The Sleuth Kit 4.15.0 był pierwszym zestawem narzędzi do odzyskiwania, przy którym klon zaczął wyglądać obiecująco. Korzystając z całego sklonowanego urządzenia, znanego przesunięcia partycji i zidentyfikowanego superbloku APFS, mogłem wyliczać rzeczywiste nazwy katalogów za pomocą fls:
fls \\
-o 264192 \\
-B 4594668 \\
-p \\
/dev/rdiskNCelowo używam N. Numery dysków w macOS zmieniały się po ponownym podłączeniu i restartach, więc nie traktowałem wcześniejszego przypisania /dev/disk6 jako identyfikatora urządzenia.
fls potrafił przechodzić przez znaczną część przestrzeni nazw. Samo jedno duże drzewo ujawniło około 8 900 katalogów. Przez chwilę wyglądało na to, że najtrudniejsza część została rozwiązana.
Potem spróbowałem pobrać zawartość plików.
Typowa awaria wyglądała tak:
libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlocktsk_recover potrafił rozpocząć ekstrakcję, a potem zakończyć się błędem na bloku APFS. Pojedyncze próby z icat pokazywały ten sam rodzaj problemu.
Kluczowe rozróżnienie było takie:
directory traversal worksnie oznaczało:
file content retrieval worksParser może mieć wystarczająco dużo ocalałych metadanych, by odkryć ścieżkę, a mimo to później nie poradzić sobie z rozwiązaniem obiektu pliku, metadanych ekstentów lub bloków zawartości potrzebnych do zwrócenia strumienia bajtów.
Pierwszy silnik masowego odzyskiwania uodporniłem na błędy, ale parser nadal nie pasował do tego rodzaju uszkodzenia
Moją pierwszą reakcją było zwiększenie odporności ekstrakcji TSK zamiast natychmiastowej zmiany parsera.
Zbudowałem w Pythonie warstwę odzyskiwania wokół fls i icat. Utrzymywał trwały stan w SQLite, rejestrował błędy, obsługiwał wznawianie, zapisywał częściowe wyniki osobno i w pierwszym przebiegu obniżał priorytet mało wartościowych danych porządkowych macOS.
Dodałem też regułę „hotspot”: jeśli cztery kolejne pliki w jednym katalogu kończyły się niepowodzeniem z tym samym wzorcem APFSBlock i zerową liczbą bajtów, skrypt przestawał tracić czas na resztę tej gałęzi, odkładał ją i kontynuował gdzie indziej. Hipoteza robocza była taka, że grupa identycznych awarii może współdzielić jedną uszkodzoną zależność metadanych, zamiast oznaczać setki niezależnie zniszczonych danych plików.
Orkiestracja była użyteczna. Bazowy czytnik APFS — nie.
W jednym z zapisanych momentów baza odzyskiwania zawierała:
DEFERRED_HOTSPOT: 30,356 files
FAILED: 486 files486 awarii rozkładało się następująco:
409 APFSBlock crashes
75 rc=0 but output-size mismatch
2 other rc=1 failuresA uporządkowany przebieg dał:
0 useful recovered user files75 niezgodności rozmiaru ujawniło błąd w mojej własnej warstwie pośredniej: niektóre pliki metadanych systemowych zwróciły dane, ale mój parser zapisał oczekiwany rozmiar równy zero. Naprawienie tej interpretacji miało znaczenie, lecz nie zmieniło dominującego wyniku. Zwykłe pliki użytkownika nadal kończyły z zerową liczbą bajtów i błędem could not read APFSBlock.
Wybrałem jeden mały plik AVIF jako powtarzalny przypadek testowy. TSK znał jego oczekiwany rozmiar:
expected size: 56,309 bytes
recovered: 0 bytesTen plik stał się znacznie cenniejszy niż kolejny wielogodzinny przebieg masowy. Jeśli nowe podejście nie potrafiło odzyskać jednego 56 KB pliku testowego, który konsekwentnie kończył się błędem, nie zasługiwało na dostęp do pozostałych terabajtów.
Bloki dysku i ścieżka metadanych APFS były odrębnymi obszarami awarii
Na tym etapie miałem trzy obserwacje:
GNU ddrescue:
almost the entire physical source was copied
TSK fls:
many real paths were discoverable
TSK icat:
many ordinary file contents still failedTe obserwacje są ze sobą zgodne, jeśli pojęciowo rozdzielić łańcuch wyszukiwania:
pathname
↓
directory record
↓
file object / inode metadata
↓
extent mapping
↓
physical data blocksBloki danych blisko końca tego łańcucha mogą przetrwać, podczas gdy wyżej położone ogniwo w łańcuchu metadanych jest uszkodzone. Inną możliwością jest to, że dwie implementacje APFS przechodzą przez te same uszkodzone struktury w różny sposób.
Nie wyizolowałem jednego uszkodzonego obiektu APFS, który wyjaśniałby każdą awarię, więc nie twierdziłbym, że była to potwierdzona przyczyna źródłowa. Dowody wspierały natomiast znacznie bardziej użyteczny eksperyment: pozostawić sklonowane bajty bez zmian i zmienić parser.
142 punkty kontrolne APFS nie stały się natychmiastową drogą do wycofania stanu
Surowe skanowanie tylko do odczytu obszaru deskryptorów punktów kontrolnych APFS znalazło 142 kandydujące superbloki punktów kontrolnych NXSB, z identyfikatorami transakcji od:
223133w dół do:
222992Oczywistym pytaniem było, czy starszy punkt kontrolny odwoływał się do zdrowszego drzewa metadanych.
Zbudowałem apfs-fuse i przez fuse-t próbowałem różnych identyfikatorów transakcji punktów kontrolnych. Najnowszy punkt kontrolny się zawieszał. Starsze XID zachowywały się tak samo. Dodałem twarde limity czasu dla każdego punktu kontrolnego i ponad 50 kolejnych prób nie doprowadziło do użytecznego montowania. Próbowałem również backendów NFS i SMB w fuse-t.
Nie dowodziło to, że wszystkie punkty kontrolne były uszkodzone. Eksperyment zależał od punktu kontrolnego, apfs-fuse, fuse-t, zachowania urządzeń w macOS oraz backendu montowania. Awaria w dowolnym miejscu tego stosu mogła dać ten sam widoczny rezultat.
Późniejszy parser zgłosił zero migawek APFS, co dodatkowo przypomniało mi o rozróżnieniu, którego musiałem pilnować: te stany transakcyjne punktów kontrolnych nie były tym samym co widoczne dla użytkownika migawki APFS.
Parser, który zawiesza się na --help, nie może diagnozować mojego dysku
Zbudowałem również go-apfs-v2. Kompilacja się zakończyła, ale nawet:
apfs --helpzawieszało się i musiało zostać przerwane po przekroczeniu limitu czasu.
Minimalna inspekcja bloków zarówno przez surową, jak i buforowaną ścieżkę urządzenia także się zawieszała. Taki sam ograniczony czasowo test wykonałem z apfsutil; obie formy urządzenia przekraczały limit po około 15 sekundach.
Były to użyteczne porażki, ponieważ powstrzymały mnie przed wyciągnięciem błędnego wniosku. Narzędzie, które nie potrafi niezawodnie ukończyć nawet własnej ścieżki pomocy, jest słabym dowodem na to, czy uszkodzony plik da się odzyskać.
Moją regułą stało się:
Najpierw zweryfikuj narzędzie do odzyskiwania, zanim potraktujesz jego awarię jako dowód dotyczący danych.
Zablokowanie automatycznego montowania klonu przez macOS usunęło kolejne źródło ryzyka
Sam macOS był kolejnym ruchomym elementem. Chciałem, aby sprawny dysk docelowy był montowany normalnie, ale nie chciałem, żeby Disk Arbitration automatycznie próbował montować uszkodzony klon APFS przy każdym ponownym podłączeniu nośników.
Bezpieczna sekwencja, której ostatecznie używałem, wyglądała tak:
- Podłącz i zamontuj sprawny dysk docelowy odzyskiwania.
- Zweryfikuj tożsamość woluminu i wolne miejsce.
- Wstrzymaj
diskarbitrationd. - Sprawdź, czy nie działa żaden proces
mount_apfs. - Podłącz klon wzorcowy.
- Dynamicznie zidentyfikuj klon wzorcowy na podstawie znanego fizycznego rozmiaru i tożsamości APFS.
- Sprawdź, czy klon wzorcowy nie jest zamontowany.
- Wykonuj odzyskiwanie wyłącznie do odczytu.
- Przy kończeniu zapisz stan, wznów Disk Arbitration, a następnie normalnie wyłącz komputer lub wysuń dysk.
Polecenie, którego faktycznie używałem do wstrzymania Disk Arbitration, brzmiało:
sudo kill -STOP "$(pgrep -x diskarbitrationd)"Sprawdzałem, czy stan procesu zawiera T, i przed kontynuowaniem szukałem zabłąkanych procesów mount_apfs.
Najważniejsza lekcja dotycząca bezpieczeństwa nie dotyczyła konkretnego numeru dysku. Wręcz przeciwnie: nigdy nie ufaj wczorajszemu numerowi dysku. Po restarcie poprzednie /dev/disk6 może oznaczać inne fizyczne urządzenie. Za tożsamość uznawałem UUID APFS i znany fizyczny rozmiar, a dopiero z nich wyprowadzałem aktualną ścieżkę urządzenia.
libfsapfs odzyskał ten sam plik, który TSK zwracał jako zero bajtów
Przełom przyniósł libfsapfs, czyli inna implementacja APFS.
Zbudowałem ją ze źródeł w macOS. Powstały plik binarny fsapfsinfo identyfikował się jako:
fsapfsinfo 20260923Wtedy ujawniła się ważna różnica między interfejsami urządzeń w macOS.
Partycja dostępna przez surowe urządzenie znakowe:
/dev/rdiskNs2szybko kończyła się błędem odczytu „invalid argument” w pobliżu przesunięcia 4096.
Buforowana forma urządzenia blokowego:
/dev/diskNs2działała.
Ten sam fizyczny klon. Ta sama partycja APFS. Inny interfejs urządzenia w macOS.
fsapfsinfo otworzył kontener i znalazł jeden wolumin. Następnie przetestowałem dokładnie ten plik o rozmiarze 56 309 bajtów, którego TSK nie potrafił wyodrębnić.
TSK dawał:
expected: 56,309
recovered: 0
APFSBlock failurePrzez libfsapfs wpis pliku dał:
size: 56,309
MD5: c6f56db33eafc1de0f52a035bc255dc7
RC: 0To był pierwszy wynik, który istotnie zmienił diagnozę. Sklonowane bajty się nie zmieniły. Plik testowy się nie zmienił. Uszkodzony APFS nie został naprawiony.
Zmieniła się implementacja APFS.
Co najmniej wiedziałem już, że rzeczywisty plik użytkownika, który przez TSK wyglądał na nieodzyskiwalny, był nadal osiągalny przez libfsapfs na tyle głęboko, by odczytać jego pełną zawartość i obliczyć skrót.
Wyodrębniłem jeden znany wadliwy plik, zanim zaufałem libfsapfs przy terabajtach danych
Poprawny skrót nie wystarczał mi do uruchomienia wieloterabajtowego odzyskiwania. Chciałem mieć rzeczywiste bajty na dysku docelowym.
Zamiast zgadywać API C biblioteki libfsapfs, przejrzałem jej kod źródłowy i podążyłem ścieżką odczytu, której fsapfsinfo już używał przy obliczaniu skrótu. Następnie zbudowałem mały ekstraktor tylko do odczytu dla jednego znanego pliku testowego.
Ekstraktor zapisał dokładnie:
56,309 bytesna osobnym dysku odzyskiwania. MD5 wyodrębnionego pliku wynosił:
c6f56db33eafc1de0f52a035bc255dc7Był zgodny z wcześniejszym skrótem.
Dopiero wtedy przeskalowałem podejście. Test jednego pliku udowodnił trzy odrębne rzeczy: ścieżkę dało się rozwiązać, można było wyodrębnić pełną oczekiwaną liczbę bajtów, a wyodrębnione bajty dawały ten sam skrót co wcześniejszy pełny odczyt zawartości.
Masowe odzyskiwanie było przede wszystkim problemem izolowania awarii i wznawiania
Gdy libfsapfs potrafił odzyskać plik, którego TSK nie potrafił, trudny problem ponownie się zmienił. Potrzebowałem systemu zdolnego przetworzyć bardzo duże drzewo katalogów tak, aby jedna uszkodzona gałąź, jeden restart lub jedno Ctrl+C nie zmieniały zadania w ponowne uruchomienie od zera.
Potok masowego odzyskiwania opierał się więc na kilku ścisłych niezmiennikach:
- Klon wzorcowy był otwierany tylko do odczytu.
- Odzyskane dane były zapisywane wyłącznie na drugim dysku 5 TB.
- Nazwy katalogów i plików były zachowywane.
- Postęp był przechowywany w SQLite, więc przetrwał zakończenia procesu i restarty.
- Każdy plik najpierw trafiał do ścieżki tymczasowej.
- Plik tymczasowy był zmieniany na docelową ścieżkę dopiero po zapisaniu pełnego oczekiwanego rozmiaru.
- Podczas wznawiania można było rozpoznać istniejące pliki o oczekiwanym rozmiarze.
- Prace zakończone niepowodzeniem lub problematyczne były trzymane osobno od ukończonych.
- Sprawdzano wolne miejsce i utrzymywano rezerwę bezpieczeństwa.
- Odtwarzalne artefakty developerskie i mało wartościowe metadane systemowe można było pomijać lub obniżać ich priorytet.
Jedna zmiana wydajności od razu miała znaczenie: przestałem niezależnie otwierać kontener APFS dla każdego pliku.
Szybka ścieżka przetwarzała po jednym katalogu naraz. Proces roboczy otwierał źródło, wyliczał dany katalog, odzyskiwał jego bezpośrednie zwykłe pliki i zwracał katalogi potomne do kolejki. Jeśli katalog kończył się błędem albo przekraczał limit czasu, kontroler oznaczał go jako DEFERRED i przechodził dalej, zamiast blokować cały przebieg.
Gdy nie pozostawały już żadne normalne oczekujące zadania, odłożone katalogi były ponownie odwiedzane wolniejszą ścieżką zapasową, z bardziej odizolowaną pracą na pojedynczych plikach. Pozostałe awarie można było potem ponawiać niezależnie.
discover directory
↓
recover immediate files
↓
verify expected sizes
↓
commit durable state
↓
queue child directories
↓
defer local failures
↓
continue globally
↓
fallback and retry laterTa architektura odpowiadała rzeczywistemu wzorcowi uszkodzeń znacznie lepiej niż jedno wielkie polecenie rekurencyjne. Uszkodzenia nie były równomierne, więc system odzyskiwania także nie powinien wymuszać równomiernego postępu.
Dlaczego używałem kontroli oczekiwanego rozmiaru i atomowej zmiany nazwy zamiast haszować miliony plików
MD5 był użyteczny podczas próby na jednym pliku, ponieważ potrzebowałem mocnego dowodu, że parser odczytuje pełną zawartość pliku, którego TSK nie potrafił odzyskać.
Drugi pełny odczyt każdego odzyskanego bajtu tylko po to, by obliczyć skróty milionów plików, dodałby ogromną ilość I/O. Dla głównego przebiegu ekstrakcji użyłem innego niezmiennika.
Dla każdego zwykłego pliku metadane APFS podawały oczekiwany rozmiar. Proces roboczy zapisywał do pliku tymczasowego i przenosił go pod ostateczną ścieżkę dopiero wtedy, gdy pełny odczyt odpowiadał oczekiwanemu rozmiarowi.
Oznacza to, że przerwanie nie powinno pozostawić skróconego pliku podszywającego się pod gotowy plik o docelowej nazwie.
Zgodność rozmiaru nie jest kryptograficznym dowodem integralności. Nie traktuję jej w ten sposób. Jednak weryfikacja oczekiwanego rozmiaru wraz z atomową zmianą nazwy stanowiła praktyczną granicę poprawności dla masowego przebiegu, podczas gdy celowane hashe nadal były użyteczne dla próbek i znanych przypadków awarii.
SQLite sprawił, że restart był nudny zamiast katastrofalny
Odzyskiwanie trwało na tyle długo, że musiałem wyłączać komputer i wracać do pracy później. Ten wymóg zmienił projekt ze „skryptu” w „wznawialny proces odzyskiwania”.
Ctrl+C nie porzucało po prostu aktywnego procesu potomnego. Kontroler przechwytywał przerwanie, zatrzymywał workera, przywracał aktywny katalog do stanu możliwego do wznowienia, zatwierdzał SQLite i kończył działanie.
Czyste zatrzymanie wyglądało koncepcyjnie tak:
CURRENT DIRECTORY -> PENDING
CTRL+C: RECOVERY STOPPED SAFELY
STATE SAVED. RUN THE SAME COMMAND TO RESUME.Po restarcie ponownie wykonywałem kontrole tożsamości dysków i uruchamiałem to samo polecenie odzyskiwania. Baza stanu wznawiała istniejącą kolejkę.
Jeden z późniejszych restartów zaczął się od:
DEFERRED: 1
DONE: 73,015
PENDING: 7,254To miało znacznie większe znaczenie niż ogólny pasek postępu. Pokazywało, że dziesiątki tysięcy ukończonych jednostek katalogowych przetrwały restart, a pozostała kolejka była jawnie znana.
Przy wcześniejszym wznowieniu katalog przerwany w poprzedniej sesji został podjęty ponownie. Pliki już obecne w oczekiwanym rozmiarze były rozpoznawane i trzeba było zapisać tylko brakującą pracę. Właśnie takiego zachowania chciałem: ponowne uruchomienie odzyskiwania powinno być rutyną, a nie powodem do strachu.
326 799 plików było pierwszym dowodem, że metoda skaluje się poprawnie
Zanim rozszerzyłem nowy ekstraktor na wszystkie wybrane dane, wykorzystałem jedno duże drzewo priorytetowe jako cel masowej walidacji.
Stan ukończenia raportował:
directories completed: 8,940
new files written: 302,541
existing/resumed files: 24,258
recorded failed files: 0Dysk docelowy zawierał:
326,799 files
145,039,215,948 bytesczyli około:
135.08 GiBLiczby plików zgadzały się dokładnie:
302,541 + 24,258 = 326,799W ukończonym drzewie nie pozostały żadne pliki .partial.*.
Plik testowy o rozmiarze 56 309 bajtów udowodnił, że parser może odnieść sukces tam, gdzie TSK zawiódł. Odzyskanie 326 799 plików dowiodło, że to samo podejście potrafi przejść przez znaczną rzeczywistą hierarchię z logiką wznawiania, wykrywaniem istniejących plików i bez zarejestrowanych awarii plików w tym ukończonym przebiegu.
Większe odzyskiwanie przekroczyło 1,6 miliona nowych plików przed zakończeniem
Gdy drzewo priorytetowe zakończyło się bez problemów, rozszerzyłem odzyskiwanie na pozostałe wybrane dane najwyższego poziomu.
Przy jednym celowym bezpiecznym zatrzymaniu SQLite raportował:
DONE directories: 39,015
PENDING directories: 17,017
DEFERRED directories: 1
new files written: 1,636,305
new bytes written: 902,715,716,335Było to około:
840.72 GiBnowo zapisanych danych zarejestrowanych przez przebiegi katalogów na tym etapie.
Jeden katalog DEFERRED nie oznaczał utraconych danych. Oznaczał, że szybka ścieżka celowo przestała pozwalać temu lokalnemu problemowi opóźniać niezależną pracę. Faza awaryjna istniała właśnie po to, by później wracać do takich przypadków.
Kolejne sesje wznawiały pracę z tej samej bazy. Liczba ukończonych katalogów rosła, a kolejka oczekujących malała. Ostatecznie odzyskałem każde wybrane, wyliczone drzewo katalogów, którego potrzebowałem.
Co mogę uczciwie powiedzieć o końcowym wyniku odzyskiwania
Nie opiszę wyniku jako „odzyskano każdy bajt”. Dowody tego nie potwierdzają.
Oryginalna mapa ddrescue nadal zawierała około 13,84 MB, których nie potwierdzono jako poprawnie skopiowane. Nie mogę również udowodnić, że żaden obiekt systemu plików nie stał się całkowicie niewykrywalny dlatego, że metadane potrzebne do jego wyliczenia znajdowały się w uszkodzonych obszarach.
Te ograniczenia mają znaczenie, ponieważ uporządkowane odzyskiwanie może dowieść, że wyliczony obiekt został wyodrębniony; nie może natomiast dowieść historycznego nieistnienia obiektu, którego uszkodzona przestrzeń nazw nie potrafi już ujawnić.
Najmocniejsze końcowe stwierdzenie jest węższe:
Każde wybrane, wyliczone drzewo katalogów, którego potrzebowałem, zostało pomyślnie odzyskane w uporządkowanym procesie odzyskiwania.
Nie musiałem naprawiać klonu wzorcowego w miejscu. Nie musiałem ponownie używać oryginalnej klikającej Toshiby do masowej ekstrakcji. Nie potrzebowałem też pełnodyskowego carvingu w stylu PhotoRec, który poświęciłby strukturę katalogów i nazwy plików.
Narzędzia, które zawiodły, nadal dostarczały użytecznych dowodów
Z perspektywy czasu udana ścieżka brzmi prosto:
ddrescue clone
↓
libfsapfs
↓
resumable extractor
↓
recovered filesTak jednak nie wyglądało to dochodzenie, kiedy trwało, a usunięcie nieudanych podejść usunęłoby dużą część użytecznej lekcji inżynierskiej.
TSK nauczył mnie, że przechodzenie po przestrzeni nazw APFS i pobieranie zawartości to dwa różne tryby awarii.
Pierwszy błąd mojej warstwy pośredniej nauczył mnie, że własna klasyfikacja błędów w skrypcie odzyskiwania nie jest niepodważalną prawdą.
Mechanizm hotspot nauczył mnie izolować lokalną awarię zamiast pozwalać jej zatrzymać globalny postęp.
Eksperyment z punktami kontrolnymi nauczył mnie nie mylić transakcyjnych punktów kontrolnych APFS z migawkami.
Próby z FUSE nauczyły mnie, że nieudane montowanie może wskazywać na problemy w kilku warstwach poza samymi danymi systemu plików.
Czytnik APFS, który zawieszał się na --help, nauczył mnie sprawdzać narzędzie przed interpretowaniem jego diagnostyki.
Zachowanie /dev/rdiskNs2 w porównaniu z /dev/diskNs2 nauczyło mnie, że ścieżka I/O systemu operacyjnego może zmienić zachowanie parsera, nawet gdy bazowy dysk jest identyczny.
A architektura z dwoma dyskami dała mi swobodę popełniania błędów wszędzie indziej, przy pozostawieniu klonu wzorcowego bez zmian.
Proces odzyskiwania, którego użyłbym ponownie
- Zatrzymaj zwykłą aktywność systemu plików na mechanicznie uszkadzającym się nośniku. Jeśli odczyty się zacinają, urządzenie znika albo klika, priorytetem byłby dla mnie wznawialny klon blokowy zamiast przeglądania w Finderze.
- Użyj GNU ddrescue z trwałym mapfile i zweryfikowaną domeną odzyskiwania. Mapfile zachowuje postęp; znany rozmiar źródła zapobiega temu, by niejasność rozmiaru urządzenia stała się częścią problemu odzyskiwania.
- Trzymaj jeden klon wzorcowy tylko do odczytu. Nie naprawiaj go, nie zmieniaj jego rozmiaru, nie przepartycjonowuj go i nie używaj jako miejsca na odzyskane pliki.
- Zapisuj odzyskane pliki na drugim fizycznym dysku. Ochrona źródła i przechowywanie wyników to dwa różne zadania.
- Najpierw diagnozuj sklonowany system plików tylko do odczytu. Sprawny fizyczny klon zawierający uszkodzone metadane APFS jest problemem logicznego odzyskiwania, a nie tym samym problemem co klikający dysk źródłowy.
- Wybierz jeden powtarzalnie wadliwy plik jako test parsera. Znany wadliwy plik 56 KB powiedział mi więcej o alternatywnych parserach niż godziny ślepej ekstrakcji masowej.
- Zweryfikuj sam parser. Jeśli narzędzie zawiesza się, zanim zacznie sensownie czytać źródło, nie interpretuj tego jako dowodu, że danych już nie ma.
- Nie zakładaj, że jedna implementacja APFS definiuje odzyskiwalność. TSK i
libfsapfszachowywały się bardzo różnie na tych samych sklonowanych bajtach. - Identyfikuj dyski według stabilnych właściwości, a nie tymczasowych numerów urządzeń. UUID systemu plików i znany fizyczny rozmiar są bezpieczniejsze niż wczorajsze
/dev/diskN. - Od początku projektuj długotrwałą ekstrakcję tak, aby można ją było wznowić. Trwały stan, pliki tymczasowe, atomowa zmiana nazwy, odłożona praca, ograniczone ponowienia i obsługa czystego zatrzymania są częścią poprawności przy tej skali.
- Rozdziel poziomy walidacji. Procent ddrescue, widoczna ścieżka, zgodność oczekiwanego rozmiaru i hash zawartości dowodzą różnych rzeczy.
Liczba wyglądająca jak meta była tylko końcem pierwszego etapu
Najbardziej mylącą liczbą w całym odzyskiwaniu nadal było:
100.00%Wyglądało jak odpowiedź na pytanie „Czy uratowałem dysk?”.
W rzeczywistości odpowiadało na znacznie węższe pytanie:
How much of the physical rescue domain did ddrescue successfully copy?Nie odpowiadało na pytanie, czy APFS potrafi odtworzyć przestrzeń nazw. Nie odpowiadało na pytanie, czy jeden parser potrafi przejść przez uszkodzone metadane odrzucane przez inny parser. Nie mówiło, czy odzyskany plik ma oczekiwaną długość. I nie mówiło, czy ekstrakcja milionów plików potrafi przetrwać błędy i restarty bez uszkodzenia własnego stanu.
Dla każdej warstwy potrzebowałem osobnych dowodów.
Najpierw udało się odzyskiwanie fizyczne. System plików nadal był uszkodzony. Pierwszy parser potrafił ujawnić nazwy, ale zawodził przy zawartości wielu plików. Inna implementacja APFS pomyślnie odczytała ten sam plik testowy. Ten dowód stał się ekstraktorem jednego pliku, ekstraktor stał się wznawialnym silnikiem odzyskiwania, a silnik ostatecznie odzyskał potrzebne mi wybrane drzewa katalogów na drugi dysk, podczas gdy klon wzorcowy pozostał nietknięty.
Oryginalny dysk twardy nigdy nie odzyskał sprawności. APFS nie naprawił się magicznie. Zmienił się model odzyskiwania.
Odzyskiwanie bloków, odzyskiwanie systemu plików i walidacja plików to osobne etapy inżynierskie. Traktowanie ich jako jednego problemu sprawiało, że sytuacja wyglądała niemal beznadziejnie. Rozdzielenie ich uczyniło ją możliwą do opanowania.