
Neko vrijeme prije suočili smo se s problemom čišćenja tuple-a u prostorima . Čišćenje se nije smjelo pokretati tek kada je tarantool već imao nedostatak memorije, već unaprijed i s određenom periodicnošću. Za tu je zadaću u tarantoolu postojao modul napisan u Lua-u, pod nazivom . Nakon kratkog korištenja ovog modula shvatili smo da nam ne odgovara: tijekom konstantnih čišćenja velikih količina podataka Lua se zaustavljao zbog GC. Stoga smo razmišljali o razvoju vlastitog capped expirationd modula, nadajući se da će kod napisan na izvornom programskom jeziku najbolje riješiti naše probleme.
Dobar primjer nam je poslužio modul tarantool pod nazivom . Korišteni pristup se temelji na tome da se u prostoru stvara posebno polje u kojem se navodi vrijeme trajanja tuple-a, drugim riječima, ttl. Modul u pozadini skenira prostor, uspoređuje ttl s trenutnim vremenom i donosi odluku hoće li obrisati tuple ili ne. Kod modula memcached je jednostavan i elegantan, ali previše općenit. Prvo, ne uzima u obzir tip indeksa prema kojem se vrši obilazak i brisanje. Drugo, pri svakom prolazu skeniraju se svi tuple-i, čija količina može biti prilično velika. Iako je u modulu expirationd prvi problem riješen (tree-indeks je izdvojen u posebnu klasu), drugi problem i dalje nije dobio nikakvu pažnju. Ova tri aspekta su odlučila izbor u korist pisanja vlastitog koda.
Opis
U dokumentaciji za tarantool postoji jako dobar o tome kako pisati vlastite pohranjene procedure na jeziku C. Prvo preporučujem da se upoznate s njim kako biste razumjeli one unose s naredbama i kodom koji će se pojaviti u nastavku. Također obratite pozornost na na objekte koji su dostupni prilikom pisanja vlastitog capped modula, a to su , , i .
Hajde da počnemo s početka i pogledamo kako izgleda capped expirationd modul izvana:
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)Radi jednostavnosti pokrećemo tarantool u direktoriju u kojem se nalazi naša biblioteka libcapped-expirationd.so. Iz biblioteke se izvoze dvije funkcije: start i kill. Prvo, potrebno je ove funkcije učiniti dostupnima iz Lua pomoću box.schema.func.create i box.schema.user.grant. Zatim kreiramo prostor, čiji će tupel imati tri polja: prvo — jedinstveni identifikator, drugo — e-poštu, treće — vrijeme trajanja tupela. Na prvo polje gradimo tree-indeks i nazivamo ga primary. Nakon toga dobivamo objekt veze s našom nativnom bibliotekom.
Nakon pripremnih radova pokrećemo funkciju 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})Ovaj primjer će raditi prilikom skeniranja identično kao i modul expirationd, koji je napisan na Lua. Prvi argument funkciji start predaje se jedinstveno ime zadatka. Drugi — identifikator prostora. Treći — jedinstveni indeks, po kojem će se izvršavati brisanje tupela. Četvrti — indeks, po kojem će se izvršavati obilazak tupela. Peti — broj polja tupela s vremenom trajanja (numeracija počinje od 1, a ne 0!). Šesti i sedmi — postavke skeniranja. 1024 — maksimalan broj tupela koji se pregledava unutar jedne transakcije. 3600 — vrijeme potpunog skeniranja u sekundama.
Obratite pažnju da se za obilazak i brisanje u primjeru koristi isti indeks. Ako je to tree-indeks, obilazak se obavlja od manjeg ključa prema većem. Ako je neki drugi, poput hash-indeksa, obilazak se obično vrši u proizvoljnom redu. U jednom skeniranju pregledavaju se svi tupeli prostora.
Učimo umetanje nekoliko tupela u prostor s vremenom trajanja od 60 sekundi:
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}Provjerimo je li umetanje prošlo uspješno:
tarantool> box.space.tester.index.primary:select()
---
- - [0, 'user0@tarantool.io', 1576418976]
- [1, 'user1@tarantool.io', 1576418976]
- [2, 'user2@tarantool.io', 1576418976]
...Ponovimo select nakon 60+ sekundi (računajući od početka umetanja prvog tupela) i vidjet ćemo da je modul capped expirationd već odradio:
tarantool> box.space.tester.index.primary:select()
---
- []
...Zaustavimo zadatak:
capped_connection:call('libcapped-expirationd.kill', {'non-indexed'})Pogledajmo drugi primjer kada se koristi posebni indeks za pretraživanje:
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)Ovdje je sve isto kao u prvom primjeru, osim sitne razlike. Na treće polje gradimo tree-indeks i nazivamo ga exp. Ovaj indeks ne mora biti jedinstven, za razliku od indeksa pod nazivom primary. Pretraživanje će se vršiti prema exp indeksu, dok će brisanje ići prema primary. Zapamtili smo da se ranije i jedno i drugo radilo samo s upotrebom primary indeksa.
Nakon pripremnih radova pokrećemo funkciju start s novim argumentima:
capped_connection:call('libcapped-expirationd.start', {'indexed', box.space.tester.id, box.space.tester.index.primary, box.space.tester.index.exp, 3, 1024, 3600})Ponovno ćemo umetnuti nekoliko tuple-a u prostor s vremenom života od 60 sekundi:
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}Nakon 30 sekundi, dodajemo još nekoliko tuple-a:
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}Provjerimo je li umetanje prošlo uspješno:
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]
...Ponovimo select nakon 60+ sekundi (računajući od početka umetanja prvog tupela) i vidjet ćemo da je modul capped expirationd već odradio:
tarantool> box.space.tester.index.primary:select()
---
- - [3, 'user3@tarantool.io', 1576421287]
- [4, 'user4@tarantool.io', 1576421287]
- [5, 'user5@tarantool.io', 1576421287]
...U prostoru su ostali tuple-i kojima je preostalo još otprilike 30 sekundi. Štoviše, skeniranje je stalo prilikom prelaska s tuple-a s identifikatorom 2 i vremenom života 1576421257 na tuple s identifikatorom 3 i vremenom života 1576421287. Tuple-i s vremenom života 1576421287 i više nisu pregledani zahvaljujući uređenosti ključeva exp indeksa. To je ta ušteda koju smo željeli postići na početku.
Zaustavimo zadatak:
capped_connection:call('libcapped-expirationd.kill', {'indexed'})Implementacija
Najbolje o svim osobitostima projekta uvijek govori njegov izvorni ! U okviru objave zadržat ćemo se samo na najvažnijim trenutcima, odnosno na algoritmima pretraživanja prostora.
Argumenti koje prosljeđujemo metodi start čuvaju se u strukturi pod nazivom 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;
};Atribut name je ime zadatka. Atribut space_id je identifikator prostora. Atribut rm_index_id je identifikator jedinstvenog indeksa prema kojem će se brisati tuplovi. Atribut it_index_id je identifikator indeksa prema kojem će se prolaziti kroz tuplove. Atribut it_index_type je vrsta indeksa prema kojem će se prolaziti kroz tuplove. Atribut field_no je broj polja tuplova s vremenom trajanja. Atribut scan_size je maksimalan broj tuplova koji se pregledava unutar jedne transakcije. Atribut scan_time je vrijeme potpunog skeniranja u sekundama.
Nećemo razmatrati parsiranje argumenata. To je mukotrpan, ali jednostavan posao, u kojem će vam pomoći knjižnica . Teškoće mogu nastati samo s indeksima koji se prosljeđuju iz Lua u obliku složene strukture podataka s tipom mp_map, a ne uz pomoć jednostavnih tipova mp_bool, mp_double, mp_int, mp_uint i mp_array. No, nije potrebno parsirati cijeli indeks. Dovoljno je samo provjeriti njegovu jedinstvenost, izračunati tip i izvući identifikator.
Nabrojat ćemo prototype svih funkcija koje se koriste za parsiranje:
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 sada prelazimo na najvažnije — logiku prolaska kroz prostor i brisanja tuple-a. Svaki blok tuple-a veličine ne veće od scan_size pretražuje se i mijenja pod jednom transakcijom. U slučaju uspjeha, ova transakcija se potvrđuje, u slučaju greške — poništava. Posljednji argument funkciji expirationd_iterate prosljeđuje se pokazivač na iterator s kojeg započinje ili se nastavlja skeniranje. Ovaj iterator se inkrementira dok ne dođe do greške, dok ne istekne prostor ili dok ne dođe mogućnost da se proces unaprijed zaustavi. Funkcija expirationd_expired provjerava vijek trajanja tuple-a, expirationd_delete — briše tuple, expirationd_breakable — provjerava trebamo li ići dalje.
Kod funkcije 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;
}Kod funkcije 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;
}Kod funkcije 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);
}Kod funkcije 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;
}Aplikacija
Izvorni kod možete pogledati na !
Izvor: habr.com
