Gravando programas para lidar com erros de E / S, causando gravações perdidas no Linux

138

TL; DR: Se o kernel do Linux perder uma gravação de E / S em buffer , existe alguma maneira de o aplicativo descobrir?

Eu sei que você tem que fsync()o arquivo (e seu diretório pai) para maior durabilidade . A questão é se o kernel perde buffers sujos com gravação pendente devido a um erro de E / S, como o aplicativo pode detectar isso e recuperar ou anular?

Pense em aplicativos de banco de dados, etc., onde a ordem de gravação e a durabilidade da gravação podem ser cruciais.

Gravações perdidas? Quão?

Pode camada do bloco do kernel Linux em algumas circunstâncias perdem solicitações de E / S que foram submetidos com sucesso por tamponada write(), pwrite()etc, com um erro como:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0

(Veja end_buffer_write_sync(...)e end_buffer_async_write(...)entrefs/buffer.c ).

Nos kernels mais recentes, o erro conterá "escrita de página assíncrona perdida" , como:

Buffer I/O error on dev dm-0, logical block 12345, lost async page write

Como o aplicativo write()já retornou sem erro, parece não haver maneira de relatar um erro ao aplicativo.

Detectando-os?

Não estou familiarizado com as fontes do kernel, mas acho que ele define AS_EIOo buffer que falhou ao ser gravado se estiver fazendo uma gravação assíncrona:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

mas não está claro para mim se ou como o aplicativo pode descobrir isso quando mais tarde for fsync()o arquivo para confirmar que está no disco.

Parece que wait_on_page_writeback_range(...)nomm/filemap.c poder por do_sync_mapping_range(...)emfs/sync.c que se vire chamado por sys_sync_file_range(...). Retorna -EIOse um ou mais buffers não puderam ser gravados.

Se, como suponho, isso se propaga para fsync()o resultado, se o aplicativo entrar em pânico e cair fora se receber um erro de E / S fsync()e souber como refazer seu trabalho quando reiniciado, isso deve ser uma proteção suficiente?

Presumivelmente, não há como o aplicativo saber quais desvios de bytes em um arquivo correspondem às páginas perdidas, para poder reescrevê-las se souber, mas se o aplicativo repetir todo o seu trabalho pendente desde o último êxito fsync()do arquivo e que reescreve quaisquer buffers de kernel sujos correspondentes a gravações perdidas no arquivo, que devem limpar os sinalizadores de erro de E / S nas páginas perdidas e permitir que o próximo fsync()seja concluído - certo?

Existem outras circunstâncias inofensivas nas quais fsync()pode voltar -EIOonde o resgate e o refazer do trabalho seriam muito drásticos?

Por quê?

Claro que esses erros não deveriam acontecer. Nesse caso, o erro surgiu de uma interação infeliz entre os dm-multipathpadrões do driver e o código de detecção usado pela SAN para relatar falha na alocação de armazenamento thin-provisioned. Mas essa não é a única circunstância em que eles podem acontecer - eu também vi relatórios sobre LVM thin provisionado, por exemplo, conforme usado por libvirt, Docker e muito mais. Um aplicativo crítico como um banco de dados deve tentar lidar com esses erros, em vez de continuar cegamente como se tudo estivesse bem.

Se o kernel achar que não há problema em perder gravações sem morrer de pânico, os aplicativos precisam encontrar uma maneira de lidar com isso.

O impacto prático é que eu encontrei um caso em que um problema de caminhos múltiplos com uma SAN causava gravações perdidas que acabavam causando corrupção no banco de dados porque o DBMS não sabia que suas gravações haviam falhado. Não tem graça.

Craig Ringer
fonte
1
Receio que isso precise de campos adicionais no SystemFileTable para armazenar e lembrar essas condições de erro. E a possibilidade de o processo do espaço do usuário receber ou inspecioná-los nas chamadas subseqüentes. (fazer fsync () e close () retornar esse tipo de histórico de informações?)
joop
@joop Obrigado. Acabei de publicar uma resposta com o que acho que está acontecendo, lembre-se de fazer uma verificação de sanidade, já que você parece saber mais sobre o que está acontecendo do que as pessoas que publicaram variantes óbvias de "write () precisam close () ou fsync ( ) por durabilidade "sem ler a pergunta?
Craig Ringer
BTW: Eu acho que você realmente deveria se aprofundar nas fontes do kernel. Os sistemas de arquivos revistos provavelmente sofreriam do mesmo tipo de problemas. Sem mencionar o tratamento da partição de troca. Como eles vivem no espaço do kernel, o tratamento dessas condições provavelmente será um pouco mais rígido. O writev (), que é visível no espaço do usuário, também parece ser um lugar para procurar. [At Craig: sim becaus eu sei o seu nome, e eu sei que você não é um idiota completo; -]
joop
1
Eu concordo, eu não era tão justo. Infelizmente, sua resposta não é muito satisfatória, quero dizer, não há solução fácil (surpreendente?).
Jean-Baptiste Yunès
1
@ Jean-BaptisteYunès True. Para o DBMS com o qual estou trabalhando, "travar e inserir refazer" é aceitável. Para a maioria dos aplicativos, isso não é uma opção e eles podem ter que tolerar o desempenho horrível da E / S síncrona ou simplesmente aceitar um comportamento mal definido e corrupção nos erros de E / S.
Craig Ringer

Respostas:

91

fsync()retorna -EIOse o kernel perdeu uma gravação

(Nota: a parte inicial faz referência a kernels antigos; atualizado abaixo para refletir os kernels modernos)

Parece que a gravação do buffer assíncrono em end_buffer_async_write(...)falhas define um -EIOsinalizador na página do buffer sujo com falha para o arquivo :

set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);

que é então detectado por wait_on_page_writeback_range(...)enquanto chamado por do_sync_mapping_range(...)enquanto chamado por sys_sync_file_range(...)enquanto chamado por sys_sync_file_range2(...)implementar a chamada biblioteca C fsync().

Mas apenas uma vez!

Este comentário em sys_sync_file_range

168  * SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any
169  * I/O errors or ENOSPC conditions and will return those to the caller, after
170  * clearing the EIO and ENOSPC flags in the address_space.

sugere que, quando fsync()retornar -EIOou (não documentado na página de manual) -ENOSPC, limpará o estado do erro, para que um subsequente fsync()relate o êxito, mesmo que as páginas nunca tenham sido gravadas.

Com certeza, wait_on_page_writeback_range(...) limpa os bits de erro ao testá-los :

301         /* Check for outstanding write errors */
302         if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
303                 ret = -ENOSPC;
304         if (test_and_clear_bit(AS_EIO, &mapping->flags))
305                 ret = -EIO;

Portanto, se o aplicativo espera que ele possa tentar novamente fsync()até que seja bem-sucedido e confie que os dados estão em disco, isso estará muito errado.

Tenho certeza de que essa é a fonte da corrupção de dados que encontrei no DBMS. Ele tenta novamente fsync()e acha que tudo ficará bem quando for bem-sucedido.

Isso é permitido?

Os documentos POSIX / SuSfsync() realmente não especificam isso de qualquer maneira:

Se a função fsync () falhar, não é garantido que operações pendentes de E / S tenham sido concluídas.

A página de manual do Linuxfsync() simplesmente não diz nada sobre o que acontece em caso de falha.

Portanto, parece que o significado de fsync()erros é "não sei o que aconteceu com suas gravações, pode ter funcionado ou não, é melhor tentar novamente para ter certeza".

Kernels mais recentes

Em 4.9 end_buffer_async_writeconjuntos -EIOna página, apenas via mapping_set_error.

    buffer_io_error(bh, ", lost async page write");
    mapping_set_error(page->mapping, -EIO);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

No lado da sincronização, acho que é semelhante, embora a estrutura agora seja bastante complexa de seguir. filemap_check_errorsno mm/filemap.cagora faz:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;

que tem o mesmo efeito. Parece que todas as verificações de erro passam pelo filemap_check_errorsteste e limpeza:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;

Estou usando btrfsno meu laptop, mas quando crio um ext4loopback para teste /mnt/tmpe configuro um probe perf nele:

sudo dd if=/dev/zero of=/tmp/ext bs=1M count=100
sudo mke2fs -j -T ext4 /tmp/ext
sudo mount -o loop /tmp/ext /mnt/tmp

sudo perf probe filemap_check_errors

sudo perf record -g -e probe:end_buffer_async_write -e probe:filemap_check_errors dd if=/dev/zero of=/mnt/tmp/test bs=4k count=1 conv=fsync

Eu encontro a seguinte pilha de chamadas em perf report -T:

        ---__GI___libc_fsync
           entry_SYSCALL_64_fastpath
           sys_fsync
           do_fsync
           vfs_fsync_range
           ext4_sync_file
           filemap_write_and_wait_range
           filemap_check_errors

Uma leitura sugere que sim, os kernels modernos se comportam da mesma maneira.

Isto parece significar que, se fsync()(ou, presumivelmente, write()ou close()retornos) -EIO, o arquivo está em algum estado indefinido entre quando você último com êxito fsync()D ou close()D, e seu mais recentemente write()estado dez.

Teste

Implementei um caso de teste para demonstrar esse comportamento .

Implicações

Um DBMS pode lidar com isso inserindo a recuperação de falhas. Como diabos um aplicativo de usuário normal deve lidar com isso? A fsync()página de manual não dá aviso de que significa "fsync-se-você-sente-o-it" e espero que muitos aplicativos não lidem bem com esse comportamento.

Relatório de erros

Leitura adicional

O lwn.net abordou isso no artigo "Manipulação de erro na camada de bloco aprimorada" .

thread da lista de discussão postgresql.org .

Craig Ringer
fonte
3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 é uma corrida possível, porque aguarda {E / S pendente e programada}, não por {E / S ainda não agendada}. Obviamente, isso evita viagens extras de ida e volta ao dispositivo. (Presumo escreve usuário () não retornar até I / O está prevista, por mmap (), isso é diferente)
joop
3
É possível que a chamada de algum outro processo para fsync para outro arquivo no mesmo disco receba o retorno de erro?
Random832
3
@ Random832 Muito relevante para um banco de dados de multi-processamento como o PostgreSQL, tão boa pergunta. Parece provável, mas não conheço o código do kernel o suficiente para entender. É melhor que seus procs estejam cooperando se ambos tiverem o mesmo arquivo aberto de qualquer maneira.
Craig Ringer
1
@DavidFoerster: os syscalls retornam falhas usando códigos de erro negativos; errnoé completamente uma construção da biblioteca C do espaço do usuário. É comum ignorar as diferenças de valor de retorno entre os syscalls e a biblioteca C dessa maneira (como Craig Ringer faz acima), pois o valor de retorno do erro identifica de forma confiável qual deles (syscall ou função da biblioteca C) está sendo referido: " -1with errno==EIO"refere-se a uma função de biblioteca C, enquanto" -EIO"refere-se a um syscall. Finalmente, as páginas de manual do Linux online são a referência mais atualizada para as páginas de manual do Linux.
Animal Nominal
2
@CraigRinger: Para responder à sua pergunta final: "Usando E / S de baixo nível e fsync()/ fdatasync()quando o tamanho da transação é um arquivo completo; Usando mmap()/ msync()quando o tamanho da transação é um registro alinhado por página; e Usando I de baixo nível / O, fdatasync()e vários descritores de arquivo simultâneos (um descritor e um encadeamento por transação) para o mesmo arquivo " . Os bloqueios de descrição de arquivo aberto específicos do Linux ( fcntl(), F_OFD_) são muito úteis com o último.
Animal Nominal
22

Como o write () do aplicativo já retornou sem erro, parece não haver maneira de relatar um erro ao aplicativo.

Eu não concordo. writepode retornar sem erro se a gravação for simplesmente enfileirada, mas o erro será relatado na próxima operação que exigirá a gravação real no disco, ou seja, na próxima fsync, possivelmente na gravação a seguir, se o sistema decidir liberar o cache e em menos no último arquivo fechado.

Essa é a razão pela qual é essencial que o aplicativo teste o valor de retorno de close para detectar possíveis erros de gravação.

Se você realmente precisa executar um processamento inteligente de erros, deve presumir que tudo o que foi escrito desde o último êxito fsync pode ter falhado e que, pelo menos, algo falhou.

Serge Ballesta
fonte
4
Sim, acho que acertou em cheio. Esta seria, de fato sugerem que o aplicativo deve voltar a fazer todo o seu trabalho desde a última confirmou-sucedida fsync()ou close()do arquivo se ele recebe um -EIOde write(), fsync()ou close(). Bem, isso é divertido.
Craig Ringer
1

write(2) fornece menos do que você espera. A página de manual é muito aberta sobre a semântica de uma write()chamada bem-sucedida :

Um retorno bem-sucedido de write()não garante que os dados foram confirmados no disco. De fato, em algumas implementações de buggy, isso nem garante que o espaço tenha sido reservado com sucesso para os dados. A única maneira de ter certeza é ligar para fsync(2) depois que você terminar de escrever todos os seus dados.

Podemos concluir que um sucesso write()significa apenas que os dados atingiram os recursos de buffer do kernel. Se a persistência do buffer falhar, um acesso subsequente ao descritor de arquivo retornará o código de erro. Como último recurso que pode ser close(). A página de manual da closechamada do sistema (2) contém a seguinte frase:

É bem possível que erros em uma writeoperação anterior (2) sejam relatados primeiro na final close().

Se seu aplicativo precisar persistir com a gravação de dados, ele deverá usar fsync/ fsyncdataregularmente:

fsync()transfere ("libera") todos os dados modificados dentro do núcleo (ou seja, páginas de cache de buffer modificadas) para o arquivo referido pelo descritor de arquivo fd para o dispositivo de disco (ou outro dispositivo de armazenamento permanente), para que todas as informações alteradas possam ser recuperadas mesmo depois que o sistema travou ou foi reiniciado. Isso inclui gravar ou liberar um cache de disco, se presente. A chamada é bloqueada até que o dispositivo relate que a transferência foi concluída.

fzgregor
fonte
4
Sim, eu sei que isso fsync()é necessário. Mas no caso específico onde o kernel perde as páginas devido a um erro de E / S irá fsync()falhar? Sob que circunstâncias ele pode ter sucesso depois?
Craig Ringer
Também não conheço a fonte do kernel. Vamos assumir fsync()retornos -EIOsobre questões de E / S (para o que seria bom, caso contrário?). Portanto, o banco de dados sabe que parte de uma gravação anterior falhou e pode entrar no modo de recuperação. Não é isso que você quer? Qual é a motivação da sua última pergunta? Deseja saber qual gravação falhou ou recuperar o descritor de arquivo para uso posterior?
fzgregor
Idealmente, um DBMS prefere não entrar na recuperação de falhas (dando início a todos os usuários e tornando-se temporariamente inacessível ou pelo menos somente leitura) se for possível evitá-lo. Mas, mesmo que o kernel possa nos dizer "bytes 4096 a 8191 de fd X", seria difícil descobrir o que (re) escrever lá sem praticamente fazer a recuperação de falhas. Portanto, acho que a questão principal é se há mais circunstâncias inocentes onde fsync()possa retornar -EIOonde é seguro tentar novamente e se é possível dizer a diferença.
Craig Ringer
A recuperação de falhas é o último recurso. Mas, como você já disse, esses problemas deverão ser muito, muito raros. Portanto, não vejo problema em entrar em recuperação em nenhum -EIO. Se cada descritor de arquivo for usado apenas por um encadeamento de cada vez, esse encadeamento poderá voltar ao último fsync()e refazer as write()chamadas. Mas, ainda assim, se esses write()apenas escrevem parte de um setor, a parte não modificada ainda pode estar corrompida.
fzgregor
1
Você está certo de que entrar na recuperação de falhas provavelmente é razoável. Quanto aos setores, em parte, corruptos, o DBMS (PostgreSQL) armazena uma imagem da página inteira a primeira vez que toca depois de um determinado ponto de verificação apenas para essa razão, então ele deve estar bem :)
Craig Ringer
0

Use o sinalizador O_SYNC ao abrir o arquivo. Ele garante que os dados sejam gravados no disco.

Se isso não lhe agradar, não haverá nada.

toughmanwang
fonte
17
O_SYNCé um pesadelo para o desempenho. Isso significa que o aplicativo não pode fazer mais nada enquanto a E / S do disco está ocorrendo, a menos que produza threads de E / S. Você também pode dizer que a interface de E / S em buffer é insegura e todos devem usar o AIO. Gravações perdidas silenciosamente não podem ser aceitáveis ​​em E / S em buffer?
Craig Ringer
3
( O_DATASYNCé apenas um pouco melhor a esse respeito)
Craig Ringer
@ CraigRinger Você deve usar o AIO se tiver essa necessidade e precisar de qualquer tipo de desempenho. Ou apenas use um DBMS; Ele lida com tudo para você.
24517 Demi
10
@ Demi A aplicação aqui é um dbms (postgresql). Tenho certeza de que você pode imaginar que reescrever o aplicativo inteiro para usar o AIO em vez de E / S em buffer não é prático. Nem deveria ser necessário.
Craig Ringer
-5

Verifique o valor de retorno do fechamento. O fechamento pode falhar enquanto as gravações em buffer parecem ter êxito.

Malcolm McLean
fonte
8
Bem, quase não quer ser open()ing e close()ing o arquivo a cada poucos segundos. é por isso que temos fsync()...
Craig Ringer