Stockage de données durable et API de fichiers Linux

Moi, recherchant la stabilité du stockage de données dans les systèmes cloud, j'ai décidé de me tester, pour m'assurer que je comprenais les choses de base. je commencé par lire la spécification NVMe afin de comprendre quelles garanties concernant la persistance des données (c'est-à-dire que les données seront disponibles après une panne du système) nous donnent les disques NMVe. J'ai tiré les principales conclusions suivantes : vous devez considérer les données corrompues à partir du moment où la commande d'écriture de données est donnée et jusqu'au moment où elles sont écrites sur le support de stockage. Cependant, la plupart des programmes utilisent très bien les appels système pour écrire des données.

Dans cet article, j'explore les mécanismes de persistance fournis par les API de fichiers Linux. Il semble que tout devrait être simple ici : le programme appelle la commande write(), et une fois l'opération de cette commande terminée, les données seront stockées en toute sécurité sur le disque. Mais write() copie uniquement les données d'application dans le cache du noyau situé dans la RAM. Afin de forcer le système à écrire des données sur le disque, certains mécanismes supplémentaires doivent être utilisés.

Stockage de données durable et API de fichiers Linux

En général, ce matériel est un ensemble de notes relatives à ce que j'ai appris sur un sujet qui m'intéresse. Si nous parlons très brièvement du plus important, il s'avère que pour organiser un stockage durable des données, vous devez utiliser la commande fdatasync() ou ouvrir des fichiers avec le drapeau O_DSYNC. Si vous souhaitez en savoir plus sur ce qu'il advient des données entre le code et le disque, consultez cette article.

Caractéristiques de l'utilisation de la fonction write()

Appel système write() défini dans la norme IEEEPOSIX comme une tentative d'écrire des données dans un descripteur de fichier. Après la bonne fin des travaux write() les opérations de lecture de données doivent renvoyer exactement les octets précédemment écrits, même si les données sont consultées à partir d'autres processus ou threads (ici section correspondante de la norme POSIX). il est, dans la section sur l'interaction des threads avec les opérations normales sur les fichiers, il y a une note qui dit que si deux threads appellent chacun ces fonctions, alors chaque appel doit soit voir toutes les conséquences indiquées auxquelles l'exécution de l'autre appel conduit, soit ne vois pas du tout aucune conséquence. Cela conduit à la conclusion que toutes les opérations d'E/S de fichier doivent détenir un verrou sur la ressource en cours de traitement.

Est-ce à dire que l'opération write() est atomique ? D'un point de vue technique, oui. Les opérations de lecture de données doivent renvoyer tout ou rien de ce qui a été écrit avec write(). Mais l'opération write(), conformément à la norme, n'a pas à se terminer, après avoir écrit tout ce qu'on lui a demandé d'écrire. Il est permis d'écrire seulement une partie des données. Par exemple, nous pourrions avoir deux flux ajoutant chacun 1024 octets à un fichier décrit par le même descripteur de fichier. Du point de vue de la norme, le résultat sera acceptable lorsque chacune des opérations d'écriture ne pourra ajouter qu'un seul octet au fichier. Ces opérations resteront atomiques, mais une fois terminées, les données qu'elles écriront dans le fichier seront mélangées. Ici discussion très intéressante sur ce sujet sur Stack Overflow.

Fonctions fsync() et fdatasync()

Le moyen le plus simple de vider les données sur le disque consiste à appeler la fonction fsync (). Cette fonction demande au système d'exploitation de déplacer tous les blocs modifiés du cache vers le disque. Cela inclut toutes les métadonnées du fichier (heure d'accès, heure de modification du fichier, etc.). Je pense que ces métadonnées sont rarement nécessaires, donc si vous savez que ce n'est pas important pour vous, vous pouvez utiliser la fonction fdatasync(). la aider sur fdatasync() il dit que pendant le fonctionnement de cette fonction, une telle quantité de métadonnées est enregistrée sur le disque, ce qui est "nécessaire à l'exécution correcte des opérations de lecture de données suivantes". Et c'est exactement ce dont la plupart des applications se soucient.

Un problème qui peut survenir ici est que ces mécanismes ne garantissent pas que le fichier puisse être retrouvé après une éventuelle panne. En particulier, lorsqu'un nouveau fichier est créé, il faut appeler fsync() pour le répertoire qui le contient. Sinon, après un plantage, il se peut que ce fichier n'existe pas. La raison en est que sous UNIX, en raison de l'utilisation de liens physiques, un fichier peut exister dans plusieurs répertoires. Par conséquent, lors de l'appel fsync() il n'y a aucun moyen pour un fichier de savoir quelles données de répertoire doivent également être vidées sur le disque (ici vous pouvez en savoir plus à ce sujet). Il semble que le système de fichiers ext4 est capable de automatiquement appliquer fsync() aux répertoires contenant les fichiers correspondants, mais cela peut ne pas être le cas avec d'autres systèmes de fichiers.

Ce mécanisme peut être implémenté différemment dans différents systèmes de fichiers. j'ai utilisé trace blk pour en savoir plus sur les opérations de disque utilisées dans les systèmes de fichiers ext4 et XFS. Les deux émettent les commandes d'écriture habituelles sur le disque à la fois pour le contenu des fichiers et le journal du système de fichiers, vident le cache et quittent en effectuant une écriture FUA (Force Unit Access, écriture de données directement sur le disque, contournant le cache) dans le journal. Ils font probablement cela pour confirmer le fait de la transaction. Sur les lecteurs qui ne prennent pas en charge FUA, cela provoque deux vidages du cache. Mes expériences ont montré que fdatasync() un peu plus vite fsync(). Utilitaire blktrace indique que fdatasync() écrit généralement moins de données sur le disque (en ext4 fsync() écrit 20 KiB, et fdatasync() - 16 Ko). De plus, j'ai découvert que XFS est légèrement plus rapide que ext4. Et ici avec l'aide blktrace a pu découvrir que fdatasync() vide moins de données sur le disque (4 Kio dans XFS).

Situations ambiguës lors de l'utilisation de fsync()

Je peux penser à trois situations ambiguës concernant fsync()que j'ai rencontré dans la pratique.

Le premier incident de ce type s'est produit en 2008. A cette époque, l'interface de Firefox 3 se « figeait » si un grand nombre de fichiers étaient écrits sur le disque. Le problème était que l'implémentation de l'interface utilisait une base de données SQLite pour stocker des informations sur son état. Après chaque changement survenu dans l'interface, la fonction était appelée fsync(), ce qui offrait de bonnes garanties de stockage stable des données. Dans le système de fichiers ext3 alors utilisé, la fonction fsync() vidait sur le disque toutes les pages "sales" du système, et pas seulement celles qui étaient liées au fichier correspondant. Cela signifiait que cliquer sur un bouton dans Firefox pouvait entraîner l'écriture de mégaoctets de données sur un disque magnétique, ce qui pouvait prendre plusieurs secondes. La solution au problème, d'après ce que j'ai compris cela matériel, consistait à déplacer le travail avec la base de données vers des tâches d'arrière-plan asynchrones. Cela signifie que Firefox avait l'habitude d'implémenter des exigences de persistance de stockage plus strictes que ce qui était réellement nécessaire, et les fonctionnalités du système de fichiers ext3 n'ont fait qu'exacerber ce problème.

Le deuxième problème est survenu en 2009. Ensuite, après une panne du système, les utilisateurs du nouveau système de fichiers ext4 ont constaté que de nombreux fichiers nouvellement créés étaient de longueur nulle, mais cela ne s'est pas produit avec l'ancien système de fichiers ext3. Dans le paragraphe précédent, j'ai expliqué comment ext3 déversait trop de données sur le disque, ce qui ralentissait beaucoup les choses. fsync(). Pour améliorer la situation, ext4 ne vide que les pages "sales" qui sont pertinentes pour un fichier particulier. Et les données des autres fichiers restent en mémoire beaucoup plus longtemps qu'avec ext3. Cela a été fait pour améliorer les performances (par défaut, les données restent dans cet état pendant 30 secondes, vous pouvez configurer cela en utilisant sale_expire_centisecs; ici vous pouvez trouver plus d'informations à ce sujet). Cela signifie qu'une grande quantité de données peut être irrémédiablement perdue après un crash. La solution à ce problème consiste à utiliser fsync() dans les applications qui doivent fournir un stockage de données stable et les protéger autant que possible des conséquences des pannes. Fonction fsync() fonctionne beaucoup plus efficacement avec ext4 qu'avec ext3. L'inconvénient de cette approche est que son utilisation, comme auparavant, ralentit certaines opérations, comme l'installation de programmes. Voir les détails à ce sujet ici и ici.

Le troisième problème concernant fsync(), né en 2018. Puis, dans le cadre du projet PostgreSQL, il a été découvert que si la fonction fsync() rencontre une erreur, il marque les pages "sales" comme "propres". En conséquence, les appels suivants fsync() ne rien faire avec de telles pages. Pour cette raison, les pages modifiées sont stockées en mémoire et ne sont jamais écrites sur le disque. C'est un véritable désastre, car l'application pensera que certaines données sont écrites sur le disque, mais en fait ce ne sera pas le cas. De tels échecs fsync() sont rares, l'application dans de telles situations ne peut presque rien faire pour combattre le problème. De nos jours, lorsque cela se produit, PostgreSQL et d'autres applications se bloquent. il est, dans l'article "Can Applications Recover from fsync Failures?", ce problème est exploré en détail. Actuellement, la meilleure solution à ce problème consiste à utiliser Direct I/O avec le drapeau O_SYNC ou avec un drapeau O_DSYNC. Avec cette approche, le système signalera les erreurs qui peuvent se produire lors de l'exécution d'opérations d'écriture de données spécifiques, mais cette approche nécessite que l'application gère elle-même les tampons. En savoir plus ici и ici.

Ouverture de fichiers à l'aide des drapeaux O_SYNC et O_DSYNC

Revenons à la discussion des mécanismes Linux qui fournissent un stockage de données persistant. À savoir, nous parlons d'utiliser le drapeau O_SYNC ou drapeau O_DSYNC lors de l'ouverture de fichiers à l'aide d'un appel système ouvrir(). Avec cette approche, chaque opération d'écriture de données est effectuée comme si après chaque commande write() le système reçoit respectivement des commandes fsync() и fdatasync(). la Spécifications POSIX cela s'appelle « Achèvement de l'intégrité des fichiers d'E/S synchronisés » et « Achèvement de l'intégrité des données ». Le principal avantage de cette approche est qu'un seul appel système doit être exécuté pour assurer l'intégrité des données, et non deux (par exemple - write() и fdatasync()). Le principal inconvénient de cette approche est que toutes les opérations d'écriture utilisant le descripteur de fichier correspondant seront synchronisées, ce qui peut limiter la possibilité de structurer le code de l'application.

Utilisation d'E/S directes avec l'indicateur O_DIRECT

Appel système open() soutient le drapeau O_DIRECT, qui est conçu pour contourner le cache du système d'exploitation, effectuer des opérations d'E / S, en interagissant directement avec le disque. Cela, dans de nombreux cas, signifie que les commandes d'écriture émises par le programme seront directement traduites en commandes visant à travailler avec le disque. Mais, en général, ce mécanisme ne remplace pas les fonctions fsync() ou fdatasync(). Le fait est que le disque lui-même peut délai ou cache commandes appropriées pour écrire des données. Et, pire encore, dans certains cas particuliers, les opérations d'E / S effectuées lors de l'utilisation du drapeau O_DIRECT, diffuser dans les opérations tamponnées traditionnelles. La façon la plus simple de résoudre ce problème est d'utiliser le drapeau pour ouvrir les fichiers O_DSYNC, ce qui signifie que chaque opération d'écriture sera suivie d'un appel fdatasync().

Il s'est avéré que le système de fichiers XFS avait récemment ajouté un "chemin rapide" pour O_DIRECT|O_DSYNC-enregistrements de données. Si le bloc est écrasé à l'aide de O_DIRECT|O_DSYNC, alors XFS, au lieu de vider le cache, exécutera la commande d'écriture FUA si le périphérique la prend en charge. J'ai vérifié cela à l'aide de l'utilitaire blktrace sur un système Linux 5.4/Ubuntu 20.04. Cette approche devrait être plus efficace, car elle écrit la quantité minimale de données sur le disque et utilise une opération, et non deux (écrire et vider le cache). j'ai trouvé un lien vers patch noyau 2018 qui implémente ce mécanisme. Il y a des discussions sur l'application de cette optimisation à d'autres systèmes de fichiers, mais pour autant que je sache, XFS est le seul système de fichiers qui le supporte jusqu'à présent.

fonction sync_file_range()

Linux a un appel système sync_file_range(), qui vous permet de ne vider qu'une partie du fichier sur le disque, et non le fichier entier. Cet appel lance un vidage asynchrone et n'attend pas qu'il se termine. Mais dans la référence à sync_file_range() cette commande est dite "très dangereuse". Il n'est pas recommandé de l'utiliser. Caractéristiques et dangers sync_file_range() très bien décrit dans Cette matériel. En particulier, cet appel semble utiliser RocksDB pour contrôler le moment où le noyau vide les données "sales" sur le disque. Mais en même temps là, pour assurer un stockage stable des données, il est aussi utilisé fdatasync(). la code RocksDB a des commentaires intéressants sur ce sujet. Par exemple, il ressemble à l'appel sync_file_range() lors de l'utilisation de ZFS, les données ne sont pas vidées sur le disque. L'expérience me dit qu'un code rarement utilisé peut contenir des bogues. Par conséquent, je déconseille d'utiliser cet appel système à moins que cela ne soit absolument nécessaire.

Appels système pour aider à assurer la persistance des données

Je suis arrivé à la conclusion qu'il existe trois approches qui peuvent être utilisées pour effectuer des opérations d'E/S persistantes. Ils nécessitent tous un appel de fonction fsync() pour le répertoire où le fichier a été créé. Ce sont les approches :

  1. Appel de fonction fdatasync() ou fsync() après la fonction write() (mieux vaut utiliser fdatasync()).
  2. Travailler avec un descripteur de fichier ouvert avec un drapeau O_DSYNC ou O_SYNC (mieux - avec un drapeau O_DSYNC).
  3. Utilisation de la commande pwritev2() avec drapeau RWF_DSYNC ou RWF_SYNC (de préférence avec un drapeau RWF_DSYNC).

Remarques sur les performances

Je n'ai pas fait une mesure approfondie de la performance des divers mécanismes que j'ai explorés. Les différences que j'ai remarquées dans la vitesse de leur travail sont très faibles. Cela signifie que je peux me tromper et que, dans d'autres conditions, la même chose peut donner des résultats différents. Je parlerai d'abord de ce qui affecte le plus les performances, puis de ce qui affecte moins les performances.

  1. L'écrasement des données d'un fichier est plus rapide que l'ajout de données à un fichier (le gain de performances peut être de 2 à 100 %). Joindre des données à un fichier nécessite des modifications supplémentaires des métadonnées du fichier, même après l'appel système fallocate(), mais l'ampleur de cet effet peut varier. Je recommande, pour de meilleures performances, d'appeler fallocate() pour pré-allouer l'espace requis. Ensuite, cet espace doit être explicitement rempli de zéros et appelé fsync(). Cela entraînera le marquage des blocs correspondants dans le système de fichiers comme "alloués" au lieu de "non alloués". Cela donne une petite (environ 2%) amélioration des performances. De plus, certains disques peuvent avoir une opération d'accès au premier bloc plus lente que d'autres. Cela signifie que remplir l'espace avec des zéros peut entraîner une amélioration significative (environ 100 %) des performances. En particulier, cela peut arriver avec des disques. AWSEBS (ce sont des données non officielles, je n'ai pas pu les confirmer). Il en va de même pour le stockage. Disque persistant GCP (et c'est déjà une information officielle, confirmée par des tests). D'autres experts ont fait de même observationsliés à différents disques.
  2. Moins il y a d'appels système, plus les performances sont élevées (le gain peut être d'environ 5%). Cela ressemble à un appel open() avec drapeau O_DSYNC ou appeler pwritev2() avec drapeau RWF_SYNC appel plus rapide fdatasync(). Je soupçonne que le point ici est qu'avec cette approche, le fait que moins d'appels système doivent être effectués pour résoudre la même tâche (un appel au lieu de deux) joue un rôle. Mais la différence de performances est très faible, vous pouvez donc facilement l'ignorer et utiliser quelque chose dans l'application qui ne complique pas sa logique.

Si vous êtes intéressé par le sujet du stockage durable des données, voici quelques documents utiles :

  • Méthodes d'accès aux E/S — un aperçu des bases des mécanismes d'entrée/sortie.
  • S'assurer que les données atteignent le disque - une histoire sur ce qu'il advient des données entre l'application et le disque.
  • Quand devriez-vous fsync le répertoire contenant - la réponse à la question de savoir quand postuler fsync() pour les répertoires. En un mot, il s'avère que vous devez le faire lors de la création d'un nouveau fichier, et la raison de cette recommandation est que sous Linux, il peut y avoir de nombreuses références au même fichier.
  • SQL Server sur Linux : composants internes de FUA - voici une description de la façon dont le stockage de données persistantes est implémenté dans SQL Server sur la plate-forme Linux. Il existe ici des comparaisons intéressantes entre les appels système Windows et Linux. Je suis presque sûr que c'est grâce à ce matériel que j'ai appris l'optimisation FUA de XFS.

Avez-vous déjà perdu des données que vous pensiez être stockées en toute sécurité sur disque ?

Stockage de données durable et API de fichiers Linux

Stockage de données durable et API de fichiers Linux

Source: habr.com