Masalah dan Kasus Penggunaan
Masalah ini mencakup tiga jaminan yang berbeda. Visibilitas atomik berarti pembaca yang membuka path target akan melihat satu versi lengkap, bukan penulisan ulang parsial langsung di tempat (in-place partial rewrite). Durabilitas kerusakan (crash durability) berarti versi yang telah diakui (acknowledged) tetap dapat diakses setelah reboot. Isolasi penulis (writer isolation) menentukan apa yang terjadi saat dua pembaruan saling beradu (race). Satu panggilan rename() hanya menangani jaminan pertama di bawah kontrak sistem berkasnya.
Asumsikan sistem berkas Linux lokal yang mengimplementasikan fsync() dan penggantian atomik pada sistem berkas yang sama dengan benar, satu penulis yang diserialisasi, dan versi-versi yang diakui sebelumnya ditulis menggunakan protokol yang sama. Ukuran 64 KiB adalah asumsi wawancara. Sistem berkas jaringan, perangkat keras rusak yang memalsukan pembersihan cache (cache flushes), transaksi multi-berkas, dan modifikasi direktori yang berbahaya memerlukan kontrak yang terpisah.
Penggunaan umum mencakup snapshot konfigurasi, status agen lokal, manifes checkpoint, metadata paket, dan penyimpanan editor. Jika status mencakup beberapa berkas atau memerlukan kueri serta transaksi konkuren, basis data tertanam (embedded database) atau jurnal biasanya lebih aman daripada memperluas protokol ini secara manual.
Apa yang Sedang Dievaluasi oleh Pewawancara
Sinyal pertama adalah apakah kandidat dapat membedakan keberhasilan write(), keatomikan rename(), dan komit yang tahan lama (durable commit). Dokumentasi Linux menyatakan bahwa write() dapat bersifat parsial dan hasil pengembalian yang sukses tidak membuktikan persistensi disk. fsync(file) membersihkan data berkas yang dimodifikasi beserta metadata terkait ke media penyimpanan, tetapi belum tentu menyimpan entri direktorinya secara persisten. Batas terakhir tersebut memerlukan fsync(parent_directory).
Sinyal kedua adalah pengurutan (ordering). Byte-byte baru harus persisten/durable sebelum namespace mengarahkan nama target ke byte-byte tersebut. Entri direktori kemudian harus dipersistenkan sebelum keberhasilan dilaporkan. Membalikkan atau menghilangkan salah satu penghalang (barrier) ini akan menciptakan celah kegagalan saat crash (crash window).
Sinyal ketiga adalah semantik kegagalan yang jujur. fsync() dapat mengungkap kegagalan EIO, ENOSPC, atau batas kuota yang tertunda. Jika sinkronisasi direktori akhir gagal, penggantian nama (rename) mungkin sudah terlihat sementara durabilitasnya belum diketahui. API harus mengembalikan kegagalan yang tidak tentu (indeterminate failure), bukan mengklaim sukses atau berpura-pura selalu dapat melakukan rollback.
Pertanyaan Klarifikasi Sebelum Menjawab
- Apakah "atomik" mencakup crash pada proses atau hilangnya daya pada host? Penghentian proses saja masih membiarkan page cache kernel tetap hidup; pertanyaan ini meminta durabilitas terhadap crash host sehingga memerlukan barrier sinkronisasi.
- Apakah target berada di sistem berkas lokal dengan semantik yang terdokumentasi? NFS, FUSE, sistem berkas overlay, dan tumpukan penyimpanan yang tidak biasa mungkin memberikan perilaku penanganan kesalahan dan persistensi yang berbeda. Ujilah kontrak penerapan yang sebenarnya.
- Apakah ada beberapa penulis (multiple writers)? Desain dasar ini menserialisasikan para penulis. Nama sementara yang unik mencegah tabrakan tetapi tidak mencegah pembaruan dengan prinsip yang-terakhir-menulis-yang-menang (last-writer-wins).
- Apakah beberapa berkas harus berubah secara bersamaan? Sebuah operasi rename hanya mengomit satu penggantian nama path. Gunakan direktori generasi, jurnal, atau basis data untuk invarian multi-berkas.
- Dapatkah deskriptor berkas terbuka yang lama terus membaca versi lama? Ya. Operasi rename mengubah pemetaan direktori; deskriptor yang sudah terbuka masih merujuk ke inode lama. Pembaca harus membuka ulang ketika mereka membutuhkan generasi terbaru.
- Apa yang menjadi syarat konten yang valid? Validasi serialisasi, skema, checksum, dan versi sebelum publikasi. Keatomikan sistem berkas tidak dapat membuat payload yang salah bentuk (malformed) tetapi lengkap menjadi benar.
Kerangka Jawaban 30 Detik
"Saya memisahkan visibilitas atomik dari durabilitas. Saya membuka direktori induk, membuat berkas sementara yang unik di direktori yang sama dengan pembuatan eksklusif, menulis dalam sebuah loop hingga semua byte diterima, mengatur metadata yang diperlukan, dan melakukan fsync pada berkas sementara tersebut. Kemudian saya menutupnya, mengganti namanya secara atomik di atas target, dan melakukan fsync pada direktori induk sebelum mengembalikan status sukses. Sinkronisasi berkas membuat konten inode baru menjadi tahan lama (durable); rename mempublikasikan satu versi lengkap; sinkronisasi direktori membuat perubahan nama-ke-inode tersebut menjadi durable. Saya menserialisasikan para penulis, memperlakukan setiap kesalahan syscall—termasuk kegagalan sinkronisasi direktori akhir—sebagai non-sukses dengan status tidak tentu yang eksplisit, membersihkan berkas sementara yatim (orphan) selama pemulihan, dan menguji titik-titik kehilangan daya setelah setiap langkah daripada hanya menggunakan pembunuhan proses (process kill) sebagai bukti durabilitas."
Penjelasan Mendalam Langkah demi Langkah
Tentukan titik komit dari sudut pandang pemanggil: hanya fsync direktori induk yang berhasil yang mengizinkan pengakuan keberhasilan (acknowledged success). Garis besar implementasinya adalah:
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 committedBerkas sementara harus berada di direktori yang sama (sibling) dengan target. Penempatan di direktori yang sama memastikan operasi rename tetap berada di dalam satu sistem berkas dan menghindari fallback penyalinan EXDEV. Gunakan pembuatan eksklusif dan sufiks lokal-proses yang tidak dapat diprediksi; jangan ikuti symlink yang sudah ada dan dikendalikan oleh penyerang. Tetapkan izin (permissions) sebelum sinkronisasi berkas agar metadata yang diperlukan bergabung ke dalam status berkas yang durable.
writeAll tetap penting bahkan untuk berkas reguler. Nilai kembalian positif yang lebih kecil dari jumlah yang diminta adalah penulisan parsial, jadi majukan buffer dan lanjutkan. Ulangi (retry) hanya operasi terinterupsi yang sesuai. write() yang berhasil berarti data telah mencapai status yang diterima kernel, bukan media penyimpanan yang stabil. close() saja bukanlah barrier komit, dan kesalahan mungkin dilaporkan kemudian oleh fsync().
Selanjutnya, sinkronkan berkas sementara. fsync mencakup data berkas yang dimodifikasi dan metadata inode, serta menunggu perangkat penyimpanan melaporkan penyelesaian. fdatasync dapat mengurangi pemrosesan metadata yang tidak terkait, tetapi masih harus memulihkan metadata yang diperlukan untuk mengambil data, seperti ukuran berkas. Untuk jawaban yang sederhana dan dapat diaudit, gunakan fsync kecuali jika pengukuran dan kontrak sistem berkas yang tepat membenarkan penggunaan fdatasync.
Hanya setelah sinkronisasi tersebut berhasil, renameat boleh menggantikan target. Linux menjamin bahwa, ketika tujuan sudah ada, proses lain yang membuka tujuan tersebut tidak akan mengamati momen saat berkas hilang. Pembaca dengan berkas lama yang sudah terbuka dapat menyelesaikan pembacaan inode lama; pembukaan di lain waktu akan diarahkan ke inode baru. Keduanya merupakan versi lengkap, yang merupakan kontrak pembacaan yang diharapkan.
Rename memperbarui metadata direktori. Operasi fsync pada berkas belum tentu membuat entri direktori tersebut durable, jadi buka direktori induk dan sinkronkan setelah rename. Pengurutannya adalah buktinya:
new contents durable
-> target name atomically points to new inode
-> target-name mapping durable
-> caller may receive successSebelum rename, crash mungkin meninggalkan target lama ditambah berkas sementara yang yatim. Setelah rename tetapi sebelum sinkronisasi direktori, target yang terlihat mungkin adalah yang baru, sedangkan keterjangkauannya setelah reboot belum dijamin. Setelah sinkronisasi direktori berhasil, pemetaan target yang diakui dan konten yang sudah disinkronkan membentuk generasi yang telah terkomit. Pemulihan hanya menghapus berkas sementara basi yang dapat dikenali setelah memvalidasi kepemilikan dan penamaan; pemulihan tidak pernah menghapus target semata-mata karena operasi sebelumnya mengembalikan kesalahan.
Pelaporan kesalahan membutuhkan status yang lebih kaya daripada sekadar keberhasilan Boolean. Sebelum rename, kegagalan adalah not_committed dan target lama yang telah terkomit tetap menjadi acuan yang sah (authoritative). Setelah rename, kegagalan sinkronisasi direktori adalah indeterminate: berkas baru mungkin terlihat dan mungkin atau mungkin tidak bertahan saat crash. Kembalikan kesalahan, pertahankan diagnostik, dan biarkan pemulihan memeriksa nomor generasi atau checksum. Menulis ulang nilai lama secara membabi buta akan menciptakan transisi lain yang tidak terkomit dan dapat menimpa penulis yang lebih baru.
Untuk beberapa penulis, terapkan advisory lock di sekitar read-modify-write hanya jika semua penulis mematuhinya, atau simpan generasi yang diharapkan dan tolak pembaruan yang basi. Berkas sementara yang unik hanya memisahkan berkas pementasan (staging). Tanpa serialisasi, dua operasi rename yang valid secara individual bersifat atomik tetapi yang lebih akhir akan menang secara diam-diam. Untuk beberapa berkas yang terkait, publikasikan satu direktori generasi yang tidak dapat diubah (immutable) melalui satu pointer yang durable, tambahkan write-ahead journal, atau gunakan SQLite; pengulangan operasi rename tidak menciptakan transaksi multi-berkas.
Durabilitas yang ketat membutuhkan setidaknya satu flush berkas dan satu flush direktori per pembaruan yang dikomit. Jika tidak setiap pembaruan harus bertahan dari kehilangan daya, sediakan mode santai (relaxed mode) bernama terpisah yang menggunakan pola temp-plus-rename untuk visibilitas atomik dan secara eksplisit mengizinkan hilangnya versi terbaru. Untuk tingkat pembaruan yang tinggi, kumpulkan beberapa perubahan logis menjadi satu komit jurnal atau gunakan basis data dengan group commit. Jangan pernah melemahkan kontrak secara diam-diam demi meningkatkan skor tolok ukur (benchmark).
Uji protokol pada setiap batas: penulisan parsial; EINTR; ENOSPC; EIO pada sinkronisasi berkas; crash sebelum dan sesudah sinkronisasi berkas; crash sebelum, selama, dan sesudah rename; serta kegagalan sinkronisasi direktori. Setelah reboot, pastikan bahwa target dapat diuraikan (parses), cocok dengan generasi terakhir yang diakui atau generasi baru yang belum diakui yang diizinkan, dan tidak pernah menjadi campuran byte. Pastikan juga bahwa setiap generasi yang diakui berhasil bertahan. Pengujian pembunuhan proses SIGKILL memeriksa pembersihan dan perilaku deskriptor berkas, tetapi hanya pengujian crash VM atau penyimpanan yang terkendali yang benar-benar melatih skenario kehilangan cache volatil.
Contoh Jawaban Berkualitas Tinggi
"Saya akan mulai dengan mendefinisikan tiga kontrak: pembaca melihat versi lengkap, pembaruan yang diakui bertahan dari crash host, dan penulis diserialisasikan. Target saat ini diasumsikan telah dikomit dengan protokol yang sama.
Saya membuka direktori induk dan membuat berkas sementara yang unik dan eksklusif di samping target. Saya melakukan loop pada write karena dapat mengembalikan hitungan pendek, menerapkan izin yang diperlukan, memanggil fsync pada berkas sementara, dan memeriksa setiap hasilnya. Kemudian saya mengganti nama berkas sibling tersebut di atas target. Operasi rename tersebut adalah batas visibilitas atomik: pembaca yang ada dapat mempertahankan inode lama, sementara pembukaan baru akan mengarah ke inode baru yang lengkap.
Rename tersebut belum menjadi komit durable saya. Operasi itu mengubah entri direktori, dan menyinkronkan berkas belum tentu memulihkan entri tersebut secara persisten. Oleh karena itu, saya melakukan fsync pada direktori induk yang terbuka dan mengembalikan keberhasilan hanya setelah proses tersebut berhasil. Crash sebelum rename akan menyisakan target lama dan mungkin berkas temp yang dapat dihapus. Kegagalan setelah rename tetapi sebelum sinkronisasi direktori bersifat tidak tentu, jadi saya mengembalikan kesalahan dan memeriksa metadata generasi selama pemulihan daripada menjanjikan rollback.
Saya akan melakukan injeksi kesalahan pada penulisan parsial dan setiap kegagalan syscall, lalu melakukan crash pada VM di setiap batas. Invariannya adalah bahwa target selalu merupakan generasi lama atau baru yang valid, tidak pernah berupa campuran yang terpotong/rusak, dan setiap generasi yang diakui tetap ada setelah reboot. Jika saya membutuhkan beberapa penulis konkuren atau transaksi multi-berkas, saya menambahkan pemeriksaan generasi dan penguncian atau beralih ke jurnal/basis data daripada mengklaim bahwa operasi rename dapat menyelesaikan masalah-masalah tersebut."
Kesalahan Umum
- Menimpa target langsung di tempat (in place) → crash menyebabkan berkas terpotong atau konten bercampur → siapkan berkas sibling yang lengkap dan ganti namanya (rename).
- Mengasumsikan satu
write()menulis segalanya → short write secara diam-diam memotong berkas staging → lakukan loop hingga semua byte tertulis atau kesalahan menghentikan operasi. - Memanggil rename sebelum menyinkronkan berkas sementara → nama yang durable dapat merujuk ke data yang tidak lengkap atau hilang → sinkronkan inode baru sebelum publikasi.
- Memperlakukan rename sebagai komit durable → visibilitas atomik tidak memulihkan entri direktori secara persisten → sinkronkan direktori induk setelah rename.
- Menggunakan direktori temp pada titik kait (mount) yang berbeda → rename gagal dengan
EXDEVatau menurun menjadi penyalinan non-atomik → buat berkas temp di direktori target. - Mengabaikan kesalahan
fsync→ kegagalan penyimpanan yang tertunda menjadi keberhasilan palsu → propagasikan hasil non-sukses atau tidak tentu yang eksplisit. - Hanya menguji dengan
SIGKILL→ cache kernel tetap bertahan saat proses mati → tambahkan injeksi crash host/penyimpanan yang terkendali. - Membiarkan penulis konkuren saling beradu (race) → pembaruan yang secara individual atomik tetap kehilangan satu generasi → serialisasikan atau bandingkan generasi yang diharapkan.
Pertanyaan Lanjutan dan Tanggapan
Pertanyaan Lanjutan 1: Mengapa fsync(temp) saja tidak cukup?
Perintah tersebut membuat konten inode sementara dan metadata terkait menjadi durable di bawah kontrak sistem berkas. Nama path target adalah sebuah entri direktori. Setelah rename, pemetaan tersebut telah berubah, dan Linux secara eksplisit menyatakan bahwa sinkronisasi berkas belum tentu memulihkan entri direktori yang memuatnya secara persisten. Sinkronkan direktori induk sebelum memberikan pengakuan (acknowledging).
Pertanyaan Lanjutan 2: Apakah rename() atomik jika proses lain sudah membuka target?
Penggantian namespace bersifat atomik untuk pencarian path (path lookup). Deskriptor yang ada terus merujuk ke inode lama, sedangkan pembukaan baru akan mengarah ke inode baru. Jika pembaca membutuhkan satu generasi di beberapa pembacaan, pembaca tersebut harus tetap membuka satu deskriptor daripada membukanya kembali di tengah jalan.
Pertanyaan Lanjutan 3: Apa yang harus dikembalikan oleh API ketika fsync direktori gagal setelah rename?
Kembalikan kesalahan dengan status komit yang tidak tentu (indeterminate). Rename mungkin sudah terlihat, dan aplikasi tidak dapat membuktikan apakah itu akan bertahan setelah reboot. Catat generasi dan kesalahan yang dimaksud, berhenti mengklaim sukses, dan biarkan proses pemulihan memvalidasi target. Rollback otomatis yang membabi buta dapat menghancurkan pembaruan valid yang terjadi belakangan.
Pertanyaan Lanjutan 4: Bisakah fdatasync menggantikan fsync?
Bisa, jika kontrak platform dan hasil pengukuran membenarkannya. Perintah ini mengabaikan metadata yang tidak terkait dengan pengambilan data di masa mendatang, tetapi tetap harus memulihkan metadata yang diperlukan seperti ukuran berkas secara persisten. Ini tidak menghilangkan kebutuhan untuk menyinkronkan direktori induk setelah rename. Lebih disukai fsync dalam jawaban umum karena kontraknya lebih mudah diaudit.
Pertanyaan Lanjutan 5: Bagaimana cara memperbarui tiga berkas secara atomik?
Tiga operasi rename yang independen dapat mengekspos generasi yang bercampur. Tulis semua berkas di bawah satu direktori generasi yang immutable, buat direktori tersebut durable, lalu alihkan dan sinkronkan satu manifes atau pointer secara atomik; atau gunakan jurnal atau basis data tertanam. Pembaca mengambil satu generasi dan mempertahankannya selama operasi berlangsung.
Pertanyaan Lanjutan 6: Bagaimana dua penulis menghindari pembaruan yang hilang (lost updates)?
Gunakan kunci (lock) yang dipatuhi oleh setiap penulis di sekitar urutan lengkap read-modify-commit, atau sertakan generasi yang diharapkan dan tolak komit yang basi. Nama sementara yang unik hanya mencegah tabrakan staging. Operasi rename atomik tidak membandingkan versi bisnis dan secara alami mengizinkan pembaruan dengan prinsip yang-terakhir-menulis-yang-menang (last-writer-wins).