Penyimpanan Data Tahan Lama dan API Fail Linux

Saya, meneliti kestabilan storan data dalam sistem awan, memutuskan untuk menguji diri saya sendiri, untuk memastikan bahawa saya memahami perkara asas. saya dimulakan dengan membaca spesifikasi NVMe untuk memahami apa yang menjamin tentang kegigihan data (iaitu, jaminan bahawa data akan tersedia selepas kegagalan sistem) berikan kami cakera NMVe. Saya membuat kesimpulan utama berikut: anda perlu mempertimbangkan data yang rosak dari saat arahan menulis data diberikan, dan sehingga saat ia ditulis ke medium storan. Walau bagaimanapun, dalam kebanyakan program, panggilan sistem agak selamat digunakan untuk menulis data.

Dalam artikel ini, saya meneroka mekanisme kegigihan yang disediakan oleh API fail Linux. Nampaknya segala-galanya sepatutnya mudah di sini: program memanggil arahan write(), dan selepas operasi arahan ini selesai, data akan disimpan dengan selamat pada cakera. Tetapi write() hanya menyalin data aplikasi ke cache kernel yang terletak dalam RAM. Untuk memaksa sistem menulis data ke cakera, beberapa mekanisme tambahan mesti digunakan.

Penyimpanan Data Tahan Lama dan API Fail Linux

Secara amnya, bahan ini adalah satu set nota yang berkaitan dengan apa yang telah saya pelajari mengenai topik yang saya minati. Jika kita bercakap secara ringkas tentang yang paling penting, ternyata untuk mengatur penyimpanan data yang mampan, anda perlu menggunakan arahan fdatasync() atau buka fail dengan bendera O_DSYNC. Jika anda berminat untuk mengetahui lebih lanjut tentang perkara yang berlaku kepada data dalam perjalanan dari kod ke cakera, sila lihat ini artikel.

Ciri menggunakan fungsi write().

Panggilan sistem write() ditakrifkan dalam piawaian IEEE POSIX sebagai percubaan untuk menulis data kepada deskriptor fail. Selepas berjaya menyiapkan kerja write() operasi membaca data mesti mengembalikan tepat bait yang ditulis sebelum ini, berbuat demikian walaupun data sedang diakses daripada proses atau benang lain (di sini bahagian sepadan standard POSIX). ia adalah, dalam bahagian interaksi utas dengan operasi fail biasa, terdapat nota yang mengatakan bahawa jika dua utas setiap satu memanggil fungsi ini, maka setiap panggilan mesti sama ada melihat semua akibat yang ditunjukkan yang membawa kepada pelaksanaan panggilan lain, atau tidak melihat sama sekali tiada akibat. Ini membawa kepada kesimpulan bahawa semua operasi I/O fail mesti memegang kunci pada sumber yang sedang diusahakan.

Adakah ini bermakna bahawa operasi write() adakah atom? Dari sudut teknikal, ya. Operasi membaca data mesti mengembalikan sama ada semua atau tiada apa yang ditulis dengannya write(). Tetapi operasi write(), mengikut piawaian, tidak perlu berakhir, setelah menulis semua yang dia diminta untuk menuliskannya. Ia dibenarkan untuk menulis hanya sebahagian daripada data. Sebagai contoh, kami mungkin mempunyai dua strim setiap satu menambahkan 1024 bait pada fail yang diterangkan oleh deskriptor fail yang sama. Dari sudut pandangan standard, hasilnya akan diterima apabila setiap operasi tulis boleh menambah hanya satu bait pada fail. Operasi ini akan kekal atom, tetapi selepas ia selesai, data yang mereka tulis ke fail akan bercampur aduk. di sini ialah perbincangan yang sangat menarik mengenai topik ini di Stack Overflow.

fungsi fsync() dan fdatasync().

Cara paling mudah untuk mengepam data ke cakera adalah dengan memanggil fungsi tersebut fsync(). Fungsi ini meminta sistem pengendalian untuk mengalihkan semua blok yang diubah suai dari cache ke cakera. Ini termasuk semua metadata fail (masa akses, masa pengubahsuaian fail dan sebagainya). Saya percaya metadata ini jarang diperlukan, jadi jika anda tahu ia tidak penting kepada anda, anda boleh menggunakan fungsi tersebut fdatasync(). Π’ menolong pada fdatasync() ia mengatakan bahawa semasa operasi fungsi ini, jumlah metadata sedemikian disimpan ke cakera, yang "diperlukan untuk pelaksanaan yang betul bagi operasi membaca data berikut." Dan inilah yang paling penting bagi kebanyakan aplikasi.

Satu masalah yang boleh timbul di sini ialah mekanisme ini tidak menjamin bahawa fail itu boleh ditemui selepas kemungkinan kegagalan. Khususnya, apabila fail baru dibuat, seseorang harus memanggil fsync() untuk direktori yang mengandunginya. Jika tidak, selepas ranap sistem, fail ini mungkin tidak wujud. Sebabnya ialah di bawah UNIX, disebabkan penggunaan pautan keras, fail boleh wujud dalam berbilang direktori. Oleh itu, apabila memanggil fsync() tidak ada cara untuk fail mengetahui data direktori mana yang juga harus dibuang ke cakera (di sini anda boleh membaca lebih lanjut mengenai ini). Nampaknya sistem fail ext4 mampu secara automatik untuk memohon fsync() ke direktori yang mengandungi fail yang sepadan, tetapi ini mungkin tidak berlaku dengan sistem fail lain.

Mekanisme ini boleh dilaksanakan secara berbeza dalam sistem fail yang berbeza. sudah biasa blktrace untuk mengetahui tentang operasi cakera yang digunakan dalam sistem fail ext4 dan XFS. Kedua-duanya mengeluarkan arahan tulis biasa ke cakera untuk kedua-dua kandungan fail dan jurnal sistem fail, siram cache dan keluar dengan melakukan FUA (Akses Unit Daya, menulis data terus ke cakera, memintas cache) menulis ke jurnal. Mereka mungkin berbuat demikian untuk mengesahkan fakta transaksi. Pada pemacu yang tidak menyokong FUA, ini menyebabkan dua cache flushes. Eksperimen saya telah menunjukkannya fdatasync() cepat sikit fsync(). Utiliti blktrace menunjukkan bahawa fdatasync() biasanya menulis kurang data ke cakera (dalam ext4 fsync() menulis 20 KiB, dan fdatasync() - 16 KiB). Juga, saya mendapati bahawa XFS adalah lebih pantas sedikit daripada ext4. Dan di sini dengan bantuan blktrace dapat mengetahuinya fdatasync() kurangkan data ke cakera (4 KiB dalam XFS).

Situasi samar-samar apabila menggunakan fsync()

Saya boleh memikirkan tiga situasi yang samar-samar berkenaan fsync()yang saya temui dalam amalan.

Kejadian pertama seumpama itu berlaku pada tahun 2008. Pada masa itu, antara muka Firefox 3 "dibekukan" jika sejumlah besar fail sedang ditulis ke cakera. Masalahnya ialah pelaksanaan antara muka menggunakan pangkalan data SQLite untuk menyimpan maklumat tentang keadaannya. Selepas setiap perubahan yang berlaku dalam antara muka, fungsi itu dipanggil fsync(), yang memberikan jaminan storan data yang stabil. Dalam sistem fail ext3 yang digunakan kemudian, fungsi fsync() dibuang ke cakera semua halaman "kotor" dalam sistem, dan bukan hanya halaman yang berkaitan dengan fail yang sepadan. Ini bermakna mengklik butang dalam Firefox boleh menyebabkan megabait data ditulis ke cakera magnetik, yang boleh mengambil masa beberapa saat. Penyelesaian masalah, sejauh yang saya faham dari ia material, adalah untuk memindahkan kerja dengan pangkalan data ke tugas latar belakang tak segerak. Ini bermakna bahawa Firefox digunakan untuk melaksanakan keperluan ketekunan storan yang lebih ketat daripada yang sebenarnya diperlukan, dan ciri sistem fail ext3 hanya memburukkan lagi masalah ini.

Masalah kedua berlaku pada tahun 2009. Kemudian, selepas ranap sistem, pengguna sistem fail ext4 baharu mendapati bahawa banyak fail yang baru dibuat adalah panjang sifar, tetapi ini tidak berlaku dengan sistem fail ext3 yang lebih lama. Dalam perenggan sebelumnya, saya bercakap tentang bagaimana ext3 membuang terlalu banyak data pada cakera, yang memperlahankan keadaan. fsync(). Untuk memperbaiki keadaan, ext4 hanya membuang halaman "kotor" yang berkaitan dengan fail tertentu. Dan data fail lain kekal dalam ingatan untuk masa yang lebih lama berbanding dengan ext3. Ini dilakukan untuk meningkatkan prestasi (secara lalai, data kekal dalam keadaan ini selama 30 saat, anda boleh mengkonfigurasi ini menggunakan dirty_expire_centisecs; di sini anda boleh mendapatkan maklumat lanjut tentang ini). Ini bermakna bahawa sejumlah besar data boleh hilang tanpa dapat dikembalikan selepas ranap sistem. Penyelesaian kepada masalah ini adalah dengan menggunakan fsync() dalam aplikasi yang perlu menyediakan storan data yang stabil dan melindunginya sebanyak mungkin daripada akibat kegagalan. Fungsi fsync() berfungsi dengan lebih cekap dengan ext4 berbanding ext3. Kelemahan pendekatan ini ialah penggunaannya, seperti sebelumnya, melambatkan beberapa operasi, seperti memasang program. Lihat butiran mengenai ini di sini ΠΈ di sini.

Masalah ketiga berkenaan fsync(), bermula pada 2018. Kemudian, dalam rangka projek PostgreSQL, didapati bahawa jika fungsi fsync() menghadapi ralat, ia menandakan halaman "kotor" sebagai "bersih". Akibatnya, panggilan berikut fsync() tidak melakukan apa-apa dengan halaman sedemikian. Oleh sebab itu, halaman yang diubah suai disimpan dalam ingatan dan tidak pernah ditulis ke cakera. Ini adalah bencana sebenar, kerana aplikasi akan berfikir bahawa beberapa data ditulis ke cakera, tetapi sebenarnya tidak. Kegagalan sedemikian fsync() jarang berlaku, aplikasi dalam situasi sedemikian hampir tidak dapat melakukan apa-apa untuk memerangi masalah itu. Hari ini, apabila ini berlaku, PostgreSQL dan aplikasi lain ranap. ia adalah, dalam artikel "Bolehkah Aplikasi Pulih daripada Kegagalan fsync?", masalah ini diterokai secara terperinci. Pada masa ini penyelesaian terbaik untuk masalah ini ialah menggunakan Direct I/O dengan bendera O_SYNC atau dengan bendera O_DSYNC. Dengan pendekatan ini, sistem akan melaporkan ralat yang mungkin berlaku semasa menjalankan operasi penulisan data tertentu, tetapi pendekatan ini memerlukan aplikasi untuk menguruskan penimbal itu sendiri. Baca lebih lanjut mengenainya di sini ΠΈ di sini.

Membuka fail menggunakan bendera O_SYNC dan O_DSYNC

Mari kita kembali kepada perbincangan tentang mekanisme Linux yang menyediakan storan data yang berterusan. Iaitu, kita bercakap tentang menggunakan bendera O_SYNC atau bendera O_DSYNC apabila membuka fail menggunakan panggilan sistem buka(). Dengan pendekatan ini, setiap operasi menulis data dilakukan seolah-olah selepas setiap arahan write() sistem diberi, masing-masing, arahan fsync() ΠΈ fdatasync(). Π’ Spesifikasi POSIX ini dipanggil "Penyelesaian Integriti Fail I/O Tersegerak" dan "Penyiapan Integriti Data". Kelebihan utama pendekatan ini ialah hanya satu panggilan sistem perlu dilaksanakan untuk memastikan integriti data, dan bukan dua (contohnya - write() ΠΈ fdatasync()). Kelemahan utama pendekatan ini ialah semua operasi tulis menggunakan deskriptor fail yang sepadan akan disegerakkan, yang boleh mengehadkan keupayaan untuk menstruktur kod aplikasi.

Menggunakan I/O Terus dengan bendera O_DIRECT

Panggilan sistem open() menyokong bendera O_DIRECT, yang direka untuk memintas cache sistem pengendalian, melaksanakan operasi I / O, berinteraksi secara langsung dengan cakera. Ini, dalam banyak kes, bermakna arahan tulis yang dikeluarkan oleh program akan diterjemahkan terus ke dalam arahan yang bertujuan untuk bekerja dengan cakera. Tetapi, secara umum, mekanisme ini bukan pengganti untuk fungsi fsync() atau fdatasync(). Hakikatnya ialah cakera itu sendiri boleh kelewatan atau cache arahan yang sesuai untuk menulis data. Dan, lebih teruk lagi, dalam beberapa kes khas, operasi I / O dilakukan apabila menggunakan bendera O_DIRECT, siaran ke dalam operasi penimbal tradisional. Cara paling mudah untuk menyelesaikan masalah ini ialah menggunakan bendera untuk membuka fail O_DSYNC, yang bermaksud bahawa setiap operasi tulis akan diikuti dengan panggilan fdatasync().

Ternyata sistem fail XFS baru-baru ini telah menambah "laluan pantas" untuk O_DIRECT|O_DSYNC-rekod data. Jika blok ditimpa menggunakan O_DIRECT|O_DSYNC, kemudian XFS, bukannya membuang cache, akan melaksanakan arahan tulis FUA jika peranti menyokongnya. Saya mengesahkan ini menggunakan utiliti blktrace pada sistem Linux 5.4/Ubuntu 20.04. Pendekatan ini sepatutnya lebih cekap, kerana ia menulis jumlah minimum data ke cakera dan menggunakan satu operasi, bukan dua (tulis dan siram cache). Saya jumpa pautan ke tampalan kernel 2018 yang melaksanakan mekanisme ini. Terdapat beberapa perbincangan tentang menggunakan pengoptimuman ini kepada sistem fail lain, tetapi setakat yang saya tahu, XFS adalah satu-satunya sistem fail yang menyokongnya setakat ini.

sync_file_range() fungsi

Linux mempunyai panggilan sistem sync_file_range(), yang membolehkan anda mengepam hanya sebahagian daripada fail ke cakera, bukan keseluruhan fail. Panggilan ini memulakan siram tak segerak dan tidak menunggu sehingga selesai. Tetapi dalam rujukan kepada sync_file_range() arahan ini dikatakan "sangat berbahaya". Ia tidak disyorkan untuk menggunakannya. Ciri dan bahaya sync_file_range() sangat baik diterangkan dalam ini bahan. Khususnya, panggilan ini nampaknya menggunakan RocksDB untuk mengawal apabila kernel mengalirkan data "kotor" ke cakera. Tetapi pada masa yang sama di sana, untuk memastikan penyimpanan data yang stabil, ia juga digunakan fdatasync(). Π’ kod RocksDB mempunyai beberapa komen yang menarik mengenai perkara ini. Sebagai contoh, ia kelihatan seperti panggilan sync_file_range() apabila menggunakan ZFS tidak membuang data ke cakera. Pengalaman memberitahu saya bahawa kod yang jarang digunakan mungkin mengandungi pepijat. Oleh itu, saya akan menasihatkan agar tidak menggunakan panggilan sistem ini melainkan benar-benar perlu.

Panggilan sistem untuk membantu memastikan kegigihan data

Saya telah membuat kesimpulan bahawa terdapat tiga pendekatan yang boleh digunakan untuk melaksanakan operasi I/O yang berterusan. Mereka semua memerlukan panggilan fungsi fsync() untuk direktori tempat fail dibuat. Ini adalah pendekatan:

  1. Panggilan fungsi fdatasync() atau fsync() selepas fungsi write() (lebih baik digunakan fdatasync()).
  2. Bekerja dengan deskriptor fail dibuka dengan bendera O_DSYNC atau O_SYNC (lebih baik - dengan bendera O_DSYNC).
  3. Penggunaan arahan pwritev2() dengan bendera RWF_DSYNC atau RWF_SYNC (sebaik-baiknya dengan bendera RWF_DSYNC).

Nota Prestasi

Saya tidak mengukur dengan teliti prestasi pelbagai mekanisme yang saya siasat. Perbezaan yang saya perhatikan dalam kelajuan kerja mereka sangat kecil. Ini bermakna saya boleh salah, dan dalam keadaan lain perkara yang sama mungkin menunjukkan hasil yang berbeza. Mula-mula, saya akan bercakap tentang perkara yang lebih mempengaruhi prestasi, dan kemudian, tentang perkara yang kurang mempengaruhi prestasi.

  1. Menimpa data fail adalah lebih pantas daripada menambahkan data pada fail (pertambahan prestasi boleh 2-100%). Melampirkan data pada fail memerlukan perubahan tambahan pada metadata fail, walaupun selepas panggilan sistem fallocate(), tetapi magnitud kesan ini mungkin berbeza-beza. Saya cadangkan, untuk prestasi terbaik, untuk menelefon fallocate() untuk pra-peruntukkan ruang yang diperlukan. Kemudian ruang ini mesti diisi secara eksplisit dengan sifar dan dipanggil fsync(). Ini akan menyebabkan blok yang sepadan dalam sistem fail ditandakan sebagai "diperuntukkan" dan bukannya "tidak diperuntukkan". Ini memberikan peningkatan prestasi yang kecil (kira-kira 2%). Selain itu, sesetengah cakera mungkin mempunyai operasi capaian blok pertama yang lebih perlahan daripada yang lain. Ini bermakna mengisi ruang dengan sifar boleh membawa kepada peningkatan prestasi yang ketara (kira-kira 100%). Khususnya, ini boleh berlaku dengan cakera. AWS EBS (ini adalah data tidak rasmi, saya tidak dapat mengesahkannya). Perkara yang sama berlaku untuk penyimpanan. Cakera Gigih GCP (dan ini sudah pun maklumat rasmi, disahkan oleh ujian). Pakar lain telah melakukan perkara yang sama pemerhatianberkaitan dengan cakera yang berbeza.
  2. Semakin sedikit panggilan sistem, semakin tinggi prestasi (keuntungan boleh menjadi kira-kira 5%). Ia kelihatan seperti panggilan open() dengan bendera O_DSYNC atau hubungi pwritev2() dengan bendera RWF_SYNC panggilan lebih pantas fdatasync(). Saya mengesyaki bahawa perkara di sini ialah dengan pendekatan ini, hakikat bahawa lebih sedikit panggilan sistem perlu dilakukan untuk menyelesaikan tugas yang sama (satu panggilan bukannya dua) memainkan peranan. Tetapi perbezaan prestasi adalah sangat kecil, jadi anda boleh dengan mudah mengabaikannya dan menggunakan sesuatu dalam aplikasi yang tidak membawa kepada komplikasi logiknya.

Jika anda berminat dengan topik storan data mampan, berikut ialah beberapa bahan berguna:

  • Kaedah Akses I/O β€” gambaran keseluruhan asas mekanisme input / output.
  • Memastikan data sampai ke cakera - cerita tentang apa yang berlaku kepada data dalam perjalanan dari aplikasi ke cakera.
  • Bilakah anda harus menyegerakkan direktori yang mengandungi - jawapan kepada soalan bila hendak memohon fsync() untuk direktori. Secara ringkasnya, ternyata anda perlu melakukan ini semasa membuat fail baharu, dan sebab cadangan ini ialah di Linux terdapat banyak rujukan kepada fail yang sama.
  • Pelayan SQL pada Linux: FUA Dalaman - berikut ialah penerangan tentang cara penyimpanan data berterusan dilaksanakan dalam SQL Server pada platform Linux. Terdapat beberapa perbandingan menarik antara panggilan sistem Windows dan Linux di sini. Saya hampir pasti bahawa terima kasih kepada bahan ini saya belajar tentang pengoptimuman FUA XFS.

Pernahkah anda kehilangan data yang anda fikir telah disimpan dengan selamat pada cakera?

Penyimpanan Data Tahan Lama dan API Fail Linux

Penyimpanan Data Tahan Lama dan API Fail Linux

Sumber: www.habr.com