
Pred nejakým časom sme čelili problému s čistením tupiek v priestoroch . Čistenie sme potrebovali spúšťať nie vtedy, keď u tarantool už dochádzala pamäť, ale vopred a s určitými intervalmi. Na tento účel má tarantool modul napísaný v Luá, ktorý sa volá . Po krátkom používaní tohto modulu sme zistili, že nám nevyhovuje: pri trvalých čistkách veľkých objemov dát Luá zamieňal pri GC. Preto sme začali uvažovať o vývoji vlastného capped expirationd modulu, dúfajúc, že kód napísaný v nativnom programovacom jazyku vyrieši naše úlohy najlepším spôsobom.
Dobrým príkladom bol modul tarantool s názvom . Používaný prístup spočíva v tom, že v priestore sa vytvára samostatné pole, v ktorom je uvedený čas životnosti tupky, inými slovami, ttl. Modul na pozadí skenuje priestor, porovnáva ttl s aktuálnym časom a rozhoduje o tom, či sa tupka odstráni alebo nie. Kód modulu memcached je jednoduchý a elegantný, ale príliš všeobecný. Po prvé, nezohľadňuje typ indexu, podľa ktorého prebieha prehľadávanie a odstraňovanie. Po druhé, na každom prechode sa skenujú všetky tupky, ktorých počet môže byť pomerne veľký. A ak v module expirationd bol prvý problém vyriešený (tree-index vyčlenený do samostatnej triedy), tak druhému sa naďalej nevenovala žiadna pozornosť. Tieto tri body predurčili voľbu v prospech napísania vlastného kódu.
Popis
V dokumentácii k tarantoolu je veľmi dobrý o tom, ako písať vlastné uložené procedúry v jazyku C. V prvom rade odporúčam so ním oboznámiť, aby ste rozumeli tým vložkám s príkazmi a kódom, ktoré sa objavia nižšie. Taktiež stojí za to venovať pozornosť k objektom, ktoré sú dostupné pri písaní vlastného capped modulu, konkrétne k , , a .
Poďme začať z diaľky a pozrieme sa na to, ako vyzerá capped expirationd modul zvonku:
fiber = require('fiber')
net_box = require('net.box')
box.cfg{listen = 3300}
box.schema.func.create('libcapped-expirationd.start', {language = 'C'})
box.schema.user.grant('guest', 'execute', 'function', 'libcapped-expirationd.start')
box.schema.func.create('libcapped-expirationd.kill', {language = 'C'})
box.schema.user.grant('guest', 'execute', 'function', 'libcapped-expirationd.kill')
box.schema.space.create('tester')
box.space.tester:create_index('primary', {unique = true, parts = {1, 'unsigned'}})
capped_connection = net_box:new(3300)Pre jednoduchosť spustíme tarantool v priečinku, kde sa nachádza naša knižnica libcapped-expirationd.so. Z knižnice sú exportované dve funkcie: start a kill. Najprv je potrebné sprístupniť tieto funkcie z Lua pomocou box.schema.func.create a box.schema.user.grant. Potom vytvoríme space, ktorého tuple budú obsahovať tri polia: prvé — unikátny identifikátor, druhé — e-mail, tretie — životnosť tuple. Na prvé pole vytvoríme tree-index a nazveme ho primary. Následne získame objekt pripojenia k našej natívnej knižnici.
Po prípravných prácach spustíme funkciu start:
capped_connection:call('libcapped-expirationd.start', {'non-indexed', box.space.tester.id, box.space.tester.index.primary, box.space.tester.index.primary, 3, 1024, 3600})Tento príklad bude fungovať pri skenovaní presne tak, ako aj modul expirationd, ktorý je napísaný v Lua. Prvým argumentom do funkcie start sa predáva unikátne meno úlohy. Druhým — identifikátor space. Tretím — unikátny index, podľa ktorého sa budú mazať tuple. Štvrtým — index, podľa ktorého sa budú prechádzať tuple. Piati — číslo poľa tuple so životnosťou (číslovanie začína od 1, nie od 0!). Šiestym a siedmym — nastavenia skenovania. 1024 — je maximálny počet tuple, ktorý sa prezerá v rámci jednej transakcie. 3600 — čas celej skenovania v sekundách.
Všimnite si, že na prechod a mazanie v príklade sa používa jeden a ten istý index. Ak ide o tree-index, prechod sa uskutočňuje od menšieho kľúča k väčšiemu. Ak je to nejaký iný, napríklad hash-index, prechod sa zvyčajne uskutočňuje v náhodnom poradí. Počas jedného skenovania sa prezerajú všetky tuple v space.
Poďme vložiť do space niekoľko tuple s životnosťou 60 sekúnd:
box.space.tester:insert{0, 'user0@tarantool.io', math.floor(fiber.time()) + 60}
box.space.tester:insert{1, 'user1@tarantool.io', math.floor(fiber.time()) + 60}
box.space.tester:insert{2, 'user2@tarantool.io', math.floor(fiber.time()) + 60}Skontrolujme, či bola vloženie úspešné:
tarantool> box.space.tester.index.primary:select()
---
- - [0, 'user0@tarantool.io', 1576418976]
- [1, 'user1@tarantool.io', 1576418976]
- [2, 'user2@tarantool.io', 1576418976]
...Opakujme select po 60+ sekundách (pocítajúc od začiatku vloženia prvej tuple) a uvidíme, že modul capped expirationd už fungoval:
tarantool> box.space.tester.index.primary:select()
---
- []
...Zastavíme úlohu:
capped_connection:call('libcapped-expirationd.kill', {'non-indexed'})Poďme sa pozrieť na druhý príklad, kde sa na prehliadanie používa samostatný index:
fiber = require('fiber')
net_box = require('net.box')
box.cfg{listen = 3300}
box.schema.func.create('libcapped-expirationd.start', {language = 'C'})
box.schema.user.grant('guest', 'execute', 'function', 'libcapped-expirationd.start')
box.schema.func.create('libcapped-expirationd.kill', {language = 'C'})
box.schema.user.grant('guest', 'execute', 'function', 'libcapped-expirationd.kill')
box.schema.space.create('tester')
box.space.tester:create_index('primary', {unique = true, parts = {1, 'unsigned'}})
box.space.tester:create_index('exp', {unique = false, parts = {3, 'unsigned'}})
capped_connection = net_box:new(3300)Tu je všetko rovnaké ako v prvom príklade, s malým rozdielom. Na treťom poli budujeme tree-index a nazývame ho exp. Tento index nemusí byť jedinečný, na rozdiel od indexu nazvaného primary. Prehliadanie sa bude realizovať cez exp index, zatiaľ čo vymazanie cez primary. Pamätáme si, že predtým sa to robilo iba s použitím primary indexu.
Po prípravných prácach spúšťame funkciu start s novými argumentmi:
capped_connection:call('libcapped-expirationd.start', {'indexed', box.space.tester.id, box.space.tester.index.primary, box.space.tester.index.exp, 3, 1024, 3600})Opäť vložíme do priestoru niekoľko tuple s časom života 60 sekúnd:
box.space.tester:insert{0, 'user0@tarantool.io', math.floor(fiber.time()) + 60}
box.space.tester:insert{1, 'user1@tarantool.io', math.floor(fiber.time()) + 60}
box.space.tester:insert{2, 'user2@tarantool.io', math.floor(fiber.time()) + 60}O 30 sekúnd pridáme na analógiu ďalšie tuple:
box.space.tester:insert{3, 'user3@tarantool.io', math.floor(fiber.time()) + 60}
box.space.tester:insert{4, 'user4@tarantool.io', math.floor(fiber.time()) + 60}
box.space.tester:insert{5, 'user5@tarantool.io', math.floor(fiber.time()) + 60}Skontrolujme, či bola vloženie úspešné:
tarantool> box.space.tester.index.primary:select()
---
- - [0, 'user0@tarantool.io', 1576421257]
- [1, 'user1@tarantool.io', 1576421257]
- [2, 'user2@tarantool.io', 1576421257]
- [3, 'user3@tarantool.io', 1576421287]
- [4, 'user4@tarantool.io', 1576421287]
- [5, 'user5@tarantool.io', 1576421287]
...Opakujme select po 60+ sekundách (pocítajúc od začiatku vloženia prvej tuple) a uvidíme, že modul capped expirationd už fungoval:
tarantool> box.space.tester.index.primary:select()
---
- - [3, 'user3@tarantool.io', 1576421287]
- [4, 'user4@tarantool.io', 1576421287]
- [5, 'user5@tarantool.io', 1576421287]
...V priestore zostali tuple, ktorým ostáva žiť približne 30 sekúnd. Navyše, skenovanie sa zastavilo pri prechode od tuple s identifikátorom 2 a časom života 1576421257 k tuple s identifikátorom 3 a časom života 1576421287. Tuple s časom života 1576421287 a vyšším neboli prehľadané vďaka usporiadanosti kľúčov exp indexu. Toto je tá úspora, ktorú sme chceli dosiahnuť na samom začiatku.
Zastavíme úlohu:
capped_connection:call('libcapped-expirationd.kill', {'indexed'})Implementácia
Najlepšie o všetkých špecifikách projektu vždy porozpráva jeho pôvodný ! V rámci publikácie sa zastavíme len na najdôležitejších momentoch, a to na algoritmoch prehliadania priestoru.
Argumenty, ktoré odovzdávame metóde start, sa ukladajú do štruktúry nazvanej expirationd_task:
struct expirationd_task
{
char name[256];
uint32_t space_id;
uint32_t rm_index_id;
uint32_t it_index_id;
uint32_t it_index_type;
uint32_t field_no;
uint32_t scan_size;
uint32_t scan_time;
};Atribút name je názov úlohy. Atribút space_id je identifikátor priestoru. Atribút rm_index_id je identifikátor unikátneho indexu, na základe ktorého sa budú odstraňovať tuple. Atribút it_index_id je identifikátor indexu, na základe ktorého sa budú prehľadávať tuple. Atribút it_index_type je typ indexu, na základe ktorého sa budú prehľadávať tuple. Atribút field_no je číslo poľa tuple s dobou životnosti. Atribút scan_size je maximálny počet tuple, ktorý sa prezerá v rámci jednej transakcie. Atribút scan_time je čas úplného skenovania v sekundách.
Parsisovanie argumentov nebudeme rozoberať. Ide o prácu, ktorá si vyžaduje pozornosť, ale nie je zložitá, a môžete na ňu využiť knižnicu . Ťažkosti môžu nastať iba s indexami, ktoré sú prenášané z Lua ako zložitá dátová štruktúra s typom mp_map, a nie pomocou jednoduchých typov mp_bool, mp_double, mp_int, mp_uint a mp_array. Ale celú indexáciu nie je potrebné parsovať. Stačí skontrolovať jej jedinečnosť, vypočítať typ a extrahovať identifikátor.
Vymenujme prototypy všetkých funkcií, ktoré sa používajú na parsovanie:
bool expirationd_parse_name(struct expirationd_task *task, const char **pos);
bool expirationd_parse_space_id(struct expirationd_task *task, const char **pos);
bool expirationd_parse_rm_index_id(struct expirationd_task *task, const char **pos);
bool expirationd_parse_rm_index_unique(struct expirationd_task *task, const char **pos);
bool expirationd_parse_rm_index(struct expirationd_task *task, const char **pos);
bool expirationd_parse_it_index_id(struct expirationd_task *task, const char **pos);
bool expirationd_parse_it_index_type(struct expirationd_task *task, const char **pos);
bool expirationd_parse_it_index(struct expirationd_task *task, const char **pos);
bool expirationd_parse_field_no(struct expirationd_task *task, const char **pos);
bool expirationd_parse_scan_size(struct expirationd_task *task, const char **pos);
bool expirationd_parse_scan_time(struct expirationd_task *task, const char **pos);A teraz prejdime k najdôležitejšiemu — logike prechodu cez priestor a odstránenia kociek. Každý blok kociek s maximálnou veľkosťou scan_size sa prehliada a upravuje v rámci jednej transakcie. V prípade úspechu sa táto transakcia potvrdi, ak sa vyskytne chyba, vráti sa späť. Posledným argumentom v funkcii expirationd_iterate je ukazovateľ na iterátor, s ktorého začína alebo pokračuje skenovanie. Tento iterátor sa inkrementuje dovtedy, kým nenastane chyba, neskončí priestor alebo sa neponúkne možnosť proces predčasne zastaviť. Funkcia expirationd_expired kontroluje čas trvania kociek, expirationd_delete — odstraňuje kocku, expirationd_breakable — kontroluje, či sa máme posunúť ďalej.
Kód funkcie expirationd_iterate:
static bool
expirationd_iterate(struct expirationd_task *task, box_iterator_t **iterp)
{
box_iterator_t *iter = *iterp;
box_txn_begin();
for (uint32_t i = 0; i scan_size; ++i) {
box_tuple_t *tuple = NULL;
if (box_iterator_next(iter, &tuple) < 0) {
box_iterator_free(iter);
*iterp = NULL;
box_txn_rollback();
return false;
}
if (!tuple) {
box_iterator_free(iter);
*iterp = NULL;
box_txn_commit();
return true;
}
if (expirationd_expired(task, tuple))
expirationd_delete(task, tuple);
else if (expirationd_breakable(task))
break;
}
box_txn_commit();
return true;
}Kód funkcie expirationd_expired:
static bool
expirationd_expired(struct expirationd_task *task, box_tuple_t *tuple)
{
const char *buf = box_tuple_field(tuple, task->field_no - 1);
if (!buf || mp_typeof(*buf) != MP_UINT)
return false;
uint64_t val = mp_decode_uint(&buf);
if (val > fiber_time64() / 1000000)
return false;
return true;
}Kód funkcie expirationd_delete:
static void
expirationd_delete(struct expirationd_task *task, box_tuple_t *tuple)
{
uint32_t len;
const char *str = box_tuple_extract_key(tuple, task->space_id, task->rm_index_id, &len);
box_delete(task->space_id, task->rm_index_id, str, str + len, NULL);
}Kód funkcie expirationd_breakable:
static bool
expirationd_breakable(struct expirationd_task *task)
{
return task->it_index_id != task->rm_index_id && task->it_index_type == ITER_GT;
}Aplikácia
Zdrojový kód si môžete pozrieť na !
Zdroj: habr.com
