¿Cómo copiar un archivo transaccionalmente?

9

Quiero copiar un archivo de A a B, que puede estar en diferentes sistemas de archivos.

Hay algunos requisitos adicionales:

  1. La copia es todo o nada, no queda ningún archivo parcial o corrupto B en caso de bloqueo;
  2. No sobrescriba un archivo B existente;
  3. No compita con una ejecución concurrente del mismo comando, a lo sumo uno puede tener éxito.

Creo que esto se acerca:

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

Pero 3. es violado porque el cp no falla si existe B.part (incluso con el indicador -n). Posteriormente 1. podría fallar si el otro proceso 'gana' el cp y el archivo vinculado en su lugar está incompleto. B.part también podría ser un archivo no relacionado, pero estoy feliz de fallar sin probar otros nombres ocultos en ese caso.

Creo que bash noclobber ayuda, ¿funciona esto completamente? ¿Hay alguna manera de obtener sin el requisito de la versión bash?

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

Seguimiento, sé que algunos sistemas de archivos fallarán en esto de todos modos (NFS). ¿Hay alguna manera de detectar tales sistemas de archivos?

Algunas otras preguntas relacionadas pero no exactamente las mismas:

¿Movimiento atómico aproximado a través de los sistemas de archivos?

¿Es mv atomic en mi fs?

¿hay alguna manera de mover atómicamente el archivo y el directorio de tempfs a la partición ext4 en eMMC

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

Evan Benn
fuente
2
¿Solo le preocupa la ejecución simultánea del mismo comando (es decir, podría ser suficiente bloquear dentro de su herramienta), o también otras interferencias externas con los archivos?
Michael Homer
3
"Transaccional" podría ser mejor
muru
1
@MichaelHomer dentro de la herramienta es lo suficientemente bueno, ¡creo que afuera haría las cosas muy difíciles! Si es posible con bloqueos de archivos, aunque ...
Evan Benn
1
@marcelm mvsobrescribirá un archivo existente B. mv -nno notificará que ha fallado. ln(1)( rename(2)) fallará si B ya existe.
Evan Benn
1
@EvanBenn Buen punto! Debería haber leído mejor tus requisitos. (Que tienden a necesitar actualizaciones atómicas de un objetivo existente, y yo estaba respondiendo con esto en mente)
marcelm

Respuestas:

11

rsynchace este trabajo Se O_EXCLcrea un archivo temporal de forma predeterminada (solo se deshabilita si lo usa --inplace) y luego renamedsobre el archivo de destino. Use --ignore-existingpara no sobrescribir B si existe.

En la práctica, nunca tuve ningún problema con esto en ext4, zfs o incluso montajes NFS.

Hermann
fuente
rsync probablemente lo hace muy bien, pero la página man extremadamente complicada me asusta. opciones que implican otras opciones, son incompatibles entre sí, etc.
Evan Benn
Rsync no ayuda con el requisito # 3, por lo que puedo decir. Aún así, es una herramienta fantástica, y no debe rehuir un poco de lectura de páginas de manual. También puedes probar github.com/tldr-pages/tldr/blob/master/pages/common/rsync.md o cheat.sh/rsync . (tldr y cheat son dos proyectos diferentes que tienen como objetivo ayudar con el problema que usted planteó, a saber, "la página de manual es TL; DR"; se admiten muchos comandos comunes y verá los usos más comunes mostrados.
sitaram
¡@EvanBenn rsync es una herramienta increíble y vale la pena aprender! Su página de manual es complicada porque es muy versátil. No se deje intimidar :)
Josh
@sitaram, # 3 podría resolverse con un archivo pid. Un pequeño guión como en la respuesta aquí .
Robert Riedl
2
Esta es la mejor respuesta. Rsync es el estándar de la industria para transferencias de archivos atómicos, y en varias configuraciones puede satisfacer todos sus requisitos.
wKavey
4

No te preocupes, noclobberes una característica estándar .

ilkkachu
fuente
Gracias, tentado a aceptar esta respuesta sucinta. ¿Algún comentario sobre sistemas de archivos poco fiables como NFS?
Evan Benn
@EvanBenn, quise agregar que no estoy seguro de si NFS te va a estropear de alguna manera, pero lo olvidé.
ilkkachu
4

Preguntaste sobre NFS. Es probable que este tipo de código se rompa bajo NFS, ya que la verificación noclobberimplica dos operaciones NFS separadas (verificar si existe un archivo, crear un nuevo archivo) y dos procesos de dos clientes NFS separados pueden entrar en una condición de carrera donde ambos tienen éxito ( ambos verifican que B.parttodavía no existe, luego ambos proceden a crearlo con éxito, como resultado se sobrescriben entre sí).

Realmente no hay que hacer una verificación genérica para saber si el sistema de archivos en el que está escribiendo admitirá algo como noclobberatómicamente o no. Puede verificar el tipo de sistema de archivos, ya sea NFS, pero eso sería heurístico y no necesariamente una garantía. Es probable que los sistemas de archivos como SMB / CIFS (Samba) sufran los mismos problemas. Los sistemas de archivos expuestos a través de FUSE pueden o no comportarse correctamente, pero eso depende principalmente de la implementación.


Un enfoque posiblemente mejor es evitar la colisión en el B.partpaso, utilizando un nombre de archivo único (a través de la cooperación con otros agentes) para que no necesite depender de él noclobber. Por ejemplo, podría incluir, como parte del nombre de archivo, su nombre de host, PID y una marca de tiempo (+ posiblemente un número aleatorio). Dado que debería haber un solo proceso ejecutándose bajo un PID específico en un host en un momento dado, esto debería Garantizar la unicidad.

Entonces cualquiera de:

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.

O:

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"

Entonces, si tiene una condición de carrera entre dos agentes, ambos procederán con la operación, pero la última operación será atómica, por lo que B existe con una copia completa de A o B no existe.

Puede reducir el tamaño de la carrera por el control de nuevo después de la copia y antes de que el mvo la lnoperación, pero todavía hay una condición de carrera pequeña allí. Pero, independientemente de la condición de carrera, el contenido de B debe ser coherente, suponiendo que ambos procesos estén tratando de crearlo desde A (o una copia de un archivo válido como origen).

Tenga en cuenta que en la primera situación con mv, cuando existe una carrera, el último proceso es el que gana, ya que renombrar (2) reemplazará atómicamente un archivo existente:

Si newpath ya existe, será reemplazado atómicamente, de modo que no haya ningún punto en el que otro proceso que intente acceder a newpath lo encuentre perdido. [...]

Si newpath existe pero la operación falla por alguna razón, rename()garantiza dejar una instancia de newpath en su lugar.

Por lo tanto, es muy posible que los procesos que consumen B en ese momento puedan ver diferentes versiones de él (diferentes inodos) durante este proceso. Si los escritores simplemente intentan copiar el mismo contenido, y los lectores simplemente consumen el contenido del archivo, eso podría estar bien, si obtienen diferentes inodos para archivos con el mismo contenido, estarán contentos de todos modos.

El segundo enfoque que usa un enlace duro se ve mejor, pero recuerdo haber hecho experimentos con enlaces duros en un circuito cerrado en NFS de muchos clientes concurrentes y contando el éxito y todavía parecía haber algunas condiciones de carrera allí, donde parecía que dos clientes emitían un enlace duro operación al mismo tiempo, con el mismo destino, ambos parecían tener éxito. (Es posible que este comportamiento esté relacionado con la implementación particular del servidor NFS, YMMV). En cualquier caso, es probable que sea el mismo tipo de condición de carrera, donde podría terminar obteniendo dos inodos separados para el mismo archivo en los casos en que haya mucha carga. concurrencia entre escritores para desencadenar estas condiciones de carrera. Si sus escritores son consistentes (ambos copian A a B), y sus lectores solo consumen el contenido, eso podría ser suficiente.

Finalmente, mencionaste el bloqueo. Desafortunadamente, el bloqueo es muy deficiente, al menos en NFSv3 (no estoy seguro acerca de NFSv4, pero apuesto a que tampoco es bueno). Si está considerando bloquear, debería buscar diferentes protocolos para el bloqueo distribuido, posiblemente fuera de banda con el copias de archivos reales, pero eso es a la vez perjudicial, complejo y propenso a problemas como puntos muertos, por lo que diría que es mejor evitarlo.


Para obtener más información sobre el tema de la atomicidad en NFS, es posible que desee leer en el formato de buzón Maildir , que fue creado para evitar bloqueos y funcionar de manera confiable incluso en NFS. Lo hace manteniendo nombres de archivo únicos en todas partes (para que ni siquiera obtenga una B final al final).

Quizás algo más interesante para su caso particular, el formato Maildir ++ extiende Maildir para agregar soporte para la cuota del buzón y lo hace actualizando atómicamente un archivo con un nombre fijo dentro del buzón (para que pueda estar más cerca de su B). Creo que Maildir ++ intenta para agregar, que no es realmente seguro en NFS, pero hay un enfoque de recálculo que utiliza un procedimiento similar a este y es válido como un reemplazo atómico.

¡Esperemos que todos estos consejos sean útiles!

filbranden
fuente
2

Puedes escribir un programa para esto.

Use open(O_CREAT|O_RDWD)para abrir el archivo de destino, lea todos los bytes y metadatos para verificar si el archivo de destino es completo, de lo contrario, hay dos posibilidades,

  1. Escritura incompleta

  2. Otro proceso está ejecutando el mismo programa.

Intente adquirir un bloqueo de descripción de archivo abierto en el archivo de destino.

Fallo significa que hay un proceso concurrente, el proceso actual debería existir.

El éxito significa que la última escritura se bloqueó, debe comenzar de nuevo o intentar solucionarlo escribiendo en el archivo.

También tenga en cuenta que será mejor fsync()después de escribir en el archivo de destino antes de cerrar el archivo y liberar el bloqueo, u otro proceso podría leer datos que aún no están en el disco.

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

Esto es importante para ayudarlo a distinguir entre un programa que se ejecuta simultáneamente y una operación bloqueada por último.

炸鱼 薯条 德里克
fuente
Gracias por la información, estoy interesado en implementar esto yo mismo y lo intentaré. ¡Me sorprende que aún no exista como parte de algunos paquetes básicos / similares!
Evan Benn
Este enfoque no puede cumplir con el archivo no parcial o corrupto B dejado en su lugar en el requisito de bloqueo . Realmente es mejor usar el enfoque estándar de copiar el archivo a un nombre temporal, luego moverlo a su lugar: el movimiento puede ser atómico, lo que no puede ser la copia.
reinierpost
@reinierpost Si falla, pero los datos no se copian completamente, los datos parcialmente copiados se dejarán sin importar qué. Pero mi enfoque detectará esto y lo solucionará. Mover un archivo no puede ser atómico, los datos escritos en el disco cruzan el sector físico no serán atómicos, pero el software (por ejemplo, el controlador del sistema de archivos del sistema operativo, este enfoque) puede arreglarlo (si es rw) o informar un estado consistente (si es ro) , como se menciona en la sección de comentarios de la pregunta. También la pregunta es sobre copiar, no mover.
炸鱼 薯条 德里克
También vi O_TMPFILE, que probablemente ayudaría. (y si no está disponible en el FS, debería causar un error)
Evan Benn
@Evan, ¿has leído el documento o has pensado alguna vez por qué O_TMPFILE dependería del soporte del sistema de archivos?
炸鱼 薯条 德里克
0

Obtendrá el resultado correcto haciendo un cpjunto con mv. Esto reemplazará "B" con una copia nueva de "A" o dejará "B" como estaba antes.

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

actualizar para acomodar existente B:

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

Esto no es 100% atómico, pero se acerca. Hay una condición de carrera en la que se ejecutan dos de estas cosas, ambas ingresan a la ifprueba al mismo tiempo, ambas ven que Bno existe, y ambas ejecutan la mv.

kaan
fuente
mv B.tmp B sobrescribirá una B. prep existente B. cp A B.tmp sobrescribirá una B.tmp preexistente, ambas fallas.
Evan Benn
mv B.tmp Bno se ejecutará a menos que se ejecute cp A B.tmpprimero y devuelva un código de resultado correcto. ¿Cómo es eso un fracaso? Además, estoy de acuerdo en que cp A B.tmpsobrescribiría un existente B.tmpque es lo que desea hacer. Las &&garantías que el segundo comando se ejecutará si y sólo si el primero se completa con normalidad.
kaan
En la pregunta, el éxito se define como no sobrescribir el archivo B. preexistente. El uso de B.tmp es un mecanismo, pero tampoco debe sobrescribir ningún archivo preexistente.
Evan Benn
Actualicé mi respuesta. En última instancia, si necesita 100% de atomicidad total cuando los archivos pueden o no existir, y múltiples hilos, necesita un bloqueo exclusivo en algún lugar (crear un archivo especial, o usar una base de datos, o ...) que todos sigan como parte del proceso de copiar / mover.
kaan
Esta actualización aún sobrescribe B.tmp y tiene una condición de carrera entre la prueba y el mv. Sí, el punto es hacer las cosas correctamente, no más o menos, tal vez lo suficientemente bueno, con suerte. Otras respuestas muestran por qué no se necesitan bloqueos y bases de datos.
Evan Benn