Menulis program untuk mengatasi kesalahan I / O menyebabkan penulisan yang hilang di Linux

138

TL; DR: Jika kernel Linux kehilangan tulis I / O yang disangga , apakah ada cara bagi aplikasi untuk mengetahuinya?

Saya tahu Anda harus ke fsync()file (dan direktori induknya) untuk daya tahan . Pertanyaannya adalah jika kernel kehilangan buffer kotor yang tertunda tulis karena kesalahan I / O, bagaimana aplikasi dapat mendeteksi ini dan memulihkan atau membatalkan?

Pikirkan aplikasi basis data, dll, di mana urutan ketahanan menulis dan menulis bisa menjadi sangat penting.

Hilang menulis? Bagaimana?

Lapisan blok kernel Linux dalam beberapa keadaan kehilangan permintaan I / O buffered yang telah berhasil dikirim oleh write(), pwrite()dll, dengan kesalahan seperti:

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

(Lihat end_buffer_write_sync(...)dan end_buffer_async_write(...)dalamfs/buffer.c ).

Pada kernel yang lebih baru kesalahannya malah akan mengandung "lost async page write" , seperti:

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

Karena aplikasi write()sudah kembali tanpa kesalahan, sepertinya tidak ada cara untuk melaporkan kesalahan kembali ke aplikasi.

Mendeteksi mereka?

Saya tidak begitu familiar dengan sumber kernel, tetapi saya pikir itu menetapkan AS_EIOpada buffer yang gagal dituliskan jika itu melakukan penulisan async:

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

tetapi tidak jelas bagi saya apakah atau bagaimana aplikasi dapat mengetahui tentang hal ini ketika nanti fsync()file untuk mengkonfirmasi itu pada disk.

Sepertinya wait_on_page_writeback_range(...)dimm/filemap.c kekuatan oleh do_sync_mapping_range(...)difs/sync.c yang belok disebut oleh sys_sync_file_range(...). Itu kembali -EIOjika satu atau lebih buffer tidak bisa ditulis.

Jika, seperti yang saya tebak, ini menyebar ke fsync()hasil, maka jika panik dan menalangi aplikasi jika mendapat kesalahan I / O fsync()dan tahu bagaimana melakukan kembali pekerjaannya ketika dihidupkan ulang, apakah itu cukup perlindungan?

Mungkin tidak ada cara bagi aplikasi untuk mengetahui offset byte mana dalam file yang sesuai dengan halaman yang hilang sehingga dapat menulis ulang mereka jika mengetahui caranya, tetapi jika aplikasi mengulangi semua pekerjaan yang tertunda sejak keberhasilan terakhir fsync()file, dan yang menulis ulang setiap buffer kernel kotor yang terkait dengan penulisan yang hilang terhadap file, yang seharusnya menghapus semua flag kesalahan I / O pada halaman yang hilang dan memungkinkan berikutnya fsync()untuk menyelesaikan - benar?

Apakah ada keadaan lain yang tidak berbahaya di mana fsync()mungkin kembali di -EIOmana bailing out dan mengulang pekerjaan akan terlalu drastis?

Mengapa?

Tentu saja kesalahan seperti itu seharusnya tidak terjadi. Dalam hal ini kesalahan muncul dari interaksi yang tidak menguntungkan antara dm-multipathdefault pengemudi dan kode akal yang digunakan oleh SAN untuk melaporkan kegagalan untuk mengalokasikan penyimpanan yang disediakan secara tipis. Tapi ini bukan satu-satunya keadaan di mana mereka bisa terjadi - saya juga melihat laporan dari LVM yang disediakan misalnya, seperti yang digunakan oleh libvirt, Docker, dan banyak lagi. Aplikasi kritis seperti basis data harus berusaha mengatasi kesalahan semacam itu, alih-alih membabi buta seolah-olah semuanya baik-baik saja.

Jika kernel berpikir tidak masalah untuk kehilangan penulisan tanpa mati karena kernel panik, aplikasi harus menemukan cara untuk mengatasinya.

Dampak praktisnya adalah saya menemukan kasus di mana masalah multipath dengan SAN menyebabkan penulisan hilang yang menyebabkan korupsi basis data karena DBMS tidak tahu bahwa penulisan gagal. Tidak menyenangkan.

Craig Ringer
sumber
1
Saya khawatir ini akan membutuhkan bidang tambahan di SystemFileTable untuk menyimpan & mengingat kondisi kesalahan ini. Dan kemungkinan proses userspace menerima atau memeriksanya pada panggilan berikutnya. (lakukan fsync () dan tutup () kembalikan informasi bersejarah semacam ini ?)
joop
@ Joop, terima kasih. Saya baru saja memposting jawaban dengan apa yang saya pikir sedang terjadi, keberatan melakukan cek kewarasan karena Anda tampaknya tahu lebih banyak tentang apa yang terjadi daripada orang-orang yang telah memposting varian yang jelas dari "tulis () perlu dekat () atau fsync ( ) untuk daya tahan "tanpa membaca pertanyaan?
Craig Ringer
BTW: Saya pikir Anda benar-benar harus menyelidiki sumber kernel. Filesystem yang dijurnal mungkin akan menderita dari jenis masalah yang sama. Belum lagi penanganan partisi swap. Karena ini tinggal di ruang kernel, penanganan kondisi ini mungkin akan sedikit lebih kaku. writev (), yang terlihat dari userspace, juga tampak seperti tempat untuk melihat. [di Craig: ya karena saya tahu nama Anda, dan saya tahu Anda bukan orang idiot; -]
joop
1
Saya setuju, saya tidak begitu adil. Sayangnya jawaban Anda tidak terlalu memuaskan, maksud saya tidak ada solusi yang mudah (mengejutkan?).
Jean-Baptiste Yunès
1
@ Jean-BaptisteYunès Benar. Untuk DBMS yang saya kerjakan, "crash and enter redo" dapat diterima. Untuk sebagian besar aplikasi yang bukan opsi dan mereka mungkin harus mentolerir kinerja mengerikan I / O sinkron atau hanya menerima perilaku dan korupsi yang didefinisikan dengan buruk pada kesalahan I / O.
Craig Ringer

Jawaban:

91

fsync()kembali -EIOjika kernel kehilangan penulisan

(Catatan: bagian awal merujuk kernel yang lebih tua; diperbarui di bawah ini untuk mencerminkan kernel modern)

Sepertinya penghapusan buffer async end_buffer_async_write(...)gagal membuat -EIOtanda pada halaman buffer kotor yang gagal untuk file :

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

yang kemudian dideteksi oleh wait_on_page_writeback_range(...)yang disebut oleh do_sync_mapping_range(...)yang disebut oleh sys_sync_file_range(...)yang disebut dengan sys_sync_file_range2(...)menerapkan panggilan C library fsync().

Tapi hanya sekali!

Komentar ini di 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.

menyarankan bahwa ketika fsync()kembali -EIOatau (tidak terdokumentasi di halaman manual) -ENOSPC, itu akan menghapus status kesalahan sehingga selanjutnya fsync()akan melaporkan keberhasilan meskipun halaman tidak pernah ditulis.

Cukup jelas wait_on_page_writeback_range(...) menghapus kesalahan bit saat mengujinya :

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;

Jadi, jika aplikasi berharap dapat mencoba kembali fsync()sampai berhasil dan percaya bahwa data di-disk, itu sangat salah.

Saya cukup yakin ini adalah sumber dari korupsi data yang saya temukan di DBMS. Mencoba ulang fsync()dan berpikir semua akan baik-baik saja ketika berhasil.

Apakah ini diizinkan?

Dokumen POSIX / SuS difsync() tidak benar-benar menentukan cara ini:

Jika fungsi fsync () gagal, operasi I / O yang beredar tidak dijamin telah selesai.

Halaman manual Linuxfsync() hanya tidak mengatakan apa-apa tentang apa yang terjadi pada kegagalan.

Jadi sepertinya arti fsync()kesalahan adalah "tidak tahu apa yang terjadi pada tulisan Anda, mungkin berhasil atau tidak, lebih baik coba lagi untuk memastikan".

Kernel yang lebih baru

Pada 4,9 end_buffer_async_writeset -EIOpada halaman, cukup via 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);

Di sisi sinkronisasi saya pikir itu mirip, meskipun strukturnya sekarang cukup rumit untuk diikuti. filemap_check_errorsdi mm/filemap.csekarang tidak:

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

yang memiliki banyak efek yang sama. Semua pemeriksaan error tampaknya harus filemap_check_errorsdilakukan dengan cara test-and-clear:

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

Saya menggunakan btrfslaptop saya, tetapi ketika saya membuat ext4loopback untuk pengujian /mnt/tmpdan mengatur probe perf di atasnya:

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

Saya menemukan tumpukan panggilan berikut di 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

Pembacaan ulang menunjukkan bahwa ya, kernel modern berperilaku sama.

Ini tampaknya berarti bahwa jika fsync()(atau mungkin write()atau close()) kembali -EIO, file tersebut dalam keadaan tidak terdefinisi antara kapan Anda terakhir berhasil fsync()atau close()tidak dan terakhir write()sepuluh negara.

Uji

Saya telah menerapkan uji kasus untuk menunjukkan perilaku ini .

Implikasi

DBMS dapat mengatasi hal ini dengan memasukkan crash recovery. Bagaimana mungkin aplikasi pengguna normal untuk mengatasi ini? The fsync()halaman manual tidak memberikan peringatan bahwa itu berarti "fsync-jika-anda-merasa-seperti-itu" dan aku berharap banyak aplikasi tidak akan mengatasi dengan baik dengan perilaku ini.

Laporan bug

Bacaan lebih lanjut

lwn.net menyentuh ini dalam artikel "Peningkatan penanganan kesalahan blok-layer" .

utas milis postgresql.org .

Craig Ringer
sumber
3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 adalah kemungkinan balapan, karena menunggu {pending & terjadwal I / O}, bukan untuk {I / O yang belum dijadwalkan}. Ini jelas untuk menghindari perjalanan bolak-balik ke perangkat. (Saya kira pengguna menulis () jangan kembali sampai I / O dijadwalkan, untuk mmap (), ini berbeda)
joop
3
Apakah mungkin panggilan beberapa proses lain ke fsync untuk beberapa file lain pada disk yang sama mendapatkan kesalahan kembali?
Random832
3
@ Random832 Sangat relevan untuk DB multi-pemrosesan seperti PostgreSQL, pertanyaan yang sangat bagus. Sepertinya mungkin, tapi saya tidak tahu kode kernel cukup baik untuk mengerti. Procs Anda sebaiknya bekerja sama jika keduanya memiliki file yang sama tetap terbuka.
Craig Ringer
1
@ DavidFoerster: Syscalls mengembalikan kegagalan menggunakan kode errno negatif; errnosepenuhnya merupakan konstruksi dari library C userspace. Adalah umum untuk mengabaikan perbedaan nilai kembali antara syscalls dan pustaka C seperti ini (seperti yang dilakukan Craig Ringer, di atas), karena nilai pengembalian kesalahan secara andal mengidentifikasi yang mana (fungsi syscall atau pustaka C) sedang dirujuk ke: " -1dengan errno==EIO"merujuk ke fungsi pustaka C, sedangkan" -EIO"merujuk ke syscall. Akhirnya, halaman manual Linux online adalah referensi terbaru untuk halaman manual Linux.
Hewan Nominal
2
@CraigRinger: Untuk menjawab pertanyaan terakhir Anda: "Dengan menggunakan I / O tingkat rendah dan fsync()/ fdatasync()ketika ukuran transaksi adalah file yang lengkap; dengan menggunakan mmap()/ msync()ketika ukuran transaksi adalah catatan yang disejajarkan dengan halaman; dan dengan menggunakan level rendah I / O,, fdatasync()dan beberapa deskriptor file konkuren (satu deskriptor dan untaian per transaksi) ke file yang sama jika tidak " . Kunci deskripsi file terbuka khusus Linux ( fcntl(), F_OFD_) sangat berguna dengan yang terakhir.
Hewan Nominal
22

Karena write () aplikasi telah kembali tanpa kesalahan, sepertinya tidak ada cara untuk melaporkan kesalahan kembali ke aplikasi.

Saya tidak setuju. writedapat kembali tanpa kesalahan jika penulisan hanya diantrekan, tetapi kesalahan tersebut akan dilaporkan pada operasi berikutnya yang akan memerlukan penulisan aktual pada disk, itu berarti pada berikutnya fsync, mungkin pada penulisan berikut jika sistem memutuskan untuk menyiram cache dan pada Setidaknya pada penutupan file terakhir.

Itulah alasan mengapa sangat penting bagi aplikasi untuk menguji nilai balik dari dekat untuk mendeteksi kemungkinan kesalahan penulisan.

Jika Anda benar-benar harus dapat melakukan pemrosesan kesalahan pintar, Anda harus mengasumsikan bahwa semua yang ditulis sejak sukses terakhir fsync mungkin gagal dan bahwa dalam semua itu setidaknya ada sesuatu yang gagal.

Serge Ballesta
sumber
4
Ya, saya pikir itu berhasil. Ini memang akan menyarankan bahwa aplikasi harus melakukan kembali semua pekerjaannya sejak yang terakhir berhasil-dikonfirmasi fsync()atau close()file jika mendapat -EIOdari write(), fsync()atau close(). Yah, itu menyenangkan.
Craig Ringer
1

write(2) memberikan kurang dari yang Anda harapkan. Halaman manual sangat terbuka tentang semantik write()panggilan sukses :

Pengembalian yang sukses dari write()tidak membuat jaminan bahwa data telah berkomitmen untuk disk. Bahkan, pada beberapa implementasi kereta, itu bahkan tidak menjamin bahwa ruang telah berhasil disediakan untuk data. Satu-satunya cara untuk memastikan adalah menelepon fsync(2) setelah Anda selesai menulis semua data Anda.

Kita dapat menyimpulkan bahwa yang berhasil write()hanya berarti bahwa data telah mencapai fasilitas buffering kernel. Jika tetap ada buffer gagal, akses selanjutnya ke deskriptor file akan mengembalikan kode kesalahan. Sebagai pilihan terakhir yang mungkin close(). Halaman manual dari closepanggilan sistem (2) berisi kalimat berikut:

Sangat mungkin bahwa kesalahan pada operasi write(2) sebelumnya dilaporkan pertama kali pada final close().

Jika aplikasi Anda perlu mempertahankan data, hapuslah itu harus menggunakan fsync/ fsyncdatasecara teratur:

fsync()mentransfer ("flushes") semua data inti yang dimodifikasi dari (yaitu, halaman cache buffer yang dimodifikasi untuk) file yang dirujuk oleh deskriptor file fd ke perangkat disk (atau perangkat penyimpanan permanen lainnya) sehingga semua informasi yang diubah dapat diambil bahkan setelah sistem crash atau reboot. Ini termasuk menulis melalui atau membersihkan cache disk jika ada. Panggilan tersebut memblokir hingga perangkat melaporkan bahwa transfer telah selesai.

fzgregor
sumber
4
Ya, saya sadar itu fsync()wajib. Tetapi dalam kasus khusus di mana kernel kehilangan halaman karena kesalahan I / O akan fsync()gagal? Dalam keadaan apa kemudian dapat berhasil setelah itu?
Craig Ringer
Saya juga tidak tahu sumber kernelnya. Mari kita asumsikan fsync()pengembalian -EIOpada masalah I / O (Apa untungnya jika sebaliknya?). Jadi database tahu beberapa dari penulisan sebelumnya gagal dan bisa masuk ke mode pemulihan. Bukankah ini yang kamu inginkan? Apa motivasi dari pertanyaan terakhir Anda? Apakah Anda ingin tahu yang gagal menulis atau memulihkan deskriptor file untuk digunakan lebih lanjut?
fzgregor
Idealnya, DBMS akan memilih untuk tidak memasukkan pemulihan kerusakan (menendang semua pengguna dan menjadi sementara tidak dapat diakses atau setidaknya hanya baca-saja) jika mungkin dapat menghindarinya. Tetapi bahkan jika kernel dapat memberitahu kita "byte 4096 ke 8191 dari fd X" akan sulit untuk mencari tahu apa yang harus (kembali) tulis di sana tanpa cukup banyak melakukan crash recovery. Jadi saya kira pertanyaan utamanya adalah apakah ada keadaan yang lebih tidak bersalah di mana fsync()dapat kembali ke -EIOtempat yang aman untuk mencoba lagi, dan jika mungkin untuk mengatakan perbedaannya.
Craig Ringer
Tentu pemulihan kecelakaan adalah pilihan terakhir. Tapi seperti yang sudah Anda katakan, masalah ini sangat jarang terjadi. Karena itu, saya tidak melihat masalah dengan pemulihan apa pun -EIO. Jika setiap deskriptor file hanya digunakan oleh satu utas pada satu waktu, utas ini dapat kembali ke yang terakhir fsync()dan mengulang write()panggilan. Tapi tetap saja, jika write()itu hanya menulis bagian dari sektor, bagian yang tidak dimodifikasi mungkin masih korup.
fzgregor
1
Anda benar bahwa masuk ke pemulihan crash mungkin masuk akal. Adapun sebagian sektor yang korup, DBMS (PostgreSQL) menyimpan gambar dari seluruh halaman saat pertama kali menyentuhnya setelah setiap pos pemeriksaan yang diberikan hanya untuk alasan itu, jadi itu akan baik-baik saja :)
Craig Ringer
0

Gunakan bendera O_SYNC ketika Anda membuka file. Ini memastikan data ditulis ke disk.

Jika ini tidak memuaskan Anda, tidak akan ada apa-apa.

toughmanwang
sumber
17
O_SYNCadalah mimpi buruk bagi kinerja. Itu berarti aplikasi tidak dapat melakukan hal lain ketika I / O disk terjadi kecuali ia memunculkan thread I / O. Anda mungkin juga mengatakan bahwa antarmuka I / O buffered tidak aman dan semua orang harus menggunakan AIO. Tentunya penulisan yang hilang secara diam-diam tidak dapat diterima dalam buffer I / O?
Craig Ringer
3
( O_DATASYNCHanya sedikit lebih baik dalam hal itu)
Craig Ringer
@CraigRinger Anda harus menggunakan AIO jika Anda memiliki kebutuhan ini dan memerlukan segala jenis kinerja. Atau cukup gunakan DBMS; itu menangani segalanya untuk Anda.
Demi
10
@Demi Aplikasi di sini adalah dbms (postgresql). Saya yakin Anda dapat membayangkan bahwa menulis ulang seluruh aplikasi untuk menggunakan AIO alih-alih buffer I / O tidak praktis. Juga tidak perlu.
Craig Ringer
-5

Periksa nilai balik dari penutupan. tutup dapat gagal sementara penulisan buffer tampaknya berhasil.

Malcolm McLean
sumber
8
Yah, kita tidak ingin menjadi open()ing dan close()ing file setiap beberapa detik. itulah sebabnya kami memiliki fsync()...
Craig Ringer