web-dev-qa-db-fra.com

Ecriture de programmes pour faire face aux erreurs d'E / S, provoquant des écritures perdues sous Linux

TL; DR: Si le noyau Linux perd une écriture d’E/S mise en tampon , l’application at-elle un moyen de le savoir?

Je sais que vous devez fsync() le fichier (et son répertoire parent) pour sa durabilité . La question est si le noyau perd les mémoires tampons en attente d'écriture en raison d'une erreur d'entrée-sortie, comment l'application peut-elle détecter cela et récupérer ou abandonner?

Pensez aux applications de base de données, etc., où l'ordre d'écriture et la durabilité de l'écriture peuvent être cruciaux.

Perdu écrit? Comment?

La couche de blocage du noyau Linux peut dans certaines circonstances perdre mettre en mémoire cache les demandes d’entrée/sortie soumises avec succès par write(), pwrite(), etc., avec une erreur telle que:

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

(Voir end_buffer_write_sync(...) et end_buffer_async_write(...) dans fs/buffer.c ).

Sur les nouveaux noyaux, l'erreur contiendra "une écriture de page asynchrone perdue" , comme:

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

Puisque write() de l'application aura déjà été renvoyé sans erreur, il semble n'y avoir aucun moyen de signaler une erreur à l'application.

Les détecter?

Je ne connais pas très bien les sources du noyau, mais je pense qu'il définit AS_EIO Sur le tampon dont l'écriture a échoué si l'écriture est asynchrone:

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

mais il m'est difficile de savoir si, ou comment, l'application peut en savoir plus quand fsync() le fichier confirmera plus tard qu'il est sur le disque.

On dirait que wait_on_page_writeback_range(...) dans mm/filemap.c pourrait par do_sync_mapping_range(...) dans fs/sync.c qui est à son tour appelé par sys_sync_file_range(...) . Il renvoie -EIO Si un ou plusieurs tampons n'ont pas pu être écrits.

Si, comme je le suppose, cela se propage dans le résultat de fsync(), alors si l'application panique et se sauve si elle reçoit une erreur d'entrée-sortie de fsync() et sait comment faire son travail une fois redémarré, cela devrait être une sauvegarde suffisante?

L’application n’a probablement aucun moyen de savoir qui les décalages d’octets dans un fichier correspondent aux pages perdues; elle peut donc les réécrire si elle sait comment, mais si l’application répète tout le travail en attente depuis le dernier succès fsync() du fichier et réécrit les tampons du noyau sale correspondant aux écritures perdues dans le fichier, ce qui devrait effacer tous les indicateurs d'erreur d'E/S sur les pages perdues et permettre le prochain fsync() à compléter - pas vrai?

Y a-t-il alors d'autres circonstances inoffensives où fsync() peut renvoyer -EIO Où il serait trop radical de renflouer et de refaire le travail?

Pourquoi?

Bien sûr, de telles erreurs ne devraient pas arriver. Dans ce cas, l’erreur résultait d’une interaction malheureuse entre les valeurs par défaut du pilote dm-multipath Et le code de détection utilisé par SAN pour signaler l’échec de l’allocation du stockage à allocation dynamique. Ce n'est pas la seule circonstance où ils se produisent peut - j'ai également vu des rapports à ce sujet provenant de LVM provisionné mince, par exemple, tel qu'utilisé par libvirt, Docker, etc. Une application critique telle qu'une base de données devrait essayer de gérer de telles erreurs, plutôt que de continuer aveuglément comme si tout allait bien.

Si le kernel pense qu'il est correct de perdre des écritures sans mourir avec la panique du noyau, les applications doivent trouver un moyen de s'en sortir.

L'impact pratique est que j'ai trouvé un cas où un problème de propagation par trajets multiples avec un SAN a causé des écritures perdues qui ont atterri, causant la corruption de la base de données car le SGBD ne savait pas que ses écritures avaient échoué. Pas amusant.

135
Craig Ringer

fsync() renvoie -EIO Si le noyau a perdu une écriture

(Remarque: les premières parties font référence aux anciens noyaux; mises à jour ci-dessous pour refléter les noyaux modernes)

Cela ressemble à les échecs de tampon asynchrone dans end_buffer_async_write(...) ont défini un indicateur -EIO Sur la page de tampon sale ayant échoué pour le fichier :

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

qui est ensuite détecté par wait_on_page_writeback_range(...) appelé par do_sync_mapping_range(...) appelé par sys_sync_file_range(...) appelé par sys_sync_file_range2(...) pour mettre en œuvre l'appel de la bibliothèque C fsync() .

Mais une seule fois!

Ce commentaire sur 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.

suggère que lorsque fsync() renvoie -EIO ou (non documenté dans la page de manuel) -ENOSPC, il efface le statut d'erreur, de sorte que fsync() signalera le succès même si les pages n'ont jamais été écrites.

Assez sûr wait_on_page_writeback_range(...) efface les bits d'erreur quand il les teste :

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;

Donc, si l'application s'attend à pouvoir réessayer fsync() jusqu'à ce qu'elle réussisse et qu'elle ait confiance que les données sont sur le disque, elle est terriblement fausse.

Je suis à peu près sûr que c'est la source de la corruption des données trouvée dans le SGBD. Il réessaie fsync() et pense que tout ira bien lorsqu'il réussira.

Est-ce permis?

Les les documents POSIX/SuS sur fsync() ne spécifient pas vraiment ceci:

Si la fonction fsync () échoue, les opérations d’E/S en attente ne sont pas garanties.

La page de manuel de Linux pour fsync() ne dit rien sur ce qui se passe en cas d'échec.

Il semble donc que le sens des erreurs fsync() est "tu ne sais pas ce qui est arrivé à tes écritures, qu'elles aient fonctionné ou non, il vaut mieux essayer à nouveau pour en être sûr".

Nouveaux noyaux

Le 4.9 end_buffer_async_write définit -EIO Sur la page, simplement 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);

Du côté de la synchronisation, je pense que c'est similaire, bien que la structure soit maintenant assez complexe à suivre. filemap_check_errors Dans mm/filemap.c Fait maintenant:

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

qui a à peu près le même effet. Les vérifications d'erreur semblent toutes passer filemap_check_errors qui effectue un test-and-clear:

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

J'utilise btrfs sur mon ordinateur portable, mais lorsque je crée un bouclage ext4 Pour le tester sur /mnt/tmp Et que je configure une sonde de performance sur celui-ci:

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

Je trouve la pile d'appels suivante dans 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

Une lecture à haute voix suggère que les noyaux modernes se comportent de la même manière.

Cela semble vouloir dire que si fsync() (ou vraisemblablement write() ou close()) renvoie -EIO, Le fichier est dans un état non défini entre le moment où vous avez réussi fsync() d ou close() d et son dernier état write().

Tester

J'ai implémenté un scénario de test pour illustrer ce problème .

Les implications

Un SGBD peut y faire face en entrant en reprise sur incident. Comment une application utilisateur normale est-elle supposée faire face à cela? La page de manuel fsync() n'indique pas qu'elle signifie "fsync-si-vous-sentez-vous-comme", et j'attends qu'un beaucoup d'applications ne soit pas compatible avec ce comportement.

Rapports de bugs

Lectures complémentaires

lwn.net en a parlé dans l'article "Traitement amélioré des erreurs de la couche de bloc" .

fil de discussion de la liste de diffusion postgresql.org .

89
Craig Ringer

Comme write () de l'application aura déjà été renvoyé sans erreur, il semble n'y avoir aucun moyen de signaler une erreur à l'application.

Je ne suis pas d'accord. write peut renvoyer sans erreur si l'écriture est simplement mise en file d'attente, mais l'erreur sera signalée lors de la prochaine opération nécessitant l'écriture sur le disque, c'est-à-dire lors de la prochaine fsync, éventuellement sur une suite à l’écriture si le système décide de vider le cache et au moins lors de la fermeture du dernier fichier.

C’est la raison pour laquelle il est essentiel que l’application teste la valeur de retour close pour détecter les erreurs d’écriture possibles.

Si vous avez vraiment besoin de pouvoir effectuer un traitement intelligent des erreurs, vous devez supposer que tout ce qui a été écrit depuis la dernière transaction réussie fsyncpeut a échoué et que, dans tous les cas, quelque chose a échoué. .

22
Serge Ballesta

write (2) fournit moins que prévu. La page de manuel est très ouverte sur la sémantique d’un appel réussi write():

Un retour réussi de write() ne garantit en rien que les données ont été validées sur le disque. En fait, sur certaines implémentations erronées, cela ne garantit même pas que l’espace a bien été réservé pour les données. Le seul moyen de vous en assurer est d'appeler fsync (2) après avoir écrit toutes vos données.

Nous pouvons en conclure qu'une write() réussie signifie simplement que les données ont atteint les fonctions de mise en mémoire tampon du noyau. Si la persistance de la mémoire tampon échoue, un accès ultérieur au descripteur de fichier renverra le code d'erreur. En dernier recours, il peut s’agir de close(). La page de manuel de l'appel système close (2) contient la phrase suivante:

Il est fort possible que les erreurs sur une précédente opération write (2) soient d'abord signalées à la dernière opération close ().

Si votre application doit conserver les données, elle doit utiliser fsync/fsyncdata régulièrement:

fsync() transfère ("chasse") toutes les données en cœur modifiées (c'est-à-dire, les pages de cache tampon modifiées pour) le fichier référencé par le descripteur de fichier fd vers le périphérique de disque (ou un autre périphérique de stockage permanent) afin que toutes les informations modifiées peuvent être récupérées même après la panne du système ou son redémarrage. Cela inclut l'écriture ou le vidage d'un cache de disque, le cas échéant. L'appel est bloqué jusqu'à ce que l'appareil signale que le transfert est terminé.

1
fzgregor

Utilisez l'indicateur O_SYNC lorsque vous ouvrez le fichier. Cela garantit que les données sont écrites sur le disque.

Si cela ne vous satisfait pas, il n'y aura rien.

0
toughmanwang