U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

In u vaghjimu di u 2019, un avvenimentu assai aspittatu hè accadutu in a squadra Mail.ru Cloud iOS. A basa di dati principale per u almacenamentu persistente di u statu di l'applicazione hè diventata abbastanza esotica per u mondu mobile Base di dati di carta di memoria Lightning (LMDB). Sottu u cut, a vostra attenzione hè invitata à a so rivisione dettagliata in quattru parti. Prima, parlemu di i mutivi di una scelta cusì micca triviale è difficiule. Allora andemu à a cunsiderazione di trè balene in u core di l'architettura LMDB: i schedarii mappati in memoria, l'arbulu B +, l'approcciu di copia in scrittura per l'implementazione di transazzione è multiversione. Infine, per a postre - a parte pratica. In questu, guardemu cumu cuncepisce è implementà un schema di basa cù parechje tavule, cumpresu un indice, in cima di l'API di valore chjave di livellu bassu.

Cuntenuti

  1. Implementazione Motivazione
  2. Posizionamentu LMDB
  3. Trè balene LMDB
    3.1. Balena #1. I fugliali mappati in memoria
    3.2. Balena #2. B+-arburu
    3.3. Balena #3. copia à scrittura
  4. Cuncepimentu di un schema di dati in cima di l'API chjave-valore
    4.1. Astrazzioni basi
    4.2. Modellazione di tavulinu
    4.3. Mudelle di relazioni trà e tavule

1. Mutivazione di implementazione

Una volta à l'annu, in 2015, avemu cura di piglià una metrica, quantu spessu l'interfaccia di a nostra applicazione lags. Ùn avemu micca solu fà questu. Avemu più è più lagnanza annantu à u fattu chì qualchì volta l'applicazione cessà di risponde à l'azzioni di l'utilizatori: i buttoni ùn sò micca pressati, e liste ùn scorri micca, etc. Circa a meccanica di e misure dettu nantu à AvitoTech, cusì quì aghju datu solu l'ordine di i numeri.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

I risultati di a misurazione sò diventati una doccia fredda per noi. Ci hè stata chì i prublemi causati da i freezes sò assai più di qualsiasi altru. Se, prima di rializà stu fattu, l'indicatore tecnicu principale di a qualità era senza crash, dopu à u focu spustatu in freeze free.

Dopu avè custruitu dashboard cù freezes è avè passatu quantitative и qualità analisi di i so causi, u principale nimicu divintò chjaru - logica di cummerciale pesante esecutà in u filu principale di l'applicazione. Una reazione naturale à sta disgrazia era un desideriu ardente di spinghje in i flussi di travagliu. Per una suluzione sistematica di stu prublema, avemu ricursu à una architettura multi-threaded basatu nantu à attori ligeri. Aghju dedicatu i so adattazioni per u mondu iOS dui fili in u twitter cullettivu è articulu nantu à Habré. Comu parte di a storia attuale, vogliu enfatizà quelli aspetti di a decisione chì hà influinzatu a scelta di a basa di dati.

U mudellu attore di l'urganizazione di u sistema assume chì u multithreading diventa a so seconda essenza. L'oggetti mudeli in ellu piace à attraversà e fruntiere di filu. È ùn facenu micca qualchì volta è in certi lochi, ma quasi constantemente è in ogni locu.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

A basa di dati hè unu di i cumpunenti di a basa di u schema presentatu. U so compitu principale hè di implementà un mudellu macro Database spartutu. Se in u mondu di l'impresa hè utilizatu per urganizà a sincronizazione di dati trà i servizii, allora in u casu di l'architettura di l'attore, i dati trà i fili. Cusì, avemu bisognu di una tale basa di dati, u travagliu cù quale in un ambiente multi-threaded ùn pruvucarà mancu difficultà minima. In particulare, questu significa chì l'uggetti derivati ​​da questu deve esse almenu thread-safe, è idealmente micca mutabile in tuttu. Comu sapete, l'ultime pò esse usata simultaneamente da parechji filamenti senza ricorrere à qualsiasi tipu di chjusi, chì hà un effettu beneficu nantu à u rendiment.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOSU sicondu fattore significativu chì hà influinzatu a scelta di a basa di dati era a nostra API di nuvola. Hè stata inspirata da l'approcciu di git à a sincronizazione. Cum'è ellu avemu avutu per scopu prima API offline, chì pare più apprupriatu per i clienti cloud. Ci hè stata presuntu chì solu una volta pumpà u statu sanu di u nuvulu, è dopu a sincronizazione in a maiò parte di i casi avaristi attraversu cambiamenti rotulanti. Alas, sta pussibilità hè sempre solu in a zona teorica, è in pratica, i clienti ùn anu micca amparatu à travaglià cù parche. Ci hè una quantità di ragiuni oggettivi per questu, chì, per ùn ritardà l'intruduzioni, lasciaremu fora di i parentesi. Avà assai più interessanti sò i risultati instructivi di a lezzione nantu à ciò chì succede quandu l'API hà dettu "A" è u so cunsumadore ùn hà micca dettu "B".

Allora, se imaginate git, chì, quandu eseguite un cumandamentu di pull, invece di applicà patches à una snapshot lucale, paraguna u so statu sanu cù u servitore sanu, allora avete una idea abbastanza precisa di cumu a sincronizazione. si trova in i clienti cloud. Hè faciule guessà chì per a so implementazione hè necessariu di assignà dui arburi DOM in memoria cù meta-informazioni nantu à tutti i servitori è i schedarii lucali. Ci hè chì se un utilizatore guarda 500 mila schedari in u nuvulu, dopu per sincronizà, hè necessariu di ricreà è distrughje dui arburi cù 1 million nodes. Ma ogni node hè un aggregatu chì cuntene un gràficu di suboggetti. In questa luce, i risultati di prufilu eranu previsti. Hè risultatu chì ancu senza piglià in contu l'algoritmu di fusione, a prucedura stessa di creà è poi distrughje un gran numaru d'uggetti chjuchi costa un bellu centesimu. A situazione hè aggravata da u fattu chì l'operazione di sincronizazione basica hè inclusa in un gran numaru. di script d'utilizatori. In u risultatu, riparamu u sicondu criteriu impurtante in a scelta di una basa di dati - a capacità di implementà l'operazioni CRUD senza l'assignazione dinamica di l'uggetti.

L'altri esigenze sò più tradiziunali, è a so lista completa hè a siguenti.

  1. Sicurezza di filu.
  2. Multiprocessing. Dittata da u desideriu di utilizà a listessa istanza di basa di dati per sincronizà u statu micca solu trà i fili, ma ancu trà l'applicazione principale è l'estensioni iOS.
  3. A capacità di rapprisintà entità almacenate cum'è oggetti non mutabili
  4. Mancanza di allocazioni dinamiche in l'operazioni CRUD.
  5. Supportu di transazzione per e pruprietà di basa àcituParolle chjave: atomicità, cuerenza, isolamentu è affidabilità.
  6. Velocità nantu à i casi più populari.

Cù stu settore di esigenze, SQLite era è hè sempre una bona scelta. Tuttavia, cum'è parte di u studiu di l'alternattivi, aghju scontru un libru "Inizià cù LevelDB". Sottu a so dirigenza, hè statu scrittu un benchmark chì compara a velocità di u travagliu cù diverse basa di dati in scenarii di nuvola reale. U risultatu hà superatu l'aspettattivi più salvatichi. Nant'à i casi più populari - ottene un cursore nantu à una lista di tutti i schedari è una lista di tutti i schedari per un repertoriu determinatu - LMDB hè diventatu 10 volte più veloce di SQLite. A scelta hè diventata evidenti.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

2. Posizionamentu LMDB

LMDB hè una biblioteca, assai chjuca (solu 10K linee), chì implementa a più bassa capa fundamentale di basa di dati - almacenamiento.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

U diagramma di sopra mostra chì paragunà LMDB cù SQLite, chì implementa ancu livelli più alti, ùn hè generalmente micca più currettu di SQLite cù Core Data. Saria più ghjusta di cite i stessi mutori di almacenamiento cum'è cuncurrenti uguali - BerkeleyDB, LevelDB, Sophia, RocksDB, etc. Ci sò ancu sviluppi induve LMDB agisce cum'è un cumpunente di mutore di almacenamiento per SQLite. U primu tali esperimentu in u 2012 spesi autore LMDB Howard Chu. Risultati s'avère cusì intrigante chì a so iniziativa hè stata ripresa da l'amatori di l'OSS, è hà trovu a so continuazione in faccia à LumoSQL. In ghjennaghju 2020 l'autore di stu prughjettu hè Den Shearer prisentatu nantu à LinuxConfAu.

L'usu principale di LMDB hè cum'è un mutore per e basa di dati di l'applicazioni. A biblioteca deve a so apparenza à i sviluppatori OpenLDAP, chì sò stati assai dispiaciti cù BerkeleyDB cum'è a basa di u so prughjettu. Alluntanendu da l'umile biblioteca btree, Howard Chu hà sappiutu creà una di l'alternative più populari di u nostru tempu. Hà dedicatu u so rapportu assai cool à sta storia, è ancu à a struttura interna di LMDB. "A basa di dati di mappa di memoria Lightning". Leonid Yuriev (alias yleu) da Tecnologie Positive in a so discussione à Highload 2015 "U mutore LMDB hè un campione speciale". In questu, parla di LMDB in u cuntestu di un compitu simili di implementazione di ReOpenLDAP, è LevelDB hà digià subitu critichi comparativi. In u risultatu di l'implementazione, e Tecnologie Positive anu ancu ottene una forchetta attivamente sviluppata MDBX cù funziunalità assai gustosa, ottimisazioni è correzioni di bug.

LMDB hè spessu usatu cum'è almacenamiento cumu. Per esempiu, u navigatore Mozilla Firefox sceltu per una quantità di bisogni, è, partendu da a versione 9, Xcode preferitu u so SQLite per almacenà indici.

U mutore hà pigliatu ancu in u mondu di u sviluppu mobile. Tracce di u so usu pò esse truvà in u cliente iOS per Telegram. LinkedIn hà fattu un passu più avanti è hà sceltu LMDB cum'è u almacenamentu predeterminatu per u so framework di cache di dati in casa, Rocket Data, chì circa. dettu in un articulu in u 2016.

LMDB hè successu cummattimentu per un postu in u sole in u nichule lasciatu da BerkeleyDB dopu a transizione sottu u cuntrollu di Oracle. A biblioteca hè amatu per a so rapidità è affidabilità, ancu in paragunà cù u so propiu tipu. Comu sapete, ùn ci sò micca pranzi gratuiti, è vogliu enfatizà u cummerciu chì duverete affruntà quandu sceglite trà LMDB è SQLite. U diagramma sopra demustra chjaramente cumu si ottene a velocità aumentata. Prima, ùn paghemu micca per strati supplementari di astrazione nantu à u almacenamentu di discu. Di sicuru, in una bona architettura, ùn pudete ancu fà senza elli, è inevitabbilmente apparisceranu in u codice di l'applicazione, ma seranu assai più sottili. Ùn avaranu micca funziunalità chì ùn sò micca dumandati da una applicazione specifica, per esempiu, supportu per e dumande in a lingua SQL. In siconda, diventa pussibule implementà in modu ottimale a mappatura di l'operazioni di l'applicazione à e dumande à l'almacenamiento di discu. Se SQLite in u mo travagliu vene da i bisogni mediu di una applicazione media, allora voi, cum'è sviluppatore di l'applicazione, sapete bè i scenarii di carica principali. Per una suluzione più produttiva, avete da pagà un prezzu aumentatu per u sviluppu di a suluzione iniziale è u so sustegnu sussegwente.

3. Trè balene LMDB

Dopu avè vistu u LMDB da una vista d'uccello, hè ora di andà più in fondu. I prossimi trè sezzioni seranu dedicati à l'analisi di e balene principali nantu à quale si basa l'architettura di almacenamiento:

  1. I fugliali mappati in memoria cum'è un mecanismu per travaglià cù u discu è a sincronizazione di strutture di dati interni.
  2. B+-tree cum'è una urganizazione di a struttura di dati almacenati.
  3. Copy-on-write cum'è un approcciu per furnisce proprietà transazzione ACID è multiversioning.

3.1. Balena #1. I fugliali mappati in memoria

I fugliali mappati in memoria sò un elementu architettonicu cusì impurtante chì pareanu ancu in u nome di u repository. I prublemi di caching è sincronizazione di l'accessu à l'infurmazioni almacenati sò interamente à a misericòrdia di u sistema operatore. LMDB ùn cuntene micca cache in sè stessu. Questa hè una decisione cuscente da l'autore, postu chì a lettura di dati direttamente da i fugliali mappati permette di cutà assai cantoni in l'implementazione di u mutore. Quì sottu hè una lista luntanu da cumpleta di alcuni di elli.

  1. Mantene a cunsistenza di e dati in u almacenamentu quandu u travagliu cun ellu da parechji prucessi diventa a rispunsabilità di u sistema operatore. In a sezione dopu, stu meccanicu hè discutitu in detail è cù stampi.
  2. L'absenza di cache allevia cumplettamente LMDB di l'overhead assuciatu à l'assignazioni dinamiche. A lettura di dati in pratica hè di mette u punteru à l'indirizzu currettu in memoria virtuale è nunda di più. Sona cum'è fantasia, ma in a fonte di repository, tutte e chjama di calloc sò cuncentrate in a funzione di cunfigurazione di repository.
  3. L'absenza di cache significa ancu l'absenza di chjusi assuciati cù a sincronizazione per accede à elli. Lettori, di quale un numeru arbitrariu pò esiste à u stessu tempu, ùn scontru micca un solu mutex in u so modu à i dati. A causa di questu, a velocità di lettura hà una scalabilità lineale ideale in quantu à u numeru di CPU. In LMDB, solu l'operazioni di mudificazione sò sincronizate. Ci pò esse solu un scrittore à tempu.
  4. Un minimu di caching è logica di sincronizazione salva u codice da un tipu estremamente cumplessu di errori assuciati à travaglià in un ambiente multi-threaded. Ci era dui studii interessanti di basa di dati à a cunferenza Usenix OSDI 2014: "Tutti i Sistemi di File ùn sò micca creati uguali: nantu à a cumplessità di Crafting Crash-Consistent Applications" и Tortura di basa di dati per divertimentu è prufittu. Da elli pudete uttene infurmazioni nantu à a fiducia senza precedente di LMDB, è l'implementazione quasi impeccabile di e proprietà ACID di e transazzione, chì u supera in u stessu SQLite.
  5. U minimalismu di LMDB permette à a rapprisintazioni di a macchina di u so codice per esse cumpletamente pusatu in u cache L1 di u processatore cù e caratteristiche di velocità resultanti.

Sfurtunatamente, in iOS, i fugliali mappati in memoria ùn sò micca rosu cum'è vulemu. Per parlà di i disadvantages assuciati cun elli più cunsciente, hè necessariu di ricurdà i principii generale per implementà stu mecanismu in i sistemi operativi.

Infurmazioni generale nantu à i schedarii mappati in memoria

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOSCù ogni applicazione eseguibile, u sistema operatore associa una entità chjamata prucessu. Ogni prucessu hè attribuitu un intervallu contiguu di indirizzi in quale mette tuttu ciò chì deve travaglià. L'indirizzi più bassi cuntenenu rùbbriche cù codice è dati hardcoded è risorse. Dopu vene u bloccu in crescita di u spaziu di indirizzu dinamica, ben cunnisciutu da noi cum'è u munzeddu. Contene l'indirizzi di e entità chì appariscenu durante l'operazione di u prugramma. À a cima hè l'area di memoria utilizata da a pila di applicazioni. Si cresce o si riduce, in altre parolle, a so dimensione hà ancu una natura dinamica. Per chì a pila è u munzeddu ùn spinghjenu è interferiscenu l'una cù l'altru, sò siparati in diverse estremità di u spaziu di l'indirizzu Ci hè un pirtusu trà e duie sezioni dinamiche in cima è in fondu. L'indirizzi in questa sezione media sò utilizati da u sistema operatore per associà cù un prucessu di diverse entità. In particulare, pò mapà un certu settore cuntinuu di indirizzi à un schedariu nantu à u discu. Un tali schedariu hè chjamatu un schedariu mappatu di memoria

U spaziu di indirizzu attribuitu à un prucessu hè enormu. In teoria, u numeru di indirizzi hè limitatu solu da a dimensione di u puntatore, chì hè determinatu da u bitness di u sistema. Se a memoria fisica li hè stata assignata 1-in-1, allora u primu prucessu sguassate tutta a RAM, è ùn ci saria micca quistione di alcuna multitasking.

Tuttavia, sapemu da l'esperienza chì i sistemi operativi muderni ponu eseguisce tanti prucessi quant'è vulete à u stessu tempu. Questu hè pussibule per u fattu chì attribuisce assai memoria à i prucessi solu nantu à a carta, ma in a realità caricanu in a memoria fisica principale solu quella parte chì hè dumandata quì è avà. Dunque, a memoria assuciata à u prucessu hè chjamata virtuale.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

U sistema operatore urganizeghja a memoria virtuale è fisica in pagine di una certa dimensione. Appena una certa pagina di memoria virtuale hè dumandata, u sistema operatore a carica in a memoria fisica è mette una corrispondenza trà elli in una tavola speciale. Se ùn ci sò micca slots gratuiti, allora una di e pagine caricate prima hè copiata à u discu, è quellu chì dumanda u so postu. Questa prucedura, chì vulteremu in pocu tempu, hè chjamata scambià. A figura sottu illustra u prucessu descrittu. Nantu à questu, a pagina A cù l'indirizzu 0 hè stata caricata è posta nantu à a pagina di memoria principale cù l'indirizzu 4. Stu fattu hè statu riflessu in a tabella di currispundenza in a cell number 0.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Cù i fugliali mappati in memoria, a storia hè esattamente a stessa. Lògicamente, sò suppostamente cuntinuu è interamente posti in u spaziu di indirizzu virtuale. Tuttavia, entranu in memoria fisica pagina per pagina è solu nantu à dumanda. A mudificazione di tali pagine hè sincronizata cù u schedariu nantu à u discu. Cusì, pudete fà un schedariu I / O, solu travagliendu cù bytes in memoria - tutti i cambiamenti seranu automaticamente trasferiti da u kernel di u sistema operatore à u schedariu originale.
Enseñanza
L'imaghjini sottu mostra cumu LMDB sincronizza u so statu quandu travaglia cù a basa di dati da diversi prucessi. Mapendu a memoria virtuale di diversi prucessi nantu à u stessu schedariu, de facto obligemu u sistema operatore à sincronizà transitivamente certi blocchi di i so spazii d'indirizzu cù l'altri, chì hè induve LMDB vede.
Enseñanza

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Una sfumatura impurtante hè chì LMDB modifica u schedariu di dati per difettu per mezu di u mecanismu di chjama di u sistema di scrittura, è u schedariu stessu mostra in modu di sola lettura. Stu approcciu hà dui implicazioni impurtanti.

A prima cunsiquenza hè cumuna à tutti i sistemi operativi. A so essenza hè di aghjunghje a prutezzione contra i danni inadvertiti à a basa di dati per codice incorrectu. Comu sapete, l'istruzzioni eseguibili di un prucessu sò liberi di accede à e dati da ogni locu in u so spaziu di indirizzu. À u listessu tempu, cum'è avemu appena ricurdatu, affissà un schedariu in u modu di lettura-scrittura significa chì ogni struzzione pò ancu mudificà in più. S'ellu face questu per sbagliu, pruvatu, per esempiu, di soprascrive un elementu array in un indice inesistente, allora in questu modu pò cambià accidentalmente u schedariu mappatu à questu indirizzu, chì portarà à a corruzzione di a basa di dati. Se u schedariu hè visualizatu in modu di sola lettura, allora un tentativu di cambià u spaziu di l'indirizzu currispundenti à ellu hà da purtà à u prugramma crash cù u signale. SIGSEGV, è u schedariu resta intactu.

A seconda cunsiquenza hè digià specifica à iOS. Nè l'autore nè altre fonti esplicitamente menzionanu, ma senza ellu, LMDB ùn saria micca adattatu per esse esecutatu nantu à stu sistema operatore mobile. A sezzione dopu hè dedicata à a so cunsiderazione.

Specifics di i fugliali mappati in memoria in iOS

In 2018, ci hè statu un rapportu maravigliu à WWDC iOS Memory Deep Dive. Dice chì in iOS tutte e pagine situate in memoria fisica appartenenu à unu di 3 tipi: brutta, cumpressa è pulita.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

A memoria pulita hè una cullizzioni di pagine chì ponu esse scambiate in modu sicuru da a memoria fisica. I dati chì cuntenenu ponu esse ricaricati da e so fonti originali in quantu necessariu. I fugliali mappati in memoria di sola lettura entranu in questa categuria. iOS ùn hè micca a paura di scaricate e pagine mappate à un schedariu da a memoria in ogni mumentu, postu chì sò guarantiti per esse sincronizati cù u schedariu nantu à u discu.
Enseñanza
Tutte e pagine mudificate entranu in memoria brutta, ùn importa micca induve si trovanu originalmente. In particulare, i schedarii mappati in memoria mudificati scrivendu à a memoria virtuale assuciata à elli seranu ancu classificati in questu modu. Apertura LMDB cù bandiera MDB_WRITEMAP, dopu avè fattu cambiamenti in questu, pudete vede per voi stessu

Appena una applicazione cumencia à piglià troppu memoria fisica, iOS cumpressa e so pagine brutte. A cullizzioni di memoria occupata da pagine brutte è compresse hè a chjamata impronta di memoria di l'applicazione. Quandu ghjunghje à un certu valore di soglia, u daemon di u sistema assassinu OOM vene dopu à u prucessu è u termina di forza. Questa hè a peculiarità di iOS paragunatu à i sistemi operativi desktop. In cuntrastu, abbassà l'impronta di memoria per scambià e pagine da a memoria fisica à u discu ùn hè micca furnitu in iOS. Si pò solu guessà nantu à i motivi. Forsi a prucedura per u muvimentu intensivu di e pagine à u discu è u ritornu hè troppu cunsumante d'energia per i dispositi mobili, o iOS salva a risorsa di riscrittura di e cellule nantu à i dischi SSD, o forse i diseggiani ùn anu micca cuntentu di u rendiment generale di u sistema, induve tuttu hè. scambiatu constantemente. Sia com'è, u fattu ferma.

A bona nutizia, digià citata prima, hè chì LMDB ùn usa micca u mecanismu mmap per automaticamente per aghjurnà i schedari. Ne segue chì i dati renditi sò classificati cum'è memoria pulita da iOS è ùn cuntribuiscenu micca à l'impronta di memoria. Questu pò esse verificatu cù u strumentu Xcode chjamatu VM Tracker. A screenshot sottu mostra u statu di memoria virtuale di l'applicazione iOS Cloud durante l'operazione. À u principiu, 2 istanze LMDB sò state inizializzate in questu. U primu hè statu permessu di mapà u so schedariu à 1GiB di memoria virtuale, u sicondu - 512MiB. Malgradu u fattu chì i dui magazzini occupanu una certa quantità di memoria residente, nè di elli cuntribuiscenu à a dimensione brutta.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Avà hè u tempu di a mala nova. Grazie à u mecanismu di scambiu in i sistemi operativi di desktop 64-bit, ogni prucessu pò piglià quantu spaziu di indirizzu virtuale quant'è u spaziu liberu nantu à u discu duru permette u so scambiu potenziale. A rimpiazzà u scambiu cù a compressione in iOS riduce drasticamente u massimu teoricu. Avà tutti i prucessi viventi devenu intruduce in a memoria principale (leghje RAM), è tutti quelli chì ùn sò micca adattati sò sottumessi à a terminazione forzata. Hè mintuatu cum'è sopra raportu, è in documentazione ufficiale. In cunseguenza, iOS limita severamente a quantità di memoria dispunibule per l'attribuzione via mmap. Quì ccà pudete vede i limiti empirichi nantu à a quantità di memoria chì puderia esse attribuita nantu à i dispusitivi diffirenti cù questa chjama di u sistema. Nant'à i mudelli più muderni di smartphones, iOS hè diventatu generoso da 2 gigabytes, è nantu à e versioni superiore di l'iPad - da 4. In a pratica, sicuru, avete da fucalizza nantu à i mudelli più ghjovani supportati, induve tuttu hè assai tristu. Ancu peggiu, fighjendu u statu di memoria di l'applicazione in VM Tracker, truverete chì LMDB hè luntanu da l'unicu chì pretende a memoria mappata in memoria. I boni pezzi sò manghjati da l'allocatori di u sistema, i schedarii di risorse, i quadri di l'imaghjini è altri predatori più chjuchi.

Basatu nantu à i risultati di l'esperimenti in u Cloud, simu ghjunti à i seguenti valori di cumprumissu di memoria attribuiti da LMDB: 384 megabytes per i dispositi 32-bit è 768 per quelli 64-bit. Dopu chì stu voluminu hè utilizatu, qualsiasi operazioni di mudificazione cumincianu à cumpletà cù u codice MDB_MAP_FULL. Avemu osservatu tali errori in u nostru monitoraghju, ma sò abbastanza chjuchi per esse trascurati in questa fase.

Un mutivu micca ovviu per u cunsumu eccessivu di memoria da u almacenamiento pò esse transazzione longu. Per capisce cumu si sò ligati sti dui fenomeni, ci aiuterà à cunsiderà e duie balene LMDB restanti.

3.2. Balena #2. B+-arburu

Per emulà e tavule in cima à un magazinu di chjave-valore, e seguenti operazioni devenu esse presente in a so API:

  1. Inserisce un novu elementu.
  2. Cerca un elementu cù una chjave data.
  3. Sguassà un elementu.
  4. Iterate nantu à intervalli chjave in u so ordine di sorte.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOSA struttura di dati più simplice chì ponu facilmente implementà tutte e quattru operazioni hè un arbre di ricerca binariu. Ciascunu di i so nodi hè una chjave chì divide u sottumessu tutale di chjavi di u zitellu in dui subtrees. À a manca sò quelli chì sò più chjuchi di u genitore, è à a diritta - quelli chì sò più grande. L'ottenimentu di un inseme urdinatu di chjave hè ottenutu per mezu di unu di i classici traversali di l'arburu

L'arbureti binari anu dui inconvenienti fundamentali chì impediscenu di esse efficace cum'è una struttura di dati di discu. Prima, u gradu di u so equilibriu hè imprevisible. Ci hè un risicu considerableu di ottene arburi in quale l'altezza di e diverse rami pò varià assai, chì aggrava significativamente a cumplessità algoritmica di a ricerca cumparatu cù ciò chì hè previstu. Siconda, l'abbundanza di ligami incruciati trà i nodi priva l'arbureti binari di a località in memoria.Nodes vicinu (in termini di ligami trà elli) ponu esse situati in pagine completamente diverse in memoria virtuale. In cunsiquenza, ancu un semplice traversu di parechji nodi vicini in un arbulu pò esse bisognu di visità un numeru paragunabile di pagine. Questu hè un prublema ancu quandu parlemu di l'efficienza di l'arbureti binari cum'è una struttura di dati in memoria, postu chì e pagine rotanti constantemente in u cache di u processatore ùn hè micca prezzu. Quandu si tratta di elevà spessu e pagine relative à i nodi da u discu, e cose sò veramente male. deplorable.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOSL'arbureti B, essendu una evoluzione di l'arburi binari, risolve i prublemi identificati in u paràgrafu precedente. Prima, sò auto-equilibriu. Siconda, ognunu di i so nodi divide u settore di chjavi di u zitellu micca in 2, ma in M ​​subsets urdinati, è u numeru M pò esse abbastanza grande, in l'ordine di parechji centu o ancu millaie.

Cusì:

  1. Ogni node hà un gran numaru di chjave digià urdinatu è l'arburi sò assai bassu.
  2. L'arburu acquista a pruprietà di a località in memoria, postu chì i chjavi chì sò vicinu in valore sò naturali situati vicinu à l'altri in unu o nodi vicini.
  3. Reduce u numeru di nodi di transitu quandu scende l'arburu durante una operazione di ricerca.
  4. Reduce u numeru di nodi di destinazione letti per e dumande di intervallu, postu chì ognunu di elli cuntene digià un gran numaru di chjavi urdinati.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

LMDB usa una variante di l'arbulu B chjamatu l'arbulu B + per almacenà e dati. U diagramma sopra mostra i trè tippi di nodi chì cuntene:

  1. In cima hè a radica. Ùn materializza nunda più cà u cuncettu di una basa di dati in un repository. In una sola istanza LMDB, pudete creà parechje basa di dati chì sparte u spaziu di indirizzu virtuale mappatu. Ognunu di elli principia da a so propria radica.
  2. À u livellu più bassu sò e foglie (foglia). Sò elli è solu quelli chì cuntenenu i coppie chjave-valore almacenate in a basa di dati. In modu, questu hè a peculiarità di l'arbureti B +. Se un B-tree normale guarda i valori-parts à i nodi di tutti i livelli, allura a variazione B + hè solu à u più bassu. Dopu avè riparatu stu fattu, in ciò chì seguita chjameremu u subtipu di l'arburu utilizatu in LMDB solu un B-tree.
  3. Trà a radica è e foglie, ci sò 0 o più livelli tecnichi cù nodi di navigazione (ramu). U so compitu hè di sparte u settore di chjavi urdinati trà e foglie.

Fisicamente, i nodi sò blocchi di memoria di una durata predeterminata. A so dimensione hè un multiplu di a dimensione di e pagine di memoria in u sistema operatore, chì avemu parlatu sopra. A struttura di u nodu hè mostrata quì sottu. L'intestazione cuntene meta-informazioni, a più ovvia di quale, per esempiu, hè u checksum. Dopu vene l'infurmazioni nantu à l'offsets, longu à quale si trovanu e cellule cù dati. U rolu di dati pò esse sia chjave, se parlemu di nodi di navigazione, o pariglii interi chjave-valore in u casu di foglie.Pudete leghje più nantu à a struttura di e pagine in u travagliu. "Valutazione di i magazzini à valore chjave d'altu rendiment".

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Dopu avè trattatu u cuntenutu internu di i nodi di a pagina, rapprisentaremu ancu l'arbre B LMDB in modu simplificatu in a forma seguente.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

E pagine cù nodi sò disposti sequenzialmente nantu à u discu. E pagine cù un numeru più altu sò situate versu a fine di u schedariu. A chjamata pagina meta (meta pagina) cuntene infurmazioni nantu à l'offsets, chì ponu esse usatu per truvà e radiche di tutti l'arburi. Quandu un schedariu hè apertu, LMDB scansa u schedariu pagina per pagina da a fine à u principiu in cerca di una meta pagina valida è trova e basa di dati esistenti attraversu.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Avà, avè una idea di a struttura logica è fisica di l'urganizazione di dati, pudemu procederà à cunsiderà a terza balena di LMDB. Hè cù u so aiutu chì tutte e mudificazioni di l'almacenamiento si facenu transazzione è isolate l'una di l'altru, dendu a basa di dati in tuttu ancu a pruprietà multiversione.

3.3. Balena #3. copia-on-scrittura

Certi operazioni B-tree implicanu fà una seria sana di cambiamenti à i so nodi. Un esempiu hè di aghjunghje una nova chjave à un node chì hà digià righjuntu a so capacità massima. In questu casu, hè necessariu, prima, di sparte u node in dui, è in segundu, per aghjunghje un ligame à u novu node spun off child in u so parente. Sta prucedura hè putenziale assai periculosa. Se per una certa ragione (crash, power outage, etc.) solu una parte di i cambiamenti da a serie succedi, allura l'arbulu ferma in un statu inconsistente.

Una solu suluzione tradiziunale per fà una basa di dati tolerante à i difetti hè di aghjunghje una struttura di dati addiziale basata in discu, u logu di transazzione, cunnisciutu ancu u logu di scrittura in anticipu (WAL), vicinu à l'arburu B. Hè un schedariu, à a fine di quale, strettamente prima di a mudificazione di u B-tree stessu, l'operazione prevista hè scritta. Cusì, se a corruzzione di dati hè rilevata durante l'autodiagnosi, a basa di dati cunsulta u logu per pulizziari.

LMDB hà sceltu un metudu sfarente cum'è u so mecanismu di toleranza di difetti, chì hè chjamatu copy-on-write. A so essenza hè chì invece di aghjurnà e dati nantu à una pagina esistente, prima copia sanu sanu è face tutte e mudificazioni digià in a copia.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

In più, per esse dispunibule e dati aghjurnati, hè necessariu di cambià u ligame à u node chì hè diventatu aghjurnatu in u node parent in relazione à questu. Siccomu ci vole ancu esse mudificatu per questu, hè ancu pre-copiatu. U prucessu cuntinueghja recursivamente finu à a ràdica. I dati nantu à a meta pagina sò l'ultimi à cambià.​​

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Sè di colpu, u prucessu crashes durante a prucedura di aghjurnamentu, allura o una nova meta pagina ùn serà micca creatu, o ùn sarà micca scrittu à u discu finu à a fine, è u so checksum serà sbagliatu. In unu di sti dui casi, e novi pagine seranu inaccessibili è i vechji ùn saranu micca affettati. Questu elimina a necessità per LMDB di scrive un log in anticipu per mantene a coerenza di dati. De facto, a struttura di l'almacenamiento di dati nantu à u discu, descritta sopra, assume simultaneamente a so funzione. L'assenza di un logu di transazzione esplicitu hè una di e caratteristiche di LMDB, chì furnisce una alta velocità di lettura di dati

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

U custruttu risultatu, chjamatu append-only B-tree, furnisce naturalmente l'isolamentu di transazzione è multiversione. In LMDB, ogni transazzione aperta hà una radica d'arburu aghjurnata assuciata cun ella. Mentre a transazzione ùn hè micca finita, e pagine di l'arburu assuciatu cù questu ùn saranu mai cambiate o reutilizate per e novi versioni di dati. Cusì, pudete travaglià finu à chì vulete esattamente cù u settore di dati chì era pertinente à u tempu chì a transazzione hè stata aperta, ancu s'è u almacenamentu cuntinueghja à esse aghjurnatu attivamente à questu tempu. Questa hè l'essenza di a multiversione, facendu LMDB una fonte di dati ideale per i nostri amati UICollectionView. Dopu avè apertu una transazzione, ùn avete micca bisognu di aumentà l'impronta di memoria di l'applicazione, sguassate in fretta i dati attuali in una struttura in memoria, avè paura di esse lasciatu senza nunda. Questa funzione distingue LMDB da u stessu SQLite, chì ùn pò micca vantassi di un tali isolamentu tutale. Dopu avè apertu duie transazzione in l'ultime è sguassate un certu registru in unu di elli, u listessu registru ùn pò più esse acquistatu in u sicondu restu.

U flip side di a munita hè u cunsumu potenzalmentu significativamente più altu di memoria virtuale. A diapositiva mostra ciò chì a struttura di a basa di dati serà cum'è s'ellu hè mudificatu à u stessu tempu cù 3 transazzioni di lettura aperta chì fighjenu diverse versioni di a basa di dati. Siccomu LMDB ùn pò micca riutilizà i nodi chì sò raggiungibili da e radichi assuciati cù e transazzione attuale, u almacenamentu ùn hà micca altra scelta ma di assignà una altra quarta radica in memoria è una volta clone e pagine modificate sottu.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Quì ùn serà micca superfluu di ricurdà a sezione nantu à i fugliali mappati in memoria. Sembra chì u cunsumu supplementu di memoria virtuale ùn deve micca disturbà assai, postu chì ùn cuntribuisce micca à l'impronta di memoria di l'applicazione. In ogni casu, à u stessu tempu, hè statu nutatu chì iOS hè assai avari in l'assignazione, è ùn pudemu micca furnisce una regione LMDB di 1 terabyte in un servitore o desktop da a spalla di u maestru è ùn pensate micca à sta funzione. Quandu hè pussibule, avete da pruvà à mantene a vita di e transazzione u più corta pussibule.

4. Disegnu un schema di dati in cima di l'API chjave-valore

Cuminciamu à analizà l'API fighjendu l'astrazioni basi furnite da LMDB: ambiente è basa di dati, chjave è valori, transazzione è cursori.

Una nota nantu à e liste di codice

Tutte e funzioni in l'API publica LMDB tornanu u risultatu di u so travagliu in a forma di un codice d'errore, ma in tutti i listini sussegwente u so verificatu hè omessi per a cuncisione. In pratica, avemu usatu u nostru codice per interagisce cù u repository. forchetta Wrappers C++ lmdbxx, in quale l'errori si materializanu cum'è eccezzioni C++.

Cum'è u modu più veloce per cunnette LMDB à un prughjettu iOS o macOS, offre u mo CocoaPod POSLMDB.

4.1. Astrazzioni basi

Ambiente

strutura MDB_env hè u repository di u statu internu di l'LMDB. Famiglia di funzioni prefissate mdb_env permette di cunfigurà alcune di e so proprietà. In u casu più sèmplice, l'inizializazione di u mutore pare cusì.

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

In l'applicazione Mail.ru Cloud, avemu cambiatu i valori predeterminati per solu dui parametri.

U primu hè a dimensione di u spaziu di l'indirizzu virtuale chì u schedariu di almacenamiento hè mappatu. Sfurtunatamente, ancu in u stessu dispositivu, u valore specificu pò varià significativamente da una corsa à una corsa. Per piglià in contu sta funziunalità di iOS, selezziunà a quantità massima di almacenamiento dinamica. Partendu da un certu valore, si mette successivamente à a mità finu à a funzione mdb_env_open ùn vultà micca un risultatu altru ch'è ENOMEM. In teoria, ci hè un modu oppostu - prima attribuisce un minimu di memoria à u mutore, è dopu, quandu l'errore sò ricevuti MDB_MAP_FULL, cresce. Tuttavia, hè assai più spinosa. U mutivu hè chì a prucedura per rimappà a memoria utilizendu a funzione mdb_env_set_map_size invalida tutte e entità (cursori, transazzione, chjave è valori) ricivutu da u mutore prima. A cuntabilità per una tale volta di l'avvenimenti in u codice portarà à a so cumplicazione significativa. Se, però, a memoria virtuale hè assai cara per voi, allora questu pò esse un mutivu per guardà a forchetta chì hè andata assai avanti. MDBX, induve trà e caratteristiche dichjarate ci hè "l'aghjustamentu automaticu di a basa di dati nantu à a mosca".

U sicondu paràmetru, u valore predeterminatu di quale ùn ci hè micca adattatu, regula a meccanica di assicurà a sicurezza di filu. Sfortunatamente, almenu in iOS 10, ci sò prublemi cù u supportu di almacenamentu locale di filu. Per quessa, in l'esempiu di sopra, u repositoriu hè apertu cù a bandiera MDB_NOTLS. Inoltre, hè ancu necessariu forchetta Wrapper C++ lmdbxxper cutà variabili cù è in questu attributu.

Basi di dati

A basa di dati hè un esempiu separatu di u B-tree chì avemu parlatu sopra. A so apertura si trova in una transazzione, chì in prima pò sembra un pocu strana.

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);

Infatti, una transazzione in LMDB hè una entità di almacenamiento, micca una basa di dati specifica. Stu cuncettu permette di fà operazioni atomiche nantu à entità situate in diverse basa di dati. In teoria, questu apre a pussibilità di mudelli di tavule in a forma di diverse basa di dati, ma in un certu tempu aghju andatu in l'altru modu, descrittu in detail sottu.

Chjavi è valori

strutura MDB_val modella u cuncettu di una chjave è di un valore. U repositoriu ùn hà micca idea di a so semantica. Per ella, qualcosa chì hè diversu hè solu un array di bytes di una certa dimensione. A dimensione massima di a chjave hè 512 byte.

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

A tenda usa un comparatore per sorte e chjave in ordine crescente. Se ùn avete micca rimpiazzatu cù u vostru propiu, allora u predeterminatu serà utilizatu, chì l'ordina byte-by-byte in ordine lessicugraficu.

Transazzioni

U dispusitivu di transazzione hè descrittu in detail in capitulu precedente, dunque quì ripeteraghju e so proprietà principali in una corta linea:

  1. Supportu per tutte e proprietà basi àcituParolle chjave: atomicità, cuerenza, isolamentu è affidabilità. Ùn possu aiutà, ma nutà chì in termini di durabilità in macOS è iOS ci hè un bug fissu in MDBX. Pudete leghje più in u so README.
  2. L'approcciu di u multithreading hè descrittu da u schema "single writer / multiple readers". I scrittori si bluccanu l'un l'altru, ma ùn bluccanu micca i lettori. I lettori ùn bluccà micca i scrittori o l'altri.
  3. Supportu per transazzione nidificatu.
  4. Supportu multiversione.

A multiversione in LMDB hè cusì bona chì vogliu dimustrà in azzione. U codice sottu mostra chì ogni transazzione travaglia esattamente cù a versione di a basa di dati chì era pertinente à u mumentu di a so apertura, essendu cumplettamente isolata da tutti i cambiamenti successivi. L'inizializazione di u repositoriu è l'aghjunghje un registru di teste ùn hè micca d'interessu, cusì sti rituali sò lasciati sottu à u spoiler.

Aghjunghjendu una entrata di prova

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;

// Открываем 2 транзакции, каждая из которых смотрит
// на версию базы данных с одной записью.
mdb_txn_begin(env, NULL, 0, &txn1); // read-write
mdb_txn_begin(env, NULL, MDB_RDONLY, &txn2); // read-only

// В рамках первой транзакции удаляем из базы данных существующую в ней запись.
mdb_del(txn1, dbi, &key, NULL);
// Фиксируем удаление.
mdb_txn_commit(txn1);

// Открываем третью транзакцию, которая смотрит на
// актуальную версию базы данных, где записи уже нет.
mdb_txn_begin(env, NULL, MDB_RDONLY, &txn3);
// Убеждаемся, что запись по искомому ключу уже не существует.
assert(mdb_get(txn3, dbi, &key, &val) == MDB_NOTFOUND);
// Завершаем транзакцию.
mdb_txn_abort(txn3);

// Убеждаемся, что в рамках второй транзакции, открытой на момент
// существования записи в базе данных, её всё ещё можно найти по ключу.
assert(mdb_get(txn2, dbi, &key, &val) == MDB_SUCCESS);
// Проверяем, что по ключу получен не абы какой мусор, а валидные данные.
assert(*(int *)val.mv_data == 997);
// Завершаем транзакцию, работающей хоть и с устаревшей, но консистентной базой данных.
mdb_txn_abort(txn2);

Opcionalmente, ricumandemu di pruvà u stessu truccu cù SQLite è vede ciò chì succede.

Multiversioning porta assai benefizii à a vita di un sviluppatore iOS. Utilizendu sta pruprietà, pudete aghjustà facilmente è naturalmente a tarifa di l'aghjurnamentu di a fonte di dati per e forme di schermu basatu annantu à l'esperienza di l'utilizatori. Per esempiu, andemu à piglià una tale funzione di l'applicazione Mail.ru Cloud cum'è u cuntenutu autoloading da a galleria di media di u sistema. Cù una bona cunnessione, u cliente hè capaci di aghjunghje parechje foto per seconda à u servitore. Sè aghjurnà dopu ogni scaricamentu UICollectionView cù cuntenutu media in u nuvulu di l'utilizatori, pudete scurdà di 60 fps è scorri liscia durante stu prucessu. Per impediscenu l'aghjurnamenti frequenti di u screnu, avete bisognu di limità in qualchì modu a tarifa di cambiamentu di dati in a basa UICollectionViewDataSource.

Se a basa di dati ùn sustene micca a multiversione è vi permette di travaglià solu cù u statu attuale attuale, allora per creà una snapshot di dati stabile in u tempu, avete bisognu di copià sia in una struttura di dati in memoria o in una tavola temporale. Qualchese di sti approcci hè assai caru. In u casu di l'almacenamiento in memoria, uttene sia i costi di memoria causati da l'almacenamiento di l'uggetti custruiti è i costi di tempu assuciati cù trasfurmazioni ORM redundante. In quantu à a tavola temporale, questu hè un piacè ancu più caru, chì hè sensu solu in casi micca triviali.

Multiversioning LMDB risolve u prublema di mantene una fonte di dati stabile in una manera assai elegante. Hè abbastanza solu per apre una transazzione è voilà - finu à chì l'avemu cumpletu, u settore di dati hè garantitu per esse fissu. A logica di a so tarifa d'aghjurnamentu hè oghji interamente in manu di a strata di presentazione, senza overhead of significant resources.

Cursori

I cursori furniscenu un mecanismu per l'iterazione ordinata nantu à coppie chjave-valore attraversu un B-tree. Senza elli, ùn saria impussibile di mudificà in modu efficace e tavule in a basa di dati, chì avemu turnatu avà.

4.2. Modellazione di tavulinu

A pruprietà di l'ordine chjave permette di custruisce una astrazione di primu livellu cum'è una tavola sopra l'astrazioni basiche. Cunsideremu stu prucessu nantu à l'esempiu di a tavola principale di u cliente nuvola, in quale l'infurmazioni nantu à tutti i schedari è i caratteri di l'utilizatori sò cache.

Schema di table

Unu di i scenarii cumuni per quale a struttura di una tavula cù un arbulu di cartulare deve esse affilata hè di selezziunà tutti l'elementi situati in un repertoriu determinatu. Un bonu mudellu d'urganizazione di dati per dumande efficienti di stu tipu hè. Lista di Adjacenza. Per implementà nantu à a cima di l'almacenamiento di u valore di chjave, hè necessariu di sorte e chjavi di i fugliali è i cartulare in tale manera chì sò raggruppati in base à l'appartenenza à u cartulare parent. Inoltre, per vede u cuntenutu di u repertoriu in a forma familiar à un utilizatore di Windows (cartulari prima, dopu i schedari, i dui sò urdinati alfabeticamente), hè necessariu include i campi supplementari currispondenti in a chjave.

A stampa quì sottu mostra cumu, basatu annantu à u compitu, a rapprisintazioni di e chjave cum'è un array di bytes pò esse. Prima, i byte cù l'identificatore di u repertoriu parentale (rossu) sò posti, dopu cù u tipu (verde), è digià in a cuda cù u nome (blu). u modu necessariu. Traversà sequenzialmente e chjave cù u stessu prefissu rossu ci dà i valori assuciati cù elli in l'ordine in quale deve esse affissatu in l'interfaccia d'utilizatore (a destra), senza bisognu di post-processamentu supplementu.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Serializing Keys and Values

Ci hè parechje metudi per serializà l'ogetti in u mondu. Siccomu ùn avemu micca altru requisitu altru ch'è a velocità, avemu sceltu u più veloce pussibule per noi stessu - un dump di memoria occupatu da una istanza di a struttura di a lingua C. Allora, a chjave di un elementu di u repertoriu pò esse modellata da a struttura seguente. NodeKey.

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

Per salvà NodeKey in u bisognu di almacenamentu in l'ughjettu MDB_val pusizziunà u punteru à i dati à l'indirizzu di u principiu di a struttura, è calculate a so dimensione cù a funzione sizeof.

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

In u primu capitulu nantu à i criteri di selezzione di basa di dati, aghju mintuatu di minimizzà l'allocazioni dinamiche cum'è parte di l'operazioni CRUD cum'è un fattore di selezzione impurtante. Codice di funzione serialize mostra cumu, in u casu di LMDB, ponu esse completamente evitati quandu novi registri sò inseriti in a basa di dati. L'array entrante di bytes da u servitore hè prima trasfurmatu in strutture di stack, è dopu sò trivially dumped in u almacenamiento. Dapoi chì ùn ci hè ancu micca allocazioni dinamiche in LMDB, pudete ottene una situazione fantastica per i standard di iOS - utilizate solu memoria di pila per travaglià cù dati da a reta à u discu!

Ordine di chjave cù un comparatore binariu

A relazione di ordine chjave hè datu da una funzione speciale chjamata comparatore. Siccomu u mutore ùn sà nunda di a semantica di i byte chì cuntenenu, u comparatore predeterminatu ùn hà micca altra scelta ma di organizà e chjave in ordine lessicugraficu, ricorrendu à u so paragone byte-by-byte. Aduprà per organizà e strutture hè simile à rasa cù un ascia di scultura. In ogni casu, in i casi simplici, mi pare stu metudu accettabile. L'alternativa hè descritta quì sottu, ma quì aghju nutatu un coppiu di razzi spargugliati nantu à a strada.

A prima cosa da tene in mente hè a rapprisintazioni di memoria di tipi di dati primitivi. Allora, in tutti i dispositi Apple, i variàbili integer sò almacenati in u formatu Pocu Endian. Questu significa chì u byte menu significativu serà nantu à a manca, è ùn puderete micca sorte l'interi cù u so paragone byte per byte. Per esempiu, pruvà à fà questu cù un inseme di numeri da 0 à 511 risultatu in u risultatu seguente.

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

Per risolve stu prublema, i numeri interi deve esse guardatu in a chjave in un formatu adattatu per u comparatore di byte. Funzioni da a famiglia aiutanu à realizà a trasfurmazioni necessaria. hton* (in particulare htons per numeri doppiu byte da l'esempiu).

U furmatu per rapprisintà strings in a prugrammazione hè, cum'è sapete, un sanu storia. Se a semantica di e stringhe, è ancu a codificazione utilizata per rapprisintà in memoria, suggerisce chì pò esse più di un byte per caratteru, allora hè megliu abbandunà immediatamente l'idea di utilizà un comparatore predeterminatu.

A seconda cosa da tene in mente hè principii di allineamentu compilatore di campu struct. Per via di elli, i byte cù valori di basura ponu esse furmati in memoria trà i campi, chì, sicuru, rompe l'ordine di byte. Per eliminà a basura, duvete sia dichjarà i campi in un ordine strettu definitu, tenendu in mente e regule di allineamentu, o utilizate l'attributu in a dichjarazione di struttura. packed.

Ordine chjave da un comparatore esternu

A logica di paraguni chjave pò esse troppu cumplessa per un comparatore binariu. Unu di i tanti mutivi hè a prisenza di campi tecnichi ind'u strutture. Illustreraghju a so occurrence nantu à l'esempiu di una chjave chì hè digià cunnisciuta per un elementu di repertoriu.

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

Per tutta a so simplicità, in a maiò parte di i casi cunsuma troppu memoria. U buffer di tìtulu hè 256 byte, ancu s'è in media, i nomi di i schedarii è di i cartulare raramente superanu 20-30 caratteri.

Una di e tecniche standard per ottimisà a dimensione di un registru hè di "cutà" per adattà à a dimensione attuale. A so essenza hè chì u cuntenutu di tutti i campi di lunghezza variabile sò guardati in un buffer à a fine di a struttura, è e so lunghezze sò guardati in variàbili separati. NodeKey hè trasfurmatu in u modu seguenti.

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

In più, durante a serializazione, micca specificatu cum'è a dimensione di dati sizeof tutta a struttura, è a dimensione di tutti i campi hè a lunghezza fissa più a dimensione di a parte veramente usata di u buffer.

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

In u risultatu di a refactoring, avemu un risparmiu significativu in u spaziu occupatu da i chjavi. Tuttavia, per via di u campu tecnicu nameLength, u comparatore binariu predeterminatu ùn hè più adattatu per a comparazione chjave. Se ùn avemu micca rimpiazzatu cù u nostru propiu, allora a durata di u nome serà un fattore più priurità in a classificazione di u nome stessu.

LMDB permette à ogni basa di dati per avè a so propria funzione di paragone chjave. Questu hè fattu cù a funzione mdb_set_compare strettamente prima di apre. Per ragioni evidenti, una basa di dati ùn pò esse cambiata in tutta a so vita. À l'input, u comparatore riceve duie chjave in formatu binariu, è à l'output torna u risultatu di a comparazione: menu di (-1), più grande di (1) o uguale (0). Pseudocodice per NodeKey pari cusì.

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 // ...
}​

Sempre chì tutte e chjave in a basa di dati sò di u listessu tipu, hè legale à scaccià incondizionatamente a so rapprisentazione di byte à u tipu di struttura di l'applicazione di a chjave. Ci hè una sfumatura quì, ma serà discututu un pocu più bassu in a subsezzione "Reading Records".

Serializazione di u valore

Cù i chjavi di i registri almacenati, LMDB travaglia assai intensivamente. Sò paragunati cù l'altri in u quadru di ogni operazione di l'applicazione, è a prestazione di a suluzione sana dipende da a velocità di u comparatore. In un mondu ideale, u comparatore binariu predeterminatu deve esse abbastanza per paragunà i chjavi, ma s'ellu avete daveru aduprà u vostru propiu, allora a prucedura per deserializing keys in questu deve esse u più veloce pussibule.

A basa di dati ùn hè micca particularmente interessata in a parte di u valore di u record (valore). A so cunversione da una rapprisintazioni di byte à un ughjettu hè solu quandu hè digià dumandatu da u codice di l'applicazione, per esempiu, per vede nantu à u screnu. Siccomu questu succede relativamente raramente, i requisiti per a rapidità di sta prucedura ùn sò micca cusì critichi, è in a so implementazione simu assai più liberi di fucalizza nantu à a cunvenzione.Per esempiu, per serializza metadata nantu à i schedari chì ùn sò micca stati scaricati, avemu aduprà. NSKeyedArchiver.

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

Tuttavia, ci sò volte quandu u rendiment importa. Per esempiu, quandu salvate meta-informazioni nantu à a struttura di u schedariu di u nuvulu di l'utilizatori, usemu u stessu dump di memoria d'ughjettu. U puntu culminante di u compitu di generà a so rapprisintazioni seriale hè u fattu chì l'elementi di un repertoriu sò modellati da una ghjerarchia di classi.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Per a so implementazione in a lingua C, i campi specifichi di l'eredi sò pigliati in strutture separati, è a so cunnessione cù a basa hè specificatu per mezu di un campu di u tipu di unione. U cuntenutu propiu di l'unione hè specificatu per via di l'attributu tecnicu di tipu.

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

Aghjunghje è aghjurnà i registri

A chjave seriale è u valore pò esse aghjuntu à a tenda. Per questu, a funzione hè aduprata mdb_put.

// key и value имеют тип MDB_val​
mdb_put(..., &key, &value, MDB_NOOVERWRITE);

In u stadiu di cunfigurazione, u repositoriu pò esse permessu o pruibitu di almacenà parechji registri cù a listessa chjave. Se a duplicazione di e chjave hè pruibita, quandu inserite un registru, pudete stabilisce se l'aghjurnamentu di un registru esistente hè permessu o micca. Se u sfilacciamentu pò accade solu per via di un errore in u codice, pudete assicurà contr'à ellu specificendu a bandiera. NOOVERWRITE.

Records di lettura

A funzione per leghje i registri in LMDB hè mdb_get. Se a coppia chjave-valore hè rapprisintata da strutture precedentemente scaricate, allora sta prucedura s'assumiglia cusì.

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

A lista presentata mostra cumu a serializazione attraversu un dump di strutture permette di sbarazzarsi di l'allocazioni dinamiche micca solu quandu scrive, ma quandu leghje dati. Derivatu da a funzione mdb_get u punteru guarda esattamente à l'indirizzu di memoria virtuale induve a basa di dati guarda a rapprisintazioni byte di l'ughjettu. In fatti, avemu un tipu di ORM, quasi gratuitu chì furnisce una velocità assai alta di lettura di dati. Cù tutta a bellezza di l'approcciu, hè necessariu di ricurdà parechje caratteristiche assuciate cù questu.

  1. Per una transazzione di sola lettura, un punteru à una struttura di valore hè garantitu per esse validu solu finu à chì a transazzione hè chjusa. Comu nutatu prima, e pagine di l'arbulu B nantu à quale l'ughjettu reside, grazia à u principiu di copia-in-scrittura, restanu invariati finu à chì almenu una transazzione si riferisce à elli. À u listessu tempu, quandu l'ultima transazzione assuciata cun elli hè cumpletata, e pagine ponu esse reutilizate per novi dati. S'ellu hè necessariu per l'uggetti per sopravvive à a transazzione chì l'hà creatu, allora anu sempre esse copiatu.
  2. Per una transazzione di readwrite, l'indicatore à a struttura-valore risultante serà validu solu finu à a prima prucedura di mudificazione (scrittura o eliminazione di dati).
  3. Ancu s'è a struttura NodeValue micca full-fledged, ma trimmed (vede subsection "Order chjavi da un comparator esternu"), attraversu lu puntatore, vi ponu facirmenti accede à i so campi. A cosa principal ùn hè micca di dereference!
  4. In nisun casu pudete mudificà a struttura attraversu u puntatore ricevutu. Tutti i cambiamenti deve esse fattu solu per mezu di u metudu mdb_put. Tuttavia, cù tuttu u desideriu di fà questu, ùn hà micca travagliatu, postu chì l'area di memoria induve sta struttura si trova hè mappata in modu di sola lettura.
  5. Remap un schedariu à u spaziu di indirizzu di un prucessu per, per esempiu, aumentà a dimensione massima di almacenamento cù a funzione mdb_env_set_map_size invalida cumplettamente tutte e transazzione è l'entità cunnesse in generale è i puntatori per leghje l'uggetti in particulare.

Infine, una funzione più hè cusì insidiosa chì a divulgazione di a so essenza ùn si mette micca solu in un puntu più. In u capitulu nantu à u B-tree, aghju datu un diagramma di l'urganizazione di e so pagine in memoria. Ne segue chì l'indirizzu di u principiu di u buffer cù dati serializatu pò esse assolutamente arbitrariu. Per via di questu, u punteru à elli, ottenutu in a struttura MDB_val è cast à un puntatore à una struttura hè generalmente unaligned. À u listessu tempu, l'architetture di certi chips (in u casu di iOS, questu hè armv7) esigenu chì l'indirizzu di qualsiasi dati sia un multiplu di a dimensione di una parolla di macchina, o, in altre parolle, u bitness di u sistema. (per armv7, questu hè 32 bit). In altre parolle, una operazione cum'è *(int *foo)0x800002 nantu à elli hè uguagliatu à scappà è porta à l'esekzione cù un verdict EXC_ARM_DA_ALIGN. Ci hè dui modi per evitari un destinu cusì tristu.

U primu hè di cupià i dati in una struttura allinata cunnisciuta prima. Per esempiu, nantu à un comparatore persunalizatu, questu serà riflessu cusì.

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 // ...
}

Un modu alternativu hè di avvisà u compilatore in anticipu chì e strutture cù una chjave è un valore ùn ponu micca esse allinati cù un attributu. aligned(1). In ARM u listessu effettu pò esse ghjunghje è utilizendu l'attributu imballatu. Cunsiderendu chì cuntribuisci ancu à l'ottimisazione di u spaziu occupatu da a struttura, stu metudu mi pare preferibile, anche se приводит per aumentà u costu di l'operazioni d'accessu di dati.

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

Interrogazioni di gamma

Per iterà nantu à un gruppu di registri, LMDB furnisce una astrazione di cursore. Fighjemu cumu travaglià cun ellu cù l'esempiu di una tavula cù metadati di nuvola d'utilizatori chì sò digià familiarizati per noi.

Cum'è una parte di vede una lista di i schedari in un annuariu, avete bisognu di truvà tutte e chjave cù quale sò assuciati i so fugliali è i fugliali. In i subsezzioni previ, avemu ordenatu i chjavi NodeKey in modu chì sò prima urdinati da u so ID di repertoriu parenti. Cusì, tècnicamente, u compitu di ottene u cuntenutu di un cartulare hè ridutta à pusà u cursore nantu à u cunfini superiore di un gruppu di chjavi cù un prefissu datu, seguitu da iterazione à u cunfini inferjuri.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Pudete truvà u limite superiore "nantu à a fronte" per una ricerca sequenziale. Per fà questu, u cursore hè situatu à l'iniziu di a lista completa di chjavi in ​​a basa di dati è poi aumentatu finu à chì a chjave cù l'identificatore di u repertoriu parenti appare sottu. Stu approcciu hà 2 svantaghji evidenti:

  1. A cumplessità lineale di a ricerca, ancu s'è, cum'è sapete, in l'arburi in generale è in un B-tree in particulare, pò esse fattu in tempu logaritmicu.
  2. In vain, tutte e pagine chì precedenu a desiderata sò elevate da u schedariu à a memoria principale, chì hè assai caru.

Fortunatamente, l'API LMDB furnisce una manera efficaci di pusizioni inizialmente u cursore.Per fà questu, avete bisognu di furmà una chjave chì u valore hè cunnisciutu per esse menu o uguale à a chjave situata in u limite superiore di l'intervallu. Per esempiu, in relazione à a lista in a figura sopra, pudemu fà una chjave in quale u campu parentId serà uguali à 2, è tuttu u restu hè pienu di zeri. Tale chjave parzialmente piena hè alimentata à l'input di a funzione mdb_cursor_get chì indica u funziunamentu 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);

Se si trova u limite superiore di u gruppu di chjave, allora iteremu sopra finu à chì o scuntràmu o a chjave cù un altru. parentId, o i chjavi ùn sguassate micca in tuttu.​

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​​

Ciò chì hè bellu, cum'è parte di l'iterazione cù mdb_cursor_get, avemu micca solu a chjave, ma ancu u valore. Se, per cumpiendu e cundizioni di selezzione, hè necessariu di verificà, frà altri cose, i campi da a parte di valore di u record, allora sò abbastanza accessibili per elli stessi senza gesti supplementari.

4.3. Mudelle di relazioni trà e tavule

Finu a data, avemu sappiutu di cunsiderà tutti l'aspettu di cuncepimentu è di travaglià cù una basa di dati unicu. Pudemu dì chì una tavula hè un inseme di registri ordinati custituiti da coppie chjave-valore di u stessu tipu. Se vede una chjave cum'è un rectangulu è u so valore assuciatu cum'è una casella, avete un diagramma visuale di a basa di dati.

Enseñanza

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

In ogni casu, in a vita reale, hè raramente pussibule di passà cù pocu sangue. Spessu in una basa di dati hè necessariu, prima, per avè parechje tavule, è secondu, per fà selezzione in elli in un ordine diffirenti da a chjave primaria. Quest'ultima sezione hè dedicata à i prublemi di a so creazione è interconnessione.

Tabelle indici

L'app nuvola hà una sezione "Galleria". Mostra u cuntenutu media da tuttu u nuvulu, ordinatu per data. Per l'implementazione ottimale di una tale selezzione, vicinu à a tavola principale, avete bisognu di creà un altru cù un novu tipu di chjave. Conteneranu un campu cù a data chì u schedariu hè statu creatu, chì agisce cum'è u criteriu primariu di sorte. Perchè i novi chjavi riferenu à i stessi dati cum'è i chjavi in ​​a tavula sottostante, sò chjamati chjavi d'indici. Sò evidenziati in aranciu in a stampa sottu.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Per separà e chjavi di e diverse tavule l'una di l'altru in a listessa basa di dati, un tableId di campu tècnicu supplementu hè statu aghjuntu à tutti. Facendu a più alta priorità per a classificazione, avemu da raggruppà e chjave prima per tavule, è digià in e tavule - secondu e nostre regule.

A chjave d'indici si riferisce à i stessi dati cum'è a chjave primaria. L'implementazione diretta di sta pruprietà associendu cù una copia di a parte di u valore di a chjave primaria hè suboptimale da parechji punti di vista à una volta:

  1. Da un puntu di vista spaziu occupatu, i metadata pò esse abbastanza riccu.
  2. Da un puntu di vista di u performance, postu chì quandu aghjurnà i metadati di u nodu, duverete sovrascrive duie chjave.
  3. Da u puntu di vista di u supportu di codice, dopu à tuttu, si scurdate di aghjurnà e dati per una di e chjave, avemu da ottene un bug sottile di inconsistenza di dati in u almacenamiento.

In seguitu, avemu da cunsiderà cumu eliminà sti difetti.

Urganisazione di rilazioni trà tavule

U mudellu hè bè adattatu per ligà una tavola d'indici cù u principale "chiave cum'è valore". Cum'è u so nome implica, a parte di valore di u registru d'indici hè una copia di u valore di chjave primaria. Stu approcciu elimina tutti i disadvantages elencati sopra assuciati cù l'almacenamiento di una copia di a parte di valore di u registru primariu. L'unicu tarifu hè chì per uttene u valore da a chjave d'indici, avete bisognu di fà 2 dumande à a basa di dati invece di una. Schematically, u schema di basa di dati risultatu hè a siguenti.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Un altru mudellu per urganizà relazioni trà e tavule hè "chiave redundante". A so essenza hè di aghjunghje attributi supplementari à a chjave, chì ùn sò micca necessarii micca per sorte, ma per ricreà a chjave assuciata.Ci sò esempi veri di u so usu in l'applicazione Mail.ru Cloud, in ogni modu, per evità di immersione in profonda. cuntestu di frameworks iOS specifichi, daraghju un esempiu fittiziu, ma un esempiu più capisci.

I clienti mobili in u cloud anu una pagina chì mostra tutti i fugliali è i cartulare chì l'utilizatore hà spartutu cù altre persone. Siccomu ci sò relativamente pochi di tali schedari, è ci hè assai infurmazione specifica nantu à a publicità assuciata cun elli (à quale l'accessu hè cuncessu, cù quale diritti, etc.), ùn serà micca raziunale per caricallu cù u valore-parte di u record in a tavula principale. Tuttavia, sè vo vulete vede tali schedari offline, allora avete sempre bisognu di almacenà in un locu. Una suluzione naturali hè di creà una tavola separata per questu. In u diagramma sottu, a so chjave hè prefissata cù "P", è u placeholder "propname" pò esse rimpiazzatu cù u valore più specificu "informazioni publica".

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

Tutti i metadati unichi, per u quale a nova tavula hè stata creata, hè spustatu à a parte di valore di u record. À u listessu tempu, ùn vogliu micca duplicà e dati nantu à i schedarii è i cartulare chì sò digià guardati in a tavola principale. Invece, i dati redundante sò aghjuntu à a chjave "P" in a forma di i campi "ID node" è "timestamp". Grazie à elli, pudete custruisce una chjave d'indici, da quale pudete ottene a chjave primaria, da quale, infine, pudete ottene a metadata di u node.

Cunclusioni

Evaluemu i risultati di l'implementazione di LMDB positivamente. Dopu à quessa, u numeru di appiicazioni freezes diminuì da 30%.

U splendore è a miseria di a basa di dati chjave-valore LMDB in l'applicazioni iOS

I risultati di u travagliu fattu anu truvatu una risposta fora di a squadra iOS. Attualmente, una di e principali sezioni "Files" in l'applicazione Android hà ancu cambiatu à utilizà LMDB, è altre parti sò in strada. A lingua C, in quale hè implementatu l'almacenamiento di u valore di chjave, hè stata una bona aiutu per fà inizialmente l'applicazioni vinculate intornu à a piattaforma cross-platform in a lingua C ++. Per una cunnessione perfetta di a libreria C ++ risultante cù u codice di piattaforma in Objective-C è Kotlin, hè stata utilizata un generatore di codice. Djinni da Dropbox, ma hè una altra storia.

Source: www.habr.com

Add a comment