Vzdržljivo shranjevanje podatkov in datotečni API-ji za Linux

Pri raziskovanju vzdržljivosti shranjevanja podatkov v oblačnih sistemih sem se odločil preizkusiti samega sebe, da se prepričam, da razumem osnovne stvari. Jaz sem začel z branjem specifikacije NVMe , da bi se poglobil v to, katere garancije glede vzdržljivega shranjevanja podatkov (tj. garancije, da bodo podatki dostopni po sistemskem okvari) nam nudijo diski NVMe. Ugotovil sem naslednje ključne točke: treba je šteti podatke za poškodovane od trenutka, ko je izdana ukaz za zapis podatkov, do trenutka, ko se konča njihov zapis na nosilec informacij. Vendar pa se v večini programov za zapis podatkov mirno uporabljajo sistemski klici.

V tem gradivu raziskujem mehanizme vzdržljivega shranjevanja podatkov, ki jih nudijo datotečni API-ji za Linux. Zdi se, da bi moralo biti to preprosto: program izvede ukaz write(), in po tem, ko se ta ukaz zaključi, bi morali biti podatki zanesljivo shranjeni na disku. Vendar write() samo kopira podatke aplikacije v predpomnilnik jedra, ki se nahaja v pomnilniku. Da bi prisilili sistem k zapisovanju podatkov na disk, je potrebno uporabiti nekatere dodatne mehanizme.

Vzdržljivo shranjevanje podatkov in datotečni API-ji za Linux

Na splošno to gradivo predstavlja zbirko opomb o tem, kar sem se naučil o meni zanimivem področju. Če zelo kratko povem najpomembnejše, je potrebno za organizacijo vzdržljivega shranjevanja podatkov uporabiti ukaz fdatasync() ali odpirati datoteke z zastavico O_DSYNC. Če vas zanima podrobno, kaj se dogaja s podatki na poti od programske kode do diska, si oglejte to članek.

Posebnosti uporabe funkcije write()

Sistemski klic write() je definiran v standardu IEEE POSIX kot poskus zapisa podatkov v datotečni opisnik. Po uspešni izvedbi write() operacije bi morale vrniti tiste bajte, ki so bili pred tem zapisani, tudi v primeru, da se podatkom dostopa iz drugih procesov ali niti (tukaj je ustrezni del standarda POSIX). Tukaj, v razdelu, posvećenom interakciji tokova s običnim operacijama s datotekama, postoji napomena koja govori da ako svaki od dva toka poziva te funkcije, svaki poziv treba vidjeti ili sve označene posljedice koje proistječu iz izvršenja drugog poziva, ili uopće nijednu posljedicu. To omogućuje zaključak da sve operacije ulaza/izlaza s datotekama trebaju 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 bilo zapisano pomoću write(). No, operacija write(), prema standardu, ne mora nužno završiti zapisivanjem svega što je ponuđeno za zapisivanje. Dozvoljeno joj je da izvrši zapisivanje samo dijela podataka. Na primjer, moguća su dva toka, svaki od kojih pridružuje 1024 bajta datoteci opisanoj istim datotečnim deskriptorom. S aspekta standarda, prihvatljiv rezultat bi bio kada svaka od operacija zapisivanja može pridružiti datoteci samo po jedan bajt. Te operacije će ostati atomske, ali nakon što završe, podaci zapisani njima u datoteku bit će pomiješani. Evo vrlo zanimljive diskusije o ovoj temi na Stack Overflow.

Funkcije fsync() i fdatasync()

Najjednostavniji način za ispuštanje podataka na disk je poziv funkcije fsync(). Ova funkcija traži od operativnog sustava da prenese sve modificirane blokove iz predmemorije na disk. Tu su uključeni i svi metapodaci datoteke (vrijeme pristupa, vrijeme izmjene datoteke i tako dalje). Smatram da je potreba za tim metapodacima rijetka, stoga, ako znate da vam nisu važni, možete koristiti funkciju fdatasync(). U pomći o fdatasync() stoji da tijekom rada ove funkcije dolazi do spremanja na disk onog obujma metapodataka koji je "neophodan za ispravno izvođenje sljedećih operacija čitanja podataka". A to je upravo ono što brine većinu aplikacija.

Jedan od problema koji ovdje može nastati je to da ti mehanizmi ne jamče da će se datoteka moći otkriti nakon mogućeg kvara. Konkretno, kada se stvara nova datoteka, potrebno je pozvati fsync() za mapu, koja ga sadrži. Inače, nakon kvara može se dogoditi da taj fajl ne postoji. Razlog tome je u tome što u UNIX-u, zbog korišćenja tvrdih linkova, fajl može postojati u više mapa. Stoga prilikom poziva fsync() fajl nema načina da sazna iz koje tačno mape takođe treba prebaciti na disk (tukaj o tome se može više pročitati). Čini se da datotečni sistem ext4 može automatski primenjivati fsync() na direktorijume koji sadrže odgovarajuće fajlove, ali u slučaju drugih datotečnih sistema to možda nije slučaj.

Omekani mehanizam može biti različito implementiran u različitim datotečnim sistemima. Koristio sam blktrace da bih saznao koje se diskovne operacije koriste u datotečnim sistemima ext4 i XFS. Obe izbacuju standardne komande za upis na disk i za sadržaj fajlova i za dnevnik datotečnog sistema, prebacuju keš i završavaju rad, izvršavajući FUA-upis (Force Unit Access, upis podataka direktno na disk, preskočivši keš) u dnevnik. Verovatno to rade upravo da bi potvrdili činjenicu izvršenja operacije. Na diskovima koji ne podržavaju FUA, to izaziva dva prebacivanja keša. Moji eksperimenti su pokazali da fdatasync() pomalo brže fsync(). Alat blktrace ukazuje na to da fdatasync() obično piše na disk manje podataka (u ext4 fsync() zapisuje 20 KiB, a fdatasync() — 16 KiB). Pored toga, otkrio sam da je XFS malo brži od ext4. A ovde je uz pomoć blktrace uspeo da sazna da fdatasync() prebacuje na disk manje podataka (4 KiB u XFS).

Nejasne situacije koje nastaju prilikom korišćenja fsync()

Mogu se setiti tri nejasne situacije koje se tiču fsync(), s kojima sam se susreo u praksi.

Prvi takav slučaj dogodio se 2008. godine. Tada je interfejs Firefox 3 „zabrzavao“ kada je vršena upisivanje na disk velikog broja fajlova. Problem je bio u tome što je u implementaciji interfejsa za skladištenje podataka o svom stanju korišćena baza podataka SQLite. Nakon svake promene koja se dogodila u interfejsu, pozivana je funkcija fsync(), što je davalo dobre garancije stabilnog skladištenja podataka. U upotrebljenom datotečnom sistemu ext3 funkcija fsync() sistemaga barcha "iflos" sahifalarni diskka qaytarardi, faqat tegishli faylga aloqador bo'lganlarini emas. Bu shuni anglatadiki, Firefox-da tugmani bosish diskda megabayt ma'lumotlarni yozishni boshlashi mumkin edi, bu esa ko'p soniyalarni olishi mumkin. Muammoni hal qilish, men tushunganimdek, tega ma'lumotni asinxron fon vazifalariga ko'chirishda edi. Bu Firefox dastlab ma'lumotlarni saqlashda yanada qattiq talablarni qabul qilganligini anglatadi, buning esa aslida zaruriyati yo'q edi, va ext3 fayl tizimining xususiyatlari bu muammoni yanada kuchaytirgan.

Ikkinchi muammo 2009 yilda yuz berdi. Shunda, tizimning muvaffaqiyatsizligi sababli, ext4 fayl tizimidan foydalanuvchilar ko'p yangi yaratilgan fayllarning nol uzunlikka ega ekanini ko'rishdi, eski ext3 fayl tizimi bilan bunday holat yuz bermadi. Oldingi bo'limda men ext3 diskga juda ko'p ma'lumotlarni qaytarganligini aytgan edim, bu esa ishlashni sekinlashtirardi. fsync()Muammolarni yaxshilash uchun ext4da diskka faqat ma'lum faylga aloqador "iflos" sahifalar qaytariladi. Boshqa fayllar ma'lumotlari ext3ga nisbatan ancha uzoq vaqt davomida xotirada qoladi. Bu ish samaradorligini oshirish uchun amalga oshirildi (odatda ma'lumotlar bunday holatda 30 soniya qoladi, buni dirty_expire_centisecs; tukaj bilan o'zgartirish mumkin). Bu esa ma'lumotlar kattaroq hajmda bir necha marta yo'qolishi mumkinligini anglatadi. Ushbu muammoni hal qilish uchun fsync() ma'lumotlarni doimiy saqlash va ularni muvaffaqiyatsizliklardan maksimal darajada himoya qilish zarur bo'lgan dasturlarda qo'llaniladi. Ushbu fsync() ext4da farq qiladi ext3ga qaraganda. Bunday yondashuvning kamchiligi shundaki, bu kabi yondashuv, avvalgi kabi, ba'zi operatsiyalarni sekinlashtiradi, masalan, dasturlarni o'rnatish. Buni batafsil ko'ring ovde in ovde.

Uchinchi muammo, fsync(), 2018 yilda yuz berdi. Shunda, PostgreSQL loyihasi doirasida, agar fsync() xato yuzaga kelsa, u "iflos" sahifalarni "toza" deb belgilashini aniqladi. Natijada keyingi chaqiruvlar fsync() nič s takimi stranmi ne delajo. Zaradi tega se spremenjene strani shranjujejo v pomnilniku in nikoli ne zapišejo na disk. To je prava katastrofa, saj bo aplikacija mislila, da so nekateri podatki zapisani na disk, v resnici pa ne bodo. Takšne napake fsync() redko pridejo do izraza, aplikacija v takšnih situacijah skoraj nima možnosti za reševanje težave. Danes, ko to pride do izraza, PostgreSQL in druge aplikacije nenadoma prenehajo delovati. Tukaj, v članku „Ali se aplikacije lahko obnovijo po napakah fsync?“, je ta problem podrobno raziskan. Trenutno je najboljša rešitev tega problema uporaba Direct I/O s parametrom O_SYNC ali s parametrom O_DSYNC. S takim pristopom bo sistem poročal o napakah, ki se lahko pojavijo pri izvajanju specifičnih zapisovalnih operacij, a ta pristop zahteva, da aplikacija sama upravlja z oblogami. Podrobnosti o tem preberite ovde in ovde.

Odprite datoteke z uporabo parametrov O_SYNC in O_DSYNC

Vrnimo se k razpravi o mehanizmih Linuxa, ki zagotavljajo zanesljivo shranjevanje podatkov. To pomeni uporabo parametra O_SYNC ali parametra O_DSYNC pri odpiranju datotek z uporabo sistemskega klica open(). S takim pristopom se vsaka zapisovalna operacija izvaja tako, kot da po vsaki ukaz write() sistemu daje, ustrezno, ukaze fsync() in fdatasync(). U specifikacije POSIX to se imenuje „Synchronized I/O File Integrity Completion“ in „Data Integrity Completion“. Glavna prednost takšnega pristopa je, da je za zagotavljanje celovitosti podatkov potrebno izvesti le en sistemski klic, ne pa dveh (na primer - write() in fdatasync()). Glavna slabost tega pristopa pa je, da bodo vse zapisovalne operacije, ki uporabljajo ustrezen opis datoteke, sinhronizirane, kar lahko omeji možnosti strukturiranja kode aplikacije.

Uporaba Direct I/O s parametrom O_DIRECT

Sistemski klic open() podpira parameter O_DIRECT, ki je zasnovan tako, da zaobide predpomnilnik operacijskega sistema in izvaja vhodno-izhodne operacije, ki neposredno komunicirajo z diskom. To v mnogih primerih pomeni, da se zapisni ukazi, ki jih izda program, neposredno pretvarjajo v ukaze, usmerjene na delo z diskom. A na splošno ta mehanizem ni zamenjava funkcij fsync() ili fdatasync(). Gre za to, da sam disk lahko odložiti ili keširati odgovarajuće komande za zapis podataka. I, što je još gore, u nekim posebnim slučajevima operacije ulaza-izlaza, izvršene korišćenjem zastavice O_DIRECT, se prenose u tradicionalne baferisane operacije. Najlakše je rešiti ovaj problem koristeći dodatnu zastavicu prilikom otvaranja fajlova O_DSYNC, što će značiti da će nakon svake operacije zapisa uslediti poziv fdatasync().

Ispostavilo se da je u XFS fajl sistemu nedavno dodat "brzi put" za O_DIRECT|O_DSYNC-zapis podataka. Ako prepisuju blok koristeći O_DIRECT|O_DSYNC, onda će XFS, umesto ispuštanja keša, izvršiti FUA-zapis komandnog poziva ako uređaj to podržava. To sam potvrdio koristeći alat blktrace u Linuxu 5.4/Ubuntu 20.04. Ovaj pristup bi trebao biti efikasniji, jer se pri njegovoj primeni na disk upisuje minimalna količina podataka i koristi se jedna operacija, a ne dve (upis i ispuštanje keša). Našao sam link na zakrpu jezgra iz 2018. godine, u kojem je implementiran ovaj mehanizam. Tu se vodi rasprava koja se tiče primene ove optimizacije i u drugim fajl sistemima, ali koliko ja znam, XFS je za sada jedini fajl sistem koji to podržava.

Funkcija sync_file_range()

U Linuxu postoji sistemski poziv sync_file_range(), koji omogućava ispuštanje na disk samo dela fajla, a ne celog fajla. Ovaj poziv inicira asinhrono ispuštanje podataka i ne čeka njegovo završavanje. Međutim, u uputstvu za sync_file_range() se navodi da je ova komanda "veoma opasna". Njena upotreba se ne preporučuje. Osobine i opasnosti sync_file_range() veoma dobro su opisane u ovom materijalu. Konkretno, očigledno je da ovaj poziv koristi RocksDB za upravljanje vremenom kada jezgro ispušta "prljave" podatke na disk. Ali pritom se koristi i fdatasync(). U kodu RocksDB postoje zanimljivi komentari o ovoj temi. Na primer, čini se da poziv sync_file_range() prilikom korišćenja ZFS ne dovodi do ispuštanja podataka na disk. Iskustvo mi sugerira da kod koji se retko koristi može sadržati greške. Zbog toga bih preporučio da se ovaj sistemski poziv ne koristi bez velike potrebe.

Sistemski pozivi koji pomažu u obezbeđivanju trajnog skladištenja podataka

Prišel sem do zaključka, da je za izvajanje vhodno/izhodnih operacij, ki zagotavljajo trajno shranjevanje podatkov, mogoče uporabiti tri pristope. Vsi zahtevajo klic funkcije fsync() za direktorij, v katerem je bila ustvarjena datoteka. Tukaj so ti pristopi:

  1. Klic funkcije fdatasync() ili fsync() po funkciji write() (bolje je uporabiti fdatasync()).
  2. Delo s pripomočkom za datoteke, odprtim s zastavico O_DSYNC ili O_SYNC (bolje — z zastavico O_DSYNC).
  3. Uporaba ukaza pwritev2() z zastavico RWF_DSYNC ili RWF_SYNC (prednostno — z zastavico RWF_DSYNC).

Opombe o zmogljivosti

Nisem se ukvarjal s temeljitim merjenjem zmogljivosti različnih mehanizmov, ki sem jih raziskal. Opazne razlike v hitrosti njihovega delovanja so precej majhne. To pomeni, da bi se lahko motil, in da bi v drugih pogojih lahko isti mehanizmi pokazali drugačne rezultate. Najprej bom govoril o tem, kar močneje vpliva na zmogljivost, nato pa o tistem, kar na zmogljivost vpliva manj.

  1. Prepisovanje podatkov datoteke je hitrejše kot dodajanje podatkov k datoteki (prihranek v zmogljivosti je lahko od 2 do 100%). Dodajanje podatkov datoteki zahteva dodatne spremembe v metapodatkih datoteke, celo po sistemskem klicu fallocate(), a obseg tega učinka se lahko spreminja. Priporočam, da za zagotovitev najboljše zmogljivosti, pokličete fallocate() za predhodno dodelitev potrebnega prostora. Nato je treba ta prostor izrecno napolniti z ničlami in poklicati fsync(). S tem bodo ustrezni bloki v datotečnem sistemu označeni kot 'dodeljeni', ne kot 'nedodeljeni'. To prinaša majhno (približno 2%) izboljšanje zmogljivosti. Poleg tega lahko nekateri diski upočasnijo prvo operacijo dostopa do bloka v primerjavi z drugimi. To pomeni, da lahko polnjenje prostora z ničlami privede do znatnega (približno 100%) izboljšanja zmogljivosti. Še posebej se to lahko zgodi pri diskih AWS EBS (to so neuradni podatki, ki jih nisem mogel potrditi). Enako velja za shrambe GCP Persistent Disk (in to so že uradne informacije, potrjene s testi). Drugi strokovnjaki so opazili enake opazovanja, ki se nanašajo na različne diske.
  2. Manj kot je sistemskih klicev — višja je zmogljivost (prihranek znaša približno 5%). Zdi se, da je klic open() z zastavico O_DSYNC ali klic pwritev2() z zastavico RWF_SYNC bržji klic fdatasync(). Sumim, da je razlog v tem, da pri takšnem pristopu igra vlogo to, da za reševanje iste naloge opravimo manj sistemskih klicev (en klic namesto dveh). A razlika v zmogljivosti je zelo majhna, zato se lahko nanjo povsem ne ozrete in v aplikaciji uporabite tisto, kar ne bo povzročilo zapletov v njeni logiki.

Če vas zanima tema trajne shrambe podatkov — tukaj je nekaj koristnih gradiv:

  • I/O metode dostopa — pregled osnovnih mehanizmov vnosa/izhoda.
  • Zagotavljanje, da podatki dosežejo disk — pripoved o tem, kaj se zgodi s podatki na poti od aplikacije do diska.
  • Kdaj naj uporabite fsync v vsebniku imenika — odgovor na vprašanje, kdaj je treba uporabiti fsync() za imenike. Če to povzamemo v dveh besedah, lahko rečemo, da je treba to narediti pri ustvarjanju nove datoteke, razlog za to priporočilo pa je, da je v Linuxu lahko veliko povezav do iste datoteke.
  • SQL Server na Linuxu: Notranjost FUA — tukaj je opisana implementacija trajne shrambe podatkov v SQL Serverju na platformi Linux. Tu so predstavljene nekatere zanimive primerjave med sistemskimi klici Windows in Linux. Skoraj sem prepričan, da sem prav preko tega gradiva izvedel za optimizacijo FUA v XFS.

Ste kdaj izgubili podatke, za katere ste menili, da so zanesljivo shranjeni na disku?

Vzdržljivo shranjevanje podatkov in datotečni API-ji za Linux

Vzdržljivo shranjevanje podatkov in datotečni API-ji za Linux

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster