Как скопировать файл транзакционно?

9

Я хочу скопировать файл из A в B, который может быть на разных файловых системах.

Есть несколько дополнительных требований:

  1. Копия - это все или ничего, частичный или поврежденный файл B не остается на месте при сбое;
  2. Не перезаписывать существующий файл B;
  3. Не соревнуйтесь с одновременным выполнением одной и той же команды, максимум можно добиться успеха.

Я думаю, что это близко

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

Но 3. нарушается сбоем cp, если B.part существует (даже с флагом -n). Впоследствии 1. может произойти сбой, если другой процесс «выиграет» cp и файл, связанный на место, будет неполным. B.part также может быть несвязанным файлом, но я счастлив потерпеть неудачу, не пытаясь использовать другие скрытые имена в этом случае.

Я думаю, что bash noclobber помогает, это работает полностью? Есть ли способ получить без требования версии bash?

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

Следите, я знаю, что некоторые файловые системы в любом случае не сработают (NFS). Есть ли способ обнаружить такие файловые системы?

Некоторые другие связанные, но не совсем те же вопросы:

Приближается атомарный ход по файловым системам?

Mv atomic на моем фс?

Есть ли способ, чтобы атомарно переместить файл и каталог из tempfs в раздел ext4 на eMMC

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

Эван Бенн
источник
2
Вы обеспокоены только одновременным выполнением одной и той же команды (т. Е. Может ли быть достаточно блокировки в вашем инструменте) или другим внешним вмешательством в файлы?
Майкл Гомер
3
«Транзакционный» может быть лучше
Muru
1
@MichaelHomer в инструменте достаточно хорош, я думаю, что снаружи будет очень сложно! Если это возможно с блокировками файлов, хотя ...
Эван Бенн
1
@marcelm mvперезапишет существующий файл B. mv -nне будет уведомлять, что это не удалось. ln(1)( rename(2)) потерпит неудачу, если B уже существует.
Эван Бенн
1
@EvanBenn Хороший вопрос! Я должен был прочитать ваши требования лучше. (Мне, как правило, нужны атомарные обновления существующей цели, и я отвечал с учетом этого)
marcelm

Ответы:

11

rsyncделает эту работу Временный файл O_EXCLсоздается по умолчанию (отключается только при использовании --inplace), а затем renamedповерх целевого файла. Используйте, --ignore-existingчтобы не перезаписывать B, если он существует.

На практике у меня никогда не возникало проблем с этим на ext4, zfs или даже на NFS.

Герман
источник
rsync, вероятно, делает это хорошо, но чрезвычайно сложная справочная страница меня пугает. варианты, подразумевающие другие варианты, несовместимые друг с другом и т. д.
Эван Бенн
Rsync не помогает с требованием № 3, насколько я могу судить. Тем не менее, это фантастический инструмент, и вы не должны уклоняться от небольшого чтения man-страниц. Вы также можете попробовать либо github.com/tldr-pages/tldr/blob/master/pages/common/rsync.md, либо cheat.sh/rsync . (tldr и cheat - это два разных проекта, которые направлены на решение указанной вами проблемы, а именно: "man-страница - это TL; DR"; поддерживается множество общих команд, и вы увидите наиболее распространенные
примеры
@EvanBenn rsync - удивительный инструмент, который стоит изучить! Это man-страница сложна, потому что она настолько универсальна. Не
Джош
@sitaram, # 3 можно разрешить с помощью pid-файла. Небольшой сценарий, как в ответе здесь .
Роберт Ридл
2
Это лучший ответ. Rsync является отраслевым стандартом для атомарной передачи файлов, и в различных конфигурациях может удовлетворить все ваши требования.
wKavey
4

Не волнуйтесь, noclobberэто стандартная функция .

ilkkachu
источник
Спасибо, соблазнился принять этот краткий ответ. Любой комментарий к хитрым файловым системам, таким как NFS?
Эван Бенн
@EvanBenn, я хотел добавить, что я не уверен, что NFS будет мешать тебе каким-то образом, но я забыл.
ilkkachu
4

Вы спрашивали о NFS. Этот вид кода, скорее всего, сломается в NFS, поскольку проверка noclobberвключает две отдельные операции NFS (проверка, существует ли файл, создание нового файла) и два процесса из двух отдельных клиентов NFS могут попасть в состояние состязания, когда оба они преуспеют ( оба проверяют, что B.partеще не существует, затем оба продолжают успешно создавать его, в результате они перезаписывают друг друга.)

На самом деле не нужно делать общую проверку того, будет ли файловая система, в которую вы пишете, поддерживать что-то наподобие noclobberатомарной или нет. Вы можете проверить тип файловой системы, будь то NFS, но это будет эвристическим и не обязательно гарантией. Файловые системы, такие как SMB / CIFS (Samba), могут страдать от тех же проблем. Файловые системы, предоставляемые через FUSE, могут или не могут вести себя правильно, но это в основном зависит от реализации.


Возможно, лучший подход состоит в том, чтобы избежать столкновения на этом B.partэтапе, используя уникальное имя файла (благодаря сотрудничеству с другими агентами), чтобы вам не нужно было зависеть noclobber. Например, вы можете включить, как часть имени файла, ваше имя хоста, PID и временную метку (+ возможно, случайное число). Поскольку должен быть один процесс, выполняющийся под определенным PID на хосте в любой момент времени, это должно гарантия уникальности.

Так что либо одно из:

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.

Или:

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"

Таким образом, если у вас есть условие состязания между двумя агентами, они оба продолжат выполнение операции, но последняя операция будет атомарной, поэтому либо B существует с полной копией A, либо B не существует.

Вы можете уменьшить размер гонки, проверив еще раз после копирования и перед операцией mvили ln, но там все еще есть небольшое состояние гонки. Но, независимо от состояния гонки, содержимое B должно быть согласованным, если предположить, что оба процесса пытаются создать его из A (или копии из действительного файла в качестве источника).

Обратите внимание, что в первой ситуации mv, когда существует гонка, выигрывает последний процесс, поскольку rename (2) атомно заменит существующий файл:

Если newpath уже существует, он будет атомарно заменен, так что не будет точки, в которой другой процесс, пытающийся получить доступ к newpath , обнаружит, что он отсутствует. [...]

Если newpath существует, но операция по какой-то причине завершается неудачно, rename()гарантирует сохранение экземпляра newpath на месте.

Таким образом, вполне возможно, что процессы, потребляющие B в то время, могут видеть разные его версии (разные inode) во время этого процесса. Если авторы всего лишь пытаются скопировать одно и то же содержимое, а читатели просто используют содержимое файла, это может быть хорошо, если они получат разные inode для файлов с одинаковым содержимым, они будут счастливы одинаково.

Второй подход с использованием жесткой ссылки выглядит лучше, но я вспоминаю, как проводил эксперименты с жесткими ссылками в узком цикле на NFS от многих одновременных клиентов и подсчитывал успех, и все же, похоже, были некоторые условия гонки, когда казалось, что два клиента выпустили жесткую ссылку операции в одно и то же время с одним и тем же пунктом назначения, похоже, были успешными. (Возможно, это поведение было связано с конкретной реализацией NFS-сервера, YMMV.) В любом случае, это, вероятно, тот же тип состояния гонки, когда вы можете получить два отдельных inode для одного и того же файла в случаях, когда есть большие параллелизм между авторами, чтобы вызвать эти условия гонки. Если ваши авторы последовательны (оба копируют от A до B), и ваши читатели потребляют только содержимое, этого может быть достаточно.

Наконец, вы упомянули блокировку. К сожалению, строго отсутствует блокировка, по крайней мере, в NFSv3 (не уверен насчет NFSv4, но я бы поспорил, что это тоже не хорошо.) Если вы рассматриваете блокировку, вы должны изучить различные протоколы для распределенной блокировки, возможно, вне полосы с фактические копии файлов, но это разрушительно, сложно и подвержено таким проблемам, как взаимоблокировки, поэтому я бы сказал, что лучше избегать.


Для получения дополнительной информации об атомарности в NFS вы можете прочитать в формате почтового ящика Maildir , который был создан, чтобы избежать блокировок и надежно работать даже в NFS. Он делает это, сохраняя уникальные имена файлов везде (так что вы даже не получите окончательный B в конце).

Возможно, несколько более интересный для вашего конкретного случая, формат Maildir ++ расширяет Maildir для добавления поддержки квоты почтового ящика и делает это путем атомарного обновления файла с фиксированным именем внутри почтового ящика (так, чтобы он был ближе к вашему B.) Я думаю, что Maildir ++ пытается добавить, что на самом деле небезопасно для NFS, но есть метод пересчета, который использует процедуру, подобную этой, и он действителен как атомарная замена.

Надеюсь, все эти указатели будут полезны!

filbranden
источник
2

Вы можете написать программу для этого.

Используйте, open(O_CREAT|O_RDWD)чтобы открыть целевой файл, прочитать все байты и метаданные, чтобы проверить, является ли целевой файл полным, если нет, есть две возможности,

  1. Неполная запись

  2. Другой процесс выполняет ту же программу.

Попробуйте установить блокировку открытого описания файла на целевой файл.

Отказ означает, что есть параллельный процесс, текущий процесс должен существовать.

Успех означает, что последняя запись завершилась сбоем, вы должны начать сначала или попытаться исправить это, записав в файл.

Также обратите внимание, что вам лучше fsync()после записи в целевой файл, прежде чем закрыть файл и снять блокировку, иначе другой процесс может прочитать данные, которых еще нет на диске.

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

Это важно, чтобы помочь вам отличить одновременно работающую программу от последней сбойной операции.

炸鱼 薯条 德里克
источник
Спасибо за информацию, я заинтересован в том, чтобы реализовать это сам и попробую. Я удивлен, что его уже нет в составе пакета coreutils / аналогичного!
Эван Бенн
Этот подход не может удовлетворить частичный или поврежденный файл B, оставленный на месте по требованию сбоя . На самом деле лучше всего использовать стандартный подход: скопировать файл во временное имя, а затем переместить его на место: перемещение может быть атомарным, а копирование - нет.
фоторепортаж
@reinierpost Если происходит сбой, но данные копируются не полностью, частично скопированные данные останутся, несмотря ни на что. Но мой подход обнаружит это и исправит. Перемещение файла не может быть атомарным, любые данные, записанные на межфизический диск, не будут атомарными, но программное обеспечение (например, драйвер файловой системы ОС, этот подход) может исправить это (если rw) или сообщить о согласованном состоянии (если ro) , как уже упоминалось в разделе комментариев вопроса. Также вопрос о копировании, а не о перемещении.
德里克 薯条 德里克
Я также видел O_TMPFILE, который, вероятно, поможет. (и если не доступно на ФС, должно вызвать ошибку)
Эван Бенн
@ Эван, вы читали документ или задумывались, почему O_TMPFILE полагается на поддержку файловой системы?
德里克 薯条 德里克
0

Вы получите правильный результат, сделав cpвместе с mv. Это либо заменит «B» новой копией «A», либо оставит «B», как это было раньше.

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

Обновление для размещения существующих B:

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

Это не на 100% атомно, но это близко. Есть условие гонки, при котором две из этих вещей выполняются, оба входят в ifтест одновременно, оба видят, что Bне существует, затем оба выполняют mv.

Каан
источник
mv B.tmp B перезапишет ранее существовавший B. cp A B.tmp перезапишет ранее существовавший B.tmp, оба сбоя.
Эван Бенн
mv B.tmp Bне запустится, пока не запустится cp A B.tmpи не вернет код результата успеха. как это провал? Кроме того, я согласен, cp A B.tmpчто перезаписать существующий, B.tmpчто вы хотите сделать. Эти &&гарантии , что вторая команда будет работать , если и только если первый один завершается нормально.
Каан
В вопросе успех определяется как не перезапись уже существующего файла B. Использование B.tmp - это один из механизмов, но он также не должен перезаписывать любой существующий файл.
Эван Бенн
Я обновил свой ответ. В конечном счете, если вам нужна полностью 100% атомарность, когда файлы могут существовать или не существовать, и несколько потоков, вам нужна где-то одна эксклюзивная блокировка (создание специального файла, или использование базы данных, или ...), за которой все следуют как часть процесс копирования / перемещения.
Каан
Это обновление все еще перезаписывает B.tmp и имеет состояние гонки между тестом и mv. Да, дело в том, чтобы делать вещи правильно, а не, может быть, достаточно хорошо, надеюсь. Другие ответы показывают, почему блокировки и базы данных не нужны.
Эван Бенн