Bagaimana cara menyalin file secara transaksi?


9

Saya ingin menyalin file dari A ke B, yang mungkin ada di sistem file yang berbeda.

Ada beberapa persyaratan tambahan:

  1. Salinan adalah semua atau tidak sama sekali, tidak ada file B sebagian atau rusak yang tersisa pada saat crash;
  2. Jangan menimpa file B yang ada;
  3. Jangan bersaing dengan eksekusi perintah yang sama secara bersamaan, paling banyak orang bisa berhasil.

Saya pikir ini semakin dekat:

cp A B.part && \
ln B B.part && \
rm B.part

Tetapi 3. dilanggar oleh cp tidak gagal jika B.part ada (bahkan dengan flag -n). Selanjutnya 1. bisa gagal jika proses lain 'memenangkan' cp dan file yang ditautkan ke tempatnya tidak lengkap. B.part juga bisa berupa file yang tidak terkait, tapi saya senang gagal tanpa mencoba nama tersembunyi lainnya dalam kasus itu.

Saya pikir bash noclobber membantu, apakah ini berfungsi sepenuhnya? Apakah ada cara untuk mendapatkannya tanpa persyaratan versi bash?

#!/usr/bin/env bash
set -o noclobber
cat A > B.part && \
ln B.part B && \
rm B.part

Tindak lanjut, saya tahu beberapa sistem file akan gagal saat ini (NFS). Apakah ada cara untuk mendeteksi sistem file seperti itu?

Beberapa pertanyaan lain yang terkait tetapi tidak persis sama:

Mendekati perpindahan atom melintasi sistem file?

Apakah atom saya ada pada fs saya?

apakah ada cara untuk memindahkan file dan direktori secara atom dari tempfs ke partisi ext4 pada eMMC

https://rcrowley.org/2010/01/06/things-unix-can-do-atomically.html


2
Apakah Anda hanya khawatir tentang eksekusi bersamaan dari perintah yang sama (mis. Cukup mengunci di dalam alat Anda), atau tentang gangguan luar lainnya dengan file juga?
Michael Homer

3
"Transaksional" mungkin lebih baik
muru

1
@ MichaelHomer dalam alat ini cukup baik, saya pikir di luar akan membuat semuanya sangat sulit! Jika memungkinkan dengan kunci file ...
Evan Benn

1
@marcelm mvakan menimpa file B. yang ada mv -ntidak akan memberi tahu bahwa ia gagal. ln(1)( rename(2)) akan gagal jika B sudah ada.
Evan Benn

1
@ EvanBenn Poin bagus! Saya harus membaca persyaratan Anda dengan lebih baik. (Saya cenderung membutuhkan pembaruan atom dari target yang ada, dan saya membalasnya dengan pikiran)
marcelm

Jawaban:


11

rsyncmelakukan pekerjaan ini. File sementara O_EXCLdibuat secara default (hanya dinonaktifkan jika Anda menggunakan --inplace) dan kemudian di renamedatas file target. Gunakan --ignore-existinguntuk tidak menimpa B jika ada.

Dalam prakteknya, saya tidak pernah mengalami masalah dengan ini pada ext4, zfs atau bahkan NFS mounts.


rsync mungkin melakukan ini dengan baik, tetapi halaman manual yang sangat rumit membuatku takut. opsi yang menyiratkan pilihan lain, tidak kompatibel satu sama lain, dll.
Evan Benn

Rsync tidak membantu dengan persyaratan # 3, sejauh yang saya tahu. Namun, ini adalah alat yang fantastis, dan Anda tidak boleh menghindar dari sedikit bacaan halaman manual. Anda juga dapat mencoba github.com/tldr-pages/tldr/blob/master/pages/common/rsync.md atau cheat.sh/rsync . (tldr dan cheat adalah dua proyek berbeda yang bertujuan untuk membantu masalah yang Anda nyatakan, yaitu, "halaman manual adalah TL; DR"; banyak perintah umum didukung, dan Anda akan melihat penggunaan yang paling umum ditampilkan.
sitaram

@EvanBenn rsync adalah alat yang luar biasa dan layak dipelajari! Halaman manualnya rumit karena sangat fleksibel. Jangan terintimidasi :)
Josh

@sitaram, # 3 dapat diatasi dengan file pid. Sebuah skrip kecil seperti pada jawabannya di sini .
Robert Riedl

2
Ini jawaban terbaik. Rsync adalah standar industri untuk transfer file atom, dan dalam berbagai konfigurasi dapat memenuhi semua kebutuhan Anda.
wKavey

4

Jangan khawatir, noclobberini fitur standar .


Terima kasih, tergoda untuk menerima jawaban ringkas ini. Adakah komentar tentang sistem file yang cerdik seperti NFS?
Evan Benn

@ EvanBenn, saya bermaksud menambahkan bahwa saya tidak yakin apakah NFS akan mengacaukan Anda dalam beberapa cara, tapi saya lupa.
ilkkachu

4

Anda bertanya tentang NFS. Jenis kode ini kemungkinan akan pecah di bawah NFS, karena pemeriksaan untuk noclobbermelibatkan dua operasi NFS yang terpisah (memeriksa apakah ada file, membuat file baru) dan dua proses dari dua klien NFS yang terpisah dapat masuk ke kondisi balapan di mana keduanya berhasil ( keduanya memverifikasi yang B.partbelum ada, lalu keduanya melanjutkan untuk berhasil membuatnya, sebagai akibatnya mereka saling menimpa.)

Tidak ada benar-benar melakukan pemeriksaan generik untuk apakah sistem file yang Anda tulis akan mendukung sesuatu seperti noclobberatom atau tidak. Anda dapat memeriksa tipe sistem file, apakah itu NFS, tetapi itu akan menjadi heuristik dan belum tentu menjadi jaminan. Sistem file seperti SMB / CIFS (Samba) kemungkinan akan mengalami masalah yang sama. Filesystem mengekspos melalui FUSE mungkin atau mungkin tidak berperilaku dengan benar, tetapi itu sebagian besar tergantung pada implementasinya.


Pendekatan yang mungkin lebih baik adalah menghindari tabrakan pada B.partlangkah tersebut, dengan menggunakan nama file unik (melalui kerja sama dengan agen lain) sehingga Anda tidak perlu bergantung noclobber. Misalnya, Anda dapat memasukkan, sebagai bagian dari nama file, nama host Anda, PID dan stempel waktu (+ mungkin nomor acak). Karena harus ada satu proses yang berjalan di bawah PID tertentu pada host pada waktu tertentu, ini harus menjamin keunikan.

Jadi salah satu dari:

test -f B && continue  # skip already existing
unique=$(hostname).$$.$(date +%s).$RANDOM
cp A B.part."$unique"
# Maybe check for existance of B again, remove
# the temporary file and bail out in that case.
mv B.part."$unique" B
# mv (rename) should always succeed, overwrite a
# previously copied B if one exists.

Atau:

test -f B && continue  # skip already existing
unique=$(hostname).$$.$(date +%s).$RANDOM
cp A B.part."$unique"
if ln B.part."$unique" B ; then
    echo "Success creating B"
else
    echo "Failed creating B, already existed"
fi
# Both cases require cleanup.
rm B.part."$unique"

Jadi jika Anda memiliki kondisi balapan antara dua agen, mereka berdua akan melanjutkan dengan operasi, tetapi operasi terakhir akan menjadi atom, sehingga B ada dengan salinan penuh A, atau B tidak ada.

Anda dapat mengurangi ukuran balapan dengan memeriksa lagi setelah salinan dan sebelum mvatau lnoperasi, tetapi masih ada kondisi balapan kecil di sana. Tetapi, terlepas dari kondisi balapan, isi B harus konsisten, dengan asumsi kedua proses mencoba membuatnya dari A (atau salinan dari file yang valid sebagai asal.)

Perhatikan bahwa dalam situasi pertama dengan mv, ketika ada perlombaan, proses terakhir adalah yang menang, karena mengubah nama (2) secara atom akan menggantikan file yang ada:

Jika newpath sudah ada, maka akan atom diganti, sehingga tidak ada titik di mana proses lain mencoba untuk akses newpath akan menemukannya hilang. [...]

Jika newpath ada tapi operasi gagal karena beberapa alasan, rename()jaminan untuk meninggalkan sebuah contoh dari newpath di tempat.

Jadi, sangat mungkin proses memakan B pada saat itu mungkin melihat versi yang berbeda (inode berbeda) selama proses ini. Jika penulis hanya mencoba menyalin konten yang sama, dan pembaca hanya mengkonsumsi konten file, itu mungkin baik-baik saja, jika mereka mendapatkan inode berbeda untuk file dengan konten yang sama, mereka akan senang sama saja.

Pendekatan kedua menggunakan hardlink terlihat lebih baik, tapi saya ingat membuat percobaan dengan hardlink dalam loop ketat pada NFS dari banyak klien bersamaan dan menghitung keberhasilan dan tampaknya masih ada beberapa kondisi lomba di sana, di mana tampaknya jika dua klien mengeluarkan hardlink Operasi pada saat yang sama, dengan tujuan yang sama, keduanya tampaknya berhasil. (Ada kemungkinan bahwa perilaku ini terkait dengan implementasi server NFS tertentu, YMMV.) Bagaimanapun, itu mungkin kondisi ras yang sama, di mana Anda mungkin mendapatkan dua inode terpisah untuk file yang sama dalam kasus di mana ada berat konkurensi antara penulis untuk memicu kondisi lomba ini. Jika penulis Anda konsisten (keduanya menyalin A ke B), dan pembaca Anda hanya mengkonsumsi konten, itu mungkin cukup.

Akhirnya, Anda menyebutkan penguncian. Sayangnya penguncian sangat kurang, setidaknya di NFSv3 (tidak yakin tentang NFSv4, tapi saya yakin itu juga tidak baik.) Jika Anda mempertimbangkan penguncian, Anda harus melihat ke protokol yang berbeda untuk penguncian yang didistribusikan, mungkin keluar dari band dengan salinan file yang sebenarnya, tapi itu mengganggu, kompleks dan rentan terhadap masalah seperti kebuntuan, jadi saya akan mengatakan lebih baik untuk dihindari.


Untuk latar belakang lebih lanjut tentang masalah atomisitas pada NFS, Anda mungkin ingin membaca pada format kotak surat Maildir , yang dibuat untuk menghindari kunci dan bekerja dengan andal bahkan pada NFS. Itu dilakukan dengan menjaga nama file unik di mana-mana (sehingga Anda bahkan tidak mendapatkan B akhir di akhir.)

Mungkin agak lebih menarik untuk kasus khusus Anda, format Maildir ++ memperluas Maildir untuk menambahkan dukungan untuk kuota kotak surat dan melakukannya dengan memperbarui secara atomis file dengan nama tetap di dalam kotak surat (sehingga mungkin lebih dekat ke B. Anda) Saya pikir Maildir ++ mencoba untuk menambahkan, yang tidak benar-benar aman di NFS, tetapi ada pendekatan perhitungan kembali yang menggunakan prosedur yang mirip dengan ini dan itu valid sebagai pengganti atom.

Semoga semua petunjuk ini bermanfaat!


2

Anda dapat menulis program untuk ini.

Gunakan open(O_CREAT|O_RDWD)untuk membuka file target, baca semua byte dan metadata untuk memeriksa apakah file target sudah lengkap, jika tidak, ada dua kemungkinan,

  1. Menulis tidak lengkap

  2. Proses lain menjalankan program yang sama.

Cobalah untuk mendapatkan kunci deskripsi file terbuka pada file target.

Kegagalan berarti ada proses bersamaan, proses saat ini harus ada.

Sukses berarti penulisan terakhir macet, Anda harus memulai kembali atau mencoba memperbaikinya dengan menulis ke file.

Juga perhatikan bahwa Anda akan lebih baik fsync()setelah menulis ke file target sebelum Anda menutup file dan melepaskan kunci, atau proses lain mungkin membaca data yang belum-pada-disk.

https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html

Ini penting untuk membantu Anda membedakan antara program yang berjalan bersamaan dan operasi yang macet terakhir.


Terima kasih atas informasinya, saya tertarik untuk mengimplementasikan ini sendiri dan akan mencobanya. Saya terkejut itu belum ada sebagai bagian dari beberapa paket coreutils / serupa!
Evan Benn

Pendekatan ini tidak dapat memenuhi file B parsial atau rusak yang tersisa pada persyaratan crash . Cara terbaik adalah menggunakan pendekatan standar untuk menyalin file ke nama sementara, kemudian memindahkannya ke tempatnya: langkah itu bisa berupa atom, yang tidak bisa disalin.
reinierpost

@reinierpost Jika macet, tetapi data tidak sepenuhnya disalin, sebagian data yang disalin akan dibiarkan apa pun yang terjadi. Tetapi pendekatan saya akan mendeteksi ini dan memperbaikinya. Memindahkan file tidak dapat berupa atom, data apa pun yang ditulis ke sektor fisik lintas disk tidak akan berupa atom, tetapi perangkat lunak (mis. Driver sistem file OS, pendekatan ini) dapat memperbaikinya (jika rw) atau melaporkan keadaan yang konsisten (jika ro) , seperti yang disebutkan di bagian komentar dari pertanyaan. Juga pertanyaannya adalah tentang menyalin, tidak bergerak.
炸鱼 薯条 德里克

Saya juga melihat O_TMPFILE, yang mungkin akan membantu. (dan jika tidak tersedia di FS, harus menyebabkan kesalahan)
Evan Benn

@ Evan apakah Anda sudah membaca dokumen atau pernahkah Anda memikirkan mengapa O_TMPFILE akan bergantung pada dukungan sistem file?
炸鱼 薯条 德里克

0

Anda akan mendapatkan hasil yang benar dengan melakukan cpbersama mv. Ini akan menggantikan "B" dengan salinan baru "A", atau meninggalkan "B" seperti sebelumnya.

cp A B.tmp && mv B.tmp B

pembaruan untuk mengakomodasi yang ada B:

cp A B.tmp && if [ ! -e B ]; then mv B.tmp B; else rm B.tmp; fi

Ini bukan atom 100%, tetapi mendekati. Ada kondisi balapan di mana dua hal ini berjalan, keduanya masuk iftes pada saat yang sama, keduanya melihat bahwa Btidak ada, kemudian keduanya menjalankan mv.


mv B.tmp B akan menimpa B.cp yang sudah ada B. B.tmp akan menimpa B.tmp yang sudah ada sebelumnya, keduanya gagal.
Evan Benn

mv B.tmp Btidak akan berjalan kecuali cp A B.tmpberjalan pertama dan mengembalikan kode hasil yang sukses. bagaimana itu gagal? juga, saya setuju bahwa cp A B.tmpakan menimpa yang sudah ada B.tmpyang ingin Anda lakukan. The &&jaminan bahwa 2 perintah akan berjalan jika dan hanya jika yang pertama selesai normal.
kaan

Dalam pertanyaan sukses didefinisikan sebagai tidak menimpa file yang sudah ada B. Menggunakan B.tmp adalah salah satu mekanisme, tetapi juga tidak boleh menimpa file yang sudah ada sebelumnya.
Evan Benn

Saya memperbarui jawaban saya. Pada akhirnya jika Anda membutuhkan atomisitas 100% sepenuhnya ketika file mungkin ada atau tidak ada, dan beberapa utas, Anda memerlukan satu kunci eksklusif di suatu tempat (buat file khusus, atau gunakan database, atau ...) yang diikuti semua orang sebagai bagian dari proses salin / pindahkan.
kaan

Pembaruan ini masih menimpa B.tmp, dan memiliki kondisi balapan antara tes dan mv. Ya intinya adalah melakukan hal-hal yang benar tidak kira-kira mungkin cukup baik semoga. Jawaban lain menunjukkan mengapa kunci dan basis data tidak diperlukan.
Evan Benn
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.