Istražujući održivost pohrane podataka u oblaku, odlučio sam provjeriti svoje znanje i uvjeriti se da razumijem osnovne stvari. Ja kako bih razumio koje nam garancije vezane za održivu pohranu podataka (tj. garancije da će podaci biti dostupni nakon sustavnog kvara) pružaju NMVe diskovi. Moji glavni zaključci su sljedeći: podaci se moraju smatrati oštećenima od trenutka kada se izdaje naredba za zapis podataka, do trenutka kada završi zapis na medij. Međutim, u većini programa za zapis podataka, sustavni pozivi se koriste bez problema.
U ovom materijalu ispitat ću mehanizme održive pohrane podataka koje pružaju Linuxove datotečne API-je. Čini se da bi to ovdje trebalo biti jednostavno: program izvršava naredbu write(), a nakon završetka rada te naredbe, podaci će biti pouzdano pohranjeni na disku. No write() samo kopira podatke aplikacije u keš jezgre smješten u radnoj memoriji. Da bi se prisilila sustav na zapis podataka na disk, potrebno je koristiti neke dodatne mehanizme.
U cjelini, ovaj materijal predstavlja skup bilješki o tome što sam saznao o temi koja me zanima. Ako bih vrlo kratko rekao najvažnije, to bi bilo da za organizaciju održive pohrane podataka treba koristiti naredbu fdatasync() ili otvoriti datoteke s oznakom O_DSYNC. Ako vas zanima što se događa s podacima na putu od programskog koda do diska, pogledajte članak.
Osobitosti korištenja funkcije write()
Sistem poziv write() definiran je u standardu kao pokušaj zapisivanja podataka u datotečni deskriptor. Nakon uspješnog završetka write() operacije čitanja podaci moraju vraćati upravo one bajtove koji su prethodno zapisani, čineći to čak i kada se podacima pristupa iz drugih procesa ili niti ( odgovarajući dio standarda POSIX). , u odjeljku posvećenom interakciji tokova s običnim datotečnim operacijama, postoji napomena koja kaže da ako svaki od dva toka poziva te funkcije, svaki poziv treba vidjeti ili sve označene posljedice koje proizlaze iz izvršenja drugog poziva, ili ne vidjeti nikakve posljedice. To omogućuje zaključak da sve datotečne operacije ulaza/izlaza moraju zadržati zaključavanje resursa s kojim rade.
Znači li to da je operacija write() atomarna? Tehnički gledano — da. Operacije čitanja podataka trebaju vraćati ili sve, ili ništa od onoga što je zapisano pomoću write(). No, operacija write(), prema standardu, ne mora nužno završiti zapisivanjem svega što je bila odobrena za zapisivanje. Dozvoljeno joj je zapisati samo dio podataka. Na primjer, možemo imati dva toka, svaki od kojih pridružuje 1024 bajta datoteci koja se opisuje istim deskriptorom datoteke. Iz perspektive standarda, rezultat u kojem svaka od operacija zapisivanja može pridružiti samo jedan bajt datoteci bit će prihvatljiv. Ove operacije ostat će atomske, ali nakon što završe, podaci koje su zapisali u datoteku bit će pomiješani. vrlo zanimljiva rasprava na ovu temu na Stack Overflow-u.
Funkcije fsync() i fdatasync()
Najjednostavniji način za isključivanje podataka na disk je pozivanje funkcije . Ova funkcija traži od operativnog sustava da prenese sve modificirane blokove iz predmemorije na disk. To uključuje i sve metapodatke datoteke (vrijeme pristupa, vrijeme modifikacije datoteke itd.). Smatram da je potreba za tim metapodacima rijetka, stoga, ako znate da vam nisu važni, možete koristiti funkciju fdatasync(). U o fdatasync() navodi da tijekom rada ove funkcije dolazi do spremanja na disk tog volumena metapodataka koji je „neophodan za pravilno izvršavanje sljedećih operacija čitanja podataka”. A to je ono što brine većinu aplikacija.
Jedan od problema koji ovdje može nastati jest to što ti mehanizmi ne jamče da će datoteka biti otkrivena nakon mogućeg kvara. Konkretno, kada se stvara nova datoteka, potrebno je pozvati fsync() za direktorij, koji ga sadrži. Inače, nakon kvara može se dogoditi da ovaj datoteka ne postoji. Razlog za to je što u UNIX-u, zbog korištenja tvrdih veza, datoteka može postojati u više direktorija. Stoga prilikom pozivanja fsync() za datoteku nema načina da sazna koja točno direktorija također treba zapisati na disk ( o tome se može pročitati detaljnije). Čini se da datotečni sustav ext4 može se primjenjivati fsync() do direktorija koji sadrže odgovarajuće datoteke, ali u slučaju drugih datotečnih sustava to možda nije tako.
Omekanik može biti različito implementiran u različitim datotečnim sustavima. Koristio sam da saznam koje diskovne operacije se koriste u datotečnim sustavima ext4 i XFS. I jedan i drugi daju uobičajene naredbe za zapis na disk i za sadržaj datoteka, i za dnevnik datotečnog sustava, ispuštaju keš i završavaju rad, izvršavajući FUA-zapis (Force Unit Access, zapis podataka izravno na disk, mimo keša) u dnevnik. Vjerojatno to čine kako bi potvrdili činjenicu da se operacija dogodila. Na diskovima koji ne podržavaju FUA, to uzrokuje dva ispisa keša. Moji eksperimenti su pokazali da fdatasync() malo brže fsync(). Alat blktrace ukazuje na to da fdatasync() obično piše manje podataka na disk (u ext4 fsync() zapisuje 20 KiB, a fdatasync() — 16 KiB). Osim toga, otkrio sam da je XFS malo brži od ext4. A ovdje je uz pomoć blktrace uspjelo saznati da fdatasync() ispisuje manje podataka na disk (4 KiB u XFS).
Nejasne situacije koje nastaju prilikom korištenja fsync()
Mogu se sjetiti tri nejasne situacije koje se odnose fsync(), s kojima sam se susreo u praksi.
Prvi takav slučaj dogodio se 2008. godine. Tada je sučelje Firefox 3 „zalegalo“ ako se izvođena zapisa na disk velikog broja datoteka. Problem je bila u tome što se u implementaciji sučelja za pohranu informacija o njegovom stanju koristila baza podataka SQLite. Nakon svake promjene koja se dogodila u sučelju, pozivala se funkcija fsync(), što je davalo dobra jamstva trajnog pohranjivanja podataka. U tada korištenom datotečnom sustavu ext3 funkcija fsync() spremala je na disk sve "prljave" stranice u sustavu, a ne samo one koje su se odnosile na određenu datoteku. To je značilo da je klik na gumb u Firefoxu mogao inicirati zapis megabajta podataka na magnetski disk, što je moglo potrajati i nekoliko sekundi. Rješenju problema, koliko sam shvatio iz materijala, bilo je potrebno premjestiti rad s bazom podataka u asinkrone pozadinske zadatke. To znači da su u Firefoxu prije bili implementirani rigorozniji zahtjevi za otpornost pohrane podataka nego što je to stvarno bilo potrebno, a osobine sustava datoteka ext3 su samo pogoršale ovaj problem.
Druga neusklađenost dogodila se 2009. godine. Tada su, nakon kvara sustava, korisnici nove datotečne sustave ext4 naišli na to da mnoge nedavno stvorene datoteke imaju nulu dužinu, a s starijom datotečnom sustavom ext3 to se nije dogodilo. U prethodnom odlomku govorio sam o tome da je ext3 na disk isporučivala previše podataka, što je znatno usporilo rad fsync(). Da bi se poboljšala situacija, ext4 na disk isporučuje samo one "prljave" stranice koje su vezane uz određenu datoteku. A podaci drugih datoteka ostaju u memoriji znatno duže nego kod primjene ext3. Ovo je učinjeno radi poboljšanja performansi (po defaultu, podaci ostaju u tom stanju 30 sekundi, to se može podesiti uz pomoć ; možete pronaći dodatne materijale o tome). To znači da se veliki volumen podataka može trajno izgubiti nakon kvara. Rješenje ovog problema leži u korištenju fsync() u aplikacijama koje trebaju osigurati otpornu pohranu podataka i maksimalno ih zaštititi od posljedica kvarova. Funkcija fsync() djeluje pri korištenju ext4 mnogo učinkovitije nego pri korištenju ext3. Minus ovog pristupa je da njegovo korištenje, kao i prije, usporava izvršenje nekih operacija, poput instalacije programa. Pojedinosti o tome pogledajte i .
Treći problem, koji se odnosi na fsync(), dogodio se 2018. godine. Tada, u okviru projekta PostgreSQL, otkriveno je da ako funkcija fsync() naleti na pogrešku, označava "prljave" stranice kao "čiste". Kao rezultat toga, sljedeći pozivi fsync() ništa se ne radi s takvim stranicama. Zbog toga modificirane stranice ostaju u memoriji i nikada se ne zapisuju na disk. To je prava katastrofa, jer će aplikacija smatrati da su neki podaci zapisani na disku, dok to zapravo neće biti tako. Takvi kvarovi fsync() javljaju se rijetko, aplikacija u takvim situacijama gotovo ništa ne može učiniti za borbu protiv problema. Danas, kada se to dogodi, PostgreSQL i druge aplikacije naglo završavaju s radom. , u materijalu „Mogu li aplikacije oporaviti se od fsync kvarova?“, ovaj problem se istražuje u svim detaljima. Trenutno je najbolje rješenje za ovaj problem korištenje Direct I/O s oznakom O_SYNC ili s oznakom O_DSYNC. Takvim pristupom sustav će prijaviti greške koje se mogu pojaviti tijekom izvršavanja određenih operacija zapisivanja podataka, ali ovaj pristup zahtijeva da aplikacija samostalno upravlja memorijskim bufferima. Više detalja o tome možete pročitati i .
Otvaranje datoteka s oznakama O_SYNC i O_DSYNC
Vratimo se raspravi o mehanizmima Linuxa koji osiguravaju pouzdano pohranjivanje podataka. Naime, riječ je o upotrebi oznake O_SYNC ili oznake O_DSYNC prilikom otvaranja datoteka koristeći sistemski poziv . Takvim pristupom, svaka operacija zapisivanja podataka izvršava se tako kao da sustav dobiva, prema tome, naredbe write() sustavu daju, odnosno, naredbe fsync() i fdatasync(). U to se naziva „Sinhronizirano I/O završava cjelovitost datoteke“ i „Završetak cjelovitosti podataka“. Glavna prednost ovog pristupa je u tome što je za osiguranje cjelovitosti podataka potreban samo jedan sistemski poziv, a ne dva (na primjer — write() i fdatasync()). Glavni nedostatak ovog pristupa je taj što će sve operacije zapisivanja koje koriste odgovarajući deskriptor datoteke biti sinkronizirane, što može ograničiti mogućnosti strukturiranja koda aplikacije.
Korištenje Direct I/O s oznakom O_DIRECT
Sistem poziv open() podržava oznaku O_DIRECT, koja je namijenjena za to da, zaobišavši cache operativnog sustava, izvršava operacije ulaz/izlaz, direktno komunicirajući s diskom. To, u mnogim slučajevima, znači da će naredbe zapisivanja koje izdaje program biti direktno prenesene u naredbe usmjerene na rad s diskom. No, općenito, ovaj mehanizam nije zamjena za funkcije fsync() ili fdatasync(). Naime, sam disk može odgovarajuće komande za zapis podataka. I, što je još gore, u nekim posebnim slučajevima operacije ulaza-izlaza izvršene upotrebom oznake O_DIRECT, na tradicionalne buferizirane operacije. Najlakše je riješiti ovaj problem koristeći za otvaranje datoteka i oznaku O_DSYNC, što će značiti da za svaku operaciju zapisivanja slijedi poziv fdatasync().
Ispostavilo se da je u datotečnom sustavu XFS nedavno dodan „brzi put“ za O_DIRECT|O_DSYNC-zapis podataka. Ako se prepisuje blok koristeći O_DIRECT|O_DSYNC, tada XFS, umjesto da isprazni keš, izvršit će FUA-zapisnu naredbu u slučaju da uređaj to podržava. To sam potvrdio koristeći alat blktrace u sustavu Linux 5.4/Ubuntu 20.04. Ovaj pristup trebao bi biti učinkovitiji, jer se pri njemu na disk zapisuje minimalna količina podataka, a pritom se koristi jedna operacija, a ne dvije (zapis i ispuštanje keša). Pronašao sam poveznicu na jezgru iz 2018. godine koja implementira ovaj mehanizam. Tu se raspravlja o primjeni ove optimizacije i u drugim datotečnim sustavima, ali koliko mi je poznato, XFS je trenutno jedini datotečni sustav koji to podržava.
Funkcija sync_file_range()
U Linuxu postoji sistemski poziv , koji omogućuje ispuštanje na disk samo dijela datoteke, a ne cijele datoteke. Ovaj poziv pokreće asinhrono ispuštanje podataka i ne čeka njegovo dovršavanje. No, u priručniku za sync_file_range() stoji da je ova naredba „vrlo opasna“. Preporučuje se da se s njom ne koristi. Karakteristike i opasnosti sync_file_range() vrlo su dobro opisane u materijalu. Konkretno, očito, ovaj poziv koristi RocksDB za upravljanje vremenom kada jezgra ispušta „prljave“ podatke na disk. Ali pritom se koristi i fdatasync(). U RocksDB ima zanimljive komentare na tu temu. Na primjer, čini se da poziv sync_file_range() prilikom korištenja ZFS ne dovodi do ispuštanja podataka na disk. Iskustvo mi govori da kod koji se rijetko koristi, može sadržavati greške. Stoga bih preporučio da se ovaj sistemski poziv ne koristi bez krajnje nužde.
Sistemski pozivi koji pomažu osigurati trajno čuvanje podataka
Došao sam do zaključka da se za izvođenje ulazno/izlaznih operacija koje osiguravaju pouzdano čuvanje podataka mogu koristiti tri pristupa. Svi oni zahtijevaju pozivanje funkcije fsync() za direktorij u kojem je datoteka stvorena. Evo tih pristupa:
- Poziv funkcije
fdatasync()ilifsync()nakon funkcijewrite()(bolje koristitifdatasync()). - Rad s opisnikom datoteke otvorenim s zastavicom
O_DSYNCiliO_SYNC(bolje — s zastavicomO_DSYNC). - Korištenje naredbe
pwritev2()s oznakomRWF_DSYNCiliRWF_SYNC(preferabilno — s zastavicomRWF_DSYNC).
Bilješke o performansama
Nisam se bavio detaljnim mjerenjem performansi različitih mehanizama koje sam istraživao. Primijećene razlike u brzini njihovog rada su vrlo male. To znači da se mogu varati, a to što pod drugim uvjetima isto može dati druge rezultate. Prvo ću govoriti o onome što snažnije utječe na performanse, a zatim o onome što manje utječe.
- Ponovno pisanje podataka u datoteku je brže od pridruživanja podataka datoteci (dobitak u performansama može iznositi 2-100%). Pridruživanje podataka datoteci zahtijeva dodatne promjene u metapodacima datoteke, čak i nakon sistemskog poziva
fallocate(), ali razmjeri ovog učinka se mogu mijenjati. Preporučujem, za osiguranje najboljih performansi, pozivanjefallocate()za prethodno dodjeljivanje potrebnog prostora. Nakon toga, taj prostor treba izričito ispuniti nulama i pozvatifsync(). Time će odgovarajući blokovi u datotečnom sustavu biti označeni kao „dodijeljeni“, a ne kao „neodređeni“. To daje malo (oko 2%) poboljšanje performansi. Osim toga, kod nekih diskova prva operacija pristupa bloku može se izvoditi sporije od drugih. To znači da punjenje prostora nulama može dovesti do značajnog (oko 100%) poboljšanja performansi. Konkretno, to se može dogoditi s diskovima (to su neovisni podaci, nisam ih mogao potvrditi). Isto vrijedi i za skladišta (a ovo su već službene informacije, potvrđene ispitivanjima). Drugi stručnjaci su napravili slična , koja se odnose na različite diskove. - Što je manje sistemskih poziva — to su veće performanse (dobitak može biti oko 5%). Čini se da je poziv
open()s oznakomO_DSYNCili pozivpwritev2()s oznakomRWF_SYNCbrži od pozivafdatasync()Sumnjam da je razlog u tome što se takvim pristupom smanjuje broj sistemskih poziva potrebnih za rješavanje iste zadaće (jedan poziv umjesto dva). No, razlika u performansama je vrlo mala, stoga to možete zanemariti i koristiti u aplikaciji ono što neće komplikovati njezinu logiku.
Ako vas zanima tema održivog pohranjivanja podataka — evo nekoliko korisnih materijala:
- — pregled osnovnih mehanizama ulaza/izlaza.
- — priča o tome što se događa s podacima na putu od aplikacije do diska.
- — odgovor na pitanje kada treba primijeniti
fsync()za direktorije. Ako to sažmemo u dvije rečenice, trebate to raditi prilikom stvaranja novog datoteke, a razlog ove preporuke je taj što u Linuxu može biti mnogo referenci na istu datoteku. - — ovdje je opisano kako je održivo pohranjivanje podataka implementirano u SQL Serveru na Linux platformi. Ovdje su prikakazne zanimljive usporedbe između sistemskih poziva na Windows-u i Linux-u. Gotovo sam siguran da sam zahvaljujući ovom materijalu saznao o FUA optimizaciji XFS.
Jeste li ikada izgubili podatke koje ste smatrali sigurno pohranjene na disku?
Izvor: habr.com
