Emmagatzematge de dades durador i API de fitxers Linux

Jo, investigant l'estabilitat de l'emmagatzematge de dades en sistemes al núvol, vaig decidir provar-me per assegurar-me que entenc les coses bàsiques. jo va començar llegint les especificacions de NVMe per tal d'entendre quines garanties pel que fa a la persistència de les dades (és a dir, les garanties que les dades estaran disponibles després d'una fallada del sistema) ens ofereixen els discos NMVe. Vaig treure les següents conclusions principals: cal tenir en compte les dades danyades des del moment en què es dóna l'ordre d'escriptura de dades i fins al moment en què s'escriuen al suport d'emmagatzematge. Tanmateix, a la majoria de programes, les trucades al sistema s'utilitzen amb força seguretat per escriure dades.

En aquest article, exploro els mecanismes de persistència que proporcionen les API de fitxers de Linux. Sembla que tot hauria de ser senzill aquí: el programa crida l'ordre write(), i un cop finalitzada l'operació d'aquesta ordre, les dades s'emmagatzemaran de manera segura al disc. Però write() només copia les dades de l'aplicació a la memòria cau del nucli situada a la memòria RAM. Per forçar el sistema a escriure dades al disc, s'han d'utilitzar alguns mecanismes addicionals.

Emmagatzematge de dades durador i API de fitxers Linux

En general, aquest material és un conjunt d'apunts relacionats amb el que he après sobre un tema del meu interès. Si parlem molt breument del més important, resulta que per organitzar l'emmagatzematge de dades sostenible, cal utilitzar l'ordre fdatasync() o obrir fitxers amb bandera O_DSYNC. Si us interessa aprendre més sobre què passa amb les dades en el camí del codi al disc, feu una ullada a això article.

Característiques de l'ús de la funció write().

Trucada del sistema write() definit a la norma IEEE POSIX com un intent d'escriure dades en un descriptor de fitxer. Després de finalitzar amb èxit el treball write() Les operacions de lectura de dades han de retornar exactament els bytes que s'havien escrit anteriorment, fins i tot si s'accedeix a les dades des d'altres processos o fils (aquí secció corresponent de l'estàndard POSIX). Aquí, a la secció sobre la interacció de fils amb operacions de fitxers habituals, hi ha una nota que indica que si dos fils cada un criden a aquestes funcions, llavors cada trucada ha de veure totes les conseqüències indicades a les quals comporta l'execució de l'altra crida, o no. no veuen cap conseqüència. Això porta a la conclusió que totes les operacions d'E/S de fitxers han de mantenir un bloqueig al recurs en què s'està treballant.

Vol dir això que l'operació write() és atòmic? Des del punt de vista tècnic, sí. Les operacions de lectura de dades han de retornar tot o cap del que s'ha escrit write(). Però l'operació write(), d'acord amb la norma, no ha d'acabar, havent anotat tot el que se li va demanar que anotés. Només es permet escriure una part de les dades. Per exemple, podríem tenir dos fluxos cadascun afegint 1024 bytes a un fitxer descrit pel mateix descriptor de fitxer. Des del punt de vista de l'estàndard, el resultat serà acceptable quan cadascuna de les operacions d'escriptura pugui afegir només un byte al fitxer. Aquestes operacions romandran atòmiques, però un cop finalitzades, les dades que escriuen al fitxer es barrejaran. aquí està una discussió molt interessant sobre aquest tema a Stack Overflow.

Funcions fsync() i fdatasync().

La manera més senzilla d'esborrar dades al disc és cridar la funció fsync(). Aquesta funció demana al sistema operatiu que mogui tots els blocs modificats de la memòria cau al disc. Això inclou totes les metadades del fitxer (hora d'accés, temps de modificació del fitxer, etc.). Crec que aquestes metadades poques vegades es necessiten, així que si sabeu que no és important per a vosaltres, podeu utilitzar la funció fdatasync(). En ajuda en fdatasync() diu que durant el funcionament d'aquesta funció, aquesta quantitat de metadades es desa al disc, la qual cosa és "necessària per a la correcta execució de les següents operacions de lectura de dades". I això és exactament el que preocupa a la majoria de les aplicacions.

Un problema que pot sorgir aquí és que aquests mecanismes no garanteixen que el fitxer es pugui trobar després d'una possible fallada. En particular, quan es crea un fitxer nou, s'ha de trucar fsync() per al directori que el conté. En cas contrari, després d'un error, pot resultar que aquest fitxer no existeix. La raó d'això és que sota UNIX, a causa de l'ús d'enllaços durs, un fitxer pot existir en diversos directoris. Per tant, en trucar fsync() no hi ha manera que un fitxer sàpiga quines dades del directori també s'han d'esborrar al disc (aquí podeu llegir més sobre això). Sembla que el sistema de fitxers ext4 és capaç de fer-ho automàticament per aplicar fsync() als directoris que contenen els fitxers corresponents, però això pot no ser el cas amb altres sistemes de fitxers.

Aquest mecanisme es pot implementar de manera diferent en diferents sistemes de fitxers. jo solia traça blktrace per conèixer quines operacions de disc s'utilitzen als sistemes de fitxers ext4 i XFS. Tots dos emeten les ordres d'escriptura habituals al disc tant per al contingut dels fitxers com per al diari del sistema de fitxers, esborren la memòria cau i surten realitzant una escriptura FUA (Forçar l'accés a la unitat, escrivint dades directament al disc, obviant la memòria cau) escrivint al diari. Probablement ho facin per confirmar el fet de la transacció. A les unitats que no admeten FUA, això provoca dos buidatges de memòria cau. Els meus experiments ho han demostrat fdatasync() una mica més ràpid fsync(). Utilitat blktrace indica que fdatasync() normalment escriu menys dades al disc (a ext4 fsync() escriu 20 KiB, i fdatasync() - 16 KiB). A més, vaig descobrir que XFS és una mica més ràpid que ext4. I aquí amb l'ajuda blktrace va poder esbrinar-ho fdatasync() buida menys dades al disc (4 KiB a XFS).

Situacions ambigües quan s'utilitza fsync()

Puc pensar en tres situacions ambigües fsync()que he trobat a la pràctica.

El primer incident d'aquest tipus es va produir l'any 2008. En aquell moment, la interfície de Firefox 3 es "congelava" si s'estaven escrivint un gran nombre de fitxers al disc. El problema era que la implementació de la interfície utilitzava una base de dades SQLite per emmagatzemar informació sobre el seu estat. Després de cada canvi que es va produir a la interfície, es va cridar la funció fsync(), que donava bones garanties d'emmagatzematge estable de dades. Al sistema de fitxers ext3 utilitzat aleshores, la funció fsync() es va esborrar al disc totes les pàgines "brutes" del sistema, i no només les que estaven relacionades amb el fitxer corresponent. Això significava que fer clic a un botó al Firefox podria provocar que s'escriguessin megabytes de dades en un disc magnètic, cosa que podria trigar molts segons. La solució al problema, pel que he entès el material, era traslladar el treball amb la base de dades a tasques de fons asíncrones. Això vol dir que el Firefox solia implementar requisits de persistència d'emmagatzematge més estrictes del que era realment necessari, i les funcions del sistema de fitxers ext3 només van agreujar aquest problema.

El segon problema va passar el 2009. Aleshores, després d'un error del sistema, els usuaris del nou sistema de fitxers ext4 van trobar que molts fitxers de nova creació eren de longitud zero, però això no va passar amb el sistema de fitxers ext3 anterior. En el paràgraf anterior, vaig parlar de com ext3 va abocar massa dades al disc, cosa que va alentir molt les coses. fsync(). Per millorar la situació, ext4 esborra només aquelles pàgines "brutes" que són rellevants per a un fitxer concret. I les dades d'altres fitxers romanen a la memòria durant molt més temps que amb ext3. Això es va fer per millorar el rendiment (per defecte, les dades es mantenen en aquest estat durant 30 segons, podeu configurar-ho mitjançant dirty_expire_centisecs; aquí podeu trobar més informació al respecte). Això significa que una gran quantitat de dades es poden perdre de manera irrecuperable després d'un accident. La solució a aquest problema és utilitzar fsync() en aplicacions que necessiten proporcionar un emmagatzematge estable de dades i protegir-les tant com sigui possible de les conseqüències dels errors. Funció fsync() funciona molt més eficientment amb ext4 que amb ext3. L'inconvenient d'aquest enfocament és que el seu ús, com abans, alenteix algunes operacions, com ara la instal·lació de programes. Veure detalls sobre això aquí и aquí.

El tercer problema referent fsync(), es va originar el 2018. Aleshores, en el marc del projecte PostgreSQL, es va descobrir que si la funció fsync() troba un error, marca les pàgines "brutes" com a "netes". En conseqüència, les següents convocatòries fsync() no fer res amb aquestes pàgines. Per això, les pàgines modificades s'emmagatzemen a la memòria i mai s'escriuen al disc. Això és un autèntic desastre, perquè l'aplicació pensarà que algunes dades s'escriuen al disc, però de fet no ho serà. Aquests fracassos fsync() són rars, l'aplicació en aquestes situacions no pot fer gairebé res per combatre el problema. En aquests dies, quan això passa, PostgreSQL i altres aplicacions es bloquegen. Aquí, a l'article "Les aplicacions es poden recuperar dels errors de fsync?", s'explora aquest problema amb detall. Actualment, la millor solució a aquest problema és utilitzar Direct I/O amb la bandera O_SYNC o amb una bandera O_DSYNC. Amb aquest enfocament, el sistema informarà dels errors que es poden produir quan es realitzen operacions específiques d'escriptura de dades, però aquest enfocament requereix que l'aplicació gestioni les memòries intermèdies. Llegeix més sobre això aquí и aquí.

Obrint fitxers utilitzant els senyaladors O_SYNC i O_DSYNC

Tornem a la discussió dels mecanismes Linux que proporcionen emmagatzematge de dades persistent. És a dir, estem parlant d'utilitzar la bandera O_SYNC o bandera O_DSYNC en obrir fitxers mitjançant la trucada del sistema obert (). Amb aquest enfocament, cada operació d'escriptura de dades es realitza com si després de cada comanda write() al sistema se li donen, respectivament, ordres fsync() и fdatasync(). En Especificacions POSIX això s'anomena "Finalització de la integritat del fitxer d'E/S sincronitzada" i "Finalització de la integritat de les dades". El principal avantatge d'aquest enfocament és que només cal executar una trucada al sistema per garantir la integritat de les dades, i no dues (per exemple - write() и fdatasync()). El principal desavantatge d'aquest enfocament és que totes les operacions d'escriptura utilitzant el descriptor de fitxers corresponent es sincronitzaran, cosa que pot limitar la capacitat d'estructurar el codi de l'aplicació.

Utilitzant l'E/S directa amb el senyalador O_DIRECT

Trucada del sistema open() suporta la bandera O_DIRECT, que està dissenyat per evitar la memòria cau del sistema operatiu, realitzar operacions d'E/S, interactuant directament amb el disc. Això, en molts casos, significa que les ordres d'escriptura emeses pel programa es traduiran directament en ordres destinades a treballar amb el disc. Però, en general, aquest mecanisme no és un substitut de les funcions fsync() o fdatasync(). El fet és que el propi disc pot retard o memòria cau ordres adequades per escriure dades. I, encara pitjor, en alguns casos especials, les operacions d'E/S realitzades en utilitzar la bandera O_DIRECT, emissió a les operacions tradicionals de buffer. La manera més senzilla de resoldre aquest problema és utilitzar el senyalador per obrir fitxers O_DSYNC, el que significarà que cada operació d'escriptura anirà seguida d'una trucada fdatasync().

Va resultar que el sistema de fitxers XFS havia afegit recentment un "camí ràpid" per O_DIRECT|O_DSYNC- Registres de dades. Si el bloc es sobreescriu amb O_DIRECT|O_DSYNC, llavors XFS, en lloc d'esborrar la memòria cau, executarà l'ordre d'escriptura de FUA si el dispositiu l'admet. Ho he verificat amb la utilitat blktrace en un sistema Linux 5.4/Ubuntu 20.04. Aquest enfocament hauria de ser més eficient, ja que escriu la quantitat mínima de dades al disc i utilitza una operació, no dues (escriure i esborrar la memòria cau). He trobat un enllaç a pegat 2018 nucli que implementa aquest mecanisme. Hi ha una mica de discussió sobre l'aplicació d'aquesta optimització a altres sistemes de fitxers, però pel que jo sé, XFS és l'únic sistema de fitxers que ho admet fins ara.

funció sync_file_range().

Linux té una trucada al sistema rang_de_fitxers de sincronització(), que us permet esborrar només una part del fitxer al disc, no tot el fitxer. Aquesta trucada inicia un buidat asíncron i no espera que es completi. Però en la referència a sync_file_range() Es diu que aquesta ordre és "molt perillosa". No es recomana utilitzar-lo. Característiques i perills sync_file_range() molt ben descrit a això material. En particular, aquesta trucada sembla utilitzar RocksDB per controlar quan el nucli envia dades "brutes" al disc. Però al mateix temps, també s'utilitza per garantir un emmagatzematge estable de dades fdatasync(). En codi RocksDB té alguns comentaris interessants sobre aquest tema. Per exemple, sembla la trucada sync_file_range() quan s'utilitza ZFS no esborra les dades al disc. L'experiència em diu que el codi poc utilitzat pot contenir errors. Per tant, desaconsellaria utilitzar aquesta trucada del sistema tret que sigui absolutament necessari.

Trucades al sistema per ajudar a garantir la persistència de les dades

He arribat a la conclusió que hi ha tres enfocaments que es poden utilitzar per realitzar operacions d'E/S persistents. Tots requereixen una trucada de funció fsync() per al directori on es va crear el fitxer. Aquests són els enfocaments:

  1. Crida de funció fdatasync() o fsync() després de la funció write() (millor utilitzar fdatasync()).
  2. Treballant amb un descriptor de fitxer obert amb una bandera O_DSYNC o O_SYNC (millor - amb una bandera O_DSYNC).
  3. Ús de comandaments pwritev2() amb bandera RWF_DSYNC o RWF_SYNC (preferiblement amb bandera RWF_DSYNC).

Notes d'actuació

No vaig mesurar acuradament el rendiment dels diferents mecanismes que vaig investigar. Les diferències que vaig notar en la velocitat del seu treball són molt petites. Això vol dir que puc equivocar-me i que en altres condicions el mateix pot mostrar resultats diferents. En primer lloc, parlaré sobre què afecta més el rendiment i, després, sobre què afecta menys el rendiment.

  1. Sobreescriure les dades del fitxer és més ràpid que afegir dades a un fitxer (el guany de rendiment pot ser del 2 al 100%). Adjuntar dades a un fitxer requereix canvis addicionals a les metadades del fitxer, fins i tot després de la trucada al sistema fallocate(), però la magnitud d'aquest efecte pot variar. Recomano, per obtenir el millor rendiment, trucar fallocate() per assignar prèviament l'espai necessari. Aleshores, aquest espai s'ha d'omplir explícitament de zeros i cridar-lo fsync(). Això farà que els blocs corresponents del sistema de fitxers es marquin com a "assignats" en lloc de "no assignats". Això proporciona una petita millora de rendiment (aproximadament un 2%). A més, alguns discs poden tenir una operació d'accés al primer bloc més lenta que d'altres. Això vol dir que omplir l'espai amb zeros pot conduir a una millora significativa (al voltant del 100%) del rendiment. En particular, això pot passar amb els discos. AWS EBS (es tracta de dades no oficials, no les he pogut confirmar). El mateix passa amb l'emmagatzematge. Disc persistent GCP (i això ja és informació oficial, confirmada per proves). Altres experts han fet el mateix observaciórelacionades amb diferents discos.
  2. Com menys trucades al sistema, més gran serà el rendiment (el guany pot ser d'un 5%). Sembla una trucada open() amb bandera O_DSYNC o truca pwritev2() amb bandera RWF_SYNC trucada més ràpida fdatasync(). Sospito que la qüestió aquí és que amb aquest enfocament, el fet que s'hagin de realitzar menys trucades al sistema per resoldre la mateixa tasca (una trucada en lloc de dues) juga un paper important. Però la diferència de rendiment és molt petita, de manera que podeu ignorar-la fàcilment i utilitzar alguna cosa a l'aplicació que no comporti la complicació de la seva lògica.

Si esteu interessats en el tema de l'emmagatzematge de dades sostenible, aquí teniu alguns materials útils:

  • Mètodes d'accés a E/S — una visió general dels conceptes bàsics dels mecanismes d'entrada/sortida.
  • Assegureu-vos que les dades arribin al disc - una història sobre què passa amb les dades en el camí de l'aplicació al disc.
  • Quan hauríeu de fsync el directori que conté - la resposta a la pregunta de quan s'ha de sol·licitar fsync() per a directoris. En poques paraules, resulta que ho heu de fer quan creeu un fitxer nou, i el motiu d'aquesta recomanació és que a Linux hi pot haver moltes referències al mateix fitxer.
  • SQL Server a Linux: FUA Internals - aquí hi ha una descripció de com s'implementa l'emmagatzematge de dades persistent a SQL Server a la plataforma Linux. Aquí hi ha algunes comparacions interessants entre les trucades al sistema Windows i Linux. Estic gairebé segur que va ser gràcies a aquest material que vaig conèixer l'optimització FUA de XFS.

Alguna vegada has perdut dades que creies que estaven emmagatzemades de manera segura al disc?

Emmagatzematge de dades durador i API de fitxers Linux

Emmagatzematge de dades durador i API de fitxers Linux

Font: www.habr.com