Eri kokoisten JSON-olioiden tallentaminen yhteen suureen muistialueeseen on
klassinen muistinhallinnan haaste, sillC$ se johtaa vC$istC$mC$ttC$
fragmentaatioon (reikC$isyyteen), kun olioita pC$ivitetC$C$n tai poistetaan.
Koska tietokantasi nojaa rinnakkaisiin vC$yliin ja event stream -tyyppiseen
viestintC$C$n, perinteinen dynaaminen muistinvaraus (malloc/free -tyyppinen
ratkaisu vapaiden lohkojen listalla) on todennC$kC6isesti liian hidas ja
monimutkainen.
TC$ssC$ on kaksi parasta arkkitehtuurivaihtoehtoa SharedArrayBufferin (SAB)
hallintaan tC$llaisessa jC$rjestelmC$ssC$:
Vaihtoehto 1: Append-Only Log (LSM-Tree tyyppinen lC$hestymistapa) Suositeltu
TC$mC$ on ylivoimaisesti nopein ja yksinkertaisin tapa toteuttaa lock-free
tietokanta, ja se sopii tC$ydellisesti event-sourced -ajatteluun.
* Toimintaperiaate: Dataa ei koskaan pC$ivitetC$ paikallaan (in-place). Uudet
oliot, pC$ivitykset ja poistot vain lisC$tC$C$n SAB:n loppuun perC$kkC$in.
* Rakenne: Jokainen muistiin kirjoitettu blokki sisC$ltC$C$ metadatan ja
payloadin:
[ ID (4 bytes) | Tila (1 byte: insert/update/tombstone) | Pituus (4 bytes)
| JSON UTF-8 Data ]
* PC$ivitykset ja poistot: Jos oliota ID:llC$ 5 pC$ivitetC$C$n, uusi JSON
kirjoitetaan bufferin hC$nnille. Jos se poistetaan, hC$nnille kirjoitetaan
"Tombstone" (hautakivi) -merkintC$, jolla on pituutena 0.
* Eheys (Lock-free etu): Kirjoittaja (core) vain kasvattaa atomisesti
tail-osoitinta. Lukijat voivat lukea vanhaa dataa turvallisesti
keskeytymC$ttC$, koska vanhan datan pC$C$lle ei koskaan kirjoiteta.
* Compaction (Roskienkeruu): Kun SAB alkaa tC$yttyC$ (esim. 80%
kapasiteetista), ydin kC$ynnistC$C$ taustaprosessin (Compaction). Se luo uuden
SAB:n, lukee vanhasta bufferista vain olioiden uusimmat versiot ja kopioi ne
perC$kkC$in uuteen bufferiin. Lopuksi vanha bufferi vapautetaan ja osoitin
vaihdetaan uuteen.
Vaihtoehto 2: Slab Allocator (Slab-luokat)
Jos haluat vC$lttC$C$ Compaction-operaatiot ja pitC$C$ muistialueen
staattisena, Slab Allocator on perinteinen tapa hallita erikokoista dataa
(kC$ytC6ssC$ mm. Memcachedissa ja kC$yttC6jC$rjestelmien kerneleissC$).
* Toimintaperiaate: SAB jaetaan kiinteC$n kokoisiin "luokkiin" (Slab
Classes). Esimerkiksi:
* Luokka 1: 64 tavun lohkot
* Luokka 2: 128 tavun lohkot
* Luokka 3: 512 tavun lohkot
* Luokka 4: 2048 tavun lohkot
* Allokointi: Kun sisC$C$n tulee 100 tavun JSON-olio, core laskee sen koon ja
sijoittaa sen Luokkaan 2 (128 tavua).
* Vapautus: Kun olio poistetaan tai pC$ivitetC$C$n suurempaan, sen
alkuperC$inen 128 tavun slotti merkitC$C$n vapaaksi (esim. bitmaskilla tai
free-listillC$), ja se voidaan kC$yttC$C$ vC$littC6mC$sti uudelleen
seuraavalle alle 128 tavun oliolle.
* Plussat ja miinukset: TC$mC$ eliminoi ulkoisen fragmentaation (muisti ei
mene reikC$iseksi niin, ettei isoja lohkoja enC$C$ lC6ydy), mutta aiheuttaa
sisC$istC$ fragmentaatiota (100 tavun olio 128 tavun slotissa jC$ttC$C$ 28
tavua hukkaan).
Miten nC$mC$ yhdistetC$C$n vC$yliin ja indeksointiin?
Vaikka data makaisi SAB:ssa jommalla kummalla logiikalla, tarvitset tavan
lC6ytC$C$ sen nopeasti. Et voi iteroida koko SAB:ia lC$pi joka kerta, kun haet
yhden olion.
TC$mC$ vaatii erillisen indeksin.
Koska haluat kaiken toimivan ilman lukkoja ja olevan synkronoitavissa, indeksi
voi itsessC$C$n olla toinen, optimoitu muistirakenne (esim. C-tyylinen Hash
Table toteutettuna toisen SharedArrayBufferin pC$C$lle).
Kun IN-vC$ylC$C$n tulee komento: {"cmd": "GET", "id": "user_123"}, prosessi
toimii nC$in:
* Core hashaa "user_123" ja katsoo indeksistC$ (SAB 1) sen muistiosoitteen/off
setin.
* Core hyppC$C$ data-SAB:iin (SAB 2) kyseiseen offsettiin.
* Core lukee pituuden, poimii tavut, dekoodaa UTF-8 JSONiksi ja puskee sen
OUT-vC$ylC$C$n.
Append-only -malli soveltuu parhaiten kuvailemaasi ympC$ristC6C6n, koska sen
tuottama datavirta (binC$C$rimuotoinen tapahtumaloki) on sellaisenaan valmis
synkronoitavaksi "mihin tahansa streamiin" (levylle, verkkoon, toiseen
prosessiin) tC$ydellisesti jC$rjestyksessC$.