145 votes

Écriture de programmes pour faire face aux erreurs d'E/S causant des écritures perdues sous Linux

TL;DR : Si le noyau Linux perd une écriture d'E/S en mémoire tampon L'application a-t-elle un moyen de le savoir ?

Je sais que tu dois fsync() le fichier (et son répertoire parent) pour la durabilité . La question est si le noyau perd les tampons sales qui sont en attente d'écriture. en raison d'une erreur d'E/S, comment l'application peut-elle le détecter et récupérer ou abandonner ?

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

Des écrits perdus ? Comment ?

La couche de blocs du noyau Linux peut dans certaines circonstances perdre les demandes d'E/S mises en mémoire tampon qui ont été soumises avec succès par write() , pwrite() etc., avec une erreur du type :

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(...) y end_buffer_async_write(...) en fs/buffer.c ).

Sur les noyaux plus récents, l'erreur contiendra plutôt "lost async page write". comme :

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

Puisque l'application write() sera déjà retourné sans erreur, il semble qu'il n'y ait aucun moyen de signaler une erreur à l'application.

Vous les détectez ?

Je ne suis pas très familier avec les sources du noyau, mais je pensez à qu'il fixe AS_EIO sur le tampon qui n'a pas pu être écrit s'il s'agit d'une écriture asynchrone :

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

mais je ne vois pas très bien si ou comment l'application peut découvrir cela plus tard. fsync() s le fichier pour confirmer qu'il est sur le disque.

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

Si, comme je le suppose, cela se propage à fsync() puis si l'application panique et abandonne si elle reçoit une erreur d'E/S de la part de fsync() et sait comment refaire son travail lorsqu'il est redémarré, cela devrait être une garantie suffisante ?

Il n'y a vraisemblablement aucun moyen pour l'application de savoir dont Les décalages d'octets dans un fichier correspondent aux pages perdues, de sorte qu'il peut les réécrire s'il sait comment, mais si l'application répète tout son travail en attente depuis le dernier succès de l'opération de réécriture, il n'y a pas de problème. fsync() du fichier, et qui réécrit tous les tampons du noyau sales correspondant aux écritures perdues dans le fichier, ce qui devrait effacer tous les drapeaux d'erreur d'E/S sur les pages perdues et permettre à la page suivante de s'exécuter. fsync() à compléter - n'est-ce pas ?

Y a-t-il donc d'autres circonstances inoffensives où fsync() peut revenir -EIO où se désister et refaire le travail serait trop radical ?

Pourquoi ?

Bien sûr, de telles erreurs ne devraient pas se produire. Dans ce cas, l'erreur provient d'une interaction malencontreuse entre la fonction dm-multipath et le code de détection utilisé par le SAN pour signaler l'échec de l'allocation d'espace de stockage à allocation légère. Mais ce n'est pas la seule circonstance dans laquelle ils peut J'ai également vu des rapports à ce sujet dans des LVM à provisionnement fin, par exemple, tels qu'utilisés par libvirt, Docker, etc. Une application critique comme une base de données devrait essayer de faire face à de telles erreurs, plutôt que de continuer aveuglément comme si tout allait bien.

Si le Noyau pense qu'il est possible de perdre des écritures sans mourir d'une panique du noyau, les applications doivent trouver un moyen d'y faire face.

L'impact pratique est que j'ai trouvé un cas où un problème de multivoie avec un SAN a causé des écritures perdues qui ont fini par provoquer une corruption de la base de données parce que le SGBD ne savait pas que ses écritures avaient échoué. Ce n'est pas drôle.

1 votes

Je crains que cela ne nécessite des champs supplémentaires dans la table SystemFileTable pour stocker et mémoriser ces conditions d'erreur. Et une possibilité pour le processus en espace utilisateur de les recevoir ou de les inspecter lors des appels suivants. (est-ce que fsync() et close() renvoient ce genre d'informations ? historique informations ?)

0 votes

@joop Merci. J'ai juste posté une réponse avec ce que je pense qu'il se passe, cela me dérange d'avoir un contrôle de bon sens puisque vous semblez en savoir plus sur ce qui se passe que les personnes qui ont posté des variantes évidentes de "write() needs close() ou fsync() pour la durabilité" sans lire la question ?

0 votes

BTW : Je pense que vous devriez vraiment vous plonger dans les sources du noyau. Les systèmes de fichiers journalisés souffriraient probablement du même genre de problèmes. Sans parler de la gestion de la partition swap. Puisque celles-ci vivent dans l'espace noyau, la gestion de ces conditions sera probablement un peu plus rigide. writev() , qui est visible depuis l'espace utilisateur, semble également être un endroit à regarder. [ à Craig : oui parce que je connais votre nom, et je sais que vous n'êtes pas un idiot complet ;-]

98voto

Craig Ringer Points 72371

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

(Note : la première partie fait référence à des noyaux plus anciens ; mise à jour ci-dessous pour refléter les noyaux modernes)

On dirait que écriture asynchrone de la mémoire tampon en end_buffer_async_write(...) les échecs fixent un -EIO sur la page de tampon sale échouée 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(...) tel qu'appelé par do_sync_mapping_range(...) tel qu'appelé par sys_sync_file_range(...) tel qu'appelé par sys_sync_file_range2(...) pour implémenter l'appel de la bibliothèque C fsync() .

Mais seulement une 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 sera effacer l'état d'erreur de sorte qu'un fsync() rapportera un succès même si les pages n'ont jamais été écrites.

C'est sûr. wait_on_page_writeback_range(...) efface les bits d'erreur lorsqu'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 à ce qu'elle puisse réessayer fsync() jusqu'à ce qu'il réussisse et qu'il ait la certitude que les données sont sur le disque, il est terriblement mauvais.

Je suis presque sûr que c'est la source de la corruption des données que j'ai trouvée dans le SGBD. Il réessaie fsync() et pense que tout ira bien quand il aura réussi.

Est-ce autorisé ?

Le site Documentation POSIX/SuS sur fsync() ne le précisent pas vraiment :

Si la fonction fsync() échoue, les opérations d'E/S en cours ne sont pas garanties comme ayant été achevées.

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

Il semble donc que la signification de fsync() les erreurs sont "Je ne sais pas ce qui est arrivé à vos écritures, cela a pu fonctionner ou non, il vaut mieux réessayer pour être sûr".

Noyaux plus récents

Le 4.9 end_buffer_async_write fixe -EIO sur la page, juste 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 désormais assez complexe à suivre. filemap_check_errors en mm/filemap.c le 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 contrôles d'erreurs semblent tous passer par filemap_check_errors qui fait 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 quand je crée une ext4 boucle pour les tests sur /mnt/tmp et mettre en place une sonde de perforation dessus :

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 attentive suggère que oui, les noyaux modernes se comportent de la même manière.

Cela semble signifier que si fsync() (ou vraisemblablement write() o close() ) retourne -EIO le fichier se trouve dans un état indéfini entre le moment où vous avez réussi à vous connecter et le moment où vous avez réussi à vous connecter. fsync() d ou close() d'elle et de son plus récent write() dix états.

Test

J'ai mis en place un scénario de test pour démontrer ce comportement. .

Implications

Un SGBD peut y faire face en entrant dans une procédure de récupération en cas de panne. Comment diable une application utilisateur normale est-elle censée y faire face ? Le site fsync() ne donne aucun avertissement sur le fait qu'il s'agit de "fsync-if-you-feel-like-it" et je m'attends à une lot des applications ne s'accommodent pas bien de ce comportement.

Rapports de bogue

Autres lectures

lwn.net a abordé ce sujet dans l'article "Amélioration de la gestion des erreurs de la couche de blocs". .

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

3 votes

lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 est une course possible, parce qu'il attend {les entrées/sorties en attente et programmées}, et non {les entrées/sorties non encore programmées}. Ceci est évidemment pour éviter des allers-retours supplémentaires vers le périphérique. (Je présume que les write() de l'utilisateur ne reviennent pas tant que les E/S ne sont pas programmées, pour mmap(), c'est différent).

4 votes

Est-il possible que l'appel à fsync d'un autre processus pour un autre fichier sur le même disque obtienne le retour d'erreur ?

4 votes

@Random832 Très pertinent pour une BD multi-processus comme PostgreSQL, donc bonne question. On dirait que c'est probable, mais je ne connais pas assez bien le code du noyau pour comprendre. Vos processeurs ont intérêt à coopérer s'ils ont tous les deux le même fichier ouvert, de toute façon.

22voto

Serge Ballesta Points 12850

Étant donné que la fonction write() de l'application est déjà retournée sans erreur, il ne semble pas y avoir de moyen de signaler une erreur à l'application.

Je ne suis pas d'accord. write peut retourner sans erreur si l'écriture est simplement mise en file d'attente, mais l'erreur sera signalée lors de la prochaine opération qui nécessitera l'écriture réelle sur le disque, c'est-à-dire lors de l'opération suivante fsync éventuellement lors d'une écriture suivante 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 pour l'application de tester la valeur de retour de close pour détecter d'éventuelles erreurs d'écriture.

Si vous avez vraiment besoin d'être capable de faire un traitement intelligent des erreurs, vous devez supposer que tout ce qui a été écrit depuis la dernière erreur réussie fsync mai ont échoué et que dans tout cela, au moins quelque chose a échoué.

4 votes

Oui, je pense que c'est ça. Cela suggère en effet que l'application devrait refaire tout son travail depuis la dernière confirmation de succès. fsync() o close() du fichier s'il reçoit un -EIO de write() , fsync() o close() . Eh bien, c'est amusant.

2voto

fzgregor Points 1285

write (2) fournit moins que ce que vous attendez. La page de manuel est très ouverte au sujet de la sémantique d'une commande réussie. write() appeler :

Un retour réussi de write() ne garantit pas que les données ont été transférées sur le disque. En fait, sur certaines implémentations boguées, il ne garantit même pas que l'espace a été réservé avec succès. pour les données. La seule façon d'en être sûr est d'appeler fsync (2) après que vous que vous avez fini d'écrire toutes vos données.

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

Il est tout à fait possible que des erreurs sur un précédent write (2) les opérations sont d'abord signalées lors de la close ().

Si votre application a besoin de persister les données en écriture, elle doit utiliser fsync / fsyncdata sur une base régulière :

fsync() transfère (" flush ") toutes les données modifiées du noyau (c.-à-d., les données modifiées du pages de cache de la mémoire tampon) du fichier auquel fait référence le descripteur de fichier fd vers le disque (ou tout autre dispositif de stockage permanent) de sorte que de façon à ce que toutes les informations modifiées puissent être récupérées système s'est arrêté ou a été redémarré. Cela inclut l'écriture ou le le vidage d'un cache disque s'il est présent. L'appel se bloque jusqu'à ce que le périphérique signale que le transfert est terminé.

5 votes

Oui, je suis conscient que fsync() est nécessaire. Mais dans le cas spécifique où le noyau perd les pages à cause d'une erreur d'E/S sera fsync() échouer ? Dans quelles circonstances peut-il ensuite réussir ?

0 votes

Je ne connais pas non plus la source du noyau. Supposons que fsync() renvoie à -EIO sur des questions d'E/S (à quoi cela servirait-il sinon ?). Ainsi, la base de données sait qu'une partie d'une écriture précédente a échoué et peut passer en mode de récupération. N'est-ce pas ce que vous voulez ? Quelle est la motivation de votre dernière question ? Voulez-vous savoir quelle écriture a échoué ou récupérer le descripteur de fichier pour une utilisation ultérieure ?

1 votes

Dans l'idéal, un SGBD préférera ne pas entrer dans une phase de récupération en cas de panne (en mettant hors service tous les utilisateurs et en devenant temporairement inaccessible ou au moins en lecture seule) s'il peut l'éviter. Mais même si le noyau pouvait nous dire "octets 4096 à 8191 de fd X", il serait difficile de trouver quoi (ré)écrire à cet endroit sans faire une récupération en cas de crash. Donc je pense que la question principale est de savoir s'il y a des circonstances plus innocentes dans lesquelles fsync() peut revenir -EIO où il est pour réessayer, et si c'est possible de faire la différence.

0voto

toughmanwang Points 39

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

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

19 votes

O_SYNC est un cauchemar pour les performances. Cela signifie que l'application ne peut rien faire sinon pendant que l'E/S du disque se produit, à moins qu'il ne crée des threads d'E/S. Vous pourriez tout aussi bien dire que l'interface d'E/S tamponnée n'est pas sûre et que tout le monde devrait utiliser AIO. Les écritures perdues silencieusement ne peuvent pas être acceptables en E/S tamponnées ?

4 votes

( O_DATASYNC n'est que légèrement meilleur à cet égard)

0 votes

@CraigRinger Vous devrait Utilisez AIO si vous avez ce besoin et si vous voulez une quelconque performance. Ou utilisez simplement un SGBD ; il s'occupe de tout pour vous.

-6voto

Malcolm McLean Points 5437

Vérifiez la valeur de retour de close. close peut échouer alors que les écritures en mémoire tampon semblent réussir.

10 votes

Eh bien, nous ne voulons pas être open() et close() le fichier toutes les quelques secondes. c'est pourquoi nous avons fsync() ...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X