Написання програм для вирішення помилок вводу / виводу, що спричиняють втрачені записи в Linux


138

TL; DR: Якщо ядро ​​Linux втрачає захищене записування вводу / виводу , чи є програма, як це дізнатись?

Я знаю, що ви повинні мати fsync()файл (і його батьківський каталог) для довговічності . Питання полягає в тому, якщо ядро ​​втрачає брудні буфери, які очікують на запит через помилку вводу / виводу, як програма може виявити це і відновити або перервати?

Подумайте про додатки для баз даних тощо, де порядок стійкості запису та запису може мати вирішальне значення.

Втрачено пише? Як?

Блок шар чи ядро в деяких обставинах втрачають буферном запити введення / виведення , які були успішно представлені write(), і pwrite()т.д., з повідомленням про помилку , як:

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

(Див. end_buffer_write_sync(...)І end_buffer_async_write(...)вfs/buffer.c ).

На нових ядрах помилка замість цього буде містити "загублене записування сторінки асинхронізації" , наприклад:

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

Оскільки програми write()вже повернулися без помилок, схоже, немає ніякого способу повідомити про помилку в додатку.

Виявляючи їх?

Я не такий знайомий з джерелами ядра, але я думаю, що він встановлюється AS_EIOна буфер, який не вдалося виписати, якщо він виконує запис асинхронізації:

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

але мені незрозуміло, чи може програма чи дізнатися про це, коли пізніше fsync()файл буде підтверджений на диску.

Схоже , wait_on_page_writeback_range(...)вmm/filemap.c могутність на do_sync_mapping_range(...)вfs/sync.c якому повертаємо викликається sys_sync_file_range(...). Він повертається, -EIOякщо один або більше буферів не вдалося записати.

Якщо, як я здогадуюсь, це поширюється на fsync()результат, то якщо додаток панікує і не виходить, якщо воно отримує помилку вводу / виводу fsync()і знає, як виконати свою роботу при перезапуску, це повинно бути достатньою гарантією?

Імовірно, додаток не може знати, які байтові зрушення у файлі відповідають загубленим сторінкам, щоб він міг переписати їх, якщо знає як, але якщо додаток повторює всі очікувані роботи з останнього успішного fsync()файлу, і це переписує будь-які буфери брудного ядра, що відповідають втраченому запису у файл, які повинні очистити будь-які прапор помилок вводу / виводу на загублених сторінках і дозволити fsync()завершити наступне - так?

Чи є тоді якісь інші, нешкідливі обставини, коли fsync()можуть повернутися, -EIOколи випуск і переробка робіт були б занадто різкими?

Чому?

Звичайно, таких помилок не повинно бути. У цьому випадку помилка виникла через нещасну взаємодію за dm-multipathзамовчуванням драйвера та сенсорного коду, який використовує SAN для повідомлення про відмову у виділенні місця з обмеженим зберіганням. Але це не єдина обставина, коли вони можуть трапитися - я також бачив повідомлення про це, наприклад, з тонкого розміщеного LVM, як його використовують libvirt, Docker тощо. Такий критичний додаток, як база даних, повинен намагатися впоратися з такими помилками, а не сліпо продовжувати, як ніби все добре.

Якщо ядро вважає, що нормально втрачати записи, не вмираючи з панікою ядра, додатки повинні знайти спосіб впоратися.

Практичний вплив полягає в тому, що я виявив випадок, коли проблема з багатостороннім зв'язком з SAN викликала втрачені записи, які приземлилися, викликаючи пошкодження бази даних, тому що СУБД не знала, що її запис провалився. Не смішно.


1
Боюся, що для зберігання та запам'ятовування цих умов помилок знадобляться додаткові поля в SystemFileTable. І можливість процесу користувацького простору отримувати або перевіряти їх при наступних дзвінках. (повернути fsync () та закрити () повернути такий тип історичної інформації?)
joop

@joop Дякую Я щойно опублікував відповідь на те, що, на мою думку, відбувається, маючи на увазі перевірку на розсудливість, оскільки ви, здається, знаєте більше про те, що відбувається, ніж люди, які розмістили очевидні варіанти "write () потребує close () або fsync ( ) для довговічності ", не читаючи питання?
Крейг Рінгер

BTW: Я думаю, ви дійсно повинні заглибитися в джерела ядра. Файлова система, що ведеться в журналі, ймовірно, матиме ті ж проблеми. Не кажучи вже про обробку розділів підкачки. Оскільки вони живуть у просторі ядра, обробка цих умов, ймовірно, трохи жорсткіша. writev (), який видно з простору користувачів, також здається місцем для пошуку. [у Craig: так becaus Я знаю ваше ім’я, і я знаю, що ви не повний ідіот; -]
joop

1
Я погоджуюся, я був не таким чесним. На жаль, ваша відповідь не дуже задовольняє, я маю на увазі, що немає простого рішення (дивно?).
Жан-Батист Юньєс

1
@ Jean-BaptisteYunès True. Для СУБД, з якою я працюю, "збої та введення повтору" прийнятні. Для більшості програм це не є можливим, і їм, можливо, доведеться терпіти жахливу продуктивність синхронного вводу / виводу або просто приймати погано визначене поведінку та пошкодження помилок вводу / виводу.
Крейг Рінгер

Відповіді:


91

fsync()повертається, -EIOякщо ядро ​​втратило запис

(Примітка: ранні посилання на частину старих ядер; оновлено нижче, щоб відобразити сучасні ядра)

Це схоже на списання буфера асинхронізації при end_buffer_async_write(...)помилках, встановивши -EIOпрапор на сторінці невдалого брудного буфера для файлу :

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

який потім виявляється , wait_on_page_writeback_range(...)як викликається do_sync_mapping_range(...)при виклику , sys_sync_file_range(...)як викликається sys_sync_file_range2(...)для виконання виклику бібліотеки З fsync().

Але лише один раз!

Цей коментар на 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.

припускає, що при fsync()поверненні -EIOабо (без документації на сторінці сторінки) -ENOSPCбуде очищено стан помилки, тому подальший fsync()повідомить про успіх, навіть якщо сторінки ніколи не писалися.

Впевнений, що достатньо wait_on_page_writeback_range(...) очищає біти помилок під час їх тестування :

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;

Тож якщо програма очікує, що вона може повторно спробувати, fsync()поки не вдасться і не довіряє, що дані є на диску, це страшенно неправильно.

Я впевнений, що це джерело пошкодження даних, яке я виявив у СУБД. Він повторюється fsync()і думає, що все буде добре, коли це вдасться.

Це дозволено?

Документи POSIX / SuS вfsync() насправді не вказують це так:

Якщо функція fsync () не працює, незавершені операції вводу / виводу не гарантовано виконані.

Сторінка "Linux"fsync() просто нічого не говорить про те, що відбувається при відмові.

Тож здається, що сенс fsync()помилок - "не знаю, що трапилося з вашими записами, можливо, спрацювало чи ні, краще спробуйте ще раз, щоб бути впевненим".

Новіші ядра

4.9 end_buffer_async_writeнабори -EIOна сторінці, просто через 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);

Що стосується синхронізації, я думаю, що це схоже, хоча структура зараз досить складна. filemap_check_errorsв mm/filemap.cтепер робить:

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

що має такий же ефект. Схоже, всі перевірки помилок проходять через filemap_check_errorsтест і зрозуміло:

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

Я використовую btrfsна своєму ноутбуці, але коли я створюю ext4цикл зворотного зв'язку для тестування /mnt/tmpта встановлення на нього зонда 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

Я знаходжу такий стек викликів у 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

Прочитане свідчить про те, що так, сучасні ядра поводяться так само.

Це, мабуть, означає, що якщо fsync()(або, мабуть, write()або close()) повертається -EIO, файл знаходиться в певному невизначеному стані між тим, коли ви останній раз успішно fsync()d або close()d його і його останнім write()десяти станом.

Тест

Я реалізував тестовий випадок, щоб продемонструвати таку поведінку .

Наслідки

СУБД може впоратися з цим, ввівши відновлення після аварійного завершення. Як, звичайно, звичайне користувацьке додаток повинно впоратися з цим? Сторінка fsync()man не дає жодних попереджень, що це означає "fsync-if-you-feel-like-it", і я очікую, що багато додатків не впораються з цією поведінкою.

Звіти про помилки

Подальше читання

lwn.net торкнувся цього в статті "Поліпшення керування помилками на рівні шару" .

потік списку розсилки postgresql.org .


3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 - це можлива гонка, тому що вона чекає на {очікуваний та запланований введення / вивід}, а не на {ще не запланований I / O}. Це очевидно, щоб уникнути зайвих зворотних поїздок до пристрою. (Я припускаю, що користувач пише () не повертайтеся, поки не заплановано введення-виведення, для mmap (), це інше)
joop

3
Чи можливо, що якийсь інший процес на виклик fsync для якогось іншого файлу на цьому ж диску отримав повернення помилки?
Випадково832

3
@ Random832 Дуже актуально для БД, що обробляє багато користувачів, як PostgreSQL, тож гарне запитання. Схоже, напевно, але я не знаю коду ядра достатньо добре, щоб зрозуміти. Ваші програми краще співпрацювати, якщо вони обоє мають один і той же файл відкритий.
Крейг Рінгер

1
@DavidFoerster: системні дзвінки повертають відмови за допомогою негативних кодів errno; errnoє повністю конструкцією бібліотеки С користувача простору. Загальноприйнято ігнорувати різниці повернених значень між системою sscalls та бібліотекою С (як це робить Крейг Рінгер вище), оскільки значення повернення помилки надійно визначає, до якого (функція бібліотеки syscall або C) посилається: " -1with errno==EIO"відноситься до функції бібліотеки С", тоді як " -EIO" відноситься до системного виклику. Нарешті, чоловічі сторінки Linux в Інтернеті - це найновіша посилання на чоловічі сторінки Linux.
Номінальна тварина

2
@CraigRinger: Щоб відповісти на ваше остаточне запитання: "Використовуючи введення / виведення низького рівня та fsync()/ fdatasync()коли розмір транзакції є повним файлом; використовуючи mmap()/ msync()коли розмір транзакції - це сторінка вирівнювання; та використовуючи I низького рівня / O, fdatasync()і декілька одночасних дескрипторів файлів (один дескриптор і потік на транзакцію) до одного і того ж файлу інакше " . Блоки опису відкритого файлу для Linux ( fcntl(), F_OFD_) дуже корисні з останнім.
Номінальна тварина

22

Оскільки запис () програми вже повернувся без помилок, схоже, немає можливості повідомити про помилку в додатку.

Я не згоден. writeможе повернутися без помилок, якщо запис просто у черзі, але про помилку буде повідомлено про наступну операцію, яка вимагатиме фактичного запису на диску, тобто означає наступну fsync, можливо, наступну запис, якщо система вирішить очистити кеш і на принаймні на останньому файлі закрити.

Саме тому для програми важливо перевірити значення повернення близько, щоб виявити можливі помилки запису.

Якщо вам дійсно потрібно вміти обробляти помилки, ви повинні припустити, що все, що було написано з моменту останнього успішного, fsync може бути невдалим, і що в усьому цьому принаймні щось не вдалося.


4
Так, я думаю, що це нігті. Це дійсно дозволяє припустити, що програма має зробити заново всю свою роботу , так як останній підтвердили успішний fsync()або close()файл , якщо він отримує -EIOвід write(), fsync()або close(). Ну, це весело.
Крейг Рінгер

1

write(2) забезпечує менше, ніж ви очікували. Сторінка man дуже відкрита щодо семантики успішного write()дзвінка:

Успішне повернення з write()нього не дає жодної гарантії, що дані були внесені на диск. Насправді, у деяких впроваджених програмах навіть не гарантується, що простір успішно зарезервовано для даних. Єдиний спосіб бути впевненим - зателефонувати fsync(2) після написання всіх даних.

Можна зробити висновок, що успіх write()просто означає, що дані досягли буферних можливостей ядра. Якщо зберігання буфера не вдається, наступний доступ до дескриптора файлу поверне код помилки. Як крайній варіант, який може бути close(). Сторінка чоловіка closeсистемного виклику (2) містить таке речення:

Цілком можливо, що помилки під час попередньої write(2) операції спочатку повідомляються на фіналі close().

Якщо вашій програмі потрібно зберігати дані, виписуйте її, вона повинна використовувати fsync/ fsyncdataрегулярно:

fsync()передає ("промиває") всі змінені основні дані (тобто змінені сторінки кеш-пам'яті буфера для) файлу, на який посилається дескриптор файлу fd, на дисковий пристрій (або інший постійний запам'ятовуючий пристрій), щоб можна було отримати всю змінену інформацію навіть після виходу з ладу або перезавантаження системи. Сюди входить записування через або промивання кеш-диска, якщо він присутній. Виклик блокується, поки пристрій не повідомить, що передача завершена.


4
Так, я знаю, що fsync()потрібно. Але в конкретному випадку, коли ядро ​​втрачає сторінки через помилку вводу / виводу, вийде з fsync()ладу? За яких обставин це може потім досягти успіху?
Крейг Рінгер

Я також не знаю джерела ядра. Припустимо, fsync()повертається з -EIOпитань вводу / виводу (для чого було б добре інакше?). Тож база даних знає, що деякі попередні записи не вдалися і можуть перейти у режим відновлення. Це не те, чого ти хочеш? Яка мотивація вашого останнього запитання? Хочете знати, яке записування не вдалося або відновити дескриптор файлу для подальшого використання?
fzgregor

В ідеалі СУБД вважає за краще не вводити відновлення після аварійного завершення (відхиляє всіх користувачів і стає тимчасово недоступним або, принаймні, лише для читання), якщо це можливо уникнути цього. Але навіть якби ядро ​​могло сказати нам "байти 4096 до 8191 fd X", було б важко розібратися, що (повторно) писати там, не дуже сильно займаючись відновленням аварій. Тому я думаю, що головне питання полягає в тому, чи існують ще невинні обставини, коли вони fsync()можуть повернутися -EIOтам, де можна безпечно спробувати, і якщо можливо сказати різницю.
Крейг Рінгер

Звичайно, відновлення аварій - це остання можливість. Але, як ви вже говорили, очікується, що ці питання будуть дуже рідкісними. Тому я не бачу жодної проблеми з відновленням відновлення -EIO. Якщо кожен дескриптор файлу використовує одночасно один потік, цей потік може повернутися до останнього fsync()і повторити write()виклики. Але все ж, якщо ті write()записують лише частину сектору, немодифікована частина все одно може бути корумпованою.
fzgregor

1
Ви маєте рацію, що відновлення аварійних ситуацій, ймовірно, розумно. Що стосується частково корумпованих секторів, СУБД (PostgreSQL) зберігає зображення всієї сторінки вперше, коли вона торкається її після будь-якої заданої контрольної точки саме з цієї причини, тому це повинно бути добре :)
Крейг Рінгер,

0

Використовуйте прапор O_SYNC, коли ви відкриваєте файл. Це забезпечує, що дані записуються на диск.

Якщо це вас не задовольнить, нічого не буде.


17
O_SYNCє кошмаром виступу. Це означає, що програма не може робити нічого іншого, коли відбувається введення / виведення диска, якщо вона не породить потоки вводу / виводу. Ви також можете сказати, що захищений інтерфейс вводу / виводу небезпечний і всі повинні використовувати AIO. Напевно, мовчазно втрачені записи не можуть бути прийнятними в буферному вводу / виводу?
Крейг Рінгер

3
( O_DATASYNCє лише трохи кращим у цьому плані)
Крейг Рінгер

@CraigRinger Ви повинні використовувати AIO, якщо у вас є така потреба і вам потрібна будь-яка продуктивність. Або просто використовувати СУБД; він обробляє все за вас.
Демі

10
@Demi Додаток тут - dbms (postgresql). Я впевнений, що ви можете собі уявити, що переписування всієї програми для використання AIO замість буферного вводу-виводу не є практичним. Також це не повинно бути.
Крейг Рінгер

-5

Перевірте значення повернення близько. Закрити може не вдатися, хоча буферизовані записи здаються успішними


8
Ну, навряд чи ми хочемо, щоб кожні кілька секунд створювали open()та close()завантажували файл. тому ми маємо fsync()...
Крейг Рінгер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.