Penyimpanan Data yang Tahan Lama dan API File Linux

Saya, yang meneliti stabilitas penyimpanan data dalam sistem cloud, memutuskan untuk menguji diri saya sendiri, untuk memastikan bahwa saya memahami hal-hal mendasar. SAYA dimulai dengan membaca spesifikasi NVMe untuk memahami jaminan apa terkait kegigihan data (yaitu, jaminan bahwa data akan tersedia setelah kegagalan sistem) beri kami disk NMVe. Saya membuat kesimpulan utama berikut: Anda perlu mempertimbangkan data yang rusak sejak perintah penulisan data diberikan, dan hingga saat data ditulis ke media penyimpanan. Namun, di sebagian besar program, panggilan sistem cukup aman digunakan untuk menulis data.

Pada artikel ini, saya mengeksplorasi mekanisme persistensi yang disediakan oleh API file Linux. Tampaknya semuanya harus sederhana di sini: program memanggil perintah write(), dan setelah pengoperasian perintah ini selesai, data akan disimpan dengan aman di disk. Tetapi write() hanya menyalin data aplikasi ke cache kernel yang terletak di RAM. Untuk memaksa sistem menulis data ke disk, beberapa mekanisme tambahan harus digunakan.

Penyimpanan Data yang Tahan Lama dan API File Linux

Secara umum, materi ini adalah kumpulan catatan yang berkaitan dengan apa yang telah saya pelajari pada suatu topik yang menarik bagi saya. Jika kita berbicara singkat tentang yang paling penting, ternyata untuk mengatur penyimpanan data yang berkelanjutan, Anda perlu menggunakan perintah fdatasync() atau buka file dengan bendera O_DSYNC. Jika Anda tertarik untuk mempelajari lebih lanjut tentang apa yang terjadi pada data dalam perjalanan dari kode ke disk, lihatlah ini artikel.

Fitur menggunakan fungsi write()

Panggilan sistem write() didefinisikan dalam standar IEEE POSIX sebagai upaya untuk menulis data ke deskriptor file. Setelah berhasil menyelesaikan pekerjaan write() operasi pembacaan data harus mengembalikan persis byte yang ditulis sebelumnya, melakukannya bahkan jika data sedang diakses dari proses atau utas lain (di sini bagian yang sesuai dari standar POSIX). Di sini, di bagian interaksi utas dengan operasi file normal, ada catatan yang mengatakan bahwa jika dua utas masing-masing memanggil fungsi ini, maka setiap panggilan harus melihat semua konsekuensi yang ditunjukkan yang mengarah ke eksekusi panggilan lain, atau tidak melihat sama sekali tidak ada konsekuensi. Ini mengarah pada kesimpulan bahwa semua operasi I/O file harus menahan kunci pada sumber daya yang sedang dikerjakan.

Apakah ini berarti bahwa operasi write() apakah atom? Dari sudut pandang teknis, ya. Operasi baca data harus mengembalikan semua atau tidak sama sekali dari apa yang ditulis write(). Tapi operasinya write(), sesuai dengan standar, tidak harus diakhiri, setelah menuliskan semua yang diminta untuk ditulisnya. Diijinkan untuk menulis hanya sebagian dari data. Misalnya, kami mungkin memiliki dua aliran yang masing-masing menambahkan 1024 byte ke file yang dijelaskan oleh deskriptor file yang sama. Dari sudut pandang standar, hasilnya dapat diterima ketika setiap operasi tulis hanya dapat menambahkan satu byte ke file. Operasi ini akan tetap atomik, tetapi setelah selesai, data yang mereka tulis ke file akan campur aduk. di sini adalah diskusi yang sangat menarik tentang topik ini di Stack Overflow.

fungsi fsync() dan fdatasync()

Cara termudah untuk memindahkan data ke disk adalah dengan memanggil fungsi tersebut fsinkron(). Fungsi ini meminta sistem operasi untuk memindahkan semua blok yang dimodifikasi dari cache ke disk. Ini mencakup semua metadata file (waktu akses, waktu modifikasi file, dan seterusnya). Saya yakin metadata ini jarang diperlukan, jadi jika Anda tahu itu tidak penting bagi Anda, Anda dapat menggunakan fungsinya fdatasync(). Di Tolong pada fdatasync() dikatakan bahwa selama pengoperasian fungsi ini, sejumlah metadata disimpan ke disk, yang "diperlukan untuk pelaksanaan yang benar dari operasi pembacaan data berikut." Dan inilah yang menjadi perhatian sebagian besar aplikasi.

Satu masalah yang dapat muncul di sini adalah mekanisme ini tidak menjamin bahwa file dapat ditemukan setelah kemungkinan kegagalan. Secara khusus, ketika file baru dibuat, seseorang harus menelepon fsync() untuk direktori yang berisi itu. Jika tidak, setelah crash, mungkin ternyata file ini tidak ada. Alasan untuk ini adalah bahwa di bawah UNIX, karena penggunaan tautan keras, sebuah file dapat berada di banyak direktori. Karena itu, saat menelepon fsync() tidak ada cara bagi file untuk mengetahui data direktori mana yang juga harus dibuang ke disk (di sini Anda dapat membaca lebih lanjut tentang ini). Sepertinya sistem file ext4 mampu автоматичСски untuk mendaftar fsync() ke direktori yang berisi file yang sesuai, tetapi ini mungkin tidak terjadi pada sistem file lain.

Mekanisme ini dapat diimplementasikan secara berbeda dalam sistem file yang berbeda. saya menggunakan jejak hitam untuk mempelajari tentang operasi disk apa yang digunakan dalam sistem file ext4 dan XFS. Keduanya mengeluarkan perintah tulis biasa ke disk untuk konten file dan jurnal sistem file, bersihkan cache dan keluar dengan melakukan FUA (Force Unit Access, menulis data langsung ke disk, melewati cache) menulis ke jurnal. Mereka mungkin melakukan hal itu untuk mengkonfirmasi fakta transaksi. Pada drive yang tidak mendukung FUA, ini menyebabkan dua pembersihan cache. Eksperimen saya telah menunjukkan hal itu fdatasync() sedikit lebih cepat fsync(). Kegunaan blktrace mengindikasikan bahwa fdatasync() biasanya menulis lebih sedikit data ke disk (di ext4 fsync() menulis 20 KiB, dan fdatasync() - 16 KiB). Juga, saya menemukan bahwa XFS sedikit lebih cepat daripada ext4. Dan di sini dengan bantuan blktrace bisa mengetahuinya fdatasync() menyiram lebih sedikit data ke disk (4 KiB di XFS).

Situasi ambigu saat menggunakan fsync()

Saya dapat memikirkan tiga situasi ambigu tentang fsync()yang saya temui dalam praktek.

Insiden serupa pertama kali terjadi pada 2008. Pada saat itu, antarmuka Firefox 3 "dibekukan" jika sejumlah besar file sedang ditulis ke disk. Masalahnya adalah implementasi antarmuka menggunakan database SQLite untuk menyimpan informasi tentang statusnya. Setelah setiap perubahan yang terjadi pada antarmuka, fungsi tersebut dipanggil fsync(), yang memberikan jaminan penyimpanan data yang stabil. Dalam sistem file ext3 yang kemudian digunakan, fungsinya fsync() memerah ke disk semua halaman "kotor" dalam sistem, dan bukan hanya yang terkait dengan file yang sesuai. Ini berarti mengklik tombol di Firefox dapat menyebabkan megabita data ditulis ke disk magnetik, yang dapat memakan waktu beberapa detik. Solusi untuk masalah, sejauh yang saya mengerti ini materi, adalah memindahkan pekerjaan dengan database ke tugas latar belakang asinkron. Ini berarti bahwa Firefox digunakan untuk menerapkan persyaratan persistensi penyimpanan yang lebih ketat daripada yang benar-benar diperlukan, dan fitur sistem file ext3 hanya memperburuk masalah ini.

Masalah kedua terjadi pada tahun 2009. Kemudian, setelah sistem crash, pengguna sistem file ext4 yang baru menemukan bahwa banyak file yang baru dibuat berukuran nol, tetapi ini tidak terjadi dengan sistem file ext3 yang lebih lama. Di paragraf sebelumnya, saya berbicara tentang bagaimana ext3 membuang terlalu banyak data ke disk, yang memperlambat banyak hal. fsync(). Untuk memperbaiki situasi, ext4 hanya menghapus halaman "kotor" yang relevan dengan file tertentu. Dan data file lain tetap berada di memori untuk waktu yang lebih lama dibandingkan dengan ext3. Ini dilakukan untuk meningkatkan kinerja (secara default, data tetap dalam keadaan ini selama 30 detik, Anda dapat mengonfigurasinya menggunakan dirty_expire_centisecs; di sini Anda dapat menemukan informasi lebih lanjut tentang ini). Ini berarti bahwa sejumlah besar data dapat hilang setelah crash. Solusi untuk masalah ini adalah dengan menggunakan fsync() dalam aplikasi yang perlu menyediakan penyimpanan data yang stabil dan melindunginya sebanyak mungkin dari konsekuensi kegagalan. Fungsi fsync() bekerja jauh lebih efisien dengan ext4 dibandingkan dengan ext3. Kerugian dari pendekatan ini adalah penggunaannya, seperti sebelumnya, memperlambat beberapa operasi, seperti penginstalan program. Lihat detail tentang ini di sini ΠΈ di sini.

Masalah ketiga tentang fsync(), berasal dari tahun 2018. Kemudian, dalam kerangka proyek PostgreSQL, ditemukan bahwa jika fungsinya fsync() menemui kesalahan, itu menandai halaman "kotor" sebagai "bersih". Akibatnya, panggilan berikut fsync() melakukan apa-apa dengan halaman tersebut. Karena itu, halaman yang dimodifikasi disimpan dalam memori dan tidak pernah ditulis ke disk. Ini benar-benar bencana, karena aplikasi akan berpikir bahwa beberapa data ditulis ke disk, tetapi sebenarnya tidak. Kegagalan seperti itu fsync() jarang, aplikasi dalam situasi seperti itu hampir tidak dapat melakukan apa pun untuk mengatasi masalah tersebut. Hari-hari ini, ketika ini terjadi, PostgreSQL dan aplikasi lain macet. Di sini, dalam artikel "Dapatkah Aplikasi Dipulihkan dari Kegagalan fsync?", masalah ini dieksplorasi secara mendetail. Saat ini solusi terbaik untuk masalah ini adalah menggunakan Direct I/O dengan flag O_SYNC atau dengan bendera O_DSYNC. Dengan pendekatan ini, sistem akan melaporkan kesalahan yang mungkin terjadi saat melakukan operasi penulisan data tertentu, tetapi pendekatan ini memerlukan aplikasi untuk mengelola buffer itu sendiri. Baca lebih lanjut tentang itu di sini ΠΈ di sini.

Membuka file menggunakan flag O_SYNC dan O_DSYNC

Mari kembali ke pembahasan mekanisme Linux yang menyediakan penyimpanan data persisten. Yaitu, kita berbicara tentang penggunaan bendera O_SYNC atau bendera O_DSYNC saat membuka file menggunakan system call Buka(). Dengan pendekatan ini, setiap operasi penulisan data dilakukan seolah-olah setelah setiap perintah write() sistem diberikan, masing-masing, perintah fsync() ΠΈ fdatasync(). Di Spesifikasi POSIX ini disebut "Penyelesaian Integritas File I/O Tersinkronisasi" dan "Penyelesaian Integritas Data". Keuntungan utama dari pendekatan ini adalah hanya satu panggilan sistem yang perlu dijalankan untuk memastikan integritas data, dan bukan dua (misalnya βˆ’ write() ΠΈ fdatasync()). Kerugian utama dari pendekatan ini adalah bahwa semua operasi tulis menggunakan deskriptor file yang sesuai akan disinkronkan, yang dapat membatasi kemampuan untuk menyusun kode aplikasi.

Menggunakan Direct I/O dengan flag O_DIRECT

Panggilan sistem open() mendukung bendera O_DIRECT, yang dirancang untuk mem-bypass cache sistem operasi, melakukan operasi I / O, berinteraksi langsung dengan disk. Ini, dalam banyak kasus, berarti perintah tulis yang dikeluarkan oleh program akan langsung diterjemahkan ke dalam perintah yang ditujukan untuk bekerja dengan disk. Namun secara umum mekanisme ini bukanlah pengganti fungsi fsync() ΠΈΠ»ΠΈ fdatasync(). Faktanya adalah disk itu sendiri bisa penundaan atau cache perintah yang tepat untuk menulis data. Dan, lebih buruk lagi, dalam beberapa kasus khusus, operasi I/O dilakukan saat menggunakan flag O_DIRECT, siaran ke dalam operasi buffered tradisional. Cara termudah untuk mengatasi masalah ini adalah dengan menggunakan bendera untuk membuka file O_DSYNC, yang berarti bahwa setiap operasi tulis akan diikuti oleh panggilan fdatasync().

Ternyata sistem file XFS baru-baru ini menambahkan "jalur cepat" untuk O_DIRECT|O_DSYNC-catatan data. Jika blok ditimpa menggunakan O_DIRECT|O_DSYNC, maka XFS, alih-alih mengosongkan cache, akan menjalankan perintah tulis FUA jika perangkat mendukungnya. Saya memverifikasi ini menggunakan utilitas blktrace pada sistem Linux 5.4/Ubuntu 20.04. Pendekatan ini seharusnya lebih efisien, karena menulis jumlah minimum data ke disk dan menggunakan satu operasi, bukan dua (tulis dan bersihkan cache). Saya menemukan tautan ke tambalan Kernel 2018 yang mengimplementasikan mekanisme ini. Ada beberapa diskusi tentang penerapan pengoptimalan ini ke sistem file lain, tetapi sejauh yang saya tahu, XFS adalah satu-satunya sistem file yang mendukungnya sejauh ini.

fungsi sync_file_range()

Linux memiliki panggilan sistem sinkronisasi_file_range(), yang memungkinkan Anda untuk mem-flush hanya sebagian file ke disk, bukan seluruh file. Panggilan ini memulai flush asinkron dan tidak menunggu sampai selesai. Tapi dalam referensi ke sync_file_range() perintah ini dikatakan "sangat berbahaya". Tidak disarankan untuk menggunakannya. Fitur dan bahaya sync_file_range() dijelaskan dengan sangat baik di ini bahan. Secara khusus, panggilan ini tampaknya menggunakan RocksDB untuk mengontrol kapan kernel membilas data "kotor" ke disk. Tetapi pada saat yang sama, untuk memastikan penyimpanan data yang stabil, itu juga digunakan fdatasync(). Di kode RocksDB memiliki beberapa komentar menarik tentang topik ini. Misalnya, sepertinya panggilan sync_file_range() saat menggunakan ZFS tidak membuang data ke disk. Pengalaman memberi tahu saya bahwa kode yang jarang digunakan mungkin mengandung bug. Oleh karena itu, saya akan menyarankan untuk tidak menggunakan panggilan sistem ini kecuali benar-benar diperlukan.

Panggilan sistem untuk membantu memastikan persistensi data

Saya sampai pada kesimpulan bahwa ada tiga pendekatan yang dapat digunakan untuk melakukan operasi I/O yang persisten. Mereka semua membutuhkan panggilan fungsi fsync() untuk direktori tempat file itu dibuat. Ini adalah pendekatannya:

  1. Panggilan fungsi fdatasync() ΠΈΠ»ΠΈ fsync() setelah fungsi write() (lebih baik digunakan fdatasync()).
  2. Bekerja dengan deskriptor file yang dibuka dengan bendera O_DSYNC ΠΈΠ»ΠΈ O_SYNC (lebih baik - dengan bendera O_DSYNC).
  3. Penggunaan perintah pwritev2() dengan bendera RWF_DSYNC ΠΈΠ»ΠΈ RWF_SYNC (sebaiknya dengan bendera RWF_DSYNC).

Catatan Kinerja

Saya tidak hati-hati mengukur kinerja berbagai mekanisme yang saya selidiki. Perbedaan yang saya perhatikan dalam kecepatan kerja mereka sangat kecil. Artinya saya bisa saja salah, dan dalam kondisi lain hal yang sama bisa menunjukkan hasil yang berbeda. Pertama, saya akan berbicara tentang apa yang lebih memengaruhi kinerja, dan kemudian, tentang apa yang kurang memengaruhi kinerja.

  1. Menimpa data file lebih cepat daripada menambahkan data ke file (perolehan kinerja bisa 2-100%). Melampirkan data ke file memerlukan perubahan tambahan pada metadata file, bahkan setelah panggilan sistem fallocate(), tetapi besarnya efek ini dapat bervariasi. Saya merekomendasikan, untuk kinerja terbaik, untuk menelepon fallocate() untuk pra-mengalokasikan ruang yang diperlukan. Maka ruang ini harus diisi secara eksplisit dengan nol dan dipanggil fsync(). Ini akan menyebabkan blok yang sesuai dalam sistem file ditandai sebagai "dialokasikan", bukan "tidak dialokasikan". Ini memberikan peningkatan kinerja yang kecil (sekitar 2%). Selain itu, beberapa disk mungkin memiliki operasi akses blok pertama yang lebih lambat daripada yang lain. Artinya, mengisi ruang dengan angka nol dapat menghasilkan peningkatan kinerja yang signifikan (sekitar 100%). Secara khusus, ini dapat terjadi dengan disk. AWS EBS (ini data tidak resmi, saya tidak bisa memastikannya). Hal yang sama berlaku untuk penyimpanan. Disk Persisten GCP (dan ini sudah menjadi informasi resmi, dikonfirmasi dengan tes). Pakar lain juga melakukan hal yang sama pengamatanterkait dengan disk yang berbeda.
  2. Semakin sedikit panggilan sistem, semakin tinggi kinerjanya (keuntungannya bisa sekitar 5%). Sepertinya panggilan open() dengan bendera O_DSYNC atau menelepon pwritev2() dengan bendera RWF_SYNC panggilan lebih cepat fdatasync(). Saya menduga bahwa intinya di sini adalah bahwa dengan pendekatan ini, fakta bahwa panggilan sistem yang lebih sedikit harus dilakukan untuk menyelesaikan tugas yang sama (satu panggilan, bukan dua) berperan. Tetapi perbedaan kinerjanya sangat kecil, sehingga Anda dapat dengan mudah mengabaikannya dan menggunakan sesuatu dalam aplikasi yang tidak memperumit logikanya.

Jika Anda tertarik dengan topik penyimpanan data yang berkelanjutan, berikut adalah beberapa materi yang bermanfaat:

  • Metode Akses I/O β€” ikhtisar tentang dasar-dasar mekanisme input / output.
  • Memastikan data mencapai disk - cerita tentang apa yang terjadi pada data dalam perjalanan dari aplikasi ke disk.
  • Kapan Anda harus melakukan fsync direktori yang memuatnya - jawaban atas pertanyaan kapan melamar fsync() untuk direktori. Singkatnya, ternyata Anda perlu melakukan ini saat membuat file baru, dan alasan rekomendasi ini adalah karena di Linux mungkin ada banyak referensi ke file yang sama.
  • SQL Server di Linux: FUA Internal - berikut adalah deskripsi bagaimana penyimpanan data persisten diimplementasikan di SQL Server pada platform Linux. Ada beberapa perbandingan menarik antara panggilan sistem Windows dan Linux di sini. Saya hampir yakin bahwa berkat materi inilah saya belajar tentang pengoptimalan FUA XFS.

Pernahkah Anda kehilangan data yang menurut Anda disimpan dengan aman di disk?

Penyimpanan Data yang Tahan Lama dan API File Linux

Penyimpanan Data yang Tahan Lama dan API File Linux

Sumber: www.habr.com