Trajna pohrana podataka i Linux File API-ji

Ja sam se, istražujući stabilnost pohrane podataka u cloud sustavima, odlučio testirati, kako bih bio siguran da razumijem osnovne stvari. ja započeo čitanjem NVMe specifikacije kako bismo razumjeli kakva nam jamstva u pogledu postojanosti podataka (tj. jamstva da će podaci biti dostupni nakon kvara sustava) daju NMVe diskovi. Donio sam sljedeće glavne zaključke: morate uzeti u obzir podatke oštećene od trenutka kada je dana naredba za pisanje podataka, pa do trenutka kada su zapisani na medij za pohranu. 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 datoteka Linuxa. Čini se da bi ovdje sve trebalo biti jednostavno: program poziva naredbu write(), a nakon završetka operacije ove naredbe podaci će biti sigurno pohranjeni na disku. Ali write() samo kopira podatke aplikacije u predmemoriju kernela koja se nalazi u RAM-u. Da bi se sustav prisilio da zapisuje podatke na disk, moraju se koristiti neki dodatni mehanizmi.

Trajna pohrana podataka i Linux File API-ji

Općenito, ovaj materijal je skup bilješki koje se odnose na ono što sam naučio o temi koja me zanima. Ako vrlo kratko govorimo o najvažnijem, ispada da za organiziranje održive pohrane podataka morate koristiti naredbu fdatasync() ili otvorite datoteke s oznakom O_DSYNC. Ako vas zanima više o tome što se događa s podacima na putu od koda do diska, pogledajte ovo članak.

Značajke korištenja funkcije write().

Poziv sustava write() definiran u standardu IEEE POSIX kao pokušaj pisanja podataka u deskriptor datoteke. Nakon uspješno obavljenog posla write() operacije čitanja podataka moraju vratiti točno one bajtove koji su prethodno zapisani, čineći to čak i ako se podacima pristupa iz drugih procesa ili niti (ovdje odgovarajući dio standarda POSIX). Ovdje, u odjeljku o interakciji niti s normalnim operacijama datoteka, postoji napomena koja kaže da ako dvije niti pozivaju ove funkcije, tada svaki poziv mora ili vidjeti sve naznačene posljedice do kojih dovodi izvršenje drugog poziva, ili uopće ne vidim nikakve posljedice. To dovodi do zaključka da sve I/O operacije datoteka moraju zaključati resurs na kojem se radi.

Znači li to da operacija write() je atomski? S tehničkog gledišta, da. Operacije čitanja podataka moraju vratiti sve ili ništa od onoga što je zapisano write(). Ali operacija write(), sukladno standardu, ne mora završiti, nakon što je zapisala sve što je zamoljena da napiše. Dopušteno je upisati samo dio podataka. Na primjer, mogli bismo imati dva toka od kojih svaki dodaje 1024 bajta datoteci opisanoj istim deskriptorom datoteke. Sa stajališta standarda, rezultat će biti prihvatljiv kada svaka od operacija pisanja može dodati samo jedan bajt u datoteku. Ove će operacije ostati atomske, ali nakon što završe, podaci koje zapisuju u datoteku bit će pomiješani. ovdje je vrlo zanimljiva rasprava o ovoj temi na Stack Overflowu.

funkcije fsync() i fdatasync().

Najlakši način za brisanje podataka na disk je pozivanje funkcije fsync(). Ova funkcija traži od operativnog sustava da premjesti sve modificirane blokove iz predmemorije na disk. To uključuje sve metapodatke datoteke (vrijeme pristupa, vrijeme izmjene datoteke i tako dalje). Vjerujem da su ovi metapodaci rijetko potrebni, pa ako znate da vam nisu važni, možete koristiti funkciju fdatasync(). U Pomozite na fdatasync() stoji da se tijekom rada ove funkcije na disk sprema tolika količina metapodataka koja je "potrebna za ispravno izvođenje sljedećih operacija čitanja podataka." A to je upravo ono što brine većinu aplikacija.

Jedan problem koji se ovdje može pojaviti jest da ti mehanizmi ne jamče da se datoteka može pronaći nakon mogućeg kvara. Konkretno, kada se kreira nova datoteka, treba pozvati fsync() za imenik koji ga sadrži. U suprotnom, nakon pada, može se ispostaviti da ta datoteka ne postoji. Razlog tome 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 ispražnjeni na disk (ovdje možete pročitati više o ovome). Čini se da ext4 datotečni sustav može automatsko primijeniti fsync() u direktorije koji sadrže odgovarajuće datoteke, ali to možda nije slučaj s drugim datotečnim sustavima.

Ovaj mehanizam može se različito implementirati u različitim datotečnim sustavima. Koristio sam blktrace kako biste saznali koje se diskovne operacije koriste u ext4 i XFS datotečnim sustavima. Oba izdaju uobičajene naredbe za pisanje na disk i za sadržaj datoteka i za dnevnik datotečnog sustava, ispiru predmemoriju i izađu izvođenjem FUA (Force Unit Access, pisanje podataka izravno na disk, zaobilazeći predmemoriju) pisanja u dnevnik. Vjerojatno čine upravo to kako bi potvrdili činjenicu transakcije. Na pogonima koji ne podržavaju FUA, ovo uzrokuje dva ispiranja predmemorije. Moji eksperimenti su to pokazali fdatasync() malo brže fsync(). Korisnost blktrace ukazuje na to fdatasync() obično piše manje podataka na disk (u ext4 fsync() piše 20 KiB, i fdatasync() - 16 KiB). Također, saznao sam da je XFS nešto brži od ext4. I ovdje uz pomoć blktrace to uspio saznati fdatasync() ispire manje podataka na disk (4 KiB u XFS-u).

Dvosmislene situacije pri korištenju fsync()

Mogu se sjetiti tri dvosmislene situacije u vezi fsync()na koje sam naišao u praksi.

Prvi takav incident dogodio se 2008. godine. U to se vrijeme sučelje Firefoxa 3 "smrznulo" ako se velik broj datoteka zapisivao na disk. Problem je bio u tome što je implementacija sučelja koristila SQLite bazu podataka za pohranu informacija o svom stanju. Nakon svake promjene koja se dogodila u sučelju, funkcija je pozivana fsync(), što je dalo dobra jamstva stabilne pohrane podataka. U tada korištenom ext3 datotečnom sustavu, funkcija fsync() isprao na disk sve "prljave" stranice u sustavu, a ne samo one koje su se odnosile na odgovarajuću datoteku. To je značilo da klik na gumb u Firefoxu može uzrokovati zapisivanje megabajta podataka na magnetski disk, što može potrajati nekoliko sekundi. Rješenje problema, koliko sam shvatio iz to materijala, bio je preseliti rad s bazom podataka na asinkrone pozadinske zadatke. To znači da je Firefox prije implementirao strože zahtjeve postojanosti pohrane nego što je stvarno bilo potrebno, a značajke ext3 datotečnog sustava samo su pogoršale ovaj problem.

Drugi problem dogodio se 2009. godine. Zatim, nakon pada sustava, korisnici novog ext4 datotečnog sustava otkrili su da su mnoge novostvorene datoteke nulte duljine, ali to se nije dogodilo sa starijim ext3 datotečnim sustavom. U prethodnom odlomku sam govorio o tome kako je ext3 izbacio previše podataka na disk, što je dosta usporilo rad. fsync(). Kako bi poboljšao situaciju, ext4 ispire samo one "prljave" stranice koje su relevantne za određenu datoteku. A podaci drugih datoteka ostaju u memoriji mnogo dulje nego kod ext3. To je učinjeno kako bi se poboljšala izvedba (prema zadanim postavkama podaci ostaju u ovom stanju 30 sekundi, to 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 trebaju osigurati stabilnu pohranu podataka i maksimalno ih zaštititi od posljedica kvarova. Funkcija fsync() radi mnogo učinkovitije s ext4 nego s ext3. Nedostatak ovog pristupa je što njegova uporaba, kao i prije, usporava neke operacije, poput instaliranja programa. Vidi detalje o tome здесь и здесь.

Treći problem u vezi fsync(), nastao 2018. godine. Tada je u okviru projekta PostgreSQL ustanovljeno da ako funkcija fsync() naiđe na pogrešku, označava "prljave" stranice kao "čiste". Kao rezultat toga, sljedeći pozivi fsync() ne radite ništa s takvim stranicama. Zbog toga se modificirane stranice pohranjuju u memoriju i nikad se ne zapisuju na disk. Ovo je prava katastrofa, jer će aplikacija misliti da su neki podaci zapisani na disk, a zapravo neće biti. Takvi neuspjesi fsync() 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 se ruše. Ovdje, u članku "Mogu li se aplikacije oporaviti od fsync kvarova?", ovaj problem je detaljno istražen. Trenutno najbolje rješenje za ovaj problem je korištenje Direct I/O sa zastavom O_SYNC ili sa zastavom O_DSYNC. S ovim pristupom, sustav će prijaviti greške koje se mogu pojaviti prilikom izvođenja specifičnih operacija pisanja podataka, ali ovaj pristup zahtijeva da aplikacija sama upravlja međuspremnicima. Pročitajte više o tome здесь и здесь.

Otvaranje datoteka pomoću oznaka O_SYNC i O_DSYNC

Vratimo se na raspravu o mehanizmima Linuxa koji omogućuju trajnu pohranu podataka. Naime, govorimo o korištenju zastave O_SYNC ili zastava O_DSYNC prilikom otvaranja datoteka pomoću sistemskog poziva otvorena(). S ovim pristupom, svaka operacija upisa podataka izvodi se kao nakon svake naredbe write() sustavu se daju, odnosno, naredbe fsync() и fdatasync(). U POSIX specifikacije to se zove "Sinkronizirano dovršenje integriteta I/O datoteke" i "Dovršenje integriteta podataka". Glavna prednost ovog pristupa je da se samo jedan sistemski poziv mora izvršiti kako bi se osigurao integritet podataka, a ne dva (na primjer − write() и fdatasync()). Glavni nedostatak ovog pristupa je da će sve operacije pisanja pomoću odgovarajućeg deskriptora datoteke biti sinkronizirane, što može ograničiti mogućnost strukturiranja koda aplikacije.

Korištenje izravnog I/O s oznakom O_DIRECT

Poziv sustava open() podržava zastavu O_DIRECT, koji je dizajniran za zaobilaženje predmemorije operativnog sustava, obavljanje I / O operacija, u izravnoj interakciji s diskom. To u mnogim slučajevima znači da će naredbe pisanja koje izdaje program biti izravno prevedene u naredbe usmjerene na rad s diskom. No, općenito, ovaj mehanizam nije zamjena za funkcije fsync() ili fdatasync(). Činjenica je da sam disk može kašnjenje ili predmemorija odgovarajuće naredbe za pisanje podataka. I, još gore, u nekim posebnim slučajevima, I/O operacije se izvode kada se koristi zastavica O_DIRECT, emitirati u tradicionalne međuspremničke 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 nakon svake operacije pisanja uslijediti poziv fdatasync().

Ispostavilo se da je datotečni sustav XFS nedavno dodao "brzi put" za O_DIRECT|O_DSYNC- zapisi podataka. Ako je blok prebrisan korištenjem O_DIRECT|O_DSYNC, tada će XFS, umjesto ispiranja predmemorije, izvršiti naredbu FUA pisanja ako je uređaj podržava. Potvrdio sam ovo pomoću uslužnog programa blktrace na sustavu Linux 5.4/Ubuntu 20.04. Ovaj bi pristup trebao biti učinkovitiji, budući da zapisuje minimalnu količinu podataka na disk i koristi jednu operaciju, a ne dvije (upisivanje i ispiranje predmemorije). Našao sam poveznicu na zakrpa 2018 kernel koji implementira ovaj mehanizam. Postoje neke rasprave o primjeni ove optimizacije na druge datotečne sustave, ali koliko ja znam, XFS je jedini datotečni sustav koji to do sada podržava.

funkcija sync_file_range().

Linux ima sistemski poziv sync_file_range(), koji vam omogućuje brisanje samo dijela datoteke na disk, a ne cijele datoteke. Ovaj poziv pokreće asinkrono ispiranje i ne čeka da se završi. Ali u pozivanju na sync_file_range() za ovu se naredbu kaže da je "vrlo opasna". Nije preporučljivo koristiti ga. Značajke i opasnosti sync_file_range() vrlo dobro opisano u ovo materijal. Točnije, čini se da ovaj poziv koristi RocksDB za kontrolu kada kernel ispire prljave podatke na disk. Ali u isto vrijeme, kako bi se osigurala stabilna pohrana podataka, također se koristi fdatasync(). U kodirati RocksDB ima neke zanimljive komentare na ovu temu. Na primjer, izgleda kao poziv sync_file_range() kada koristi ZFS ne ispire podatke na disk. Iskustvo mi govori da kod koji se rijetko koristi može sadržavati greške. Stoga bih savjetovao da ne koristite ovaj sistemski poziv osim ako to nije apsolutno neophodno.

Pozivi sustava koji pomažu u osiguravanju postojanosti podataka

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

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

Napomene o izvedbi

Nisam pažljivo mjerio performanse različitih mehanizama koje sam istraživao. Razlike koje sam uočio u brzini njihovog rada su vrlo male. To znači da mogu biti u krivu i da u drugim uvjetima ista stvar može pokazati drugačije rezultate. Prvo ću govoriti o tome što više utječe na performanse, a zatim o tome što manje utječe na performanse.

  1. Prepisivanje podataka datoteke brže je od dodavanja podataka datoteci (dobitak izvedbe može biti 2-100%). Prilaganje podataka datoteci zahtijeva dodatne promjene metapodataka datoteke, čak i nakon poziva sustava fallocate(), ali veličina ovog učinka može varirati. Preporučam, za najbolju izvedbu, nazvati fallocate() unaprijed dodijeliti potreban prostor. Zatim se ovaj prostor mora eksplicitno ispuniti nulama i pozvati fsync(). To će uzrokovati da odgovarajući blokovi u datotečnom sustavu budu označeni kao "dodijeljeni" umjesto kao "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 ispunjavanje prostora nulama može dovesti do značajnog (oko 100%) poboljšanja performansi. Konkretno, to se može dogoditi s diskovima. AWS EBS (ovo su neslužbeni podaci, nisam ih mogao potvrditi). Isto vrijedi i za skladištenje. GCP trajni disk (i to je već službena informacija, potvrđena testovima). Drugi su stručnjaci učinili isto promatranjevezane uz različite diskove.
  2. Što je manje sistemskih poziva, veća je izvedba (dobitak može biti oko 5%). Izgleda kao poziv open() sa zastavom O_DSYNC ili nazovite pwritev2() sa zastavom RWF_SYNC brži poziv fdatasync(). Pretpostavljam da je poanta ovdje u tome da kod ovog pristupa igra ulogu činjenica da se mora izvršiti manje sistemskih poziva za rješavanje istog zadatka (jedan poziv umjesto dva). Ali razlika u performansama je vrlo mala, tako da je lako možete zanemariti i koristiti nešto u aplikaciji što ne dovodi do kompliciranja njene logike.

Ako ste zainteresirani za temu održive pohrane podataka, evo nekoliko korisnih materijala:

  • I/O pristupne metode — pregled osnova ulazno/izlaznih mehanizama.
  • Osiguravanje da podaci dospiju na disk - priča o tome što se događa s podacima na putu od aplikacije do diska.
  • Kada trebate fsync direktorij koji sadrži - odgovor na pitanje kada se prijaviti fsync() za imenike. Ukratko, ispada da to morate učiniti kada stvarate 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 interno - ovdje je opis kako je trajna pohrana podataka implementirana u SQL Server na Linux platformi. Ovdje postoje neke zanimljive usporedbe između Windows i Linux sistemskih poziva. Gotovo sam siguran da sam zahvaljujući ovom materijalu saznao za FUA optimizaciju XFS-a.

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

Trajna pohrana podataka i Linux File API-ji

Trajna pohrana podataka i Linux File API-ji

Izvor: www.habr.com