Topik temu duga representatif

Temuduga Linux: Bagaimana Anda Menjadikan Kemas Kini Fail Atomik Selamat daripada Ranap (Crash-Safe)?

UmumSukar
Pasukan Editorial Offer.ccDiterbitkan Dikemas kini

Soalan

Sebuah daemon Linux menggantikan fail keadaan 64 KiB semasa proses lain sedang membacanya. Selepas daemon melaporkan kejayaan, versi baharu mesti kekal walaupun berlaku kehilangan kuasa. Selepas sebarang ranap yang lebih awal, laluan sasaran mesti mengandungi sama ada versi lengkap yang dikomitedkan sebelumnya atau versi baharu yang lengkap, tidak sekali-kali campuran yang terpotong. Reka bentuk protokol penulisan, kontrak ralat, sempadan konkurensi, tingkah laku pemulihan, dan ujian ranap.

Masalah dan Kes Penggunaan

Masalah ini mengandungi tiga jaminan yang berbeza. Keterlihatan atomik bermakna pembaca yang membuka laluan sasaran melihat satu versi yang lengkap, bukan penulisan semula separa di tempat asal (in-place partial rewrite). Ketahanan ranap (crash durability) bermakna versi yang diperakui kekal boleh dicapai selepas but semula. Pengasingan penulis (writer isolation) menentukan apa yang berlaku apabila dua kemas kini bersaing (race). Panggilan rename() tunggal hanya menangani jaminan pertama di bawah kontrak sistem failnya.

Andaikan sistem fail Linux tempatan yang melaksanakan fsync() dan penggantian atomik pada sistem fail yang sama dengan betul, satu penulis yang disiri (serialized), dan versi yang diperakui sebelumnya ditulis oleh protokol yang sama. Saiz 64 KiB ialah andaian temuduga. Sistem fail rangkaian, perkakasan rosak yang memalsukan pembersihan cache, transaksi berbilang fail, dan pengubahsuaian direktori yang berniat jahat memerlukan kontrak yang berasingan.

Penggunaan biasa termasuk snapshot konfigurasi, keadaan ejen tempatan, manifes titik semak (checkpoint), metadata pakej, dan penyimpanan editor. Jika keadaan merangkumi beberapa fail atau memerlukan pertanyaan dan transaksi serentak, pangkalan data terbenam atau jurnal biasanya lebih selamat daripada memperluaskan protokol ini secara manual.

Perkara yang Dinilai oleh Penemuduga

Isyarat pertama ialah sama ada calon membezakan kejayaan write(), keatomikan rename(), dan komit tahan lama (durable commit). Dokumentasi Linux menyatakan bahawa write() boleh berlaku secara separa dan pulangan yang berjaya tidak membuktikan ketahanan pada cakera. fsync(file) mengepam data fail yang diubah suai dan metadata yang berkaitan, tetapi tidak semestinya mengekalkan entri direktorinya. Langkah terakhir tersebut memerlukan fsync(parent_directory).

Isyarat kedua ialah susunan (ordering). Bait baharu mesti tahan lama sebelum ruang nama menghalakan nama sasaran kepadanya. Entri direktori kemudiannya mesti tahan lama sebelum kejayaan dilaporkan. Membalikkan atau meninggalkan mana-mana sekatan (barrier) akan mewujudkan tetingkap ranap (crash window).

Isyarat ketiga ialah semantik kegagalan yang jujur. fsync() boleh mendedahkan kegagalan EIO, ENOSPC, atau kuota yang tertangguh. Jika penyegerakan direktori terakhir gagal, penamaan semula mungkin sudah kelihatan manakala ketahanannya tidak diketahui. API mesti mengembalikan kegagalan tidak tentu (indeterminate failure), bukan mendakwa kejayaan atau berpura-pura ia sentiasa boleh membuat undur balik (rollback).

Soalan Penjelasan Sebelum Menjawab

  • Adakah "atomik" merangkumi ranap proses atau kehilangan kuasa hos? Penamatan proses sahaja meninggalkan cache halaman kernel kekal hidup; gesaan ini memerlukan ketahanan terhadap ranap hos dan oleh itu memerlukan sekatan penyegerakan.
  • Adakah sasaran berada pada sistem fail tempatan dengan semantik yang didokumentasikan? NFS, FUSE, sistem fail tindanan (overlay), dan tindanan storan yang luar biasa mungkin memberikan tingkah laku ralat dan ketahanan yang berbeza. Uji kontrak pelaksanaan sebenar.
  • Adakah terdapat berbilang penulis? Reka bentuk asas ini menyirikan penulis. Nama sementara yang unik menghalang pertembungan tetapi tidak menghalang kemas kini penulis-terakhir-menang (last-writer-wins).
  • Adakah beberapa fail mesti berubah bersama-sama? Penamaan semula hanya mengomitedkan satu penggantian laluan nama. Gunakan direktori generasi, jurnal, atau pangkalan data untuk invarian berbilang fail.
  • Bolehkah deskriptor fail terbuka yang lebih lama terus membaca versi lama? Ya. Penamaan semula mengubah pemetaan direktori; deskriptor yang telah dibuka masih merujuk kepada inode lama. Pembaca mesti membuka semula apabila mereka memerlukan generasi terkini.
  • Apakah yang membentuk kandungan yang sah? Sahkan penyirikan, skema, checksum, dan versi sebelum penerbitan. Keatomikan sistem fail tidak boleh menjadikan muatan yang cacat tetapi lengkap menjadi betul.

Rangka Jawapan 30 Saat

"Saya mengasingkan keterlihatan atomik daripada ketahanan. Saya membuka direktori induk, mencipta fail sementara yang unik dalam direktori yang sama dengan penciptaan eksklusif, menulis dalam gelung sehingga semua bait diterima, menetapkan metadata yang diperlukan, dan melakukan fsync pada fail sementara tersebut. Kemudian saya menutupnya, menamakan semula secara atomik ke atas sasaran, dan melakukan fsync pada direktori induk sebelum mengembalikan kejayaan. Penyegerakan fail menjadikan kandungan inode baharu tahan lama; penamaan semula menerbitkan satu versi lengkap; penyegerakan direktori menjadikan perubahan nama-ke-inode itu tahan lama. Saya menyirikan penulis, menganggap setiap ralat panggilan sistem—termasuk kegagalan penyegerakan direktori terakhir—sebagai ketidakjayaan dengan keadaan tidak tentu yang eksplisit, membersihkan fail sementara yatim semasa pemulihan, dan menguji titik kehilangan kuasa selepas setiap langkah dan bukannya menggunakan penamatan proses sebagai bukti ketahanan."

Perbincangan Mendalam Langkah demi Langkah

Tentukan titik komit dari perspektif pemanggil: hanya fsync direktori induk yang berjaya membenarkan kejayaan yang diperakui. Garis besar pelaksanaannya adalah:

text
durableReplace(parentDir, targetName, bytes):
  dirfd = open(parentDir, read-only | directory | close-on-exec)
  tmpName = uniqueSiblingName(targetName)
  tmpfd = openat(dirfd, tmpName, create | exclusive | write-only | close-on-exec, mode)

  writeAll(tmpfd, bytes)          // retry EINTR; advance after partial writes
  setRequiredMetadata(tmpfd)      // mode/ownership if part of the contract
  fsync(tmpfd)                    // check delayed I/O and allocation errors
  close(tmpfd)                    // check the result

  renameat(dirfd, tmpName, dirfd, targetName)
  fsync(dirfd)                    // persist the namespace change
  close(dirfd)
  return committed

Fail sementara mestilah berada dalam direktori yang sama (sibling) dengan sasaran. Penempatan dalam direktori yang sama memastikan penamaan semula kekal pada satu sistem fail dan mengelakkan jalan keluar penyalinan EXDEV. Gunakan penciptaan eksklusif dan akhiran setempat-proses yang tidak dapat diramalkan; jangan ikuti pautan simbolik sedia ada yang dikawal oleh penyerang. Tetapkan kebenaran sebelum penyegerakan fail supaya metadata yang diperlukan turut serta dalam keadaan fail yang tahan lama.

writeAll penting walaupun untuk fail biasa. Pulangan positif yang lebih kecil daripada kiraan yang diminta ialah penulisan separa, jadi majukan penimbal dan teruskan. Cuba semula hanya operasi terganggu yang sesuai. write() yang berjaya bermakna data telah mencapai keadaan yang diterima kernel, bukan media yang stabil. close() sahaja bukanlah sekatan komit, dan ralat mungkin dilaporkan kemudian oleh fsync().

Seterusnya, segerakkan fail sementara. fsync merangkumi data fail yang diubah suai dan metadata inode serta menunggu peranti storan melaporkan penyelesaian. fdatasync boleh mengurangkan kerja metadata yang tidak berkaitan, tetapi ia masih mesti mengekalkan metadata yang diperlukan untuk mendapatkan semula data, seperti saiz fail. Untuk jawapan yang mudah dan boleh diaudit, gunakan fsync melainkan ukuran dan kontrak sistem fail yang tepat mewajarkan fdatasync.

Hanya selepas penyegerakan itu berjaya barulah renameat boleh menggantikan sasaran. Linux menjamin bahawa, apabila destinasi sudah wujud, proses lain yang membuka destinasi tersebut tidak akan memerhatikan detik di mana ia hilang. Pembaca dengan fail lama yang telah dibuka boleh menyelesaikan pembacaan inode lama; pembukaan kemudian akan diselesaikan kepada yang baharu. Kedua-duanya adalah versi lengkap, iaitu kontrak bacaan yang diingini.

Penamaan semula mengemas kini metadata direktori. Panggilan fsync fail tidak semestinya menjadikan entri direktori itu tahan lama, jadi buka direktori induk dan segerakkannya selepas penamaan semula. Susunan operasi adalah buktinya:

text
new contents durable
  -> target name atomically points to new inode
  -> target-name mapping durable
  -> caller may receive success

Sebelum penamaan semula, ranap sistem mungkin meninggalkan sasaran lama bersama fail sementara yang yatim. Selepas penamaan semula tetapi sebelum penyegerakan direktori, sasaran yang kelihatan mungkin baharu, manakala kebolehcapaian selepas but semula belum dijamin. Selepas penyegerakan direktori berjaya, pemetaan sasaran yang diperakui dan kandungan yang telah disegerakkan membentuk generasi yang dikomitedkan. Pemulihan hanya memadamkan fail sementara usang yang boleh dikenal pasti selepas mengesahkan pemilikan dan penamaan; ia tidak sekali-kali memadamkan sasaran semata-mata kerana operasi sebelumnya mengembalikan ralat.

Pelaporan ralat memerlukan keadaan yang lebih kaya daripada sekadar kejayaan Boolean. Sebelum penamaan semula, kegagalan ialah not_committed dan sasaran lama yang dikomitedkan kekal berwibawa. Selepas penamaan semula, kegagalan penyegerakan direktori ialah indeterminate: fail baharu mungkin kelihatan dan mungkin atau mungkin tidak kekal selepas ranap. Kembalikan ralat, simpan diagnostik, dan biarkan pemulihan memeriksa nombor generasi atau checksum. Menulis semula nilai lama secara membabi buta akan mewujudkan satu lagi peralihan yang tidak dikomitedkan dan boleh menimpa penulis yang lebih baharu.

Bagi berbilang penulis, letakkan kunci nasihat (advisory lock) di sekeliling baca-ubah suai-tulis hanya jika semua penulis mematuhinya, atau simpan generasi yang dijangkakan dan tolak kemas kini yang usang. Fail sementara yang unik hanya memastikan fail pementasan (staging) berasingan. Tanpa penyirian, dua penamaan semula yang sah adalah atomik secara individu tetapi yang kemudian akan menang secara senyap. Bagi berbilang fail yang berkaitan, terbitkan satu direktori generasi yang tidak boleh diubah melalui penuding tunggal yang tahan lama, tambah jurnal tulis-dahulu (write-ahead journal), atau gunakan SQLite; penamaan semula berulang kali tidak mewujudkan transaksi berbilang fail.

Ketahanan yang ketat memerlukan sekurang-kurangnya satu pembersihan fail dan satu pembersihan direktori bagi setiap kemas kini yang dikomitedkan. Jika tidak setiap kemas kini perlu bertahan daripada kehilangan kuasa, tawarkan mod santai dinamakan berasingan yang menggunakan temp-plus-rename untuk keterlihatan atomik dan secara eksplisit membenarkan kehilangan versi terkini. Untuk kadar kemas kini yang tinggi, kumpulkan beberapa perubahan logik ke dalam satu komit jurnal atau gunakan pangkalan data dengan komit kumpulan (group commit). Jangan sekali-kali melemahkan kontrak secara senyap untuk meningkatkan tanda aras.

Uji protokol pada setiap sempadan: penulisan separa; EINTR; ENOSPC; EIO penyegerakan fail; ranap sebelum dan selepas penyegerakan fail; ranap sebelum, semasa, dan selepas penamaan semula; serta kegagalan penyegerakan direktori. Selepas but semula, sahkan bahawa sasaran boleh dihuraikan, sepadan sama ada dengan generasi terakhir yang diperakui atau generasi baharu tanpa perakuan yang dibenarkan, dan tidak sekali-kali menjadi campuran bait. Sahkan juga bahawa setiap generasi yang diperakui kekal bertahan. Ujian SIGKILL penamatan proses memeriksa pembersihan dan tingkah laku deskriptor fail, tetapi hanya ujian ranap VM atau storan yang terkawal dapat menguji kehilangan cache meruap.

Contoh Jawapan Berkualiti Tinggi

"Saya akan bermula dengan mentakrifkan tiga kontrak: pembaca melihat versi yang lengkap, kemas kini yang diperakui bertahan daripada ranap hos, dan penulis disirikan. Sasaran semasa diandaikan telah dikomitedkan oleh protokol yang sama.

Saya membuka direktori induk dan mencipta fail sementara yang unik dan eksklusif di sebelah sasaran. Saya membuat gelung pada write kerana ia boleh mengembalikan kiraan pendek, menggunakan kebenaran yang diperlukan, memanggil fsync pada fail sementara, dan memeriksa setiap keputusan. Kemudian saya menamakan semula fail bersebelahan itu ke atas sasaran. Penamaan semula itu ialah sempadan keterlihatan atomik: pembaca sedia ada boleh mengekalkan inode lama, manakala pembukaan baharu diselesaikan kepada inode baharu yang lengkap.

Penamaan semula itu belum lagi menjadi komit tahan lama saya. Ia mengubah entri direktori, dan penyegerakan fail tidak semestinya mengekalkan entri tersebut. Oleh itu, saya melakukan fsync pada direktori induk yang terbuka dan mengembalikan kejayaan hanya selepas ia berjaya. Ranap sebelum penamaan semula meninggalkan sasaran lama dan mungkin fail sementara yang boleh dialih keluar. Kegagalan selepas penamaan semula tetapi sebelum penyegerakan direktori adalah tidak tentu, jadi saya mengembalikan ralat dan memeriksa metadata generasi semasa pemulihan dan bukannya menjanjikan undur balik.

Saya akan menyuntik kerosakan pada penulisan separa dan setiap kegagalan panggilan sistem, kemudian meranapkan VM pada setiap sempadan. Invariannya ialah sasaran sentiasa merupakan generasi lama atau baharu yang sah, tidak sekali-kali campuran yang terputus, dan setiap generasi yang diperakui kekal selepas but semula. Jika saya memerlukan penulis serentak atau transaksi berbilang fail, saya menambah pemeriksaan generasi dan penguncian atau beralih kepada jurnal/pangkalan data dan bukannya mendakwa bahawa penamaan semula menyelesaikan masalah tersebut."

Kesilapan Biasa

  • Menulis ganti sasaran di tempat asal → ranap mendedahkan pemotongan atau kandungan bercampur → sediakan fail bersebelahan yang lengkap dan namakannya semula.
  • Menganggap satu write() menulis segala-galanya → penulisan pendek secara senyap memotong fail pementasan → gelung sehingga semua bait ditulis atau ralat menamatkan operasi.
  • Memanggil rename sebelum menyegerakkan fail sementara → nama yang tahan lama mungkin menunjuk kepada data yang tidak lengkap atau hilang → segerakkan inode baharu sebelum penerbitan.
  • Menganggap rename sebagai komit tahan lama → keterlihatan atomik tidak mengekalkan entri direktori → segerakkan direktori induk selepas penamaan semula.
  • Menggunakan direktori temp pada pelekap (mount) lain → penamaan semula gagal dengan EXDEV atau menurun kepada salinan bukan atomik → cipta fail temp dalam direktori sasaran.
  • Mengabaikan ralat fsync kegagalan storan yang tertangguh menjadi kejayaan palsu → sebarkan hasil ketidakjayaan atau tidak tentu yang eksplisit.
  • Hanya menguji dengan SIGKILL cache kernel kekal hidup semasa kematian proses → tambah suntikan ranap hos/storan yang terkawal.
  • Membiarkan penulis serentak bersaing → kemas kini yang atomik secara individu masih kehilangan satu generasi → sirikan atau bandingkan generasi yang dijangkakan.

Soalan Susulan dan Maklum Balas

Soalan Susulan 1: Mengapakah fsync(temp) tidak mencukupi?

Ia menjadikan kandungan inode sementara dan metadata yang berkaitan tahan lama di bawah kontrak sistem fail. Laluan sasaran ialah entri direktori. Selepas penamaan semula, pemetaan tersebut telah berubah, dan Linux secara eksplisit menyatakan bahawa penyegerakan fail tidak semestinya mengekalkan entri direktori yang mengandungi fail tersebut. Segerakkan direktori induk sebelum membuat perakuan.

Soalan Susulan 2: Adakah rename() atomik jika proses lain telah membuka sasaran?

Penggantian ruang nama adalah atomik untuk carian laluan. Deskriptor sedia ada terus merujuk kepada inode lama, manakala pembukaan baharu diselesaikan kepada inode baharu. Jika pembaca memerlukan satu generasi merentasi beberapa bacaan, ia harus mengekalkan satu deskriptor terbuka daripada membuka semula di pertengahan jalan.

Soalan Susulan 3: Apakah yang patut dikembalikan oleh API apabila fsync direktori gagal selepas penamaan semula?

Kembalikan ralat dengan keadaan komit yang tidak tentu. Penamaan semula mungkin sudah kelihatan, dan aplikasi tidak dapat membuktikan sama ada ia akan bertahan selepas but semula. Rekodkan generasi dan ralat yang dimaksudkan, berhenti mendakwa kejayaan, dan biarkan pemulihan mengesahkan sasaran. Undur balik automatik secara membuta tuli boleh memusnahkan kemas kini sah yang berlaku kemudian.

Soalan Susulan 4: Bolehkah fdatasync menggantikan fsync?

Ia boleh apabila kontrak platform dan pengukuran mewajarkannya. Ia meninggalkan metadata yang tidak berkaitan dengan pengambilan semula data kemudian, tetapi masih mesti mengekalkan metadata yang diperlukan seperti saiz fail. Ia tidak menghapuskan keperluan untuk menyegerakkan direktori induk selepas penamaan semula. Lebih utamakan fsync dalam jawapan generik kerana kontraknya lebih mudah diaudit.

Soalan Susulan 5: Bagaimanakah anda mengemas kini tiga fail secara atomik?

Tiga penamaan semula yang bebas boleh mendedahkan generasi yang bercampur. Tulis semua fail di bawah satu direktori generasi yang tidak boleh diubah, jadikan direktori itu tahan lama, kemudian tukar dan segerakkan satu manifes atau penuding secara atomik; sebagai alternatif gunakan jurnal atau pangkalan data terbenam. Pembaca menyelesaikan satu generasi dan mengekalkannya sepanjang operasi.

Soalan Susulan 6: Bagaimanakah dua penulis mengelakkan kehilangan kemas kini (lost updates)?

Gunakan kunci yang dipatuhi oleh setiap penulis di sekeliling urutan lengkap baca-ubah suai-komit, atau sertakan generasi yang dijangkakan dan tolak komit yang usang. Nama temp yang unik hanya menghalang pertembungan pementasan. Penamaan semula atomik tidak membandingkan versi perniagaan dan secara semula jadi membenarkan penulis-terakhir-menang.

Sumber awam

Soalan berkaitan