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

138

TL; DR: Si le noyau Linux perd une écriture d'E / S tamponnée , y a-t-il un moyen pour l'application de le savoir?

Je sais que vous devez fsync()le fichier (et son répertoire parent) pour la durabilité . La question est de savoir 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 détecter cela 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.

Écritures perdues? Comment?

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

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(...)dansfs/buffer.c ).

Sur les noyaux plus récents, l'erreur contiendra à la place "écriture de page asynchrone perdue" , comme:

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

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

Les détecter?

Je ne suis pas très familier avec les sources du noyau, mais je pense qu'il est défini AS_EIOsur le tampon qui n'a pas pu être écrit s'il fait 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 sais pas si ou comment l'application peut le découvrir plus tard fsync() le fichier pour confirmer qu il est sur le disque.

Il ressemble wait_on_page_writeback_range(...)enmm/filemap.c puissance par do_sync_mapping_range(...)dansfs/sync.c ce 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 au fsync()résultat de, alors si l'application panique et renonce si elle reçoit une erreur d'E / S fsync()et sait comment refaire son travail lors du redémarrage, cela devrait être une sauvegarde suffisante?

Il n'y a probablement aucun moyen pour l'application de savoir quels décalages d'octets dans un fichier correspondent aux pages perdues afin qu'elle puisse les réécrire si elle sait comment, mais si l'application répète tout son travail en attente depuis le dernier succès fsync()du fichier, et cela réécrit des tampons de noyau sales correspondant à des écritures perdues sur le fichier, qui devraient effacer tous les indicateurs d'erreur d'E / S sur les pages perdues et permettre au suivant fsync()de se terminer - n'est-ce pas?

Y a-t-il alors d'autres circonstances, inoffensives, où fsync()peuvent revenir -EIOoù renflouer et refaire des travaux serait trop drastique?

Pourquoi?

Bien entendu, de telles erreurs ne devraient pas se produire. Dans ce cas, l'erreur provenait d'une interaction malheureuse entre les dm-multipathvaleurs par défaut du pilote et le code de détection utilisé par le SAN pour signaler l'échec de l'allocation du stockage alloué de manière dynamique. Mais ce n'est pas la seule circonstance où ils peuvent se produire - j'ai également vu des rapports de LVM à provisionnement fin par exemple, tel qu'utilisé 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 acceptable de perdre des écritures sans mourir avec une panique du noyau, les applications doivent trouver un moyen de faire face.

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

Craig Ringer
la source
1
J'ai peur que cela nécessite des champs supplémentaires dans le SystemFileTable pour stocker et se souvenir de ces conditions d'erreur. Et une possibilité pour le processus de l'espace utilisateur de les recevoir ou de les inspecter lors des appels ultérieurs. (est-ce que fsync () et close () renvoient ce genre d' informations historiques ?)
joop
@joop Merci. Je viens de publier une réponse avec ce qui se passe à mon avis, pensez à faire une vérification de bon sens puisque vous semblez en savoir plus sur ce qui se passe que les personnes qui ont publié des variantes évidentes de "write () needs close () ou fsync ( ) pour la durabilité "sans lire la question?
Craig Ringer
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 d'échange. Comme ceux-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 où chercher. [at Craig: oui parce que je connais votre nom, et je sais que vous n'êtes pas un idiot complet; -]
joop
1
Je suis d'accord, je n'étais pas si juste. Hélas votre réponse n'est pas très satisfaisante, je veux dire qu'il n'y a pas de solution facile (surprenante?).
Jean-Baptiste Yunès
1
@ Jean-BaptisteYunès Vrai. Pour le SGBD avec lequel je travaille, "crash and enter redo" est acceptable. Pour la plupart des applications, ce n'est pas une option et elles devront peut-être tolérer les performances horribles des E / S synchrones ou simplement accepter un comportement mal défini et une corruption en cas d'erreurs d'E / S.
Craig Ringer

Réponses:

91

fsync()retourne -EIOsi le noyau a perdu une écriture

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

Il semble que l' écriture de tampon asynchrone en cas d' end_buffer_async_write(...)échecs définisse un -EIOindicateur 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ée par wait_on_page_writeback_range(...)comme demandé par do_sync_mapping_range(...)comme demandé par sys_sync_file_range(...)comme demandé par sys_sync_file_range2(...)la mise en œuvre de 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()retourne -EIOou (non documenté dans la page de manuel) -ENOSPC, il effacera l'état d'erreur afin qu'un suivant 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 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 à pouvoir réessayer fsync()jusqu'à ce qu'elle réussisse et qu'elle sache que les données sont sur disque, c'est terriblement faux.

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

Est-ce permis?

Les documents POSIX / SuS surfsync() ne le spécifient pas vraiment de toute façon:

Si la fonction fsync () échoue, il n'est pas garanti que les opérations d'E / S en attente se sont terminées.

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

Il semble donc que la signification des fsync()erreurs est "je ne sais pas ce qui est arrivé à vos écritures, cela a peut-être fonctionné ou non, mieux vaut réessayer pour être sûr".

Noyaux plus récents

Sur 4.9 end_buffer_async_writeensembles -EIOsur 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, même si la structure est maintenant assez complexe à suivre. filemap_check_errorsen fait mm/filemap.cmaintenant:

    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'erreurs semblent toutes passer par filemap_check_errorsce qui fait un test et effacement:

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

J'utilise btrfssur mon ordinateur portable, mais lorsque je crée un ext4bouclage pour tester /mnt/tmpet configurer une sonde de perf 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 directe suggère que oui, les noyaux modernes se comportent de la même manière.

Cela semble signifier que si fsync()(ou vraisemblablement write()ou close()) le rendement -EIO, le fichier est dans un état indéfini entre lors de la dernière avec succès fsync()d ou close()d , cela et son dernier write()état dix.

Tester

J'ai implémenté un cas de test pour démontrer ce comportement .

Implications

Un SGBD peut faire face à cela en entrant la récupération après incident. Comment diable une application utilisateur normale est-elle censée faire face à cela? La fsync()page de manuel ne donne aucun avertissement que cela signifie "fsync-if-you-feel-like-it" et je m'attends à ce que beaucoup d'applications ne supportent pas bien ce comportement.

Rapports de bogues

Lectures complémentaires

lwn.net en a parlé dans l'article "Amélioration de la gestion des erreurs de couche de bloc" .

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

Craig Ringer
la source
3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 est une course possible, car il attend {E / S en attente et planifiées}, pas {E / S pas encore planifiées}. C'est évidemment pour éviter des allers-retours supplémentaires vers l'appareil. (Je suppose que l'utilisateur écrit () ne revient pas tant que les E / S ne sont pas planifiées, pour mmap (), c'est différent)
joop
3
Est-il possible que l'appel d'un autre processus à fsync pour un autre fichier sur le même disque obtienne le retour d'erreur?
Random832
3
@ Random832 Très pertinent pour une base de données multi-traitement comme PostgreSQL, donc bonne question. Cela semble probablement, mais je ne connais pas assez bien le code du noyau pour le comprendre. Vos processus feraient mieux de coopérer s'ils ont tous les deux le même fichier ouvert de toute façon.
Craig Ringer
1
@DavidFoerster: Les appels système renvoient des échecs en utilisant des codes errno négatifs; errnoest complètement une construction de la bibliothèque C de l'espace utilisateur. Il est courant d'ignorer les différences de valeur de retour entre les appels système et la bibliothèque C comme ceci (comme le fait Craig Ringer, ci-dessus), car la valeur de retour d'erreur identifie de manière fiable à laquelle (appel système ou fonction de bibliothèque C) fait référence: " -1avec errno==EIO"fait référence à une fonction de bibliothèque C, tandis que" -EIO"fait référence à un appel système. Enfin, les pages de manuel Linux en ligne sont la référence la plus à jour pour les pages de manuel Linux.
Nominal Animal
2
@CraigRinger: Pour répondre à votre dernière question: "En utilisant des E / S de bas niveau et fsync()/ fdatasync()lorsque la taille de la transaction est un fichier complet; en utilisant mmap()/ msync()lorsque la taille de la transaction est un enregistrement aligné sur la page; et en utilisant I de bas niveau / O, fdatasync()et plusieurs descripteurs de fichiers simultanés (un descripteur et un thread par transaction) dans le même fichier sinon " . Les verrous de description de fichier ouverts spécifiques à Linux ( fcntl(), F_OFD_) sont très utiles avec le dernier.
Nominal Animal
22

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

Je ne suis pas d'accord. writepeut retourner sans erreur si l'écriture est simplement mise en file d'attente, mais l'erreur sera signalée lors de l'opération suivante qui nécessitera l'écriture réelle sur le disque, c'est-à-dire au suivant fsync, éventuellement lors d'une écriture suivante si le système décide de vider le cache et à moins lors de la dernière fermeture de 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 de pouvoir faire un traitement intelligent des erreurs, vous devez supposer que tout ce qui a été écrit depuis le dernier succès a fsync peut- être échoué et que dans tout cela au moins quelque chose a échoué.

Serge Ballesta
la source
4
Ouais, je pense que ça cloue. Cela suggérerait en effet que l'application devrait refaire tout son travail depuis le dernier succès confirmé fsync()ou close()du fichier s'il obtient un -EIOfrom write(), fsync()ou close(). Eh bien, c'est amusant.
Craig Ringer
1

write(2) fournit moins que ce que vous attendez. La page de manuel est très ouverte sur la sémantique d'un write()appel réussi :

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

Nous pouvons conclure qu'un succès 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 de l' closeappel système (2) contient la phrase suivante:

Il est tout à fait possible que des erreurs sur une opération précédente write(2) soient d'abord signalées au final close().

Si votre application a besoin de conserver des données écrites, elle doit utiliser fsync/ fsyncdatarégulièrement:

fsync()transfère ("purge") toutes les données internes modifiées (c'est-à-dire les pages de cache de tampon modifiées pour) le fichier référencé par le descripteur de fichier fd vers le périphérique de disque (ou tout autre périphérique de stockage permanent) afin que toutes les informations modifiées puissent être récupérées même après que le système s'est écrasé ou a été redémarré. Cela inclut l'écriture ou le vidage d'un cache disque, le cas échéant. L'appel se bloque jusqu'à ce que l'appareil signale que le transfert est terminé.

fzgregor
la source
4
Oui, je sais que fsync()c'est nécessaire. Mais dans le cas spécifique où le noyau perd les pages en raison d'une erreur d' E / S va fsync()échouer? Dans quelles circonstances peut-il réussir ensuite?
Craig Ringer
Je ne connais pas non plus la source du noyau. Supposons des fsync()retours -EIOsur les problèmes d'E / S (à quoi cela servirait-il autrement?). Ainsi, la base de données sait qu'une partie d'une écriture précédente a échoué et pourrait passer en mode de récupération. N'est-ce pas ce que tu veux? 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?
fzgregor
Idéalement, un SGBD préférera ne pas entrer dans la récupération après incident (coup de pied de tous les utilisateurs et devenant temporairement inaccessible ou au moins en lecture seule) s'il peut éventuellement l'éviter. Mais même si le noyau pouvait nous dire "octets 4096 à 8191 de fd X", il serait difficile de savoir quoi (ré) écrire là-bas sans à peu près faire une reprise après incident. Donc, je suppose que la question principale est de savoir s'il existe d'autres circonstances innocentes où il estfsync() possible de revenir -EIOlà où il est sûr de réessayer, et s'il est possible de faire la différence.
Craig Ringer
La récupération après incident est le dernier recours. Mais comme vous l'avez déjà dit, ces problèmes devraient être très très rares. Par conséquent, je ne vois pas de problème avec la récupération sur aucun -EIO. Si chaque descripteur de fichier n'est utilisé que par un thread à la fois, ce thread pourrait revenir au dernier fsync()et refaire les write()appels. Mais quand même, si ces write()s n'écrit qu'une partie d'un secteur, la partie non modifiée peut toujours être corrompue.
fzgregor
1
Vous avez raison de dire que la reprise après incident est probablement raisonnable. En ce qui concerne les secteurs partiellement corrompus, le SGBD (PostgreSQL) stocke une image de la page entière la première fois qu'il la touche après un point de contrôle donné pour cette raison, donc ça devrait aller :)
Craig Ringer
0

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.

durmanwang
la source
17
O_SYNCest un cauchemar pour la performance. Cela signifie que l'application ne peut rien faire d' autre pendant les E / S disque, sauf si elle génère des threads d'E / S. Autant 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 acceptées dans les E / S tamponnées?
Craig Ringer
3
( O_DATASYNCn'est que légèrement meilleur à cet égard)
Craig Ringer
@CraigRinger Vous devriez utiliser AIO si vous avez ce besoin et avez besoin de toute sorte de performance. Ou utilisez simplement un SGBD; il gère tout pour vous.
Demi
10
@Demi L'application ici est un dbms (postgresql). Je suis sûr que vous pouvez imaginer que réécrire toute l'application pour utiliser AIO au lieu d'E / S tamponnées n'est pas pratique. Cela ne devrait pas non plus être nécessaire.
Craig Ringer
-5

Vérifiez la valeur de retour de close. close peut échouer tandis que les écritures tamponnées semblent réussir.

Malcolm McLean
la source
8
Eh bien, nous voulons être à peine open()ING et , close()du fichier toutes les quelques secondes. c'est pourquoi nous avons fsync()...
Craig Ringer