Izdržljiva pohrana podataka i API-ji za Linux datoteke

Ja sam, istražujući stabilnost skladištenja podataka u cloud sistemima, odlučio da se testiram, da se uvjerim da razumijem osnovne stvari. I počeo čitanjem NVMe specifikacije da bismo razumeli koje garancije u vezi sa postojanošću podataka (tj. garancije da će podaci biti dostupni nakon kvara sistema) nam daju NMVe diskove. Donio sam sljedeće glavne zaključke: podatke morate smatrati oštećenim od trenutka kada je data komanda za upisivanje podataka, pa do trenutka kada su upisani na medij za skladištenje. Međutim, u većini programa sistemski pozivi se sasvim sigurno koriste za pisanje podataka.

U ovom članku istražujem mehanizme postojanosti koje pružaju API-ji za Linux datoteke. Čini se da bi ovdje sve trebalo biti jednostavno: program poziva naredbu write(), a nakon što se operacija ove naredbe završi, podaci će biti sigurno pohranjeni na disku. Ali write() kopira samo podatke aplikacije u keš kernel koji se nalazi u RAM-u. Da bi se sistem prisilio da zapiše podatke na disk, moraju se koristiti neki dodatni mehanizmi.

Izdržljiva pohrana podataka i API-ji za Linux datoteke

Generalno, ovaj materijal je skup bilješki koje se odnose na ono što sam naučio o temi koja me zanima. Ako vrlo ukratko govorimo o najvažnijem, ispada da je za organiziranje održivog skladištenja podataka potrebno koristiti naredbu fdatasync() ili otvorite fajlove sa zastavicom O_DSYNC. Ako ste zainteresovani da saznate više o tome šta se dešava sa podacima na putu od koda do diska, pogledajte ovo članak.

Značajke korištenja funkcije write().

Sistemski poziv write() definisano u standardu IEEE POSIX kao pokušaj upisivanja podataka u deskriptor datoteke. Nakon uspješno završenog posla write() operacije čitanja podataka moraju vratiti točno one bajtove koji su prethodno napisani, čak i ako se podacima pristupa iz drugih procesa ili niti (Evo odgovarajući odeljak POSIX standarda). to je, u odjeljku o interakciji niti s normalnim operacijama datoteka, postoji napomena koja kaže da ako dvije niti pozivaju ove funkcije, onda svaki poziv mora ili vidjeti sve naznačene posljedice do kojih vodi izvršenje drugog poziva, ili ne vidim uopšte nikakve posledice. Ovo dovodi do zaključka da sve I/O operacije datoteke moraju držati zaključavanje na resursu na kojem se radi.

Da li to znači da je operacija write() je atomski? Sa tehničke tačke gledišta, da. Operacije čitanja podataka moraju vratiti ili sve ili ništa od onoga što je napisano write(). Ali operacija write(), u skladu sa standardom, ne mora završiti, nakon što je zapisala sve što je od nje zatraženo da zapiše. Dozvoljeno je upisivanje samo dijela podataka. Na primjer, možemo imati dva toka koji svaki dodaju 1024 bajta datoteci opisanoj istim deskriptorom datoteke. Sa stanovišta standarda, rezultat će biti prihvatljiv kada svaka od operacija pisanja može dodati samo jedan bajt datoteci. Ove operacije će ostati atomske, ali nakon što se završe, podaci koje zapišu u datoteku će biti zbrkani. ovdje vrlo zanimljiva rasprava o ovoj temi na Stack Overflowu.

funkcije fsync() i fdatasync().

Najlakši način za izbacivanje podataka na disk je pozivanje funkcije fsync(). Ova funkcija traži od operativnog sistema da premjesti sve modificirane blokove iz keša na disk. Ovo uključuje sve metapodatke datoteke (vrijeme pristupa, vrijeme izmjene datoteke, itd.). Vjerujem da su ovi metapodaci rijetko potrebni, pa ako znate da vam nisu važni, možete koristiti funkciju fdatasync(). The pomoć na fdatasync() stoji da se tokom rada ove funkcije na disk pohranjuje tolika količina metapodataka, koja je "neophodna za ispravno izvršenje sljedećih operacija čitanja podataka". A to je upravo ono o čemu većina aplikacija brine.

Jedan problem koji se ovdje može pojaviti je taj što ovi mehanizmi ne garantuju da će datoteka biti pronađena nakon mogućeg kvara. Konkretno, kada se kreira nova datoteka, treba pozvati fsync() za direktorij koji ga sadrži. U suprotnom, nakon pada, može se ispostaviti da ovaj fajl ne postoji. Razlog za to je što pod UNIX-om, zbog upotrebe tvrdih veza, datoteka može postojati u više direktorija. Stoga, prilikom poziva fsync() ne postoji način da datoteka zna koji podaci direktorija također trebaju biti prebačeni na disk (ovdje možete pročitati više o tome). Izgleda da je ext4 sistem datoteka sposoban automatski da se prijave fsync() na direktorije koji sadrže odgovarajuće datoteke, ali to možda nije slučaj s drugim sistemima datoteka.

Ovaj mehanizam se može različito implementirati u različitim sistemima datoteka. koristio sam blktrace da saznate koje se operacije diska koriste u ext4 i XFS sistemima datoteka. Oba izdaju uobičajene komande za pisanje na disk i za sadržaj datoteka i za dnevnik sistema datoteka, isprazne keš memoriju i izađu izvođenjem FUA (prisilni pristup jedinici, pisanje podataka direktno na disk, zaobilazeći keš) upisivanje u dnevnik. Vjerovatno to rade samo da bi potvrdili činjenicu transakcije. Na diskovima koji ne podržavaju FUA, ovo uzrokuje dva ispiranja keša. Moji eksperimenti su to pokazali fdatasync() malo brže fsync(). Utility blktrace ukazuje na to fdatasync() obično zapisuje manje podataka na disk (u ext4 fsync() piše 20 KiB, i fdatasync() - 16 KiB). Takođe, saznao sam da je XFS nešto brži od ext4. I ovdje uz pomoć blktrace mogao to da sazna fdatasync() ispušta manje podataka na disk (4 KiB u XFS).

Dvosmislene situacije kada se koristi fsync()

Mogu se sjetiti tri nejasne situacije fsync()na koje sam se susreo u praksi.

Prvi takav incident dogodio se 2008. U to vrijeme, Firefox 3 interfejs se „zamrznuo“ ako se veliki broj datoteka upisuje na disk. Problem je bio u tome što je implementacija interfejsa koristila SQLite bazu podataka za skladištenje informacija o svom stanju. Nakon svake promjene koja se dogodila u sučelju, funkcija je bila pozvana fsync(), što je dalo dobre garancije stabilnog skladištenja podataka. U tada korištenom ext3 sistemu datoteka, funkcija fsync() isprati na disk sve "prljave" stranice u sistemu, a ne samo one koje se odnose na odgovarajući fajl. To je značilo da bi klik na dugme u Firefox-u mogao uzrokovati zapisivanje megabajta podataka na magnetni disk, što bi moglo potrajati mnogo sekundi. Rješenje problema, koliko sam shvatio to materijal, bio je da se rad sa bazom podataka prebaci na asinkrone pozadinske zadatke. To znači da je Firefox implementirao strože zahtjeve za postojanost pohrane nego što je zaista bilo potrebno, a funkcije ext3 sistema datoteka samo su pogoršale ovaj problem.

Drugi problem se desio 2009. godine. Zatim, nakon pada sistema, korisnici novog ext4 sistema datoteka otkrili su da su mnoge novokreirane datoteke nulte dužine, ali se to nije dogodilo sa starijim ext3 sistemom datoteka. U prethodnom pasusu sam govorio o tome kako je ext3 izbacio previše podataka na disk, što je dosta usporilo stvari. fsync(). Da bi poboljšao situaciju, ext4 ispušta samo one "prljave" stranice koje su relevantne za određeni fajl. I podaci drugih datoteka ostaju u memoriji mnogo duže nego kod ext3. Ovo je učinjeno radi poboljšanja performansi (podaci ostaju u ovom stanju 30 sekundi, ovo možete konfigurirati pomoću dirty_expire_centisecs; ovdje možete pronaći više informacija o tome). To znači da velika količina podataka može biti nepovratno izgubljena nakon pada. Rješenje ovog problema je korištenje fsync() u aplikacijama koje treba da obezbede stabilno skladištenje podataka i zaštite ih što je više moguće od posledica kvarova. Funkcija fsync() radi mnogo efikasnije sa ext4 nego sa ext3. Nedostatak ovog pristupa je što njegova upotreba, kao i do sada, usporava neke operacije, kao što je instaliranje programa. Pogledajte detalje o ovome ovdje и ovdje.

Treći problem u vezi fsync(), nastao 2018. Zatim se u okviru PostgreSQL projekta saznalo da ako funkcija fsync() naiđe na grešku, "prljave" stranice označava kao "čiste". Kao rezultat, slijedeći pozivi fsync() ne radite ništa sa takvim stranicama. Zbog toga se modificirane stranice pohranjuju u memoriju i nikada se ne zapisuju na disk. Ovo je prava katastrofa, jer će aplikacija misliti da su neki podaci zapisani na disk, ali u stvari to neće biti. Takvi neuspjesi fsync() su rijetke, primjena u takvim situacijama ne može učiniti gotovo ništa u borbi protiv problema. Ovih dana, kada se to dogodi, PostgreSQL i druge aplikacije padaju. to je, u članku "Mogu li se aplikacije oporaviti od fsync grešaka?", ovaj problem je detaljno istražen. Trenutno najbolje rješenje za ovaj problem je korištenje Direct I/O sa zastavicom O_SYNC ili sa zastavom O_DSYNC. Sa ovim pristupom, sistem će prijaviti greške koje se mogu pojaviti prilikom izvođenja specifičnih operacija pisanja podataka, ali ovaj pristup zahtijeva da aplikacija sama upravlja baferima. Pročitajte više o tome ovdje и ovdje.

Otvaranje datoteka pomoću oznaka O_SYNC i O_DSYNC

Vratimo se diskusiji o Linux mehanizmima koji obezbeđuju trajno skladištenje podataka. Naime, govorimo o korištenju zastave O_SYNC ili zastavu O_DSYNC prilikom otvaranja datoteka pomoću sistemskog poziva otvori(). Sa ovim pristupom, svaka operacija pisanja podataka se izvodi kao nakon svake naredbe write() sistemu su date, odnosno komande fsync() и fdatasync(). The POSIX specifikacije ovo se zove "Sinhronizovani završetak integriteta I/O datoteke" i "Dovršetak integriteta podataka". Glavna prednost ovog pristupa je da je potrebno izvršiti samo jedan sistemski poziv da bi se osigurao integritet podataka, a ne dva (na primjer − write() и fdatasync()). Glavni nedostatak ovog pristupa je taj što će sve operacije pisanja koristeći odgovarajući deskriptor datoteke biti sinkronizirane, što može ograničiti mogućnost strukturiranja koda aplikacije.

Korištenje Direct I/O sa O_DIRECT zastavicom

Sistemski poziv open() podržava zastavu O_DIRECT, koji je dizajniran da zaobiđe keš memoriju operativnog sistema, izvrši I/O operacije, u direktnoj interakciji s diskom. To u mnogim slučajevima znači da će naredbe za pisanje koje izdaje program biti direktno prevedene u naredbe za rad s diskom. Ali, općenito, ovaj mehanizam nije zamjena za funkcije fsync() ili fdatasync(). Činjenica je da sam disk može kašnjenje ili keširanje odgovarajuće komande za pisanje podataka. I, što je još gore, u nekim posebnim slučajevima, I/O operacije se izvode kada se koristi zastavica O_DIRECT, emitovanje u tradicionalne baferske operacije. Najlakši način za rješavanje ovog problema je korištenje zastavice za otvaranje datoteka O_DSYNC, što će značiti da će svaka operacija pisanja biti praćena pozivom fdatasync().

Ispostavilo se da je XFS sistem datoteka nedavno dodao "brzi put" za O_DIRECT|O_DSYNC- evidenciju podataka. Ako je blok prepisan upotrebom O_DIRECT|O_DSYNC, tada će XFS, umjesto ispiranja keša, izvršiti FUA naredbu pisanja ako je uređaj podržava. Ovo sam potvrdio pomoću uslužnog programa blktrace na sistemu Linux 5.4/Ubuntu 20.04. Ovaj pristup bi trebao biti efikasniji, jer zapisuje minimalnu količinu podataka na disk i koristi jednu operaciju, a ne dvije (upisivanje i ispiranje keša). Našao sam link do patch 2018 kernel koji implementira ovaj mehanizam. Postoji neka rasprava o primjeni ove optimizacije na druge sisteme datoteka, ali koliko ja znam, XFS je jedini sistem datoteka koji ga podržava do sada.

sync_file_range() funkcija

Linux ima sistemski poziv sync_file_range(), što vam omogućava da ispraznite samo dio datoteke na disk, a ne cijelu datoteku. Ovaj poziv pokreće asinkrono ispiranje i ne čeka da se završi. Ali u referenci na sync_file_range() za ovu naredbu se kaže da je "veoma opasna". Nije preporučljivo koristiti ga. Karakteristike i opasnosti sync_file_range() vrlo dobro opisano u ovo materijal. Konkretno, čini se da ovaj poziv koristi RocksDB za kontrolu kada kernel izbaci "prljave" podatke na disk. Ali u isto vrijeme tamo, kako bi se osiguralo stabilno skladištenje podataka, također se koristi fdatasync(). The kod RocksDB ima nekoliko zanimljivih komentara na ovu temu. Na primjer, izgleda kao poziv sync_file_range() kada koristite ZFS ne ispušta podatke na disk. Iskustvo mi govori da rijetko korišteni kod može sadržavati greške. Stoga bih savjetovao da ne koristite ovaj sistemski poziv osim ako nije apsolutno neophodno.

Sistemski pozivi kako bi se osigurala postojanost podataka

Došao sam do zaključka da postoje tri pristupa koja se mogu koristiti za izvođenje trajnih I/O operacija. Svi oni zahtijevaju poziv funkcije fsync() za direktorij u kojem je datoteka kreirana. Ovo su pristupi:

  1. Poziv funkcije fdatasync() ili fsync() nakon funkcije write() (bolje koristiti fdatasync()).
  2. Rad sa deskriptorom datoteke otvorenim sa zastavicom O_DSYNC ili O_SYNC (bolje - sa zastavom O_DSYNC).
  3. Upotreba komandi pwritev2() sa zastavom RWF_DSYNC ili RWF_SYNC (po mogućnosti sa zastavom RWF_DSYNC).

Bilješke o performansama

Nisam pažljivo mjerio performanse različitih mehanizama koje sam istraživao. Razlike koje sam primijetio u brzini njihovog rada su vrlo male. To znači da mogu pogriješiti i da u drugim uslovima ista stvar može pokazati različite rezultate. Prvo ću govoriti o tome šta više utiče na performanse, a zatim o tome šta manje utiče na performanse.

  1. Prepisivanje podataka datoteke je brže od dodavanja podataka u datoteku (povećanje performansi može biti 2-100%). Prilaganje podataka datoteci zahtijeva dodatne promjene metapodataka datoteke, čak i nakon sistemskog poziva fallocate(), ali veličina ovog efekta može varirati. Za najbolje performanse preporučujem da pozovete fallocate() da unaprijed dodijelite potreban prostor. Tada se ovaj prostor mora eksplicitno popuniti nulama i pozvati fsync(). Ovo će uzrokovati da odgovarajući blokovi u sistemu datoteka budu označeni kao "dodijeljeni" umjesto "nedodijeljeni". Ovo daje malo (oko 2%) poboljšanje performansi. Također, neki diskovi mogu imati sporiju operaciju pristupa prvom bloku od drugih. To znači da popunjavanje prostora nulama može dovesti do značajnog (oko 100%) poboljšanja performansi. To se posebno može dogoditi s diskovima. AWS EBS (ovo su nezvanični podaci, nisam mogao da ih potvrdim). Isto važi i za skladištenje. GCP trajni disk (a ovo je već zvanična informacija, potvrđena testovima). I drugi stručnjaci su učinili isto posmatranjevezano za različite diskove.
  2. Što je manje sistemskih poziva, to su performanse veće (dobitak može biti oko 5%). Izgleda kao poziv open() sa zastavom O_DSYNC ili nazovi pwritev2() sa zastavom RWF_SYNC brži poziv fdatasync(). Pretpostavljam da je poenta ovdje u tome da s ovim pristupom igra ulogu činjenica da se manje sistemskih poziva mora izvršiti da bi se riješio isti zadatak (jedan poziv umjesto dva). Ali razlika u performansama je vrlo mala, pa je možete lako zanemariti i koristiti nešto u aplikaciji što ne dovodi do komplikacije njene logike.

Ako vas zanima tema održivog skladištenja podataka, evo nekoliko korisnih materijala:

  • I/O metode pristupa — pregled osnova ulazno/izlaznih mehanizama.
  • Osiguravanje da podaci dođu do diska - priča o tome šta se dešava sa podacima na putu od aplikacije do diska.
  • Kada biste trebali fsinkronizirati direktorij koji sadrži - odgovor na pitanje kada se prijaviti fsync() za imenike. Ukratko, ispada da to morate učiniti kada kreirate novu datoteku, a razlog za ovu preporuku je taj što u Linuxu može postojati mnogo referenci na istu datoteku.
  • SQL Server na Linuxu: FUA Internals - evo opisa kako je trajno skladištenje podataka implementirano u SQL Server na Linux platformi. Ovdje su neke zanimljive poređenja između Windows i Linux sistemskih poziva. Gotovo sam siguran da sam zahvaljujući ovom materijalu naučio o FUA optimizaciji XFS-a.

Jeste li ikada izgubili podatke za koje ste mislili da su sigurno pohranjeni na disku?

Izdržljiva pohrana podataka i API-ji za Linux datoteke

Izdržljiva pohrana podataka i API-ji za Linux datoteke

izvor: www.habr.com