Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

NĂ« vjeshtĂ«n e vitit 2019, ekipi i iOS nĂ« OblaK Mail.ru pĂ«rjetoi njĂ« ngjarje tĂ« shumĂ«pritur. Baza kryesore e tĂ« dhĂ«nave pĂ«r ruajtjen e qĂ«ndrueshme tĂ« gjendjes sĂ« aplikacionit u bĂ« njĂ« zgjedhje mjaft ekzotike pĂ«r botĂ«n mobile Lightning Memory-Mapped Database (LMDB). NĂ« vazhdim ju ofrohet njĂ« pasqyrĂ« e detajuar nĂ« katĂ«r pjesĂ«. SĂ« pari, le tĂ« flasim pĂ«r arsyet e kĂ«saj zgjedhjeje tĂ« pa zakonshme dhe tĂ« vĂ«shtirĂ«. Pastaj do tĂ« kalojmĂ« nĂ« shqyrtimin e tre themeleve nĂ« arkitekturĂ«n e LMDB: skedarĂ«t e mapuar nĂ« kujtesĂ«, B+-druri, dhe qasja copy-on-write pĂ«r tĂ« realizuar transaksionalitetin dhe shumĂ«versionĂ«sinĂ«. PĂ«rfundimisht, pĂ«rfundimisht – pjesa praktike. KĂ«tu do tĂ« shqyrtojmĂ« si tĂ« projektojmĂ« dhe tĂ« realizojmĂ« njĂ« skemĂ« baze mbi API-nĂ« e ulĂ«t key-value me disa tabela, duke pĂ«rfshirĂ« indeksin.

Përmbajtja

  1. Motivimi për implementim
  2. Pozicionimi i LMDB
  3. Tre themeloret e LMDB
    3.1. Tema nr. 1. Skedarët e mapuar në kujtesë
    3.2. Tema nr. 2. B+-druri
    3.3. Tema nr. 3. Copy-on-write
  4. Projekti i skemës së të dhënave mbi API-në key-value
    4.1. Abstraksionet bazë
    4.2. Modelimi i tabelave
    4.3. Modelimi i lidhjeve mes tabelave

1. Motivimi për implementim

Njëherë rreth vitit 2015, ne u shqetësuam për matjen e sasisë së herëve kur ndërfaqja e aplikacionit tonë ngadalësohej. E bëmë këtë jo për ndonjë arsye të rastësishme. Kishim një rritje të ankesave se ndonjëherë aplikacioni nuk reagonte ndaj veprimeve të përdoruesit: butonat nuk klikoheshin, listat nuk skrolloheshin e kështu me radhë. Rreth mekanikës së matjeve, kam folur në AvitoTech, prandaj këtu po jap vetëm rendin e shifrave.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Rezultatet e matjeve u bënë një dush i ftohtë për ne. Doli se problemet që shkaktoheshin nga ngrirjet ishin shumë më të shumta se çdo problem tjetër. Nëse deri sa kuptuam këtë fakt, treguesi kryesor teknik i cilësisë ishte pa crash, pas kësaj fokusi u zhvendos në freeze free.

Duke ndĂ«rtuar njĂ« dashboard me ngrirjet dhe duke kryer njĂ« analizĂ« dhe kualitative tĂ« shkakĂ«ve tĂ« tyre, u bĂ« i qartĂ« armiku kryesor — logjika e rĂ«ndĂ« biznesore, e cila ekzekutohej nĂ« tĂ« kaluarĂ«n kryesore tĂ« aplikacionit. Reagimi natyror ndaj kĂ«tij problemi ishte njĂ« dĂ«shirĂ« e fortĂ« pĂ«r ta shpĂ«rndarĂ« atĂ« nĂ« punĂ«t e tjera. PĂ«r zgjidhjen sistemike tĂ« kĂ«saj problemi, ne i pĂ«rmbaheshim njĂ« arkitekture multi-thread me aktorĂ« tĂ« lehtĂ«. I kushtova dy treda nĂ« Twitter-in tonĂ« kolektiv dhe artikulli nĂ« HabrĂ«. NĂ« kuadĂ«r tĂ« kĂ«tij tregimi, dua tĂ« theksoj ato aspekte tĂ« zgjidhjes qĂ« ndikuan nĂ« zgjedhjen e bazĂ«s sĂ« tĂ« dhĂ«nave.

Modeli aktor i organizimit të sistemit sugjeron se shumëllojshmëria bëhet thelbi i saj i dytë. Objektet e modelit në të pëlqejnë të kalojnë kufijtë e proceseve. Dhe ata e bëjnë këtë jo ndonjëherë e ndonjëherë, por pothuajse vazhdimisht dhe kudo.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Baza e tĂ« dhĂ«nave Ă«shtĂ« njĂ« nga komponentĂ«t themelorĂ« nĂ« skemĂ«n e paraqitur. Detyra e saj kryesore Ă«shtĂ« realizimi i makropatternit Baza e tĂ« DhĂ«nave e Ndashur. NĂ«se nĂ« botĂ«n e ndĂ«rmarrjeve kjo pĂ«rdoret pĂ«r tĂ« organizuar sinkronizimin e tĂ« dhĂ«nave midis shĂ«rbimeve, nĂ« rastin e arkitekturĂ«s aktoriale – tĂ« dhĂ«nat midis rrjedhave. KĂ«shtu, na nevojitej njĂ« bazĂ« tĂ« dhĂ«nash e tillĂ«, puna me tĂ« cilĂ«n nĂ« ambientin me shumĂ« rrjedha nuk shkakton as edhe vĂ«shtirĂ«si minimale. Kjo do tĂ« thotĂ« se objektet e marra prej saj duhet tĂ« jenĂ« sĂ« paku tĂ« sigurta pĂ«r rrjedha, dhe idealisht krejtĂ«sisht tĂ« palĂ«vizshme. Siç dihet, ato tĂ« fundit mund tĂ« pĂ«rdoren nĂ« mĂ«nyrĂ« tĂ« njĂ«kohshme nga disa rrjedha, pa aspak bllokime, qĂ« ndikon pozitivisht nĂ« performancĂ«.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOSFaktori i dytë i rëndësishëm që ndikoi në zgjedhjen e bazës së të dhënave ishte API ynë në cloud. Ky ishte frymëzuar nga qasja për sinkronizimin, e cila ishte pranuar në git. Ashtu si edhe ai, ne synonim në offline-first API, i cili për klientët e megjisë duket më shumë se i përshtatshëm. U supozua që ata do të shkarkonin një herë gjendjen e plotë të megjisë, pastaj sinkronizimi në shumicën e rasteve do të ndodhte përmes aplikimit të ndryshimeve. Fatkeqësisht, kjo mundësi ende ndodhet vetëm në zonën teorike, dhe në praktikë klientët nuk kanë arritur të punojnë me patch-et. Ka disa arsye objektive për këtë, të cilat për t'u shmangur nga zgjatja e hyrjes, do t'i lëmë jashtë diskutimit. Tani, shumë më interesant janë përfundimet përfundimtare të mësimit se çfarë ndodh kur API thotë "A", dhe përdoruesi i tij nuk thotë "B".

KĂ«shtu, nĂ«se e paraqitni git-in, i cili gjatĂ« ekzekutimit tĂ« komandĂ«s pull nĂ« vend tĂ« aplikimit tĂ« patch-eve nĂ« snapshot-in lokal krahason gjendjen e plotĂ« me atĂ« tĂ« serverit, do tĂ« keni njĂ« pĂ«rshkrim tĂ« mjaftueshĂ«m se si ndodh sinkronizimi nĂ« klientĂ«t cloud. Nuk Ă«shtĂ« e vĂ«shtirĂ« tĂ« kuptohet se pĂ«r ta realizuar kĂ«tĂ« duhet tĂ« alokoni nĂ« memorje dy pemĂ« DOM me metainformacione rreth tĂ« gjitha skedarĂ«ve tĂ« serverit dhe atyre lokalĂ«. KĂ«shtu, nĂ«se njĂ« pĂ«rdorues ruan nĂ« cloud 500 mijĂ« skedarĂ«, pĂ«r sinkronizimin e tij duhet tĂ« risjellni dhe shkatĂ«rroni dy pemĂ« me 1 milion nodet. Dhe çdo nod Ă«shtĂ« njĂ« agregat qĂ« pĂ«rmban njĂ« grafik tĂ« nĂ«nobjekteve. NĂ« kĂ«tĂ« dritĂ«, rezultatet e profilizimit ishin tĂ« pritshme. U zbulua se madje pa marrĂ« parasysh algoritmikun e bashkimit, procedura e krijimit dhe shkatĂ«rrimit tĂ« njĂ« numri tĂ« madh objektesh tĂ« vogla merrte njĂ« kosto tĂ« konsiderueshme. Situata pĂ«rkeqĂ«sohet nga fakti se operacioni bazĂ« i sinkronizimit Ă«shtĂ« i pĂ«rfshirĂ« nĂ« njĂ« numĂ«r tĂ« madh skenarĂ«sh tĂ« pĂ«rdoruesve. Si rezultat, e regjistruam kriterin e dytĂ« tĂ« rĂ«ndĂ«sishĂ«m nĂ« zgjedhjen e njĂ« baze tĂ« dhĂ«nash — mundĂ«sia pĂ«r tĂ« realizuar operacione CRUD pa alokim dinamik objektesh.

Kërkesat e tjera janë më tradicionale dhe lista e tyre e plotë është si më poshtë.

  1. Siguria e rrjedhës.
  2. Multiprocesimi. Kjo është e diktuar nga dëshira për të përdorur të njëjtën instancë të bazës së të dhënave për të sinkronizuar gjendjen jo vetëm midis rrjedhave, por edhe midis aplikacionit kryesor dhe zgjerimeve të iOS.
  3. Mundësia për të paraqitur entitetet e ruajtura si objekte të pandryshueshme.
  4. Mungesa e alokimeve dinamike në operacionet CRUD.
  5. Mbështetje për transaksionet me karakteristika thelbësore ACID: atomizmi, konsistenca, izolimi dhe besueshmëria.
  6. Shpejtësia në rastet më të njohura.

NjĂ« zgjedhje e mirĂ« me kĂ«tĂ« grup kĂ«rkesash ka qenĂ« dhe mbetet SQLite. MegjithatĂ«, gjatĂ« shqyrtimit tĂ« alternativave, mĂ« ra nĂ« dorĂ« njĂ« libĂ«r «Getting Started with LevelDB». NĂ«n drejtimin e saj u shkrua njĂ« benchmark qĂ« krahasoi shpejtĂ«sinĂ« e punĂ«s me baza tĂ« ndryshme tĂ« tĂ« dhĂ«nave nĂ« skenarĂ« tĂ« vĂ«rtetĂ« cloud. Rezultati tejkaloi pritshmĂ«ritĂ« mĂ« optimiste. NĂ« rastet mĂ« tĂ« njohura — marrja e kursorit nĂ« listĂ«n e renditur tĂ« tĂ« gjitha skedarĂ«ve dhe lista e renditur e tĂ« gjitha skedarĂ«ve pĂ«r njĂ« drejtor tĂ« caktuar — LMDB doli tĂ« ishte 10 herĂ« mĂ« e shpejtĂ« se SQLite. Zgjedhja u bĂ« e qartĂ«.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

2. Pozicionimi i LMDB

LMDB Ă«shtĂ« njĂ« bibliotekĂ«, shumĂ« e vogĂ«l (vetĂ«m 10K rreshta), qĂ« implementon nivelin mĂ« tĂ« ulĂ«t tĂ« bazĂ«s sĂ« tĂ« dhĂ«nave — depozitimin.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Diagrami i dhĂ«nĂ« tregon se krahasimi i LMDB me SQLite, e cila implementon gjithashtu nivele mĂ« tĂ« larta, nuk Ă«shtĂ« mĂ« i saktĂ« se krahasimi i SQLite me Core Data. Si konkurrentĂ« tĂ« barabartĂ«, do tĂ« ishte mĂ« e drejtĂ« tĂ« pĂ«rmendeshin motorĂ« depozitimi tĂ« ngjashĂ«m — BerkeleyDB, LevelDB, Sophia, RocksDB etj. Ka zhvillime ku LMDB shĂ«rben si komponent i motorit tĂ« depozitimit pĂ«r SQLite. Eksperimenti i parĂ« i tillĂ« u zhvillua nĂ« vitin 2012 nga autori i LMDB Howard Chu. Rezultatet , ishin kaq intriguese saqĂ« nisma e tij u pĂ«rqafua nga entuziaste tĂ« OSS, dhe gjeti vazhdimĂ«si nĂ« formĂ«n e LumoSQL. NĂ« janar 2020 autori i kĂ«tij projekti Den Shearer prezantoi atĂ« nĂ« LinuxConfAu.

PĂ«rdorimi kryesor i LMDB Ă«shtĂ« si njĂ« motor pĂ«r bazat e tĂ« dhĂ«nave aplikative. Libraria iu dedikohet zhvilluesve tĂ« OpenLDAP, tĂ« cilĂ«t ishin shumĂ« tĂ« pakĂ«naqur me BerkeleyDB si bazĂ« pĂ«r projektin e tyre. Duke u nisur nga njĂ« bibliotekĂ« modeste btree, Howard Chu arriti tĂ« krijojĂ« njĂ« nga alternativat mĂ« tĂ« njohura nĂ« kohĂ«t e sotme. Ai i pĂ«rkushtoi historinĂ« e kĂ«saj biblioteke si dhe strukturĂ«s brenda LMDB njĂ« ligjĂ«rate shumĂ« interesante «The Lightning Memory-mapped Database». NjĂ« shembull i mirĂ« i pĂ«rvetĂ«simit tĂ« ruajtjes ndau Leonid Yuryev (aka yleo) nga Positive Technologies nĂ« ligjĂ«ratĂ«n e tij nĂ« Highload 2015 «Motorin LMDB — njĂ« kampion i veçantë». NĂ« tĂ«, ai flet pĂ«r LMDB nĂ« kontekstin e njĂ« detyre tĂ« ngjashme tĂ« realizimit tĂ« ReOpenLDAP, dhe LevelDB u kritikua nĂ« mĂ«nyrĂ« krahasuese. Pas implementimit, Positive Technologies krijoi edhe njĂ« fork aktivisht nĂ« zhvillim MDBX me disa karakteristika interesante, optimizime dhe korrigjime gabimesh.

LMDB përdoret shpesh edhe si një depo as is. Për shembull, shfletuesi Mozilla Firefox zgjodhi atë për një seri qëllimesh, dhe qysh me versionin 9, Xcode prefeoi atë mbi SQLite për ruajtjen e indekseve.

Motorri u shfaq edhe në botën e zhvillimit mobil. Dëshmi e përdorimit të tij mund të gjejmë në klientin iOS për Telegram. LinkedIn shkon akoma më tej dhe zgjodhi LMDB si depozitën e paracaktuar për struktura të veta të cache për të dhëna Rocket Data, për çka rrëfeu në artikullin e tij në vitin 2016.

LMDB pĂ«rfitoi njĂ« vend nĂ« tregun qĂ« Ă«shtĂ« lĂ«nĂ« hapur nga BerkeleyDB pas kalimit tĂ« saj nĂ«n kontrollin e Oracle. Biblioteka vlerĂ«sohet pĂ«r shpejtĂ«sinĂ« dhe besueshmĂ«rinĂ«, madje edhe nĂ« krahasim me tĂ« ngjashmet e saj. Siç dihet, nuk ka drekĂ« falas, dhe Ă«shtĂ« e rĂ«ndĂ«sishme tĂ« nĂ«nvizohet trade-off-i qĂ« do tĂ« pĂ«rballeni kur zgjidhni mes LMDB dhe SQLite. Schema e mĂ«sipĂ«rme tregon qartĂ« se si arrihet shpejtĂ«sia e pĂ«rmirĂ«suar. SĂ« pari, ne nuk paguajmĂ« pĂ«r shtresa tĂ« tjera abstraksioni mbi depozitimin disk. Natyrisht, nĂ« njĂ« arkitekturĂ« tĂ« mirĂ« nuk mund tĂ« shmangen ato, dhe ato do tĂ« shfaqen patjetĂ«r nĂ« kodin e aplikacionit, megjithatĂ« ato do tĂ« jenĂ« shumĂ« mĂ« tĂ« hollĂ«. Aty nuk do tĂ« ketĂ« funksione qĂ« nuk janĂ« tĂ« nevojshme pĂ«r aplikacionin e caktuar, pĂ«r shembull, mbĂ«shtetje pĂ«r Đ·Đ°ĐżŃ€ĐŸŃĐ” nĂ« gjuhĂ«n SQL. SĂ« dyti, krijohet mundĂ«sia pĂ«r tĂ« realizuar optimalisht mapimin e operacioneve aplikative nĂ« kĂ«rkesat pĂ«r depozitimin disk. NĂ«se SQLite nĂ« punĂ«n e saj bazohet nĂ« nevojat mesatare tĂ« njĂ« aplikacioni mesatar; si zhvillues aplikacionesh, ju jeni tĂ« vetĂ«dijshĂ«m pĂ«r skenaret kryesore tĂ« ngarkesĂ«s. PĂ«r njĂ« zgjidhje mĂ« tĂ« fuqishme do tĂ« duhet tĂ« paguani njĂ« çmim mĂ« tĂ« lartĂ« si pĂ«r zhvillimin e zgjidhjes fillestare ashtu edhe pĂ«r mbĂ«shtetje tĂ« saj tĂ« mĂ«vonshme.

3. Tre shtyllat e LMDB

Duke parë LMDB nga një perspektivë më të lartë, është koha të zhytemi thellë. Seksionet e ardhshme do të përqendrohen në shqyrtimin e tre shtyllave kryesore mbi të cilat mbështetet arkitektura e depozitës:

  1. Skedarët e shfaqur në memorie si një mekanizëm për funksionimin me disqet dhe sinkronizimin e strukturave të brendshme të të dhënave.
  2. B+-pemë si një organizim të strukturës së të dhënave të ruajtura.
  3. Copy-on-write si një qasje për sigurimin e pronave ACID të transaksioneve dhe shumë-versionitim.

3.1. Shtylla e parë. Skedarët e mbushur në memorie

TĂ« dhĂ«nat e paraqitura nĂ« skedarĂ« janĂ« njĂ« element arkitektonik kaq tĂ« rĂ«ndĂ«sishĂ«m, saqĂ« ato madje pĂ«rfshihen nĂ« emrin e depozitĂ«s. Çështjet e keq-kastrojve dhe sinkronizimit tĂ« aksesit nĂ« informacionin e ruajtur i janĂ« lĂ«nĂ« krejtĂ«sisht nĂ« dorĂ« sistemit operativ. LMDB nuk pĂ«rmban brenda saj asnjĂ« keĆĄ. Kjo Ă«shtĂ« njĂ« vendim i vetĂ«dijshĂ«m i autorit, pasi leximi i tĂ« dhĂ«nave direkt nga skedarĂ«t e paraqitur lejon pĂ«rjashtimin e shumĂ« kĂ«ndeve nĂ« realizimin e motorit. MĂ« poshtĂ« po sjell njĂ« listĂ« tĂ« pjesshme tĂ« disa prej tyre.

  1. Ruajtja e konsistencës së të dhënave në depo gjatë punës me të nga procese të shumta bëhet përgjegjësi e sistemit operativ. Në seksionin e ardhshëm, kjo mekanikë do të shqyrtohet me detaje dhe me ilustrime.
  2. Mungesa e keƥave e çliron plotësisht LMDB nga shpenzimet e lidhura me alokimet dinamike. Leximi i të dhënave në praktikë përbën vendosjen e një treguesi në adresën e duhur në memorie virtuale dhe nuk ka asgjë më shumë. Dëgjohet si një fantazi, por në burimet e depozites, të gjitha thirrjet e salloc përqendrohen në funksionin e konfigurimit të depozites.
  3. Mungesa e cache-ve do të thotë gjithashtu mungesë bllokimesh të lidhura me sinkronizimin e aksesit të tyre. Lexuesit, të cilët mund të ekzistojnë një numër të arbitrueshëm në të njëjtën kohë, nuk hasin asnjë mutex në rrugën e tyre për të dhënat. Për këtë arsye, shpejtësia e leximit ka një shkallëzueshmëri ideale lineare në lidhje me numrin e CPU-ve. Në LMDB, sinkronizimi i nënshtrohet vetëm operacioneve që modifikojnë. Një shkrues mund të jetë vetëm një në çdo moment të caktuar.
  4. Minimi i logjikës së caching dhe sinkronizimit e shpëton kodin nga një pamje jashtëzakonisht e komplikuar e gabimeve që lidhen me punën në një mjedis me shumë procese. Në konferencën Usenix OSDI 2014 kishte dy studime interesante mbi bazat e të dhënave: «Të gjitha Sistemet e Skedarëve Nuk Janë Të Krijuara Në Të Njëjtën Manner: Për Komplimentin e Krijimit të Aplikacioneve që Janë Të Qëndrueshme Pas Crash-it» dhe «Torturimi i Bazave të të Dhënave për Dëfrim dhe Fitim». Nga këto, mund të nxirren informacione mbi besueshmërinë e paprecedentë të LMDB, si dhe mbi realizimin praktikisht të përsosur të pronave ACID të transaksioneve, i cili tejkalon atë të SQLite.
  5. Minimalizmi i LMDB lejon që përfaqësimi i saj në makinë të vendoset plotësisht në L1-cache të procesorit, duke sjellë shpejtësi të jashtëzakonshme.

Fatkeqësisht, në iOS, skedarët e mapuar në memorie nuk janë aq të qetë sa do të dëshironim. Për të diskutuar me më shumë vetëdije mbi disavantazhet që lidhen me ta, duhet të kujtojmë parimet e përgjithshme të implementimit të këtij mekanizmi në sistemet operative.

Përmbledhje për skedarët e mapuar në memorie

Dallo shkĂ«lqimi nĂ« varfĂ«ri i bazĂ«s sĂ« tĂ« dhĂ«nave key-value LMDB nĂ« aplikacione pĂ«r iOSÇdo aplikacion i ekzekutues asociohet nga sistemi operativ me njĂ« entitet tĂ« quajtur proces. Çdo proces i jepet njĂ« interval adresash tĂ« vazhdueshĂ«m, nĂ« tĂ« cilin ai vendos gjithçka qĂ« i nevojitet pĂ«r tĂ« punuar. NĂ« adresat mĂ« tĂ« ulta ndodhen seksionet me kod dhe tĂ« dhĂ«na e burime tĂ« hardhĂ«s. MĂ« pas vjen njĂ« bllok nĂ« rritje i hapĂ«sirĂ«s adresore dinamike, qĂ« na Ă«shtĂ« bĂ«rĂ« i njohur si heap. NĂ« tĂ« ruhet adresat e entiteteve qĂ« shfaqen gjatĂ« funksionimit tĂ« programit. NĂ« pjesĂ«n e sipĂ«rme Ă«shtĂ« zona e memories qĂ« pĂ«rdoret nga steka e aplikacionit. Kjo rritet dhe zvogĂ«lohet, pra madhĂ«sia e saj ka gjithashtu natyrĂ« dinamike. QĂ« steka dhe heap tĂ« mos preken dhe tĂ« mos pengojnĂ« njĂ«ri-tjetrin, ato janĂ« ndarĂ« nĂ« skajet e ndryshme tĂ« hapĂ«sirĂ«s adresore. NĂ« mes dy seksioneve dinamike nĂ« sipĂ«r dhe poshtĂ« ka njĂ« boshllĂ«k. Adresat nĂ« kĂ«tĂ« segment tĂ« mesĂ«m pĂ«rdoren nga sistemi operativ pĂ«r tĂ« asociohen me njĂ« sĂ«rĂ« tĂ« ndryshme entitetesh. NĂ« veçanti, ajo mund tĂ« lidhĂ« njĂ« set tĂ« vazhdueshĂ«m adresash me njĂ« skedĂ« nĂ« disk. Kjo skedĂ« quhet e mapuar nĂ« memory.

Hapësira e adresës së procesit të dedikuar është e madhe. Teorikisht, numri i adresave është i kufizuar vetëm nga madhësia e treguesit, e cila përcaktohet nga bitësia e sistemit. Po të ishte konvikti fizik i ngjashëm 1 me 1, procesi i parë do të kishte ngrënë të gjithë RAM-in dhe askush nuk do të kishte mundësi për shumëprocese.

Megjithatë, nga përvoja jonë dimë se sistemet moderne operative mund të ekzekutojnë në të njëjtën kohë shumë procese. Kjo është e mundur sepse ato në fakt ndajnë vetëm në letër shumë memorje për proceset, ndërsa në realitet ngarkojnë në memorjen fizike vetëm atë pjesë që është e kërkuar në atë moment. Prandaj, memorja e asociuar me procesin quhet virtuale.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Sistemi operativ organizon kujtesën virtuale dhe fizike në forma faqesh me një madhësi të caktuar. Sa herë që një faqe e kujtesës virtuale bëhet e kërkuar, sistemi operativ e ngarkon atë në kujtesën fizike dhe vendos një lidhje midis tyre në një tabelë speciale. Nëse nuk ka vende të lirë, një nga faqet e ngarkuara më parë kopjohet në disqin, dhe faqja e kërkuar zë vendin e saj. Kjo procedurë, në të cilën do të kthehemi së shpejti, quhet swapping. Imazhi më poshtë ilustron procesin e përshkruar. Në të, faqja A me adresë 0 u ngarkua dhe u vendos në faqen e kujtesës kryesore me adresë 4. Ky fakt u reflektua në tabelën e lidhjeve në qelzin numër 0.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Historia me skedar tĂ« shfaqur nĂ« memorje Ă«shtĂ« gjithashtu e njĂ«jtĂ«. NĂ« mĂ«nyrĂ« logjike, ato duken se vendosen vazhdimisht dhe plotĂ«sisht nĂ« hapĂ«sirĂ«n virtuale tĂ« adresave. SidoqoftĂ«, ato hyjnĂ« nĂ« memorien fizike faqe pas faqeje dhe vetĂ«m kur kĂ«rkohen. Modifikimi i kĂ«tyre faqeve sinkronizohet me skedarin nĂ« disk. NĂ« kĂ«tĂ« mĂ«nyrĂ«, Ă«shtĂ« e mundur tĂ« kryhen operacione tĂ« hyrjes/daljes me skedarĂ«, thjesht duke punuar me bajta nĂ« memorje — tĂ« gjitha ndryshimet do tĂ« transferohen automatikisht nga bĂ«rthama e sistemit operativ nĂ« skedarin origjinal.
​
Imazhi më poshtë ilustron se si LMDB sinkronizon gjendjen e tij gjatë punës me një bazë të dhënash nga procese të ndryshme. Duke mapuar memorjen virtuale të proceseve të ndryshme në të njëjtin skedar, ne de facto e obligojmë sistemin operativ të sinkronizojë në mënyrë transitore blloqet e caktuara të hapësirave të tyre të adresave, në të cilat LMDB shikon.
​

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Një detaj i rëndësishëm është se LMDB me default modifikon skedarin me të dhëna përmes mekanizmit të thirrjes sistemike write, ndërsa vetë skedari është i mapuar në mënyrë read-only. Ky qasje ka dy pasoja të rëndësishme.

PasojĂ« e parĂ« — e pĂ«rbashkĂ«t pĂ«r tĂ« gjitha sistemet operative. Thelbi i saj Ă«shtĂ« shtimi i mbrojtjes kundĂ«r dĂ«mtimit tĂ« paqĂ«llimshĂ«m tĂ« bazĂ«s sĂ« tĂ« dhĂ«nave nga kodi jo korrekt. Siç dihet, udhĂ«zimet ekzekutuese tĂ« procesit kanĂ« lirinĂ« tĂ« qasen nĂ« tĂ« dhĂ«na nga çdo vend nĂ« hapĂ«sirĂ«n e adresĂ«s sĂ« tij. NĂ« tĂ« njĂ«jtĂ«n kohĂ«, siç sapo e pĂ«rmendĂ«m, shfaqja e skedarit nĂ« modin read-write do tĂ« thotĂ« se çdo udhĂ«zim gjithashtu mund ta modifikojĂ« atĂ«. NĂ«se kjo ndodh gabimisht, duke u pĂ«rpjekur, pĂ«r shembull, tĂ« mbivendosĂ« njĂ« element tĂ« array-sĂ« me njĂ« indek tĂ« paekzistueshĂ«m, atĂ«herĂ« ndoshta do tĂ« ndryshonte rastĂ«sisht skedarin e mapuar nĂ« kĂ«tĂ« adresĂ«, duke çuar nĂ« prishjen e bazĂ«s sĂ« tĂ« dhĂ«nave. NĂ«se skedari Ă«shtĂ« shfaqur nĂ« modin read-only, pĂ«rpjekja pĂ«r tĂ« ndryshuar hapĂ«sirĂ«n pĂ«rkatĂ«se tĂ« adresĂ«s do tĂ« çonte nĂ« dĂ«shtimin e programit me njĂ« sinjal SIGSEGV, dhe skedari do tĂ« mbetet i pandryshuar.

Pasoja e dytë është specifike për iOS. As autori, as ndonjë burim tjetër nuk e përmend atë shpesh, por pa të, LMDB do të ishte e papërdorshme në këtë sistem operativ mobil. Dispozita e tij i kushtohet seksionit të ardhshëm.

Specifika e skedarëve të shfaqur në memorje në iOS

Në vitin 2018, në WWDC u zhvillua një prezantim e shkëlqyer «The Deep Dive në Memorjen e iOS». Në të sqarohet se në iOS, të gjitha faqet që ndodhen në memorien fizike i përkasin një prej 3 llojeve: dirty, compressed dhe clean.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Memoria e pastĂ«r – Ă«shtĂ« grumbullimi i faqeve qĂ« mund tĂ« dalin pa pasoja nga memoria fizike. TĂ« dhĂ«nat nĂ« tĂ« mund tĂ« ngarkohen pĂ«rsĂ«ri sipas nevojĂ«s nga burimet e tyre fillestare. SkedarĂ«t e hartuar pĂ«r lexim vetĂ«m bien nĂ« kĂ«tĂ« kategori. iOS nuk druhet tĂ« shkarkojĂ« nĂ« çdo moment faqet e shfaqura nĂ« skedar nga memoria, pasi ato janĂ« tĂ« garantuara se janĂ« tĂ« sinkronizuara me skedarin nĂ« disk.
​
Në memorien e ndotur përfshihen të gjitha faqet e modifikuara, pavarësisht nga vendndodhja e tyre fillestare. Në veçanti, skedarët e hartuar për memorien, të modifikuar përmes shkrimit në memorien virtuale të lidhur me ta, do të klasifikohen ashtu. Duke hapur LMDB me flamurin MDB_WRITEMAP, pas kryerjes së ndryshimeve, kjo mund të konfirmohet personalisht.

Sapo që aplikacioni fillon të zërë shumë memorie fizike, iOS i nënshtrohet kompresimit të faqeve dirty. Shuma e memories e zënë nga faqet dirty dhe të kompresuara përbën atë që quhet footprint-i i memories së aplikacionit. Pasi ajo arrin një vlerë prag, procesi i nënshtrohet demonit sistemik OOM killer dhe përfundimisht mbyllet. Kjo është një veçori e iOS krahasuar me sistemet operative desktop. Ndryshe nga ato, ulja e footprint-it të memories përmes swap-it të faqeve nga memoria fizike në disk nuk është e parashikuar në iOS. Për arsyet mund të spekulojmë. Ndoshta procedura e lëvizjes intensive të faqeve në disk dhe përsëri është shumë energjitike për pajisjet mobile, ose iOS kursen burimin e ri-shkrimit të qelizave në SSD, ose ndoshta projektuesit nuk ishin të kënaqur me performancën e përgjithshme të sistemit, ku gjithçka shkëmbehet vazhdimisht. Si do që të jetojë, fakti mbetet fakt.

Lajmi i mirë, siç u përmend më parë, është se LMDB nuk përdor mekanizmin mmap për të përditësuar skedarët në mënyrë default. Kjo do të thotë se të dhënat e shfaqura klasifikohen nga iOS si memorje e pastër dhe nuk ndihmojnë në footprint-in e memories. Këtë mund ta konfirmoni me mjetin e Xcode të quajtur VM Tracker. Në screenshot-in më poshtë, është treguar gjendja e memorjes virtuale të aplikacionit Cloud në kohën e funksionimit. Në fillim, ishin inicializuar 2 instanca LMDB. E para kishte leje të shfaqte skedarin e saj në 1GiB memorje virtuale, ndërsa e dyta në 512MiB. Megjithëse të dy depozitat zënë një sasi të caktuar të memorjes rezidente, asnjëra prej tyre nuk kontribuon në madhësinë dirty.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Tani është koha për lajme të këqija. Falë mekanizmit të swap-it në sistemet operative desktop me 64 bit, çdo proces mund të marrë aq hapësirë virtuale adresash sa lejon hapësira e lirë në hard disk për swap-in e tij të mundshëm. Zëvendësimi i swap-it me kompresim në iOS e ul radikalisht maksimumin teorik. Tani të gjithë proceset aktive duhet të përputhen brenda memories kryesore (kuptohet RAM), dhe të gjithë ata që nuk kanë hapësirë duhet të mbyllen me detyrim. Kjo është e thënë si në përmendjen e mësipërme raportin, si dhe në dokumenti zyrtar. Si pasojë, iOS e kufizon ashpër madhësinë e memories që është e disponueshme për alokim përmes mmap. Këtu është këtu mund të shqyrtojmë kufijtë empirike të kapaciteteve të memories që janë arritur të alokohen në pajisje të ndryshme me anë të këtij thirrjeje sisteme. Në modelet më moderne të smartphone-ve, iOS ofron 2 gigabajt, ndërsa në versionet premium të iPad-it, deri në 4. Në praktikë, natyrisht, është e nevojshme të orientohesh në modelet më të ulëta të mbështetura të pajisjeve, ku gjendja është shumë më e rëndë. Më keq akoma, duke shqyrtuar gjendjen e memories së aplikacioneve në VM Tracker, mund të zbulohet se LMDB nuk është e vetmja që pretendon për memory-mapped memory. Shumë copa zënë hapësirë nga alokatorët sistemorë, skedarët e burimeve, framework-et për punën me imazhe dhe grabitqarë të tjerë më të vegjël.

Pas eksperimenteve në Cloud, ne arritëm në këto vlera kompromisi për memorien e alokuar të LMDB: 384 megabajt për pajisjet 32-bit dhe 768 për ato 64-bit. Pas shpenzimit të këtij kapaciteti, çdo operacion modifikues fillon të përfundojë me kodin MDB_MAP_FULL. Këto gabime i vëzhgojmë në monitorimin tonë, por ato janë mjaft të pakta sa që në këtë fazë mund të injorohen.

Një shkak jo i dukshëm i konsumit të tepruar të memories nga ruajtësi mund të jenë transaksionet afatgjata. Për të kuptuar si lidhen këto dy dukuri, do të ndihmojë shqyrtimi i dy balenave të tjera të LMDB.

3.2. Balerina №2. B+-tree

Për të simuluar tabela mbi ruajtësin e çelësit-vlerës, është e nevojshme që API-ja e tij të ketë operacionet e mëposhtme:

  1. Shtimi i një elementi të ri.
  2. Kërkimi i një elementi me një çelës të caktuar.
  3. Fshirja e një elementi.
  4. Iterimi mbi intervalet e çelësave në rendin e tyre të renditur.

Dallo shkĂ«lqimi nĂ« varfĂ«ri i bazĂ«s sĂ« tĂ« dhĂ«nave key-value LMDB nĂ« aplikacione pĂ«r iOSStruktura mĂ« e thjeshtĂ« e tĂ« dhĂ«nave, me anĂ« tĂ« sĂ« cilĂ«s mund tĂ« realizohen lehtĂ«sisht tĂ« katĂ«r operacionet, Ă«shtĂ« njĂ« pemĂ« binare e kĂ«rkimit. Çdo nyje e saj pĂ«rfaqĂ«son njĂ« çelĂ«s, duke ndarĂ« tĂ« gjithĂ« nĂ«ngrupin e çelĂ«save fĂ«mijĂ« nĂ« dy nĂ«npema. NĂ« tĂ« majtĂ« janĂ« ata qĂ« janĂ« mĂ« tĂ« vegjĂ«l se prindĂ«ri, ndĂ«rsa nĂ« tĂ« djathtĂ« janĂ« ata qĂ« janĂ« mĂ« tĂ« mĂ«dhenj. Marrja e njĂ« grupi tĂ« renditur tĂ« çelĂ«save arrihet pĂ«rmes njĂ« nga kalimet klasike tĂ« pemĂ«s.

Pemendësitë binarë kanë dy disavantazhe themelore që nuk i lejojnë ato të jenë efikase si një strukturë të dhënash diskop. Së pari, shkalla e balancimit të tyre është e paparashikueshme. Ka një rrezik të konsiderueshëm për të pasur pemë ku lartësia e degëve të ndryshme mund të ndryshojë ndjeshëm, gjë që e përkeqëson ndjeshëm kompleksitetin algoritmik të kërkimit krahasuar me atë që pritet. Së dyti, abundenca e lidhjeve ndërmjet nyjave heq lokalitetin në memorje për pemët binarë. Nyjat e afërta (në kuptimin e lidhjeve ndërmjet tyre) mund të ndodhen në faqe krejtësisht të ndryshme në memorjen virtuale. Si pasojë, edhe për një kalim të thjeshtë në disa nyja fqinjësore në pemë mund të kërkohet të vizitohen një numër të ngjashëm faqesh. Kjo është një problem madje kur ne diskutojmë për efikasitetin e pemëve binarë si një strukturë të dhënash në memorje, pasi rotacioni i vazhdueshëm i faqeve në cache-in e procesorit është një luks i kushtueshëm. Kur është fjala për ngritjen e shpeshtë të faqeve të lidhura me nyjat nga disku, situata bëhet akoma më e keqe. e mjerë.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOSB-pemet dhe, duke qenë evolucion i pemëve binarë, zgjidhin problemet e përmendura në paragrafin e mëparshëm. Së pari, ato janë auto-balancuese. Së dyti, çdo nyje e tyre ndan një grup të madh çelësash fëmijë jo në 2, por në M nën-grupe të renditur, ku numri M mund të jetë mjaft i madh, duke arritur disa qindra, madje edhe mijëra.

Përmes kësaj:

  1. Në çdo nyje ndodhet një numër i madh çelësash të renditur tashmë dhe pemët rezultojnë shumë të ulëta.
  2. Pemja merr pronën e vendosjes lokale në memorie, pasi çelësat e afërt sipas vlerës natyrshëm vendosen pranë njëri-tjetrit në një nyje ose në nyje fqinje.
  3. Numri i nyjeve tranzitore zvogëlohet gjatë zbritjes në pemë gjatë operacionit të kërkimit.
  4. Numri i nyjeve të synuara të lexuara zvogëlohet gjatë kërkesave range, pasi në çdo njëri prej tyre tashmë ndodhet një numër i madh çelësash të renditur.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Në LMDB për ruajtjen e të dhënave përdoret një nga variacionet e B-pemës e cila quhet B+-pemë. Në diagramin e mësipërm janë paraqitur tre lloje nyjesh që ekzistojnë në të:

  1. NĂ« majĂ« ndodhet rrĂ«nja (root). Ajo materializon konceptin e bazĂ«s sĂ« tĂ« dhĂ«nave brenda depozites. Brenda njĂ« instance tĂ« LMDB, mund tĂ« krijoni disa baza tĂ« tĂ« dhĂ«nash qĂ« ndajnĂ« njĂ«ra-tjetrĂ«n hapĂ«sirĂ«n virtuale tĂ« adresave tĂ« mapuara. Çdo njĂ«ra prej tyre fillon me rrĂ«njĂ«n e saj.
  2. Në nivelin më të ulët ndodhen gjethet (leaf). Ato janë dhe vetëm ato që përmbajnë çiftet e ruajtur të çelësit-vlerës në bazën e të dhënave. Me këtë rast, kjo është veçoria e B+-pemëve. Nëse një pemë e zakonshme B ruan pjesët e value në nyjet e të gjithë niveleve, varianti B+-ruan ato vetëm në nivelin më të ulët. Duke e trajtuar këtë fakt, do të referohemi më tej në nënllojin e pemës të përdorur në LMDB thjesht si pemë B.
  3. Ndërmjet rrënjës dhe gjetheve ndodhen 0 ose më shumë nivele teknike me nyje naviguese (branch). Detyra e tyre është të ndajnë grupin e renditur të çelësave midis gjetheve.

Nodet fizike janĂ« blloqe memorie me njĂ« gjatĂ«si tĂ« caktuar paraprakisht. MadhĂ«sia e tyre Ă«shtĂ« njĂ« shumĂ«fish i madhĂ«sisĂ« sĂ« faqeve tĂ« memories nĂ« sistemin operativ, pĂ«r tĂ« cilin folĂ«m mĂ« sipĂ«r. MĂ« poshtĂ« shihet struktura e nodit. NĂ« header ndodhet meta-informacioni, mĂ« e dukshme nga tĂ« cilat pĂ«r shembull Ă«shtĂ« kontrollet e shumĂ«sisĂ«. MĂ« pas vjen informacioni pĂ«r offset-at ku ndodhen qelizat me tĂ« dhĂ«na. NĂ« rolin e tĂ« dhĂ«nave mund tĂ« paraqiten ose çelĂ«sat, nĂ«se flasim pĂ«r nodet naviguese, ose çifte tĂ« plota çelĂ«s-vlerĂ« nĂ« rastin e gjetheve. MĂ« shumĂ« rreth strukturĂ«s sĂ« faqeve mund tĂ« lexoni nĂ« punimin «VlerĂ«simi i Dyqaneve me PerformancĂ« tĂ« LartĂ« ÇelĂ«s-Vlerë».

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Pasi kuptuam përmbajtjen e brendshme të nodëve faqesh, më pas do të paraqesim thjesht B-pohon LMDB në formën e mëposhtme.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Faqet me nodë janë të organizuara në mënyrë të njëpasnjëshme në disk. Faqet me numra më të mëdhenj janë më afër fundit të skedarit. Kështu e quajtur faqja meta (meta page) përmban informacion mbi zhvendosjet, ku mund të gjendet rrënjët e të gjitha pemëve. Kur hapet skedari LMDB, ai skanon skedarin faqe pas faqeje nga fundi në fillim në kërkim të një faqje meta të vlefshme dhe përmes saj gjen bazat e dhënave ekzistuese.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Tani, me një kuptim të strukturës logjike dhe fizike të organizatës së të dhënave, mund të kalojmë në shqyrtimin e balenës së tretë LMDB. Me ndihmën e tij, të gjitha modifikimet e depozitës ndodhin në mënyrë transaksionale dhe të izoluar nga njëra-tjetra, duke i dhënë bazës së të dhënave gjithashtu një pronësi të shumëversionshmërisë.

3.3. Balenë e tretë. Copy-on-write

Disa operacione me B-pema parashikojnë kryerjen e një serie të tërë ndryshimesh në nodet e saj. Një nga shembujt është shtimi i një çelësi të ri në një nod, i cili tashmë ka arritur kapacitetin maksimal. Në këtë rast, është e nevojshme, së pari, të ndahet nodi në dy pjesë, dhe së dyti, të shtohet një referencë për nodin e ri të shkëputur në prindin e tij. Kjo procedurë potencialisht është shumë e rrezikshme. Nëse për ndonjë arsye (mund të jetë një dështim, ndërprerje energjie, etj.) ndodhin vetëm disa nga ndryshimet e serisë, atëherë pema do të mbetet në një gjendje të papajtueshme.

Një nga zgjidhjet tradicionale për të siguruar që baza e të dhënave të jetë të qëndrueshme ndaj dështimeve është shtimi i një strukture të dhënash diskore pranë B-tree, e njohur si logu i transaksioneve, gjithashtu i njohur si write-ahead log (WAL). Ai përbën një skedar, në fund të të cilit shkruhet një operacion i parashikuar përpara se të modifikohet vetë B-tree. Kështu, nëse gjatë vetëdiagnostikimit zbulohen dëmtime të të dhënave, baza e të dhënave konsultohet me logun për t'u rikthyer në gjendjen e saj të saktë.

LMDB ka zgjedhur një mënyrë tjetër si mekanizëm për të siguruar qëndrueshmërinë ndaj dështimeve, e cila quhet copy-on-write. E gjithë ideja është që vendi i të dhënave të mos përditësohet në faqen ekzistuese, por përkundrazi, ajo kopjohet e tëra dhe të gjitha modifikimet bëhen në kopjen e saj.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Më pas, për t'u siguruar që të dhënat e përditësuara të jenë të aksesueshme, është e nevojshme të ndryshohet lidhja ndaj nodit të bërë të rëndësishëm në nodin prind në raport me të. Duke qenë se për këtë gjithashtu duhet të modifikohet, ai gjithashtu kopjohet paraprakisht. Procesi vazhdon në mënyrë rekurosive deri në rrënjën e vet. Të dhënat në faqen meta ndryshohen si të fundit.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Nëse ndodh që gjatë procedurës së përditësimit të ndodhë një përfundim emergjent i procesit, atëherë ose nuk do të krijohet një faqe meta e re, ose ajo nuk do të shkruhet në disk deri në fund, dhe kontrollet e saj do të jenë të gabuara. Në çdo rast të tillë, faqet e reja do të jenë të paarritshme, ndërsa ato të vjetra nuk do të preken. Kjo e eliminon nevojën e LMDB për të mbajtur një write ahead log për të garantuar konsistencën e të dhënave. Struktura e ruajtjes së të dhënave në disk, e përshkruar më sipër, merr gjithashtu përsipër funksionin e saj. Mungesa e një llogarie transaksionesh në mënyrë eksplicite është një nga karakteristikat e LMDB, e cila siguron një shpejtësi të lartë në leximin e të dhënave.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Struktura e quajtur append-only B-tree ofron natyrshëm izolimin e transaksioneve dhe shumëversionalitetin. Në LMDB, çdo transaksion të hapur lidhet me rrënjën aktuale të pemës. Deri sa transaksioni të përfundojë, faqet e pemës përkatëse nuk do të ndryshohen ose ripërdoren për versione të reja të të dhënave. Kështu, mund të punoni për aq kohë sa dëshironi me grupin e të dhënave që ishte i vlefshëm në momentin e hapjes së transaksionit, madje edhe nëse depolari vazhdon të përditësohet aktivisht. Kjo është thelbi i shumëversionalitetit, që e bën LMDB burimin ideal të të dhënave për të gjithë ne të dashur. UICollectionView. Duke transaksionin, nuk është e nevojshme të rritet memory footprint e aplikacionit, duke nxituar të shkarkosh të dhënat aktuale në ndonjë strukturë in-memory, duke u frikësuar nga situata e rëndë. Kjo veçori e dallon LMDB nga SQLite, e cila nuk mund të mburrët me një izolim të tillë total. Duke hapur dy transaksione në këtë të fundit dhe duke fshirë një rekord në kuadër të njërit prej tyre, ajo e njëjtë nuk do të arrihet më as në kuadër të transaksionit tjetër të mbetur.

Anë e kundërt e medaljes është potencialisht një konsum shumë më i madh i memories virtuale. Në slajd është paraqitur si do të dukej struktura e bazës së të dhënave, nëse ndodh ndërlikimi i saj në të njëjtën kohë me 3 transaksione të hapura për lexim, që shikojnë versione të ndryshme të bazës së të dhënave. Pasi LMDB nuk mund të ribëjë përdorimin e nyjeve të arritshme nga rrënjët e lidhura me transaksionet aktuale, depozitimi nuk ka tjetër zgjedhje përveçse të vendosë në memorie një rrënjë të katërt dhe sërish të klonojë faqet që janë në modifikim nën të.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

KĂ«tu do tĂ« ishte mirĂ« tĂ« rikujtojmĂ« seksionin mbi skedarĂ«t e mapuar nĂ« kujtesĂ«. Duket se shpenzimi shtesĂ« i kujtesĂ«s virtuale nuk duhet tĂ« na shqetĂ«sojĂ« shumĂ«, pasi ai nuk ndikon nĂ« footprint-in e aplikacionit. MegjithatĂ«, Ă«shtĂ« vĂ«nĂ« re se iOS Ă«shtĂ« shumĂ« e kursyer nĂ« alokimin e saj, dhe ne nuk mund tĂ« ofrojmĂ« njĂ« rajon LMDB prej 1 terabaiti siç bĂ«jmĂ« nĂ« server ose desktop dhe tĂ« mos e kemi pĂ«r mendje kĂ«tĂ« veçori. NĂ«se Ă«shtĂ« e mundur, duhet tĂ« pĂ«rpiqemi t’i mbajmĂ« jetĂ«n e transaksioneve sa mĂ« tĂ« shkurtĂ«r tĂ« jetĂ« e mundur.

4. Projektimi i skemës së të dhënave mbi API-në key-value

Do ta nisim analizën e API-së duke shqyrtuar abstraksionet bazë që ofron LMDB: ambienti dhe bazat e të dhënave, çelësat dhe vlerat, transaksionet dhe kursoret.

Vërejtje mbi listat e kodit

Të gjitha funksionet në API-në publike të LMDB kthejnë rezultatet e punës së tyre në formën e një kodi gabimi, por në të gjitha listat pasuese kontrolli i tij është lënë mënjanë në emër të përmbledhjes. Në praktikë, ne përdorëm fork paketat C++ lmdbxx, ku gabimet materializohen në formën e përjashtimeve C++.

Si mënyra më e shpejtë për të lidhur LMDB me një projekt për iOS ose macOS, ju paraqes CocoaPod tim POSLMDB.

4.1. Abstraksionet Bazë

Mjedisi (environment)

Struktura MDB_env është një enë që përmban gjendjen e brendshme të LMDB. Grupi i funksioneve me prefiksin mdb_env lejon konfigurimin e disa cilësive të tij. Në rastin më të thjeshtë, inicializimi i motorit duket si më poshtë.

mdb_env_create(env);​
mdb_env_set_map_size(*env, 1024 * 1024 * 512)​
mdb_env_open(*env, path.UTF8String, MDB_NOTLS, 0664);

Në aplikacionin e Oblaqeve Mail.ru, ne ndryshuam vetëm dy parametra nga vlertat e paracaktuara.

I pari prej tyre Ă«shtĂ« madhĂ«sia e hapĂ«sirĂ«s virtuale tĂ« adresĂ«s, nĂ« tĂ« cilĂ«n hartohet skedari i ruajtjes. PĂ«r fat tĂ« keq, madje edhe nĂ« tĂ« njĂ«jtin pajisje, vlera e saktĂ« mund tĂ« ndryshojĂ« ndjeshĂ«m nga njĂ« ekzekutim nĂ« tjetrin. PĂ«r tĂ« marrĂ« parasysh kĂ«tĂ« veçori tĂ« iOS, sasia maksimale e ruajtjes pĂ«rcaktohet dinamikisht. Duke filluar nga njĂ« vlerĂ« e caktuar, ajo pĂ«rgjysmohet gradualisht derisa funksioni mdb_env_open nuk kthen njĂ« rezultat tĂ« ndryshĂ«m nga ENOMEM. NĂ« teori ekziston edhe njĂ« mĂ«nyrĂ« e kundĂ«rt — fillimisht tĂ« alokosh minimumin e memories pĂ«r motorin, dhe mĂ« pas, nĂ« rast tĂ« gabimeve MDB_MAP_FULL, pĂ«r ta rritur atĂ«. MegjithatĂ«, ai Ă«shtĂ« shumĂ« mĂ« i ndĂ«rlikuar. Arsyeja Ă«shtĂ« se procedura e ri-ndarjes sĂ« memories (remap) me anĂ« tĂ« funksionit mdb_env_set_map_size an invalidon tĂ« gjitha entitetet (kursoret, transaksionet, çelĂ«sat dhe vlerat) qĂ« janĂ« marrĂ« nga motorri mĂ« parĂ«. TĂ« dĂ«shmosh pĂ«r kĂ«tĂ« kthesĂ« nĂ« ngjarje nĂ« kod do tĂ« sjellĂ« njĂ« komplikim tĂ« dukshĂ«m tĂ« tij. NĂ«se, megjithatĂ«, memoria virtuale Ă«shtĂ« shumĂ« e rĂ«ndĂ«sishme pĂ«r ju, kjo mund tĂ« jetĂ« njĂ« arsye pĂ«r tĂ« shikuar njĂ« fork qĂ« ka avancuar shumĂ« pĂ«rpara MDBX, ku ndĂ«rmjet veçorive tĂ« shpallura Ă«shtĂ« edhe «korrigjimi automatik i madhĂ«sisĂ« sĂ« bazĂ«s sĂ« tĂ« dhĂ«nave nĂ« kohĂ« reale».

Parametri i dytë, vlera e paracaktuar e të cilit nuk na përshtatet, rregullon mekanikën e sigurimit të sigurisë për fijet. Fatkeqësisht, të paktën në iOS 10 ka probleme me mbështetje të ruajtjes lokale të fijeve. Për këtë arsye, në shembullin më sipër, depozita hapet me flamurin MDB_NOTLS. Përveç kësaj, nevojitej gjithashtu forking mbështjellësin C++ lmdbxx, për të hequr variablat me këtë atribut dhe në të.

Baza të dhënash

Baza e të dhënave përbën një instancë të veçantë të B-tree-ëve, për të cilët folëm më parë. Hapja e saj ndodh brenda një transaksioni, gjë që në fillim mund të duket pak e çuditshme.

MDB_txn *txn;​
MDB_dbi dbi;​
mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);​
mdb_dbi_open(txn, NULL, MDB_CREATE, &dbi);​
mdb_txn_abort(txn);

Vërtet, transaksioni në LMDB është një entitet magazinimi dhe jo një bazë të dhënash specifike. Ky koncept lejon kryerjen e operacioneve atomike në entitete që ndodhen në baza të ndryshme të dhënash. Në teori, kjo hap mundësinë për modelimin e tabelave si baza të ndryshme, por unë në atë kohë kam ndjekur një rrugë tjetër, të përshkruar mëposht.

ÇelĂ«sat dhe vlerat

Struktura MDB_val modellon konceptin si çelĂ«sin ashtu edhe vlerĂ«n. Magazina nuk ka asnjĂ« pĂ«rceptim pĂ«r semantikĂ«n e tyre. PĂ«r tĂ«, diçka qĂ« Ă«shtĂ« diçka tjetĂ«r – Ă«shtĂ« thjesht njĂ« varg bytesh me njĂ« madhĂ«si tĂ« caktuar. MadhĂ«sia maksimale e çelĂ«sit Ă«shtĂ« 512 bytes.

typedef struct MDB_val {​
    size_t mv_size;​
    void *mv_data;​
} MDB_val;​​

Me ndihmĂ«n e krahasuesit, magazina rendit çelĂ«sat nĂ« rritje. NĂ«se nuk e zĂ«vendĂ«son me tĂ«ndin, do tĂ« pĂ«rdoret ai default, i cili i rendit ato byte pĂ«r byte nĂ« rendin leksikografik.​

Transaksionet

Struktura e transaksioneve është përshkruar në detaje në kapitullin e mëparshëm, prandaj këtu do të përsëris shkurtimisht pronat e tyre kryesore:

  1. Mbështetje për të gjitha pronat bazë ACID: atomik, konsistencë, izolim dhe besueshmëri. Duhet të përmend se në lidhje me durability në macOS dhe iOS ka një defekt, i cili është rregulluar në MDBX. Më shumë mund të lexoni në README.
  2. Qasja ndaj multitreadit përshkruhet nga skema "shkrues i vetëm / lexues të shumtë". Shkruesit bllokojnë njëri-tjetrin, por nuk bllokojnë lexuesit. Lexuesit nuk bllokojnë as shkruesit dhe as njëri-tjetrin.
  3. Mbështetje për transaksione të thelluara.
  4. Mbështetje për multiversionim.

Multiversionimi në LMDB është aq i mirë, saqë dua ta demonstroj atë në veprim. Nga kodi më poshtë shihet se çdo transaksi punon me atë version të databazës që ishte aktual në momentin e hapjes së saj, duke qenë plotësisht e izoluar nga të gjitha ndryshimet në vazhdim. Inicimi i depozitës dhe shtimi i një rekord testi nuk paraqesin asgjë interesante, prandaj këto rituale janë lënë nën spoilerin.

Shtimi i një rekord testi

MDB_env *env;
MDB_dbi dbi;
MDB_txn *txn;

mdb_env_create(&env);
mdb_env_open(env, "./testdb", MDB_NOTLS, 0664);

mdb_txn_begin(env, NULL, 0, &txn);
mdb_dbi_open(txn, NULL, 0, &dbi);
mdb_txn_abort(txn);

char k = 'k';
MDB_val key;
key.mv_size = sizeof(k);
key.mv_data = (void *)&k;

int v = 997;
MDB_val value;
value.mv_size = sizeof(v);
value.mv_data = (void *)&v;

mdb_txn_begin(env, NULL, 0, &txn);
mdb_put(txn, dbi, &key, &value, MDB_NOOVERWRITE);
mdb_txn_commit(txn);

MDB_txn *txn1, *txn2, *txn3;
MDB_val val;

// Hapim 2 transaksione, secila prej të cilave shikon
// versionin e databazës me një regjistrim.
mdb_txn_begin(env, NULL, 0, &txn1); // leximi-shkrimi
mdb_txn_begin(env, NULL, MDB_RDONLY, &txn2); // leximi-i vetëm

// Brenda transaksionit të parë, fshijmë regjistrimin ekzistues nga databaza.
mdb_del(txn1, dbi, &key, NULL);
// Konfirmojmë fshirjen.
mdb_txn_commit(txn1);

// Hapim transaksionin e tretë, i cili shikon në
// versionin aktual të databazës, ku regjistrimi nuk ekziston më.
mdb_txn_begin(env, NULL, MDB_RDONLY, &txn3);
// Sigurohemi që regjistrimi për çelësin e kërkuar nuk ekziston më.
assert(mdb_get(txn3, dbi, &key, &val) == MDB_NOTFOUND);
// Përfundojmë transaksionin.
mdb_txn_abort(txn3);

// Sigurohemi që brenda transaksionit të dytë, i hapur në momentin
// e ekzistencës së regjistrimit në databazë, akoma mund ta gjejmë
// atë sipas çelësit.
assert(mdb_get(txn2, dbi, &key, &val) == MDB_SUCCESS);
// Kontrollojmë që për çelësin të ketë marrë një informacion valid.
assert(*(int *)val.mv_data == 997);
// Përfundojmë transaksionin, që punon edhe me një databazë të vjetër gjithsesi konsistente.
mdb_txn_abort(txn2);

Opcionalisht, rekomandoj të provoni të bëni të njëjtin truk me SQLite dhe të shihni se çfarë rezultati merrni.

Multi-versionimi sjell përfitime të këndshme për zhvilluesit e iOS. Me këtë veçori, mund të rregulloni lehtësisht dhe pa ndonjë vështirësi shpejtësinë e përditësimit të burimeve të të dhënave (data source) për format ekranore, duke u bazuar në përvojën e përdoruesit. Për shembull, le të marrim një karakteristikë të aplikacionit Cloud nga Mail.ru si ngarkimin automatik të përmbajtjes nga galeria mediale sistemike. Me një lidhje të mirë, klienti është në gjendje të ngarkojë disa fotografi në server në sekondë. Nëse pas çdo ngarkimi përditësoni UICollectionView me mediat në cloud të përdoruesit, atëherë mund të harrohen 60 fps dhe skrollimi i qetë gjatë këtij procesi. Për të parandaluar përditësime të shpeshta të ekranit, duhet ta kufizoni ndonjëherë shpejtësinë e ndryshimit të të dhënave në thelb UICollectionViewDataSource.

Nëse baza e të dhënave nuk mbështet shumë-versionimin dhe lejon të punoni vetëm me gjendjen aktuale, për të krijuar një snapshot të qëndrueshëm të të dhënave, është e nevojshme ta kopjoni atë në një strukturë të dhënash in-memory ose në një tabelë përkohshme. Një nga këto qasje është shumë e kushtueshme. Në rastin e depozitës in-memory, kemi shpenzime si për memorinë, për shkak të ruajtjes së objekteve të konstruktuara, ashtu edhe për kohën, të lidhura me konvertimet e tepruara të ORM. Sa i përket tabelës përkohshme, kjo është një kënaqësi edhe më e shtrenjtë, që ka kuptim vetëm në raste jo triviale.

Shumë-versionimi i LMDB zgjidh problemin e mbajtjes së një burimi të stabilizuar të të dhënave shumë eleganten. Mjafton thjesht të hapni një transaksion dhe voilà - derisa ta përfundojmë, grupi i të dhënave është garantuar të jetë i fikstuar. Logjika e shpejtësisë së përditësimit tani është plotësisht në duar të shtresës prezantuese pa asnjë ngarkesë të rëndësishme të burimeve.

Kursoret

Kursoret ofrojnë një mekanizëm për iterimin e renditur mbi çiftet çelës-vlerë përmes kalimit në pemën B. Pa to, do të ishte e pamundur të modelonim në mënyrë efektive tabelat në bazën e të dhënave, për të cilat kalojmë në shqyrtim.

4.2. Modelimi i Tabelave

Pronësia e renditjes së çelësave lejon ndërtimin mbi abstraksionet themelore të një nivelit të lartë si tabela. Le të shqyrtojmë këtë proces në shembullin e tabelës kryesore të klientit në re, ku është ruajtuar informacioni mbi të gjitha skedaret dhe dosjet e përdoruesit.

Skema e Tabelës

Një nga skenarët e zakonshëm për të cilin struktura e tabelës me pemën e dosjeve duhet të jetë e dizajnuar është marrëveshja e të gjitha elementeve që ndodhen brenda një direktoriumi të caktuar. Një model i mirë i organizatës së të dhënave për kërkesa efikase të këtij lloji është Lista e Fqinjëve. Për ta realizuar këtë, mbi depozitat e çelës-veprim duhet të renditen çelësat e skedave dhe dosjeve në një mënyrë që ato grupohen sipas përkatësisë ndaj direktorisë prind. Për më tepër, për të shfaqur përmbajtjen e direktorisë në një format të njohur për përdoruesin Windows (së pari dosjet, pastaj skedat, dhe të dyja të renditura alfabetikisht), është e nevojshme të përfshihen në çelës fushat shtesë përkatëse.

Në imazhin më poshtë tregohet se si, duke u bazuar në detyrën e vendosur, mund të duket prezantimi i çelësave si një array bajtësh. Fillimisht vendosen bajtët me identifikuesin e direktorisë prind (të kuq), pastaj ato me llojin (të gjelbra) dhe në fund emri (të kaltër). Duke u renditur me krahasuesin standart të LMDB në rendin leksikografik, ato renditen në mënyrën e kërkuar. Shkelja e rregullt e çelësave me identifikuesin e kuq të njëjtë na jep vlerat e lidhura me to në atë rend ndaj së cilës ato duhet të shfaqen në ndërfaqen e përdoruesit (të djathtën), pa kërkuar ndonjë trajtim shtesë.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Serializimi i çelësave dhe vlerave

NĂ« botĂ« janĂ« menduar shumĂ« metoda pĂ«r serializimin e objekteve. Duke marrĂ« parasysh se nuk kishim asnjĂ« kĂ«rkesĂ« tjetĂ«r pĂ«rveç shpejtĂ«sisĂ«, pĂ«r vete zgjodhĂ«m atĂ« mĂ« tĂ« shpejtin nga tĂ« gjitha — dump-in e memories, e cila Ă«shtĂ« e zĂ«nĂ« nga instanca e strukturĂ«s sĂ« gjuhĂ«s C. KĂ«shtu, çelĂ«si i elementit tĂ« drejtorisĂ« mund tĂ« modelojĂ« strukturĂ«n e mĂ«poshtme. NodeKey.

typedef struct NodeKey {
    EntityId parentId;
    uint8_t type;
    uint8_t nameBuffer[256];
} NodeKey;

Për ruajtjen NodeKey në depo duhet të pozicionohet treguesi në të dhëna në adresën e fillimit të strukturës, dhe madhësia e saj llogaritet me funksionin MDB_val sizeof MDB_val serialize(NodeKey * const key) { return MDB_val { .mv_size = sizeof(NodeKey), .mv_data = (void *)key }; }.

MDB_val serialize(NodeKey * const key) {
    return MDB_val {
        .mv_size = sizeof(NodeKey),
        .mv_data = (void *)key
    };
}

Në kapitullin e parë rreth kritereve të zgjedhjes së bazës së të dhënave, si një faktor të rëndësishëm të zgjedhjes, përmenda minimizimin e alokimeve dinamike gjatë operacioneve CRUD. Kodi i funksionit serialize tregon se si në rastin e LMDB ato mund të evitohen plotësisht kur futen në bazën e të dhënave regjistrime të reja. Arna e ardhur e bajtave nga serveri transformohet fillimisht në struktura të stack-ut, dhe më pas ato shkarkohen në mënyrë triviale në ruajtje. Duke pasur parasysh se brenda LMDB gjithashtu nuk ka alokime dinamike, mund të arrihet një situatë fantastike për standardet e iOS - të përdorësh vetëm kujtesën e stack-ut për punën me të dhënat në të gjithë rrugën e tyre nga rrjeti në disk!

Renditja e çelësave me krahasues binar

Marrëdhënia e rendit të çelësave përcaktohet nga një funksion special, i quajtur krahasues. Pasi që motori nuk di asgjë mbi semantiken e bajtave që përmban, krahasuesi standard nuk ka asgjë tjetër përveçse të rendisë çelësat në rendin leksikografik, duke i krahasuar ato bajt për bajt. Përdorimi i tij për renditjen e strukturave është si të rreshk ose grua me një thikë prerëse. Megjithatë, në raste të thjeshta, unë e gjej këtë qasje të pranueshme. Alternativa është përshkruar pak më poshtë, dhe këtu do të përmend disa kaubojë të shpërndara përgjatë kësaj rruge.

E para, që duhet të mbani mend është përfaqësimi në memorie i tipeve të dhënash primitivë. Në të gjitha pajisjet Apple, variablat e numrave të plotë ruhen në formatin Little Endian. Kjo do të thotë se byte më pak i rëndësishëm do të jetë në anën e majtë, dhe nuk do të mund të renditni numrat e plotë duke përdorur krahasimin e tyre byte për byte. Për shembull, përpjekja për ta bërë këtë me një grup numrash nga 0 deri në 511 do të çonte në rezultatin e mëposhtëm.

// value (hex dump)
000 (0000)
256 (0001)
001 (0100)
257 (0101)
...
254 (fe00)
510 (fe01)
255 (ff00)
511 (ff01)

Për të zgjidhur këtë problem, numrat e plotë duhet të ruhen në një çelës në formatin e duhur për krahasuesin byte për byte. Transformimi i nevojshëm do të ndihmohet nga funksionet nga familja hton* (kështu veçanërisht htons për numrat dy byte nga shembulli).

Formati i përfaqësimit të vargjeve në programim është, siç dihet, një historia. Nëse semantika e vargjeve si dhe kodimi i përdorur për t'i përfaqësuar ato në memorie sugjerojnë që një karakter mund të ketë më shumë se një byte, atëherë është më mirë të heqim dorë nga ideja e përdorimit të krahasuesit default.

E dyta, që duhet të mbani mend është principet e alignimit kompilatorëve të fushave të strukturës. Për shkak të tyre, në memorie midis fushave mund të formohen byte me vlera të papastra, të cilat, natyrisht, prishin renditjen byte-për-byte. Për të eliminuar papastërtitë, duhet ose të shpallni fushat në një rend të caktuar, duke mbajtur mend rregullat e rregullimit, ose të përdorni në shpalljen e strukturës atributin packed.

Renditja e çelësave me një krahasues të jashtëm

Logjika e krahasimit tĂ« çelĂ«save mund tĂ« rezultojĂ« tepĂ«r e komplikuar pĂ«r njĂ« krahasues binar. NjĂ« nga shumĂ« arsyet — Ă«shtĂ« prania e fushave teknike brenda strukturave. Do ta ilustroj kĂ«tĂ« ndodhi me shembullin e njohur tashmĂ« tĂ« çelĂ«sit pĂ«r elementin e direktorisĂ«.

typedef struct NodeKey {
    EntityId parentId;
    uint8_t type;
    uint8_t nameBuffer[256];
} NodeKey;

Pavarësisht thjeshtësisë së saj, në shumicën e rasteve ajo konsumon shumë memorie. Bufferi për emrin zë 256 byte, megjithëse mesatarisht emrat e skedarëve dhe dosjeve rrallë e kalojnë 20-30 karaktere.

NjĂ« nga teknikĂ«t standard tĂ« optimizimit tĂ« madhĂ«sisĂ« sĂ« regjistrit Ă«shtĂ« "prerja" e saj sipas madhĂ«sisĂ« reale. QĂ«llimi Ă«shtĂ« qĂ« pĂ«rmbajtja e tĂ« gjithĂ« fushave me gjatĂ«si variabĂ«l tĂ« ruhet nĂ« njĂ« tampon nĂ« fund tĂ« strukturĂ«s, dhe gjatĂ«si e tyre tĂ« mbahen nĂ« variabla tĂ« ndarĂ«. NodeKey ÇelĂ«si

typedef struct NodeKey {​
    EntityId parentId;​
    uint8_t type;​
    uint8_t nameLength;​
    uint8_t nameBuffer[256];​
} NodeKey;

Më pas, gjatë serializimit, si madhësi të të dhënave tregohet jo MDB_val serialize(NodeKey * const key) { return MDB_val { .mv_size = sizeof(NodeKey), .mv_data = (void *)key }; } e të gjithë strukturës, por madhësia e të gjithë fushave me gjatësi fikse plus madhësia e pjesës së përdorur realisht të tamponit.

MDB_val serialize(NodeKey * const key) {
    return MDB_val {
        .mv_size = offsetof(NodeKey, nameBuffer) + key->nameLength,
        .mv_data = (void *)key
    };
}

Si rezultat i refaktorizimit të bërë, ne arritëm një kursim të konsiderueshëm të hapësirës së zënë nga çelësat. Megjithatë, për shkak të fushës teknikë nameLength, krahasuesi binar default nuk është më i përshtatshëm për krahasimin e çelësave. Nëse nuk e zëvendësojmë atë me të tonën, gjatësia e emrit do të jetë një faktor më prioritar gjatë renditjes sesa vetë emri.

LMDB lejon të caktosh funksionin tënd të krahasimit të çelësave për secilën bazë të dhënash. Kjo bëhet përmes funksionit mdb_set_compare në mënyrë strikte deri në hapje. Për arsye të dukshme, gjatë gjithë jetës së bazës së të dhënave, kjo nuk mund të ndryshohet. Komparatori merr dy çelësa në format binar dhe kthen rezultatin e krahasimit: më pak (-1), më shumë (1) ose të barabartë (0). Pseudokodi për NodeKey duket kështu.

int compare(MDB_val * const a, MDB_val * const b) {​
    NodeKey * const aKey = (NodeKey * const)a->mv_data;​
    NodeKey * const bKey = (NodeKey * const)b->mv_data;​
    return // ...
}​

Derisa tĂ« gjitha çelĂ«sat nĂ« bazĂ«n e tĂ« dhĂ«nave tĂ« kenĂ« tĂ« njĂ«jtin tip, konverzimi i pamĂ«dyshimtĂ« i pĂ«rfaqĂ«simit tĂ« tyre nĂ« bajtĂ« nĂ« tipin e strukturĂ«s aplikative tĂ« çelĂ«sit Ă«shtĂ« i ligjshĂ«m. Ka njĂ« nuancĂ« kĂ«tu, por ajo do tĂ« shqyrtohet pak mĂ« poshtĂ« nĂ« nĂ«n kapitullin “Leximi i regjistrimeve”.

Serializimi i vlerave

Me çelësat e regjistrimeve të ruajtura LMDB funksionon mjaft intensivisht. Krahasimi i tyre ndodh në kuadër të çdo operacioni aplikativ, dhe performanca e krahasuesit përcakton rendimentin e të gjithë zgjidhjes. Në një botë ideale, krahasimi i çelësave do të mjaftonte me krahasuesin binar të paracaktuar, por nëse nevojitet të përdorim një të vetin, atëherë procedura e deserializimit të çelësave duhet të jetë sa më e shpejtë që të jetë e mundur.

Pjesa e vlerës së regjistrimit (vlera) nuk e shqetëson shumë bazën e të dhënave. Kjo e transformon nga përfaqësimi me byte në objekt vetëm kur kërkohet nga kodi aplikativ, për shembull, për ta shfaqur në ekran. Duke qenë se ndodh relativisht rrallë, kërkesat për shpejtësinë e kësaj procedure nuk janë aq kritike, dhe në zbatimin e saj ne jemi në një masë më të madhe të lirë për t'u orientuar në komoditet. Për shembull, për serializimin e të dhënave të metadatas për skedarët që nuk janë ngarkuar ende, ne përdorim NSKeyedArchiver.

NSData *data = serialize(object);​
MDB_val value = {​
    .mv_size = data.length,​
    .mv_data = (void *)data.bytes​
};

Megjithatë, ka raste kur performanca ka rëndësi. Për shembull, kur ruajmë metainformacionin rreth strukturës të oborrit të përdoruesit, ne përdorim sërish dump-in e memories së objekteve. Pika thelbësore e detyrës për formimin e përfaqësimit të tyre të serializuar është fakti se elementët e drejtorisë modelohen nga një hierarki klasash.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Për ta realizuar këtë në gjuhën C, fushat specifike të trashëgimtarëve nxirren në struktura të veçanta, dhe lidhja e tyre me bazën përcaktohet përmes një fushe të tipit union. përmbajtja aktuale e unionit përcaktohet përmes atributit teknik type.

typedef struct NodeValue {​
    EntityId localId;​
    EntityType type;​
    union {​
        FileInfo file;​
        DirectoryInfo directory;​
    } info;​
    uint8_t nameLength;​
    uint8_t nameBuffer[256];​
} NodeValue;​

Shtimi dhe përditësimi i regjistrimeve

ÇelĂ«si dhe vlera tĂ« serializuara mund tĂ« shtohen nĂ« magazinĂ«. PĂ«r kĂ«tĂ«, pĂ«rdoret funksioni mdb_put.

// key Đž value ĐžĐŒĐ”ŃŽŃ‚ топ MDB_val​
mdb_put(..., &key, &value, MDB_NOOVERWRITE);

Gjatë konfigurimit, mund të lejohet ose ndalohet ruajtja e regjistrimeve të shumta me të njëjtin çelës. Nëse dublimi i çelësave është i ndaluar, në momentin e shtimit të një regjistrimi, mund të përcaktohet nëse është e lejueshme të përditësohet një regjistrim ekzistues apo jo. Nëse mbivendosja mund të ndodhë vetëm për shkak të një gabimi në kod, atëherë mund të mbrohet nga ajo duke përcaktuar një flamur. NOOVERWRITE.

Leximi i regjistrimeve

Për leximin e regjistrimeve në LMDB, përdoret funksioni mdb_get. Nëse çelësi-vlera është paraqitur më parë me struktura të skenuara, atëherë procedura duket kështu.

NodeValue * const readNode(..., NodeKey * const key) {​
    MDB_val rawKey = serialize(key);​
    MDB_val rawValue;​
    mdb_get(..., &rawKey, &rawValue);​
    return (NodeValue * const)rawValue.mv_data;​
}

Listing-i i paraqitur tregon se si serializimi përmes skanimit të strukturave lejon shmangien e alokimeve dinamike jo vetëm gjatë shkrimit, por edhe gjatë leximit të të dhënave. Rezultati nga funksioni mdb_get pika tregon saktësisht adresën virtuale të memories, ku baza e të dhënave ruan përfaqësimin me byte të objektit. Në fakt, kemi një ORM të tillë, që praktikisht ofron shumë shpejtësi të lartë të leximit të të dhënave falas. Megjithatë, përveç bukurisë së këtij qasjeje, duhet të mbajmë mend disa veçori të lidhura me të.

  1. Për transaksionet readonly, pika në strukturën e vlerës do të mbetet e vlefshme vetëm derisa transaksioni të mbyllet. Siç u theksua më parë, faqet e pemës B, ku ndodhet objekti, mbeten të pandryshuara falë parimit copy-on-write për sa kohë që ka të paktën një transaksion që i referohet. Në të njëjtën kohë, sapo përfundon transaksioni i fundit i lidhur me to, faqet mund të ribashkohen për të dhëna të reja. Nëse është e nevojshme që objektet të jetojnë më gjatë se transaksioni që i krijoi, ato do të duhet të kopjohen.
  2. Për transaksionin readwrite, pika në strukturën e marrë dhe vlerën do të jetë e vlefshme vetëm deri në procedurën e parë modifikuese (shkrim ose fshirje të të dhënave).
  3. Megjithatë, struktura NodeValue nuk është e plotë, por e shkurtuar (shih nënseksionin «Rregullimi i çelësave me një krahasues të jashtëm»), përmes treguesit mund të qasemi lehtësisht në fushat e saj. E rëndësishme është që të mos e dereferencojmë!
  4. Në asnjë rast nuk duhet të modifikoni strukturën përmes treguesit të marrë. Të gjitha ndryshimet duhet të bëhen vetëm përmes metodës mdb_put. Megjithatë, përkundër dëshirës për ta bërë këtë, ajo nuk do të funksionojë, pasi zona e memories ku ndodhet kjo strukturë është e mapuar në mënyrë readonly.
  5. Remap të skedarit në hapësirën adresuese të procesit me qëllim, për shembull, për të rritur madhësinë maksimale të magazinimit duke përdorur funksionin mdb_env_set_map_size shkakton invalidimin e plotë të të gjitha transaksioneve dhe entiteteve të lidhura me to, si dhe treguesve në objektet e lexuara.

NĂ« fund, njĂ« veçori tjetĂ«r Ă«shtĂ« kaq e besueshme, saqĂ« zbulimi i thelbit tĂ« saj nuk mund tĂ« pĂ«rfshihet vetĂ«m nĂ« njĂ« pikĂ« tjetĂ«r. NĂ« kapitullin mbi pemĂ«n B kam paraqitur njĂ« skemĂ« tĂ« ndĂ«rtimit tĂ« faqeve tĂ« saj nĂ« memorie. Nga ajo, rezulton se adresa e fillimit tĂ« buffer-it me tĂ« dhĂ«na tĂ« serializuara mund tĂ« jetĂ« krejtĂ«sisht arbitrare. PĂ«r kĂ«tĂ« arsye, treguesi pĂ«r to, i marrĂ« nĂ« strukturĂ« MDB_val dhe e referuar pĂ«r njĂ« pointer nĂ« strukturĂ«, nĂ« pĂ«rgjithĂ«si nuk Ă«shtĂ« e pĂ«rputhshme. NĂ« tĂ« njĂ«jtĂ«n kohĂ«, arkitekturat e disa çipĂ«ve (nĂ« rastin e iOS, kjo Ă«shtĂ« armv7) kĂ«rkojnĂ« qĂ« adresa e çdo tĂ« dhĂ«ne tĂ« jetĂ« shumĂ«fish i madhĂ«sisĂ« sĂ« fjalĂ«s makine, ose me fjalĂ« tĂ« tjera, bitĂ«sisĂ« sĂ« sistemit (pĂ«r armv7 — kjo Ă«shtĂ« 32 bit). me fjalĂ« tĂ« tjera, njĂ« operacion si *(int *foo)0x800002 nĂ« to i barazon me arratisjen dhe çon nĂ« ekzekutim me verdiktin EXC_ARM_DA_ALIGN. TĂ« shpĂ«tohemi nga ky fat i hidhur mund tĂ« bĂ«het nĂ« dy mĂ«nyra.

E para është përgatitja e të dhënave në një strukturë të sigurtë. Për shembull, në një krahasues personal, kjo do të duket si më poshtë.

int compare(MDB_val * const a, MDB_val * const b) {
    NodeKey aKey, bKey;
    memcpy(&aKey, a->mv_data, a->mv_size);
    memcpy(&bKey, b->mv_data, b->mv_size);
    return // ...
}

Rruga alternative — tĂ« njoftohet paraprakisht kompajleri se strukturat me çelĂ«s dhe vlerĂ« mund tĂ« mos jenĂ« tĂ« pĂ«rputhshme me atributin aligned(1). NĂ« ARM, njĂ« efekt tĂ« tillĂ« mund tĂ« arrihet arritjen edhe me atributin packed. Duke pasur parasysh se kjo gjithashtu ndihmon nĂ« optimizimin e hapĂ«sirĂ«s sĂ« zĂ«nĂ« nga struktura, ky mĂ«nyrĂ« mĂ« duket e preferueshme, megjithatĂ« çon çon nĂ« rritje tĂ« kostos sĂ« operacioneve tĂ« qasjes nĂ« tĂ« dhĂ«na.

typedef struct __attribute__((packed)) NodeKey {
    uint8_t parentId;
    uint8_t type;
    uint8_t nameLength;
    uint8_t nameBuffer[256];
} NodeKey;

Kërkesat Range

Për iterimin e grupit të regjistrave në LMDB, ekziston një abstraksion për kursorin. Si të punoni me të, do ta shqyrtojmë përmes një shembulli nga tabela e njohur me metadata të përdoruesit.

Në kuadër të shfaqjes së listës së skedarëve në direktorium, është e nevojshme të gjenden të gjitha çelësat me të cilat janë të asociohen skedarët dhe dosjet e tij të bijë. Në nënpjesët e mëparshme, ne i kemi renditur çelësat NodeKey në mënyrë që ata të ishin të renditur fillimisht sipas identifikuesit të direktoriumit prind. Kështu, tekniki, detyra e marrjes së përmbajtjes së dosjes reduktohet në vendosjen e kursorit në kufirin e sipërm të grupit të çelësave me prefix të caktuar dhe duke u iteruar deri në kufirin e poshtëm.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Kufirin e sipërm mund ta gjeni "në mënyrë të drejtpërdrejtë" me një kërkim të vazhdueshëm. Për këtë, kursorit i vendoset fillimi i gjithë listës së çelësave në bazën e të dhënave dhe pastaj inkrementohet derisa nën të të ndodhet një çelës me identifikuesin e direktoriumit prind. Ky qasje ka 2 disavantazhe të dukshme:

  1. Ndërsa, siç dihet, kompleksiteti linear i kërkimit në pemë, veçanërisht në pemën B, mund të realizohet për kohë logaritmike.
  2. Nuk ka arsye të ngrihen nga skedari në memorie të plotë të gjitha faqet që i paraprinojnë kërkimit, që është jashtëzakonisht e shtrenjtë.

Faleminderit, API LMDB ofron një mënyrë efektive për pozicionimin fillestar të kursit. Për këtë, duhet të formohet një çelës i tillë, vlera e të cilit do të jetë padyshim më e vogël ose e barabartë me çelësin që ndodhet në kufirin e sipërm të intervalit. Për shembull, në lidhje me listën në ilustrimin më sipër, mund të bëjmë një çelës të tillë, ku fushat parentId do të jetë e barabartë me 2, ndërsa të gjitha të tjerat janë të mbushura me zero. Ky çelës i mbushur pjesërisht kalon si hyrje në funksionin mdb_cursor_get me shënimin e operacionit MDB_SET_RANGE.

NodeKey upperBoundSearchKey = {​
    .parentId = 2,​
    .type = 0,​
    .nameLength = 0​
};​
MDB_val value, key = serialize(upperBoundSearchKey);​
MDB_cursor *cursor;​
mdb_cursor_open(..., &cursor);​
mdb_cursor_get(cursor, &key, &value, MDB_SET_RANGE);

Nëse kufiri i sipërm i grupit të çelësave është gjetur, atëherë vazhdojmë të iterojmë mbi të derisa të takojmë ose një çelës tjetër, parentId, ose çelësat të mos përfundojnë fare.

do {​
    rc = mdb_cursor_get(cursor, &key, &value, MDB_NEXT);​
    // processing...​
} while (MDB_NOTFOUND != rc && // check end of table​
         IsTargetKey(key));    // check end of keys group​​

ËshtĂ« e kĂ«ndshme qĂ« ndĂ«rsa iteroni me mdb_cursor_get ne marrim jo vetĂ«m çelĂ«sin, por edhe vlerĂ«n. NĂ«se pĂ«r tĂ« pĂ«rmbushur kushtet e kĂ«rkimit Ă«shtĂ« e nevojshme tĂ« kontrolloni gjithashtu fushat nga pjesa e vlerĂ«s, ato janĂ« PlotĂ«sisht tĂ« aksesueshme pa ndonjĂ« pĂ«rpjekje shtesĂ«.

4.3. Modelimi i lidhjeve midis tabelave

Derisa, deri në këtë moment, kemi shqyrtuar të gjitha aspektet e dizenjimit dhe funksionimit me një bazë të dhënash të vetëm tabelar. Mund të thuhet se tabela është një grup i regjistrimeve të renditura, të përbëra nga çelësa dhe vlera të ngjashme. Nëse e paraqesim çelësin si një drejtkëndësh dhe vlerën e lidhur me të si një paralel pipë, do të kemi një diagram vizual të bazës së të dhënave.

​

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Megjithatë, në jetën reale, rrallë ndodh që të arrihet me kaq pak përpjekje. Shpesh, në një bazë të dhënash, është e nevojshme, së pari, të kemi disa tabela dhe, së dyti, të bëjmë kërkesa në një rend tjetër nga çelësi kryesor. Ky seksion i fundit është dedikuar pyetjeve për krijimin dhe lidhjen e tyre me njëra-tjetrën.

Tabela të indeksuara

Në aplikacionin e cloud ka një seksion "Galeria". Ky seksion tregon përmbajtjen mediatike nga gjithë cloud-i, të renditur sipas datës. Për një implementim optimal të një seleksioni të tillë, ngjitur me tabelën kryesore duhet të krijohet një tjetër me një tip të ri kyçesh. Në to do të përmbahen një fushë me datën e krijimit të skedarit, e cila do të shërbejë si kriteri primar i renditjes. Duke qenë se çelësat e rinj i referohen të njëjtave të dhëna si çelësat në tabelën kryesore, ato quhen indekse. Në imazhin më poshtë, ato janë të shënuara me ngjyrë portokalli.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Për të ndarë çelësat e tabelave të ndryshme brenda një baze të dhënash, të gjithë atyre iu është shtuar një fushë teknike shtesë tableId. Duke e bërë atë prioritar për renditjen, ne do të arrijmë grupimin e çelësave së pari sipas tabelave, e më pas brenda tabelave - sipas rregullave të tyre.

ÇelĂ«si indeks ndĂ«rlidhet me tĂ« njĂ«jtat tĂ« dhĂ«na si ai primar. Implementimi i drejtpĂ«rdrejtĂ« i kĂ«tij atributi pĂ«rmes njĂ« asocimi me njĂ« kopje tĂ« pjesĂ«s value tĂ« çelĂ«sit primar nuk Ă«shtĂ« optimal menjĂ«herĂ« nga disa kĂ«ndvĂ«shtrime.

  1. Nga këndvështrimi i hapësirës së zënë, duke pasur parasysh se metadatët mund të jenë shumë të pasura.
  2. Nga pikëpamja e performancës, pasi që gjatë përditësimit të letrave të metadatalle, do të duhet të bëhet riperditësimi për dy çelësa.
  3. Nga pikĂ«pamja e mbĂ«shtetjes sĂ« kodit, sepse nĂ«se harrojmĂ« tĂ« pĂ«rditĂ«sojmĂ« tĂ« dhĂ«nat pĂ«r njĂ«rin nga çelĂ«sat, do tĂ« pĂ«rballemi me njĂ« defekt tĂ« vĂ«shtirĂ« pĂ«r t’u kapur tĂ« inkonsistencĂ«s sĂ« tĂ« dhĂ«nave nĂ« magazinĂ«.

Më pas do të shqyrtojmë se si të eliminojmë këto të meta.

Organizimi i lidhjeve midis tabelave

Për të lidhur tabelën indeks me atë kryesore, patterni i përshtatshëm është "çelësi si vlerë". Siç nënkuptohet nga emri i tij, në pjesën value të regjistrimit të indeksit shërben një kopje e vlerës së çelësit primar. Ky qasje eliminon të gjitha të metat e përmendura më sipër që lidhen me ruajtjen e një kopjeje të pjesës value të regjistrimit primar. Pagesa e vetme është që për të marrë vlerën sipas çelësit të indeksit, duhet të bëhen 2 kërkesa në bazën e të dhënave në vend të një. Schema e bazës së të dhënave duket si më poshtë.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Një tjetër pattern i organizimit të lidhjes midis tabelave është "çelësi i tepruar". The essence is to add additional attributes to the key that are not needed for sorting, but for recreating a related key. In the Mail.ru Cloud app, there are real examples of its use, but to avoid delving deeply into the context of specific iOS frameworks, I will provide a fictional, yet clearer example.

In cloud mobile clients, there is a page that displays all files and folders to which the user has granted access to other people. Since there are relatively few such files, but there is a lot of related specific information about their public status (who has access, with what rights, etc.), it would not be rational to overload the value part of the record in the main table with it. However, if one wants to display such files offline, it still needs to be stored somewhere. A natural solution is to create a separate table for it. In the diagram below, its key has the prefix 'P', and the placeholder 'propname' can be replaced with a more specific value 'public info'.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Të gjitha metadatave unike, për ruajtjen e të cilave u krijua një tabelë e re, janë transferuar në pjesën e vlerës së rekordit. Në të njëjtën kohë, nuk dëshirojmë që të dyfishojmë të dhënat për skedarët dhe dosjet, të cilat tashmë ruhen në tabelën kryesore. Në vend të këtij, në çelësin "P" shtohen të dhëna të tepërta në formën e fushave "node ID" dhe "timestamp". Falë tyre, mund të konstruojmë një çelës indeks që të na mundësojë marrjen e çelësit primar, me të cilin, përfundimisht, të marrim metadatave të nodit.

Përfundimi

Ne e vlerësojmë pozitivisht implementimin e LMDB. Pas kësaj, numri i ngritjeve të aplikacionit u reduktua me 30%.

Dallo shkëlqimi në varfëri i bazës së të dhënave key-value LMDB në aplikacione për iOS

Rezultatet e punës së bërë kanë gjetur jehonë përtej ekipit të iOS. Aktualisht, një nga seksionet kryesore "Skedarët" në aplikacionin për Android gjithashtu ka kaluar në përdorimin e LMDB, ndërsa pjesët e tjera janë në radhë. Gjuha C, në të cilën është realizuar ruajtja key-value, ka qenë një ndihmë e mirë për të bërë fillimisht një mbështetje aplikative rreth saj përmes gjuhës C++. Për lidhjen pa probleme të bibliotekës C++ me kodin platformor në Objective-C dhe Kotlin, është përdorur një gjenerues kodi. Djinni nga Dropbox, por kjo është një tjetër histori.

Burimi: habr.com

Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster