Pisanie programów radzących sobie z błędami we / wy powodującymi utratę zapisów w systemie Linux

138

TL; DR: Jeśli jądro Linuksa utraci buforowany zapis we / wy , czy jest jakiś sposób, aby aplikacja się dowiedziała?

Wiem, że potrzebujesz fsync()pliku (i jego katalogu nadrzędnego) dla trwałości . Pytanie brzmi: jeśli jądro traci brudne bufory oczekujące na zapis z powodu błędu we / wy, w jaki sposób aplikacja może to wykryć i odzyskać lub przerwać?

Pomyśl o aplikacjach baz danych itp., Gdzie kolejność i trwałość zapisu mogą być kluczowe.

Zagubione zapisy? W jaki sposób?

Warstwa blok Czy Linux Kernel jest w pewnych okolicznościach stracić buforowane żądań I / O, które zostały wprowadzone z powodzeniem write(), pwrite()itp, z błędem jak:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0

(Zobacz end_buffer_write_sync(...)i end_buffer_async_write(...)wfs/buffer.c środku).

W nowszych jądrach błąd będzie zawierał „utracony asynchroniczny zapis strony” , na przykład:

Buffer I/O error on dev dm-0, logical block 12345, lost async page write

Ponieważ aplikacja write()została już zwrócona bez błędów, wydaje się, że nie ma możliwości zgłoszenia błędu z powrotem do aplikacji.

Wykrywanie ich?

Nie jestem zaznajomiony ze źródłami jądra, ale myślę , że ustawia AS_EIObufor, który nie został zapisany, jeśli wykonuje zapis asynchroniczny:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

ale nie jest dla mnie jasne, czy i w jaki sposób aplikacja może się o tym dowiedzieć, kiedy później fsync()będzie plik, aby potwierdzić, że znajduje się na dysku.

Wygląda na to, że wait_on_page_writeback_range(...)wmm/filemap.c potędze, do_sync_mapping_range(...)wfs/sync.c którym jest sprawdzany sys_sync_file_range(...). Zwraca, -EIOjeśli nie można zapisać jednego lub więcej buforów.

Jeśli, jak się domyślam, rozprzestrzeni się to do fsync()wyniku, to jeśli aplikacja panikuje i wyskakuje, jeśli otrzyma błąd we / wy fsync()i wie, jak ponownie wykonać swoją pracę po ponownym uruchomieniu, to powinno być wystarczającym zabezpieczeniem?

Prawdopodobnie nie ma sposobu, aby aplikacja wiedziała, które przesunięcia bajtów w pliku odpowiadają utraconym stronom, aby mogła je przepisać, jeśli wie jak, ale jeśli aplikacja powtórzy całą swoją oczekującą pracę od ostatniego udanego fsync()pliku, a to przepisuje wszelkie brudne bufory jądra odpowiadające utraconym zapisom w pliku, które powinny wyczyścić wszelkie flagi błędów we / wy na utraconych stronach i pozwolić na zakończenie następnej fsync()- prawda?

Czy są zatem inne, nieszkodliwe okoliczności, do których fsync()mogą powrócić, w -EIOktórych ratowanie i ponawianie pracy byłoby zbyt drastyczne?

Czemu?

Oczywiście takie błędy nie powinny się zdarzyć. W tym przypadku błąd wynikał z niefortunnej interakcji między dm-multipathustawieniami domyślnymi sterownika a kodem rozpoznawczym używanym przez sieć SAN do zgłaszania niepowodzenia alokacji alokacji elastycznej. Ale to nie jedyna okoliczność, w której mogą się zdarzyć - widziałem również raporty o tym, na przykład z cienkiego aprowizowanego LVM, używanego przez libvirt, Docker i inne. Krytyczna aplikacja, taka jak baza danych, powinna próbować radzić sobie z takimi błędami, zamiast działać na ślepo, jakby wszystko było w porządku.

Jeśli jądro uważa, że ​​można stracić zapisy bez umierania z powodu paniki jądra, aplikacje muszą znaleźć sposób, aby sobie z tym poradzić.

Praktyczny wpływ jest taki, że znalazłem przypadek, w którym problem wielościeżkowy z SAN spowodował utratę zapisów, które wylądowały, powodując uszkodzenie bazy danych, ponieważ DBMS nie wiedział, że jego zapisy nie powiodły się. Nie śmieszne.

Craig Ringer
źródło
1
Obawiam się, że wymagałoby to dodatkowych pól w SystemFileTable do przechowywania i zapamiętywania tych błędów. A także możliwość odbierania lub sprawdzania ich w przestrzeni użytkownika podczas kolejnych połączeń. (czy fsync () i close () zwracają tego rodzaju informacje historyczne ?)
joop
@joop Thanks. Właśnie opublikowałem odpowiedź z tym, co myślę, że się dzieje, pamiętaj o sprawdzeniu poczytalności, ponieważ wydaje się, że wiesz więcej o tym, co się dzieje, niż ludzie, którzy opublikowali oczywiste warianty metody „write (), potrzebują close () lub fsync ( ) dla trwałości „bez czytania pytania?
Craig Ringer
BTW: Myślę, że naprawdę powinieneś zagłębić się w źródła jądra. Systemy plików zapisane w dzienniku prawdopodobnie cierpiałyby z powodu tego samego rodzaju problemów. Nie wspominając o obsłudze partycji wymiany. Ponieważ te żyją w przestrzeni jądra, obsługa tych warunków będzie prawdopodobnie nieco bardziej sztywna. writev (), która jest widoczna z przestrzeni użytkownika, również wydaje się odpowiednim miejscem do oglądania. [w Craig: tak, ponieważ znam twoje imię i wiem, że nie jesteś kompletnym idiotą; -]
joop
1
Zgadzam się, nie byłem taki fair. Niestety Twoja odpowiedź nie jest zbyt satysfakcjonująca, to znaczy nie ma łatwego rozwiązania (zaskakujące?).
Jean-Baptiste Yunès
1
@ Jean-BaptisteYunès True. W przypadku DBMS, z którym pracuję, akceptowalna jest opcja „crash and enter redo”. W przypadku większości aplikacji nie jest to możliwe i mogą one tolerować okropną wydajność synchronicznych operacji we / wy lub po prostu akceptować źle zdefiniowane zachowanie i uszkodzenie w przypadku błędów we / wy.
Craig Ringer

Odpowiedzi:

91

fsync()zwraca, -EIOjeśli jądro utraciło zapis

(Uwaga: wczesne części odnoszą się do starszych jąder; zaktualizowane poniżej, aby odzwierciedlały nowoczesne jądra)

Wygląda na to, że asynchroniczny zapis bufora w przypadku end_buffer_async_write(...)awarii ustawia -EIOflagę na stronie uszkodzonego buforu dla pliku :

set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);

która jest wykrywana przez wait_on_page_writeback_range(...)co wywołana przez do_sync_mapping_range(...)co wywołana przez sys_sync_file_range(...)co wywołana przez sys_sync_file_range2(...)do realizacji połączenia biblioteki C fsync().

Ale tylko raz!

Ten komentarz na sys_sync_file_range

168  * SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any
169  * I/O errors or ENOSPC conditions and will return those to the caller, after
170  * clearing the EIO and ENOSPC flags in the address_space.

sugeruje, że gdy fsync()zwróci -EIOlub (nieudokumentowane na stronie podręcznika) -ENOSPC, wyczyści stan błędu, więc kolejny fsync()poinformuje o sukcesie, nawet jeśli strony nigdy nie zostały zapisane.

Oczywiście wait_on_page_writeback_range(...) czyści bity błędów podczas ich testowania :

301         /* Check for outstanding write errors */
302         if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
303                 ret = -ENOSPC;
304         if (test_and_clear_bit(AS_EIO, &mapping->flags))
305                 ret = -EIO;

Więc jeśli aplikacja spodziewa się, że może spróbować ponownie, fsync()aż się powiedzie i będzie ufać, że dane znajdują się na dysku, to jest strasznie źle.

Jestem prawie pewien, że to jest źródło uszkodzenia danych, które znalazłem w DBMS. Ponawia próbę fsync()i myśli, że wszystko będzie dobrze, gdy się powiedzie.

Czy to jest dozwolone?

Dokumentacja POSIX / SuSfsync() tak naprawdę nie określa tego w żaden sposób:

Jeśli funkcja fsync () zawiedzie, nie ma gwarancji, że zaległe operacje we / wy zostaną zakończone.

Strona podręcznika systemu Linux pofsync() prostu nie mówi nic o tym, co dzieje się w przypadku awarii.

Wygląda więc na to, że znaczenie fsync()błędów brzmi „nie wiem, co się stało z twoimi zapisami, mogło zadziałać lub nie, lepiej spróbuj ponownie, aby się upewnić”.

Nowsze jądra

W end_buffer_async_writezestawach 4.9 -EIOna stronie, po prostu przez mapping_set_error.

    buffer_io_error(bh, ", lost async page write");
    mapping_set_error(page->mapping, -EIO);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

Po stronie synchronizacji myślę, że jest podobnie, chociaż struktura jest teraz dość złożona do naśladowania. filemap_check_errorsw mm/filemap.cteraz robi:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;

co ma podobny efekt. Wydaje się, że wszystkie kontrole błędów przechodzą, filemap_check_errorsco przeprowadza test i wyczyszczenie:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;

Używam btrfsna moim laptopie, ale kiedy tworzę ext4pętlę zwrotną do testowania /mnt/tmpi ustawiam na niej sondę perf:

sudo dd if=/dev/zero of=/tmp/ext bs=1M count=100
sudo mke2fs -j -T ext4 /tmp/ext
sudo mount -o loop /tmp/ext /mnt/tmp

sudo perf probe filemap_check_errors

sudo perf record -g -e probe:end_buffer_async_write -e probe:filemap_check_errors dd if=/dev/zero of=/mnt/tmp/test bs=4k count=1 conv=fsync

Znajduję następujący stos wywołań w perf report -T:

        ---__GI___libc_fsync
           entry_SYSCALL_64_fastpath
           sys_fsync
           do_fsync
           vfs_fsync_range
           ext4_sync_file
           filemap_write_and_wait_range
           filemap_check_errors

Czytanie sugeruje, że tak, nowoczesne jądra zachowują się tak samo.

Wydaje się to oznaczać, że jeśli fsync()(lub przypuszczalnie write()lub close()) powróci -EIO, plik jest w jakimś niezdefiniowanym stanie między ostatnim pomyślnym fsync()d lub close()d, a ostatnim write()dziesięcioma stanami.

Test

Zaimplementowałem przypadek testowy, aby zademonstrować to zachowanie .

Implikacje

DBMS może sobie z tym poradzić, uruchamiając odzyskiwanie po awarii. Jak, u licha, ma sobie z tym radzić zwykła aplikacja użytkownika? fsync()Strona człowiek nie daje ostrzeżenie, że to znaczy „fsync-if-you-feel-jak-to”, a ja się spodziewać wiele z aplikacji nie będzie dobrze poradzić sobie z tym zachowaniem.

Zgłaszanie błędów

Dalsza lektura

Witryna lwn.net poruszyła ten temat w artykule „Ulepszona obsługa błędów w warstwie bloków” .

Wątek z listą dyskusyjną postgresql.org .

Craig Ringer
źródło
3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 to możliwy wyścig, ponieważ czeka na {oczekujące i zaplanowane I / O}, a nie {jeszcze nie zaplanowane I / O}. Ma to oczywiście na celu uniknięcie dodatkowych podróży w obie strony do urządzenia. (Zakładam, że użytkownik pisze () nie zwraca, dopóki I / O nie zostanie zaplanowane, dla mmap () jest inaczej)
joop
3
Czy jest możliwe, że wywołanie fsync przez inny proces dla jakiegoś innego pliku na tym samym dysku zwraca błąd?
Random832
3
@ Random832 Bardzo istotne dla wieloprocesorowej bazy danych, takiej jak PostgreSQL, więc dobre pytanie. Wygląda na to, że prawdopodobnie, ale nie znam kodu jądra na tyle dobrze, aby go zrozumieć. Lepiej, żeby twoje procy współpracowały, jeśli i tak oba mają otwarty ten sam plik.
Craig Ringer
1
@DavidFoerster: wywołania systemowe zwracają błędy przy użyciu ujemnych kodów errno; errnojest całkowicie konstrukcją biblioteki C w przestrzeni użytkownika. Często ignoruje się różnice wartości zwracanej między wywołania systemowe a biblioteką C w ten sposób (jak Craig Ringer powyżej), ponieważ wartość zwracana przez błąd niezawodnie identyfikuje, do której z nich (wywołanie systemowe lub funkcja biblioteki C) się odnosi: „ -1z errno==EIO„odnosi się do funkcji biblioteki C, podczas gdy„ -EIO”odnosi się do wywołania systemowego. Wreszcie, internetowe strony podręcznika systemu Linux są najbardziej aktualnymi źródłami informacji o stronach podręcznika systemowego.
Nominalne zwierzę
2
@CraigRinger: Odpowiadając na Twoje ostatnie pytanie: „Używając niskopoziomowych operacji we / wy i fsync()/ fdatasync()gdy rozmiar transakcji jest kompletnym plikiem; używając mmap()/, msync()gdy rozmiar transakcji jest wyrównany do strony; oraz używając niskiego poziomu I / O fdatasync()i wiele współbieżnych deskryptorów plików (jeden deskryptor i wątek na transakcję) do tego samego pliku w przeciwnym razie " . Specyficzne dla Linuksa blokady opisu otwartego pliku ( fcntl(), F_OFD_) są bardzo przydatne w przypadku ostatniego.
Nominalne zwierzę
22

Ponieważ metoda write () aplikacji zwróciła już bez błędu, wydaje się, że nie ma możliwości zgłoszenia błędu z powrotem do aplikacji.

Nie zgadzam się. writemoże powrócić bez błędu, jeśli zapis jest po prostu w kolejce, ale błąd zostanie zgłoszony podczas następnej operacji, która będzie wymagała rzeczywistego zapisu na dysku, to znaczy w następnym fsync, prawdopodobnie następnym zapisie, jeśli system zdecyduje się opróżnić pamięć podręczną io godzinie przynajmniej przy ostatnim zamknięciu pliku.

Dlatego tak ważne jest, aby aplikacja przetestowała zwracaną wartość close, aby wykryć ewentualne błędy zapisu.

Jeśli naprawdę potrzebujesz umieć sprytnie przetwarzać błędy, musisz założyć, że wszystko, co zostało napisane od ostatniego sukcesu, fsync mogło się nie udać , a przynajmniej coś zawiodło.

Serge Ballesta
źródło
4
Tak, myślę, że to jest dobre. Byłoby to rzeczywiście sugerują, że aplikacja powinna ponownie wykonać całą swoją pracę od ostatniego potwierdził-sukces fsync()lub close()pliku, jeśli otrzyma -EIOod write(), fsync()lub close(). Cóż, to zabawne.
Craig Ringer
1

write(2) zapewnia mniej niż się spodziewasz. Strona podręcznika jest bardzo otwarta na temat semantyki udanego write()połączenia:

Pomyślny powrót z write()adresu nie gwarantuje, że dane zostały zapisane na dysku. W rzeczywistości w niektórych błędnych implementacjach nie gwarantuje to nawet pomyślnego zarezerwowania miejsca na dane. Jedynym sposobem, aby się upewnić, jest wywołanie fsync(2) po zakończeniu zapisywania wszystkich danych.

Możemy wywnioskować, że sukces write()oznacza jedynie, że dane dotarły do ​​funkcji buforowania jądra. Jeśli utrwalanie bufora nie powiedzie się, późniejszy dostęp do deskryptora pliku zwróci kod błędu. To może być ostateczność close(). Strona podręcznika closewywołania systemowego (2) zawiera następujące zdanie:

Jest całkiem możliwe, że błędy w poprzedniej write(2) operacji są najpierw zgłaszane w funkcji final close().

Jeśli Twoja aplikacja musi utrwalać zapisywanie danych, musi używać fsync/ fsyncdataregularnie:

fsync()przesyła („opróżnia”) wszystkie zmodyfikowane dane w rdzeniu (tj. zmodyfikowane strony pamięci podręcznej bufora) pliku, do którego odwołuje się deskryptor pliku fd, na urządzenie dyskowe (lub inne trwałe urządzenie magazynujące), aby można było odzyskać wszystkie zmienione informacje nawet po awarii lub ponownym uruchomieniu systemu. Obejmuje to zapisywanie lub opróżnianie pamięci podręcznej dysku, jeśli jest obecna. Połączenie jest blokowane do momentu, gdy urządzenie zgłosi, że transfer został zakończony.

fzgregor
źródło
4
Tak, wiem, że fsync()jest to wymagane. Ale w konkretnym przypadku, gdy jądro traci strony z powodu błędu I / O , fsync()nie powiedzie się? W jakich okolicznościach może to potem odnieść sukces?
Craig Ringer
Nie znam też źródła jądra. Załóżmy, że fsync()zwraca się -EIOw przypadku problemów z I / O (co by się przydało w innym przypadku?). Dzięki temu baza danych wie, że część poprzedniego zapisu nie powiodła się i może przejść do trybu odzyskiwania. Czy nie tego chcesz? Jaka jest motywacja Twojego ostatniego pytania? Czy chcesz wiedzieć, który zapis nie powiódł się, lub odzyskać deskryptor pliku do dalszego użytku?
fzgregor
Idealnie byłoby, gdyby DBMS wolał nie wprowadzać odzyskiwania po awarii (wyrzucając wszystkich użytkowników i stając się tymczasowo niedostępnym lub przynajmniej tylko do odczytu), jeśli może tego uniknąć. Ale nawet jeśli jądro mogłoby nam powiedzieć „bajty od 4096 do 8191 z fd X”, trudno byłoby dowiedzieć się, co tam (ponownie) napisać bez wykonywania odzyskiwania po awarii. Więc myślę, że głównym pytaniem jest, czy są jakieś bardziej niewinne okoliczności, w których fsync()mogą powrócić -EIOtam, gdzie można bezpiecznie spróbować ponownie, i czy można stwierdzić różnicę.
Craig Ringer
Na pewno odzyskiwanie po awarii to ostatnia deska ratunku. Ale jak już powiedziałeś, oczekuje się, że problemy te będą bardzo rzadkie. Dlatego nie widzę problemu z przejściem do odzyskiwania na żadnym -EIO. Jeśli każdy deskryptor pliku jest używany tylko przez jeden wątek na raz, ten wątek może wrócić do ostatniego fsync()i powtórzyć write()wywołania. Ale nadal, jeśli te write()zapisują tylko część sektora, niezmodyfikowana część może nadal być uszkodzona.
fzgregor
1
Masz rację, że przejście do odzyskiwania po awarii jest prawdopodobnie uzasadnione. Jeśli chodzi o częściowo uszkodzone sektory, DBMS (PostgreSQL) przechowuje obraz całej strony po pierwszym dotknięciu jej po dowolnym punkcie kontrolnym właśnie z tego powodu, więc powinno być dobrze :)
Craig Ringer
0

Użyj flagi O_SYNC podczas otwierania pliku. Zapewnia zapis danych na dysku.

Jeśli to cię nie zadowoli, nic nie będzie.

toughmanwang
źródło
17
O_SYNCto koszmar wydajności. Oznacza to, że aplikacja nie może robić nic innego, gdy występuje we / wy dysku, chyba że odrodzi się z wątków we / wy. Równie dobrze można powiedzieć, że buforowany interfejs I / O jest niebezpieczny i każdy powinien używać AIO. Z pewnością dyskretnie utracone zapisy nie mogą być akceptowane w buforowanych we / wy?
Craig Ringer
3
( O_DATASYNCjest tylko trochę lepszy w tym względzie)
Craig Ringer
@CraigRinger Powinieneś użyć AIO, jeśli masz taką potrzebę i potrzebujesz jakiejkolwiek wydajności. Lub po prostu użyj DBMS; załatwia wszystko za Ciebie.
Demi
10
@Demi Aplikacja tutaj to dbms (postgresql). Jestem pewien, że możesz sobie wyobrazić, że przepisanie całej aplikacji tak, aby korzystała z AIO zamiast buforowanego wejścia / wyjścia, nie jest praktyczne. Nie powinno to być też konieczne.
Craig Ringer
-5

Sprawdź zwracaną wartość zamknięcia. close może się nie powieść, podczas gdy buforowane zapisy wydają się udać.

Malcolm McLean
źródło
8
No, prawie nie chce być open()ing i close()ing plik co kilka sekund. dlatego mamy fsync()...
Craig Ringer