Como copiar um arquivo transacionalmente?

9

Quero copiar um arquivo de A para B, que pode estar em diferentes sistemas de arquivos.

Existem alguns requisitos adicionais:

  1. A cópia é tudo ou nada, nenhum arquivo B parcial ou corrompido foi deixado no local;
  2. Não substitua um arquivo B existente;
  3. Não concorra com uma execução simultânea do mesmo comando, no máximo é possível ter sucesso.

Eu acho que isso se aproxima:

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

Mas 3. é violado pelo cp e não falha se B.part existir (mesmo com o sinalizador -n). Posteriormente, 1. poderá falhar se o outro processo 'vencer' o cp e o arquivo vinculado no local estiver incompleto. B.part também pode ser um arquivo não relacionado, mas estou feliz por falhar sem tentar outros nomes ocultos nesse caso.

Acho bash noclobber ajuda, isso funciona totalmente? Existe uma maneira de obter sem o requisito da versão bash?

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

Acompanhamento, eu sei que alguns sistemas de arquivos falharão nisso de qualquer maneira (NFS). Existe uma maneira de detectar esses sistemas de arquivos?

Algumas outras questões relacionadas, mas não exatamente as mesmas:

Movimento atômico aproximado entre sistemas de arquivos?

O mv é atômico no meu fs?

existe uma maneira de mover atomicamente arquivos e diretórios do tempfs para a partição ext4 no eMMC

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

Evan Benn
fonte
2
Você está preocupado apenas com a execução simultânea do mesmo comando (ou seja, o bloqueio dentro da sua ferramenta é suficiente) ou com outras interferências externas nos arquivos?
Michael Homer
3
"Transactional" pode ser melhor
muru
1
@MichaelHomer dentro da ferramenta é bom o suficiente, acho que lá fora tornaria as coisas muito difíceis! Se a sua possível com bloqueios de arquivos embora ...
Evan Benn
1
O @marcelm mvsobrescreverá um arquivo B. mv -nnão notificará que falhou. ln(1)( rename(2)) falhará se B já existir.
Evan Benn
1
@EvanBenn Good point! Eu deveria ter lido melhor suas necessidades. (I tendem a precisar de atualizações atômicas de um destino existente, e eu estava respondendo com isso em mente)
marcelm

Respostas:

11

rsyncfaz esse trabalho. Um arquivo temporário é O_EXCLcriado por padrão (somente desativado se você usar --inplace) e depois renamedsobre o arquivo de destino. Usar--ignore-existing para não substituir B, se existir.

Na prática, nunca tive problemas com isso em montagens ext4, zfs ou mesmo NFS.

Hermann
fonte
O rsync provavelmente faz isso muito bem, mas a página de manual extremamente complicada me assusta. opções implicando outras opções, sendo incompatíveis entre si etc.
Evan Benn
O Rsync não ajuda no requisito nº 3, pelo que sei. Ainda assim, é uma ferramenta fantástica, e você não deve evitar um pouco da leitura da página de manual. Você também pode tentar github.com/tldr-pages/tldr/blob/master/pages/common/rsync.md ou cheat.sh/rsync . (tldr e cheat são dois projetos diferentes que visam ajudar com o problema que você declarou, a saber, "a página do manual é TL; DR"; muitos comandos comuns são suportados e você verá os usos mais comuns.
sitaram
@EvanBenn rsync é uma ferramenta incrível e vale a pena aprender! Sua página de manual é complicada porque é muito versátil. Não se deixe intimidar :)
Josh
@sitaram, # 3 pode ser resolvido com um arquivo pid. Um pequeno script como na resposta aqui .
Robert Riedl
2
Esta é a melhor resposta. O Rsync é o padrão do setor para transferências atômicas de arquivos e, em várias configurações, pode satisfazer todos os seus requisitos.
WKavey
4

Não se preocupe, noclobberé um recurso padrão .

ilkkachu
fonte
Obrigado, tentado a aceitar esta resposta sucinta. Algum comentário sobre sistemas de arquivos desonestos como o NFS?
Evan Benn
@EvanBenn, eu quis acrescentar que não tenho certeza se o NFS vai atrapalhar você aqui de alguma forma, mas eu esqueci.
22919 ilkkachu
4

Você perguntou sobre o NFS. É provável que esse tipo de código seja quebrado no NFS, pois a verificação noclobberenvolve duas operações NFS separadas (verifique se existe um arquivo, crie um novo arquivo) e dois processos de dois clientes NFS separados podem entrar em uma condição de corrida em que ambos são bem-sucedidos ( ambos verificam se B.partainda não existe e, em seguida, continuam a criá-lo com êxito, como resultado da substituição um do outro.)

Não há realmente uma verificação genérica para saber se o sistema de arquivos para o qual você está escrevendo suportará algo como noclobberatomicamente ou não. Você pode verificar o tipo de sistema de arquivos, seja NFS, mas isso seria uma heurística e não necessariamente uma garantia. Sistemas de arquivos como SMB / CIFS (Samba) provavelmente sofrem dos mesmos problemas. Os sistemas de arquivos expostos através do FUSE podem ou não se comportar corretamente, mas isso depende principalmente da implementação.


Uma abordagem possivelmente melhor é evitar a colisão na B.partetapa, usando um nome de arquivo exclusivo (através da cooperação com outros agentes) para que você não precise depender denoclobber . Por exemplo, você pode incluir, como parte do nome do arquivo, seu nome de host, PID e um carimbo de data / hora (+ possivelmente um número aleatório). Como deve haver um único processo em execução em um PID específico em um host a qualquer momento, isso deve garantir exclusividade.

Então, um dos seguintes:

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.

Ou:

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"

Portanto, se você tiver uma condição de corrida entre dois agentes, os dois continuarão com a operação, mas a última operação será atômica; portanto, B existe com uma cópia completa de A ou B não existe.

Você pode reduzir o tamanho da corrida verificando novamente após a cópia e antes da operação mvou ln, mas ainda há uma pequena condição de corrida lá. Mas, independentemente da condição de corrida, o conteúdo de B deve ser consistente, supondo que os dois processos estejam tentando criá-lo a partir de A (ou uma cópia de um arquivo válido como origem).

Observe que na primeira situação em que mv, quando existe uma corrida, o último processo é aquele que vence, pois renomear (2) substituirá atomicamente um arquivo existente:

Se o newpath já existir, ele será substituído atomicamente, para que não haja nenhum ponto em que outro processo que tente acessar o newpath o encontre ausente. [...]

Se o caminho novo existir, mas a operação falhar por algum motivo, rename()garante que uma instância do caminho novo esteja em vigor.

Portanto, é bem possível que processos que consomem B ao mesmo tempo possam ver versões diferentes (inodes diferentes) durante esse processo. Se todos os escritores estiverem tentando copiar o mesmo conteúdo e os leitores estiverem consumindo o conteúdo do arquivo, isso pode ser bom, se eles tiverem inodes diferentes para arquivos com o mesmo conteúdo, eles ficarão felizes da mesma forma.

A segunda abordagem, usando um link físico , parece melhor, mas eu me lembro de fazer experimentos com links físicos em um loop restrito no NFS de muitos clientes simultâneos e contar com sucesso, e ainda parecia haver algumas condições de corrida, onde parecia que dois clientes emitiam um link físico operação ao mesmo tempo, com o mesmo destino, ambos pareciam ter sucesso. (É possível que esse comportamento tenha sido relacionado à implementação específica do servidor NFS, YMMV.) De qualquer forma, esse provavelmente é o mesmo tipo de condição de corrida, em que você pode acabar recebendo dois inodes separados para o mesmo arquivo nos casos em que há muito tráfego. concorrência entre escritores para acionar essas condições de corrida. Se seus escritores são consistentes (ambos copiam de A para B) e seus leitores estão consumindo apenas o conteúdo, isso pode ser suficiente.

Finalmente, você mencionou o bloqueio. Infelizmente, o bloqueio está em falta, pelo menos no NFSv3 (não tenho certeza sobre o NFSv4, mas aposto que também não é bom.) Se você está pensando em bloquear, deve procurar em diferentes protocolos para bloqueio distribuído, possivelmente fora de banda com o cópias de arquivos reais, mas isso é perturbador, complexo e propenso a problemas como conflitos, então eu diria que é melhor evitar.


Para obter mais informações sobre atomicidade no NFS, convém ler no formato de caixa de correio Maildir , criado para evitar bloqueios e trabalhar de maneira confiável, mesmo no NFS. Isso é feito mantendo nomes de arquivos exclusivos em todos os lugares (para que você nem obtenha um B final no final).

Talvez um pouco mais interessante para o seu caso em particular, o formato Maildir ++ estende o Maildir para adicionar suporte à cota da caixa de correio e o faz atualizando atomicamente um arquivo com um nome fixo dentro da caixa de correio (para que possa estar mais próximo do seu B.) Acho que o Maildir ++ tenta anexar, o que não é realmente seguro no NFS, mas existe uma abordagem de recálculo que usa um procedimento semelhante a esse e é válido como uma substituição atômica.

Espero que todos esses indicadores sejam úteis!

filbranden
fonte
2

Você pode escrever um programa para isso.

Use open(O_CREAT|O_RDWD)para abrir o arquivo de destino, leia todos os bytes e metadados para verificar se o arquivo de destino é completo, caso contrário, existem duas possibilidades,

  1. Gravação incompleta

  2. Outro processo está executando o mesmo programa.

Tente adquirir um bloqueio de descrição de arquivo aberto no arquivo de destino.

Falha significa que há um processo simultâneo, o processo atual deve existir.

Sucesso significa que a última gravação travou; você deve recomeçar ou tentar corrigi-lo gravando no arquivo.

Observe também que é melhor fsync()depois de gravar no arquivo de destino antes de fechar o arquivo e liberar o bloqueio, ou outro processo pode ler dados que ainda não estão no disco.

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

Isso é importante para ajudá-lo a distinguir entre um programa em execução simultâneo e a operação travada por último.

炸鱼 薯条 德里克
fonte
Obrigado pela informação, estou interessado em implementar isso sozinho e vou tentar. Estou surpreso que ainda não exista como parte de um pacote coreutils / similar!
Evan Benn
Essa abordagem não pode atender ao arquivo B parcial ou corrompido deixado no local devido ao requisito de falha . É realmente melhor usar a abordagem padrão de copiar o arquivo para um nome temporário e depois movê-lo para o lugar: a movimentação pode ser atômica, que a cópia não pode ser.
Reinierpost
@reinierpost Se travar, mas os dados não forem totalmente copiados, os dados parcialmente copiados serão deixados, não importa o quê. Mas minha abordagem detectará e corrigirá isso. A movimentação de um arquivo não pode ser atômica, nenhum dado gravado no setor físico entre discos será atômico, mas o software (por exemplo, driver do sistema de arquivos do SO, essa abordagem) pode corrigi-lo (se rw) ou reportar um estado consistente (se ro) , conforme mencionado na seção de comentários da pergunta. Também a questão é copiar, não mover.
炸鱼薯条德里克
Eu também vi O_TMPFILE, o que provavelmente ajudaria. (e se não estiver disponível no FS, deve causar um erro)
Evan Benn
@Evan, você leu o documento ou já pensou por que o O_TMPFILE contaria com o suporte ao sistema de arquivos?
炸鱼薯条德里克
0

Você obterá o resultado correto fazendo um cpconjunto com mv. Isso substituirá "B" por uma nova cópia de "A" ou deixará "B" como estava anteriormente.

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

atualização para acomodar existente B:

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

Isso não é 100% atômico, mas chega perto. Há uma condição de corrida em que duas dessas coisas estão em execução, ambas entram no ifteste ao mesmo tempo, ambas veem que Bnão existe e, então, executam a mv.

Kaan
fonte
mv B.tmp B substituirá um B. pré-existente. cp Um B.tmp substituirá um B.tmp pré-existente, ambas as falhas.
Evan Benn
mv B.tmp Bnão será executado a menos que seja cp A B.tmpexecutado pela primeira vez e retorne um código de resultado bem-sucedido. como isso é um fracasso? Além disso, concordo que cp A B.tmpsubstituiria um existente B.tmpque é o que você deseja fazer. As &&garantias de que o segundo comando será executado se e somente se o primeiro for concluído normalmente.
kaan
Na questão, o sucesso é definido como não substituindo o arquivo B. preexistente. O uso de B.tmp é um mecanismo, mas também não deve sobrescrever nenhum arquivo preexistente.
Evan Benn
Eu atualizei minha resposta. Por fim, se você precisar de 100% de atomicidade quando os arquivos existirem ou não, e vários encadeamentos, será necessário um único bloqueio exclusivo em algum lugar (crie um arquivo especial ou use um banco de dados ou ...) que todos sigam como parte do copiar / mover processo.
26419 kaan
Essa atualização ainda substitui B.tmp e tem uma condição de corrida entre o teste e o MV. Sim, o ponto é fazer as coisas corretamente, não de modo geral, talvez boas o suficiente. Outras respostas mostram por que não são necessários bloqueios e bancos de dados.
Evan Benn