Eu, explorând reziliența stocării datelor în sistemele cloud, am decis să mă testez, să mă asigur că înțeleg conceptele de bază. Eu pentru a înțelege ce garanții referitoare la stocarea rezistentă a datelor (adică garanții că datele vor fi accesibile după o defecțiune a sistemului) ne oferă disk-urile NVMe. Am ajuns la următoarele concluzii principale: trebuie să consideri datele ca fiind corupte din momentul în care a fost dată comanda de scriere a datelor și până la momentul în care scrierea lor pe suportul de informații este finalizată. Totuși, în majoritatea programelor de scriere a datelor, se folosesc în mod liniștit apeluri la sistem.
În acest material, explorez mecanismele de stocare rezistentă a datelor oferite de API-urile de fișiere Linux. Se pare că totul ar trebui să fie simplu: programul apelează comanda write(), iar după ce această comandă este finalizată, datele vor fi salvate în siguranță pe disk. Dar write() doar copiază datele aplicației în cache-ul nucleului, situat în memoria RAM. Pentru a obliga sistemul să scrie datele pe disk, trebuie să folosești câteva mecanisme suplimentare.
În general, acest material reprezintă un set de note referitoare la ceea ce am învățat pe tema care mă interesează. Dacă ar fi să rezum cel mai important aspect, ar fi că pentru a organiza stocarea rezistentă a datelor, trebuie să folosești comanda fdatasync() sau să deschizi fișiere cu flag-ul O_DSYNC. Dacă ești interesat să afli în detaliu ce se întâmplă cu datele pe parcursul drumului de la codul sursă la disk, aruncă o privire pe articol.
Particularitățile folosirii funcției write()
Apelul de sistem write() sunt definite în standardul ca o încercare de a scrie date în descriptorul de fișier. După finalizarea cu succes a write() operațiunii, citirile de date ar trebui să returneze exact acei biți care au fost anterior scriși, făcând acest lucru chiar și în cazul în care datele sunt accesate din alte procese sau fire ( secțiunea corespunzătoare din standardul POSIX). , în secțiunea dedicată interacțiunii între fire și operațiile standard cu fișiere, există o notă care menționează că, dacă fiecare dintre cele două fire apelează aceste funcții, fiecare apel trebuie să observe fie toate efectele specificate generate de executarea celuilalt apel, fie să nu observe niciun efect. Aceasta permite concluzionarea că toate operațiile de intrare/ieșire cu fișiere trebuie să blocheze resursa cu care lucrează.
Înseamnă asta că operația write() este atomică? Din punct de vedere tehnic – da. Operațiile de citire a datelor trebuie să returneze fie tot, fie nimic din ceea ce a fost scris prin write(). Dar operația write(), conform standardului, nu trebuie neapărat să se finalizeze scriind tot ceea ce i-a fost propus să scrie. Are voie să realizeze scrierea doar a unei părți din date. De exemplu, putem avea două fire, fiecare dintre ele adăugând 1024 de octeți la un fișier descris de același descriptor de fișier. Din perspectiva standardului, un rezultat acceptabil ar fi ca fiecare dintre operațiile de scriere să poată adăuga în fișier doar câte un octet. Aceste operații vor rămâne atomice, dar, după ce se finalizează, datele scrise de ele în fișier vor fi amestecate. o discuție foarte interesantă pe acest subiect pe Stack Overflow.
Funcțiile fsync() și fdatasync()
Cel mai simplu mod de a trimite datele pe disc este prin apelarea funcției . Această funcție solicită sistemului de operare transferul tuturor blocurilor modificate din cache pe disc. Acesta include și toate metadatele fișierului (timpul de acces, timpul de modificare a fișierului etc.). Cred că necesitatea acestor metadate apare rar, așa că, dacă știți că nu sunt importante pentru dumneavoastră, puteți utiliza funcția fdatasync(). În pe fdatasync() se menționează că, în cadrul acestei funcții, se realizează salvarea pe disc a unui volum de metadate „necesare pentru execuția corectă a următoarelor operații de citire a datelor”. Iar acest lucru este exact ceea ce îngrijorează cea mai mare parte a aplicațiilor.
O problemă care poate apărea aici este că aceste mecanisme nu garantează că fișierul va putea fi găsit după o posibilă defecțiune. În special, când se creează un fișier nou, trebuie apelat fsync() pentru directorul care îl conține. Altminteri, după o defectare, se poate întâmpla ca acest fișier să nu existe. Cauza acestui lucru este că, în UNIX, din cauza utilizării legăturilor dure, fișierul poate exista în mai multe directoare. De aceea, la apel fsync() pentru fișier nu există nicio modalitate de a ști care anume director trebuie să fie, de asemenea, scris pe disc ( despre acest lucru se poate citi mai în detaliu). Se pare că sistemul de fișiere ext4 este capabil aplicarea fsync() pentru directoarele care conțin fișierele corespunzătoare, dar în cazul altor sisteme de fișiere, acest lucru poate să nu fie adevărat.
Acest mecanism poate fi implementat diferit în diverse sisteme de fișiere. Eu am folosit pentru a afla despre ce operații de disc sunt utilizate în sistemele de fișiere ext4 și XFS. Ambele emit comenzi obișnuite de scriere pe disc, atât pentru conținutul fișierelor, cât și pentru jurnalul sistemului de fișiere, scriu în cache și finalizează sarcina, efectuând o scriere FUA (Force Unit Access, scrierea datelor direct pe disc, ocolind cache-ul) în jurnal. Probabil, ele procedează astfel pentru a confirma desfășurarea operației. Pe discurile care nu suportă FUA, acest lucru cauzează două scrieri în cache. Experimentele mele au arătat că fdatasync() este puțin mai rapid fsync(). Utilitară blktrace indică faptul că fdatasync() de obicei scrie pe disc mai puțini date (în ext4 fsync() scrie 20 KiB, iar fdatasync() — 16 KiB). În plus, am descoperit că XFS este puțin mai rapid decât ext4. Și aici, cu ajutorul blktrace am reușit să aflu că fdatasync() scrie pe disc mai puțini date (4 KiB în XFS).
Situații neclare care apar în utilizarea fsync()
Îmi pot aminti trei situații neclare care se referă la fsync(), cu care m-am confruntat în practică.
Prima astfel de situație a avut loc în 2008. Atunci, interfața Firefox 3 „se bloca” atunci când se efectua scrierea pe disc a unui număr mare de fișiere. Problema era că, în implementarea interfeței pentru stocarea informațiilor despre starea sa, era utilizată o bază de date SQLite. După fiecare modificare care avea loc în interfață, era apelată funcția fsync(), ceea ce oferea bune garanții de stocare durabilă a datelor. În sistemul de fișiere utilizat atunci, ext3, funcția fsync() a resetat pe disc toate paginile „murdare” din sistem, nu doar pe cele care aveau legătură cu fișierul respectiv. Aceasta însemna că un clic pe buton în Firefox putea iniția scrierea de megabytes de date pe discul magnetic, ceea ce putea dura multe secunde. Soluția problemei, așa cum am înțeles din material, consta în mutarea lucrului cu baza de date în sarcini de fundal asincrone. Aceasta înseamnă că anterior, în Firefox, au fost implementate cerințe mai stricte pentru fiabilitatea stocării datelor decât era cu adevărat necesar, iar caracteristicile sistemului de fișiere ext3 au agravat această problemă.
A doua neconcordanță a avut loc în 2009. Atunci, după o defecțiune a sistemului, utilizatorii noului sistem de fișiere ext4 s-au confruntat cu faptul că multe fișiere recent create au o lungime zero, în timp ce cu sistemul de fișiere mai vechi ext3 acest lucru nu s-a întâmplat. În paragraful anterior am menționat că ext3 reseta prea multe date pe disc, ceea ce încetinea semnificativ funcționarea fsync(). Pentru a îmbunătăți situația, în ext4 sunt resetate pe disc doar paginile „murdare” care au legătură cu un anumit fișier. Iar datele altor fișiere rămân în memorie pentru o perioadă mult mai lungă decât în cazul utilizării ext3. Acest lucru a fost realizat pentru a îmbunătăți performanța (implicit, datele rămân în această stare 30 de secunde, iar această setare poate fi ajustată prin ; , unde se pot găsi materiale suplimentare despre aceasta). Acest lucru înseamnă că un volum mare de date poate fi pierdut iremediabil după o defecțiune. Soluția acestei probleme constă în utilizarea fsync() în aplicațiile care trebuie să asigure stocarea fiabilă a datelor și să le protejeze cât mai mult posibil de efectele defecțiunilor. Funcția fsync() funcționează mult mai eficient atunci când se utilizează ext4 decât atunci când se utilizează ext3. Dezavantajul acestei abordări este că utilizarea sa, ca și înainte, încetinește executarea anumitor operațiuni, cum ar fi instalarea programelor. Detalii despre aceasta pot fi găsite și .
A treia problemă, legată de fsync(), a apărut în 2018. Atunci, în cadrul proiectului PostgreSQL, s-a descoperit că, dacă funcția fsync() se confruntă cu o eroare, ea marchează paginile „murdare” ca fiind „curate”. Drept rezultat, apelurile următoare fsync() nu se face nimic cu asemenea pagini. Din acest motiv, paginile modificate sunt stocate în memorie și nu sunt niciodată scrise pe disc. Aceasta este o adevărată catastrofă, deoarece aplicația va crede că anumite date sunt scrise pe disc, când, de fapt, acestea nu sunt. Astfel de erori fsync() apar rar, aplicația în aceste situații aproape că nu poate face nimic pentru a lupta împotriva problemei. În zilele noastre, când se întâmplă acest lucru, PostgreSQL și alte aplicații se opresc brusc. , în materialul „Pot aplicațiile să recupereze de la erorile fsync?”, această problemă este investigată în toate detaliile. În prezent, cea mai bună soluție pentru această problemă este utilizarea Direct I/O cu flagul O_SYNC sau cu flagul O_DSYNC. Prin această abordare, sistemul va raporta erorile care pot apărea în timpul executării unor operațiuni specifice de scriere a datelor, dar această abordare necesită ca aplicația să gestioneze singură bufferele. Detaliile despre aceasta pot fi citite și .
Deschiderea fișierelor folosind flagurile O_SYNC și O_DSYNC
Să revenim la discuția despre mecanismele Linux care asigură stocarea durabilă a datelor. Anume, este vorba despre utilizarea flagului O_SYNC sau flagului O_DSYNC la deschiderea fișierelor folosind apelul de sistem . Prin această abordare, fiecare operațiune de scriere a datelor se efectuează de parcă după fiecare comandă write() sistemului i se dau, respectiv, comenzi fsync() și fdatasync(). În aceasta se numește „Finalizarea integrității fișierelor I/O sincronizate” și „Finalizarea integrității datelor”. Principalul avantaj al acestei abordări este că pentru a asigura integritatea datelor trebuie efectuat un singur apel de sistem, nu două (de exemplu — write() și fdatasync()). Principalul dezavantaj al acestei abordări este că toate operațiunile de scriere care folosesc descriptorul de fișier corespunzător vor fi sincronizate, ceea ce poate limita oportunitățile de structurare a codului aplicației.
Utilizarea Direct I/O cu flagul O_DIRECT
Apelul de sistem open() sprijină flagul O_DIRECT, care este destinat să efectueze operațiuni de intrare-ieșire, interacționând direct cu discul, ocolind cache-ul sistemului de operare. Aceasta, în multe cazuri, înseamnă că comenzile de scriere emise de program vor fi transmise direct drept comenzi care lucrează cu discul. Dar, în general, acest mecanism nu este un înlocuitor pentru funcțiile fsync() sau fdatasync(). Problema este că discul însuși poate comenzile corespunzătoare pentru înregistrarea datelor. Și, ceea ce este și mai rău, în anumite cazuri speciale, operațiile de intrare-ieșire efectuate cu utilizarea flagului O_DIRECT, în operații tradiționale cu buffer. Cea mai simplă soluție pentru această problemă este să folosiți un flag O_DSYNC, ceea ce va însemna că pentru fiecare operație de înregistrare va exista un apel fdatasync().
Se pare că în sistemul de fișiere XFS a fost recent adăugat un „drum rapid” pentru O_DIRECT|O_DSYNC-înregistrarea datelor. Dacă se suprascrie un bloc utilizând O_DIRECT|O_DSYNC, atunci XFS, în loc să curățe cache-ul, va efectua comanda de înregistrare FUA, în cazul în care dispozitivul o suportă. M-am convins de acest lucru folosind utilitarul blktrace în sistemul Linux 5.4/Ubuntu 20.04. Această abordare ar trebui să fie mai eficientă, deoarece folosește o cantitate minimă de date scrisă pe disc și aplică o singură operație, nu două (înregistrare și curățare cache). Am găsit un link la nucleul anului 2018, în care este implementat acest mecanism. Există o discuție despre aplicarea acestei optimizări și în alte sisteme de fișiere, dar, din câte știu eu, XFS este singurul sistem de fișiere care suportă asta până acum.
Funcția sync_file_range()
În Linux există un apel de sistem , care permite curățarea pe disc doar a unei părți din fișier, nu a întregului fișier. Acest apel inițiază o curățare asynchronică a datelor și nu așteaptă finalizarea acesteia. Dar în documentația sync_file_range() se spune că această comandă este „foarte periculoasă”. Nu este recomandat să fie folosită. Caracteristicile și pericolele sync_file_range() sunt foarte bine descrise în material. În special, se pare că acest apel folosește RocksDB pentru a gestiona momentul în care nucleul curăță datele „murdare” pe disc. Dar, în același timp, din nou, pentru a asigura stocarea robustă a datelor, se folosește și fdatasync(). În RocksDB are comentarii interesante pe această temă. De exemplu, se pare că apelul sync_file_range() atunci când este utilizat cu ZFS nu duce la curățarea datelor pe disc. Experiența îmi spune că codul care este folosit rar, poate conține erori. De aceea, aș recomanda să nu se folosească acest apel de sistem fără necesitate extremă.
Apelurile de sistem care ajută la asigurarea stocării robuste a datelor
Am ajuns la concluzia că pentru efectuarea operațiunilor de intrare/ieșire care asigură stocarea persistentă a datelor, se pot folosi trei abordări. Toate necesită apelarea funcției fsync() pentru directorul în care a fost creat fișierul. Iată aceste abordări:
- Apelarea funcției
fdatasync()saufsync()după funcțiewrite()(este recomandat să folosițifdatasync()). - Lucrul cu un descriptor de fișier deschis cu firul
O_DSYNCsauO_SYNC(este recomandat – cu firulO_DSYNC). - Utilizarea comenzii
pwritev2()cu flag-ulRWF_DSYNCsauRWF_SYNC(preferabil – cu firulRWF_DSYNC).
Note despre performanță
Nu m-am ocupat de măsurarea detaliată a performanței diverselor mecanisme investigate de mine. Diferențele observate în viteza lor de operare sunt destul de mici. Acest lucru înseamnă că aș putea greși, iar în alte condiții același lucru poate arăta alte rezultate. Mai întâi voi vorbi despre ceea ce influențează mai mult performanța, apoi despre ceea ce influențează mai puțin performanța.
- Rescrierea datelor din fișier este mai rapidă decât adăugarea de date la fișier (câștigul de performanță poate varia între 2-100%). Adăugarea de date la fișier necesitând modificări suplimentare în metadatele fișierului, chiar și după apelul de sistem
fallocate(), dar amploarea acestui efect poate varia. Recomand, pentru a asigura cea mai bună performanță, să apelațifallocate()pentru alocarea anticipată a spațiului necesar. Apoi, acest spațiu trebuie umplut explicit cu zerouri și apelatfsync(). Datorită acestui lucru, blocurile corespunzătoare din sistemul de fișiere vor fi etichetate ca „alocate”, și nu ca „nealocate”. Aceasta oferă o mică (aproximativ 2%) îmbunătățire a performanței. În plus, la unele discuri, prima operațiune de acces la un bloc poate fi mai lentă decât altele. Aceasta înseamnă că umplerea spațiului cu zerouri poate duce la o îmbunătățire semnificativă (aproximativ 100%) a performanței. În special, acest lucru poate apărea cu discurile (acestea sunt date neoficiale, nu am reușit să le confirm). Același lucru se aplică și stocărilor (și aceasta este deja o informație oficială, confirmată prin teste). Alți specialiști au făcut observații similare Cu cât sunt mai puține apeluri de sistem - cu atât mai mare este performanța (câștigul poate fi de aproximativ 5%). Se pare că apelul - sau apelul
open()cu flag-ulO_DSYNCeste mai rapid decât apelulpwritev2()cu flag-ulRWF_SYNCmai rapid decât apelulfdatasync()Suspectez că aceasta este legată de faptul că, prin această abordare, rolul îl joacă faptul că pentru a rezolva aceeași problemă este nevoie de mai puține apeluri de sistem (o singură apelare în loc de două). Dar diferența de performanță este foarte mică, așa că puteți să nu îi acordați atenție și să folosiți în aplicație ceea ce nu va duce la complicarea logicii acesteia.
Dacă sunteți interesat de tema stocării durabile a datelor, iată câteva materiale utile:
- — o privire de ansamblu asupra principiilor de funcționare ale mecanismelor de intrare/ieșire.
- — o poveste despre ce se întâmplă cu datele pe drumul de la aplicație la disc.
- — un răspuns la întrebarea despre când ar trebui să aplicați
fsync()pentru directoare. Pe scurt, aceasta trebuie făcută atunci când creați un nou fișier, iar motivul acestei recomandări este că în Linux pot exista multe linkuri către același fișier. - — aici este descris modul în care stocarea durabilă a datelor este implementată în SQL Server pe platforma Linux. Există câteva comparații interesante între apelurile de sistem Windows și Linux. Sunt aproape sigur că datorită acestui material am aflat despre optimizarea FUA XFS.
Ați pierdut vreodată date pe care le considerați stocate în siguranță pe disc?
Sursa: habr.com
