Durable Data Storage at Linux File API

Ako, na nagsasaliksik sa katatagan ng pag-iimbak ng data sa mga cloud system, ay nagpasya na subukan ang aking sarili, upang matiyak na naiintindihan ko ang mga pangunahing bagay. ako nagsimula sa pamamagitan ng pagbabasa ng spec ng NVMe upang maunawaan kung ano ang mga garantiya tungkol sa pagtitiyaga ng data (iyon ay, ginagarantiyahan na ang data ay magiging available pagkatapos ng pagkabigo ng system) bigyan kami ng mga NMVe disk. Ginawa ko ang mga sumusunod na pangunahing konklusyon: kailangan mong isaalang-alang ang data na nasira mula sa sandaling ibinigay ang utos ng pagsulat ng data, at hanggang sa sandaling isulat ang mga ito sa daluyan ng imbakan. Gayunpaman, sa karamihan ng mga programa, ang mga system call ay medyo ligtas na ginagamit upang magsulat ng data.

Sa artikulong ito, tinutuklasan ko ang mga mekanismo ng pagtitiyaga na ibinigay ng mga Linux file API. Tila ang lahat ay dapat na simple dito: tinawag ng programa ang utos write(), at pagkatapos makumpleto ang pagpapatakbo ng command na ito, ang data ay ligtas na maiimbak sa disk. Pero write() kinokopya lamang ang data ng application sa kernel cache na matatagpuan sa RAM. Upang pilitin ang system na magsulat ng data sa disk, dapat gumamit ng ilang karagdagang mekanismo.

Durable Data Storage at Linux File API

Sa pangkalahatan, ang materyal na ito ay isang hanay ng mga tala na may kaugnayan sa kung ano ang aking natutunan sa isang paksang interesado sa akin. Kung pinag-uusapan natin nang maikli ang tungkol sa pinakamahalaga, lumalabas na upang maisaayos ang napapanatiling pag-iimbak ng data, kailangan mong gamitin ang utos fdatasync() o buksan ang mga file na may flag O_DSYNC. Kung interesado kang matuto nang higit pa tungkol sa kung ano ang nangyayari sa data sa paraan mula sa code patungo sa disk, tingnan ito artikulo.

Mga tampok ng paggamit ng write() function

System call write() tinukoy sa pamantayan IEEE POSIX bilang isang pagtatangka na magsulat ng data sa isang file descriptor. Pagkatapos ng matagumpay na pagkumpleto ng trabaho write() Ang mga operasyon sa pagbabasa ng data ay dapat na ibalik nang eksakto ang mga byte na dating isinulat, na ginagawa ito kahit na ang data ay ina-access mula sa iba pang mga proseso o mga thread (dito kaukulang seksyon ng pamantayan ng POSIX). Dito, sa seksyon ng pakikipag-ugnayan ng mga thread na may normal na pagpapatakbo ng file, mayroong isang tala na nagsasabing kung dalawang thread ang bawat isa ay tumawag sa mga function na ito, kung gayon ang bawat tawag ay dapat makita ang lahat ng ipinahiwatig na mga kahihinatnan na humahantong sa pagpapatupad ng isa pang tawag, o hindi makita sa lahat walang kahihinatnan. Ito ay humahantong sa konklusyon na ang lahat ng mga operasyon ng file I/O ay dapat magkaroon ng lock sa mapagkukunang pinagtatrabahuhan.

Nangangahulugan ba ito na ang operasyon write() ay atomic? Mula sa teknikal na pananaw, oo. Dapat ibalik ng mga operasyon sa pagbasa ng data ang alinman sa lahat o wala sa kung ano ang nakasulat write(). Ngunit ang operasyon write(), alinsunod sa pamantayan, ay hindi kailangang tapusin, na naisulat ang lahat ng hiniling sa kanya na isulat. Pinapayagan na magsulat lamang ng bahagi ng data. Halimbawa, maaari tayong magkaroon ng dalawang stream bawat isa ay nagdaragdag ng 1024 byte sa isang file na inilarawan ng parehong descriptor ng file. Mula sa punto ng view ng pamantayan, ang resulta ay magiging katanggap-tanggap kapag ang bawat isa sa mga pagpapatakbo ng pagsulat ay maaaring magdagdag lamang ng isang byte sa file. Ang mga operasyong ito ay mananatiling atomic, ngunit pagkatapos nilang makumpleto, ang data na isinulat nila sa file ay magugulo. Dito napaka-kagiliw-giliw na talakayan sa paksang ito sa Stack Overflow.

fsync() at fdatasync() function

Ang pinakamadaling paraan upang i-flush ang data sa disk ay ang tawagan ang function fsync(). Hinihiling ng function na ito sa operating system na ilipat ang lahat ng binagong bloke mula sa cache patungo sa disk. Kabilang dito ang lahat ng metadata ng file (oras ng pag-access, oras ng pagbabago ng file, at iba pa). Naniniwala ako na ang metadata na ito ay bihirang kailanganin, kaya kung alam mong hindi ito mahalaga sa iyo, maaari mong gamitin ang function fdatasync(). Sa tulungan sa fdatasync() sinasabi nito na sa panahon ng pagpapatakbo ng function na ito, ang naturang halaga ng metadata ay nai-save sa disk, na "kinakailangan para sa tamang pagpapatupad ng mga sumusunod na operasyon sa pagbabasa ng data." At ito mismo ang mahalaga sa karamihan ng mga application.

Ang isang problema na maaaring lumitaw dito ay ang mga mekanismong ito ay hindi ginagarantiyahan na ang file ay matatagpuan pagkatapos ng isang posibleng pagkabigo. Sa partikular, kapag ang isang bagong file ay nilikha, ang isa ay dapat tumawag fsync() para sa direktoryo na naglalaman nito. Kung hindi, pagkatapos ng pag-crash, maaaring lumabas na ang file na ito ay hindi umiiral. Ang dahilan nito ay sa ilalim ng UNIX, dahil sa paggamit ng mga hard link, maaaring umiral ang isang file sa maraming direktoryo. Samakatuwid, kapag tumatawag fsync() walang paraan para malaman ng isang file kung aling data ng direktoryo ang dapat ding i-flush sa disk (dito maaari mong basahin ang higit pa tungkol dito). Mukhang kaya ng ext4 file system awtomatikong gamitin fsync() sa mga direktoryo na naglalaman ng kaukulang mga file, ngunit maaaring hindi ito ang kaso sa ibang mga file system.

Ang mekanismong ito ay maaaring ipatupad sa iba't ibang mga file system. ginamit ko blktrace upang malaman kung anong mga pagpapatakbo ng disk ang ginagamit sa ext4 at XFS file system. Parehong naglalabas ng karaniwang write command sa disk para sa parehong mga nilalaman ng mga file at ng file system journal, i-flush ang cache at lumabas sa pamamagitan ng pagsasagawa ng FUA (Force Unit Access, pagsusulat ng data nang direkta sa disk, pag-bypass sa cache) na isulat sa journal. Malamang ginagawa lang nila iyon para kumpirmahin ang katotohanan ng transaksyon. Sa mga drive na hindi sumusuporta sa FUA, nagdudulot ito ng dalawang pag-flush ng cache. Ang aking mga eksperimento ay nagpakita na fdatasync() medyo mabilis fsync(). Kagamitan blktrace ay nagpapahiwatig na fdatasync() karaniwang nagsusulat ng mas kaunting data sa disk (sa ext4 fsync() nagsusulat ng 20 KiB, at fdatasync() - 16 KiB). Gayundin, nalaman ko na ang XFS ay bahagyang mas mabilis kaysa sa ext4. At narito sa tulong blktrace nagawang malaman iyon fdatasync() nag-flush ng mas kaunting data sa disk (4 KiB sa XFS).

Mga hindi maliwanag na sitwasyon kapag gumagamit ng fsync()

Maaari akong mag-isip ng tatlong hindi maliwanag na mga sitwasyon tungkol sa fsync()na naabutan ko sa pagsasanay.

Ang unang naturang insidente ay naganap noong 2008. Sa oras na iyon, ang interface ng Firefox 3 ay "na-frozen" kung ang isang malaking bilang ng mga file ay isinulat sa disk. Ang problema ay ang pagpapatupad ng interface ay gumamit ng SQLite database upang mag-imbak ng impormasyon tungkol sa estado nito. Pagkatapos ng bawat pagbabagong naganap sa interface, tinawag ang function fsync(), na nagbigay ng magandang garantiya ng matatag na imbakan ng data. Sa ginamit noon ext3 file system, ang function fsync() na-flush sa disk ang lahat ng "marumi" na mga pahina sa system, at hindi lamang ang mga nauugnay sa kaukulang file. Nangangahulugan ito na ang pag-click sa isang pindutan sa Firefox ay maaaring maging sanhi ng mga megabytes ng data na maisulat sa isang magnetic disk, na maaaring tumagal ng maraming segundo. Ang solusyon sa problema, sa pagkakaintindi ko ito materyal, ay upang ilipat ang trabaho kasama ang database sa mga asynchronous na gawain sa background. Nangangahulugan ito na ginamit ng Firefox ang mas mahigpit na mga kinakailangan sa pagtitiyaga sa imbakan kaysa sa talagang kailangan, at pinalala lang ng mga tampok ng ext3 filesystem ang problemang ito.

Ang pangalawang problema ay nangyari noong 2009. Pagkatapos, pagkatapos ng pag-crash ng system, nalaman ng mga user ng bagong ext4 file system na maraming bagong likhang file ang zero-length, ngunit hindi ito nangyari sa mas lumang ext3 file system. Sa nakaraang talata, napag-usapan ko kung paano itinapon ng ext3 ang masyadong maraming data sa disk, na nagpabagal ng mga bagay nang husto. fsync(). Upang mapabuti ang sitwasyon, ang ext4 ay nag-flush lamang ng mga "marumi" na pahina na may kaugnayan sa isang partikular na file. At ang data ng iba pang mga file ay nananatili sa memorya nang mas matagal kaysa sa ext3. Ginawa ito upang mapabuti ang pagganap (bilang default, mananatili ang data sa ganitong estado sa loob ng 30 segundo, maaari mong i-configure ito gamit ang dirty_expire_centisecs; dito makakahanap ka ng higit pang impormasyon tungkol dito). Nangangahulugan ito na ang isang malaking halaga ng data ay maaaring hindi na maibabalik pagkatapos ng isang pag-crash. Ang solusyon sa problemang ito ay ang paggamit fsync() sa mga application na kailangang magbigay ng matatag na imbakan ng data at protektahan ang mga ito hangga't maaari mula sa mga kahihinatnan ng mga pagkabigo. Function fsync() gumagana nang mas mahusay sa ext4 kaysa sa ext3. Ang kawalan ng diskarte na ito ay ang paggamit nito, tulad ng dati, ay nagpapabagal sa ilang mga operasyon, tulad ng pag-install ng mga programa. Tingnan ang mga detalye tungkol dito dito ΠΈ dito.

Ang ikatlong problema tungkol sa fsync(), nagmula noong 2018. Pagkatapos, sa loob ng balangkas ng proyektong PostgreSQL, nalaman na kung ang function fsync() nakatagpo ng isang error, minarkahan nito ang "marumi" na mga pahina bilang "malinis". Bilang resulta, ang mga sumusunod na tawag fsync() walang gawin sa mga ganyang page. Dahil dito, ang mga binagong pahina ay iniimbak sa memorya at hindi kailanman nakasulat sa disk. Ito ay isang tunay na sakuna, dahil ang application ay mag-iisip na ang ilang data ay nakasulat sa disk, ngunit sa katunayan hindi ito magiging. Ang ganitong mga kabiguan fsync() ay bihira, ang aplikasyon sa ganitong mga sitwasyon ay halos walang magagawa upang labanan ang problema. Sa mga araw na ito, kapag nangyari ito, nag-crash ang PostgreSQL at iba pang mga application. Dito, sa artikulong "Can Applications Recover from fsync Failures?", ang problemang ito ay ginalugad nang detalyado. Sa kasalukuyan ang pinakamahusay na solusyon sa problemang ito ay ang paggamit ng Direct I/O na may bandila O_SYNC o may watawat O_DSYNC. Sa diskarteng ito, mag-uulat ang system ng mga error na maaaring mangyari kapag nagsasagawa ng mga partikular na operasyon ng pagsulat ng data, ngunit ang diskarteng ito ay nangangailangan ng application na pamahalaan ang mga buffer mismo. Magbasa pa tungkol dito dito ΠΈ dito.

Pagbubukas ng mga file gamit ang O_SYNC at O_DSYNC na mga flag

Bumalik tayo sa talakayan ng mga mekanismo ng Linux na nagbibigay ng patuloy na pag-iimbak ng data. Ibig sabihin, pinag-uusapan natin ang paggamit ng watawat O_SYNC o watawat O_DSYNC kapag binubuksan ang mga file gamit ang system call bukas(). Sa pamamaraang ito, ang bawat operasyon ng pagsulat ng data ay ginagawa na parang pagkatapos ng bawat utos write() ang sistema ay ibinibigay, ayon sa pagkakabanggit, mga utos fsync() ΠΈ fdatasync(). Sa Mga pagtutukoy ng POSIX ito ay tinatawag na "Sinchronized I/O File Integrity Completion" at "Data Integrity Completion". Ang pangunahing bentahe ng diskarteng ito ay isang system call lamang ang kailangang isagawa upang matiyak ang integridad ng data, at hindi dalawa (halimbawa βˆ’ write() ΠΈ fdatasync()). Ang pangunahing kawalan ng diskarteng ito ay ang lahat ng mga operasyon sa pagsusulat gamit ang kaukulang file descriptor ay masi-synchronize, na maaaring limitahan ang kakayahang buuin ang application code.

Paggamit ng Direct I/O na may O_DIRECT flag

System call open() sumusuporta sa bandila O_DIRECT, na idinisenyo upang i-bypass ang cache ng operating system, magsagawa ng mga operasyon ng I / O, direktang nakikipag-ugnayan sa disk. Ito, sa maraming mga kaso, ay nangangahulugan na ang mga write command na inisyu ng programa ay direktang isasalin sa mga command na naglalayong magtrabaho kasama ang disk. Ngunit, sa pangkalahatan, ang mekanismong ito ay hindi isang kapalit para sa mga pag-andar fsync() o fdatasync(). Ang katotohanan ay ang disk mismo ay maaari pagkaantala o cache naaangkop na mga utos para sa pagsulat ng data. At, mas masahol pa, sa ilang mga espesyal na kaso, gumanap ang mga operasyon ng I / O kapag ginagamit ang bandila O_DIRECT, broadcast sa mga tradisyonal na buffered na operasyon. Ang pinakamadaling paraan upang malutas ang problemang ito ay ang paggamit ng bandila upang buksan ang mga file O_DSYNC, na nangangahulugan na ang bawat write operation ay susundan ng isang tawag fdatasync().

Ito ay lumabas na ang XFS filesystem ay nagdagdag kamakailan ng isang "mabilis na landas" para sa O_DIRECT|O_DSYNC-mga talaan ng datos. Kung ang block ay na-overwrite gamit ang O_DIRECT|O_DSYNC, pagkatapos ay ang XFS, sa halip na i-flush ang cache, ay isasagawa ang FUA write command kung sinusuportahan ito ng device. Na-verify ko ito gamit ang utility blktrace sa isang Linux 5.4/Ubuntu 20.04 system. Ang diskarte na ito ay dapat na mas mahusay, dahil isinusulat nito ang pinakamababang dami ng data sa disk at gumagamit ng isang operasyon, hindi dalawa (isulat at i-flush ang cache). May nakita akong link sa tambalan 2018 kernel na nagpapatupad ng mekanismong ito. Mayroong ilang talakayan tungkol sa paglalapat ng pag-optimize na ito sa iba pang mga filesystem, ngunit sa pagkakaalam ko, ang XFS ay ang tanging filesystem na sumusuporta dito sa ngayon.

sync_file_range() function

May system call ang Linux sync_file_range(), na nagbibigay-daan sa iyong i-flush ang bahagi lamang ng file sa disk, hindi ang buong file. Ang tawag na ito ay nagpapasimula ng isang asynchronous flush at hindi naghihintay na makumpleto ito. Ngunit sa pagtukoy sa sync_file_range() "very dangerous" daw ang command na ito. Hindi inirerekomenda na gamitin ito. Mga tampok at panganib sync_file_range() napakahusay na inilarawan sa Ito materyal. Sa partikular, ang tawag na ito ay tila gumagamit ng RocksDB upang makontrol kapag ang kernel ay nag-flush ng "marumi" na data sa disk. Ngunit sa parehong oras doon, upang matiyak ang matatag na imbakan ng data, ginagamit din ito fdatasync(). Sa code Ang RocksDB ay may ilang mga kawili-wiling komento sa paksang ito. Halimbawa, parang ang tawag sync_file_range() kapag gumagamit ng ZFS ay hindi nag-flush ng data sa disk. Sinasabi sa akin ng karanasan na maaaring may mga bug ang bihirang ginagamit na code. Samakatuwid, ipapayo ko laban sa paggamit ng system call na ito maliban kung talagang kinakailangan.

Mga tawag sa system upang makatulong na matiyak ang pagtitiyaga ng data

Nakarating ako sa konklusyon na mayroong tatlong mga diskarte na maaaring magamit upang maisagawa ang mga patuloy na operasyon ng I/O. Lahat sila ay nangangailangan ng isang function na tawag fsync() para sa direktoryo kung saan nilikha ang file. Ito ang mga diskarte:

  1. Function call fdatasync() o fsync() pagkatapos ng function write() (mas magandang gamitin fdatasync()).
  2. Paggawa gamit ang isang file descriptor na binuksan gamit ang isang flag O_DSYNC o O_SYNC (mas mabuti - may bandila O_DSYNC).
  3. Paggamit ng command pwritev2() may bandila RWF_DSYNC o RWF_SYNC (mas mabuti na may bandila RWF_DSYNC).

Mga Tala sa Pagganap

Hindi ko maingat na sinukat ang pagganap ng iba't ibang mekanismo na aking inimbestigahan. Ang mga pagkakaiba na napansin ko sa bilis ng kanilang trabaho ay napakaliit. Nangangahulugan ito na maaari akong magkamali, at na sa ibang mga kundisyon ang parehong bagay ay maaaring magpakita ng iba't ibang mga resulta. Una, magsasalita ako tungkol sa kung ano ang higit na nakakaapekto sa pagganap, at pagkatapos, tungkol sa kung ano ang nakakaapekto sa pagganap nang mas kaunti.

  1. Ang pag-overwrit ng data ng file ay mas mabilis kaysa sa pagdaragdag ng data sa isang file (maaaring 2-100%) ang nakuha sa performance. Ang pag-attach ng data sa isang file ay nangangailangan ng mga karagdagang pagbabago sa metadata ng file, kahit na pagkatapos ng system call fallocate(), ngunit maaaring mag-iba ang laki ng epektong ito. Inirerekomenda ko, para sa pinakamahusay na pagganap, tumawag fallocate() upang paunang ilaan ang kinakailangang espasyo. Kung gayon ang puwang na ito ay dapat na tahasang punuin ng mga zero at tawagin fsync(). Ito ay magiging sanhi ng kaukulang mga bloke sa file system na mamarkahan bilang "inilalaan" sa halip na "hindi inilalaan". Nagbibigay ito ng maliit (mga 2%) na pagpapabuti sa pagganap. Gayundin, ang ilang mga disk ay maaaring may mas mabagal na unang pag-access sa pag-block kaysa sa iba. Nangangahulugan ito na ang pagpuno sa espasyo ng mga zero ay maaaring humantong sa isang makabuluhang (mga 100%) na pagpapabuti ng pagganap. Sa partikular, ito ay maaaring mangyari sa mga disk. AWS EBS (ito ay hindi opisyal na data, hindi ko makumpirma ang mga ito). Ang parehong napupunta para sa imbakan. GCP Persistent Disk (at ito ay opisyal nang impormasyon, na kinumpirma ng mga pagsubok). Ganoon din ang ginawa ng ibang mga eksperto pagmamasidnauugnay sa iba't ibang mga disk.
  2. Ang mas kaunting mga tawag sa system, mas mataas ang pagganap (ang pakinabang ay maaaring humigit-kumulang 5%). Parang may tawag open() may bandila O_DSYNC o tawagan pwritev2() may bandila RWF_SYNC mas mabilis na tawag fdatasync(). Pinaghihinalaan ko na ang punto dito ay na sa diskarteng ito, ang katotohanan na mas kaunting mga tawag sa system ang kailangang gawin upang malutas ang parehong gawain (isang tawag sa halip na dalawa) ay gumaganap ng isang papel. Ngunit ang pagkakaiba sa pagganap ay napakaliit, kaya madali mong balewalain ito at gumamit ng isang bagay sa application na hindi humantong sa komplikasyon ng lohika nito.

Kung interesado ka sa paksa ng napapanatiling pag-iimbak ng data, narito ang ilang kapaki-pakinabang na materyales:

  • Mga pamamaraan ng I/O Access β€” isang pangkalahatang-ideya ng mga pangunahing kaalaman ng mga mekanismo ng input / output.
  • Tinitiyak na ang data ay umabot sa disk - isang kuwento tungkol sa kung ano ang nangyayari sa data sa paraan mula sa application patungo sa disk.
  • Kailan mo dapat i-fsync ang naglalaman ng direktoryo - ang sagot sa tanong kung kailan mag-aplay fsync() para sa mga direktoryo. Sa madaling sabi, lumalabas na kailangan mong gawin ito kapag lumilikha ng isang bagong file, at ang dahilan para sa rekomendasyong ito ay na sa Linux ay maaaring mayroong maraming mga sanggunian sa parehong file.
  • SQL Server sa Linux: FUA Internals - narito ang isang paglalarawan kung paano ipinapatupad ang patuloy na pag-iimbak ng data sa SQL Server sa platform ng Linux. Mayroong ilang mga kagiliw-giliw na paghahambing sa pagitan ng Windows at Linux system call dito. Halos sigurado ako na salamat sa materyal na ito na natutunan ko ang tungkol sa FUA optimization ng XFS.

Nawalan ka na ba ng data na akala mo ay ligtas na nakaimbak sa disk?

Durable Data Storage at Linux File API

Durable Data Storage at Linux File API

Pinagmulan: www.habr.com