Pisanje svojega modula z omejenim rokom trajanja za tarantool

Pisanje svojega modula z omejenim rokom trajanja za tarantool

Nekaj časa nazaj se je pred nami pojavila težava čiščenja tuples v spaces. tarantool. Čiščenje je bilo potrebno izvajati, preden je tarantool že izgubil pomnilnik, temveč vnaprej in z določeno periodičnostjo. Za to nalogo ima tarantool modul, napisan v Luu, po imenu expirationd. Po kratkotrajni uporabi tega modula smo ugotovili, da nam ne ustreza: med stalnimi čiščenji velikih količin podatkov se je Lua ustavila v GC. Zato smo razmišljali o razvoju svojega capped expirationd modula, v upanju, da bo koda, napisana v naravnem programskem jeziku, najbolje rešila naše naloge.

Dober primer nam je služil modul tarantool po imenu memcached. Uporabljeni pristop temelji na tem, da se v spaceu ustvari posebno področje, v katerem je navedeno časovno trajanje tuple, z drugimi besedami, ttl. Modul v ozadju skenira space, primerja ttl s trenutnim časom in sprejema odločitev, ali naj tuple izbriše ali ne. Koda modula memcached je preprosta in elegantna, vendar preveč splošna. Prvič, ne upošteva vrste indeksa, po katerem se izvaja pregled in brisanje. Drugič, pri vsakem prehodu se skenirajo vse tuples, katerih število je lahko precej veliko. In če je bila v modulu expirationd prva težava rešena (tree-index je bil dodeljen v samostojno razred), pa druga še vedno ni bila stvar pozornosti. Ti trije točki so določili izbiro v prid pisanja svoje kode.

Opis

V dokumentaciji tarantool je zelo dober tutorijal o tem, kako pisati svoje shranjene procedure v jeziku C. Najprej predlagam, da se z njim seznanite, da boste razumeli vstave s ukazi in kodo, ki se bodo pojavljale spodaj. Prav tako je vredno pozornost posvetiti referenci na objekte, ki so na voljo pri pisanju svojega lastnega capped modula, natančneje na box, fiber, index in txn.

Poglejmo na začetku, kako izgleda capped expirationd modul od zunaj:

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)

Za lažji zagon zaženemo tarantool v imeniku, kjer se nahaja naša knjižnica libcapped-expirationd.so. Iz knjižnice se izvozita dve funkciji: start in kill. Na začetku moramo te funkcije narediti dostopne iz Lua z uporabo box.schema.func.create in box.schema.user.grant. Nato ustvarimo prostor, katerega tuplji bodo vsebovali tri polja: prvo - edinstven identifikator, drugo - elektronski naslov, tretje - časovno dobo tupla. Čez prvo polje zgradimo tree-indeks in ga imenujemo primary. Nato pridobimo objekt povezave z našo nativno knjižnico.

Po pripravljalnih delih zaženemo funkcijo 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})

Ta primer bo deloval pri skeniranju natanko tako, kot modul expirationd, napisan v Luu. Prvi argument funkciji start je edinstveno ime naloge. Drugi je identifikator prostora. Tretji je edinstven indeks, po katerem bo izvedeno brisanje tupljev. Četrti je indeks, po katerem bo potekalo obhod tupljev. Peti je številka polja tupla s časovno dobo (numeracija se začne pri 1, ne pri 0!). Šesti in sedmi sta nastavitvi skeniranja. 1024 je največje število tupljev, ki se pregledujejo v okviru ene transakcije. 3600 je čas trajanja skeniranja v sekundah.

Upoštevajte, da se pri obhodu in brisanju v primeru uporablja isti indeks. Če gre za tree-indeks, se obhod vrši od manjšega ključa do večjega. Če gre za kakšen drug, na primer, hash-indeks, se obhod običajno vrši v poljubnem vrstnem redu. Pri enem skeniranju se pregledajo vsi tuplji prostora.

Vstavimo v prostor več tupljev s časovno dobo 60 sekund:

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}

Preverimo, da je vstavljanje uspelo:

tarantool> box.space.tester.index.primary:select()
---
- - [0, 'user0@tarantool.io', 1576418976]
  - [1, 'user1@tarantool.io', 1576418976]
  - [2, 'user2@tarantool.io', 1576418976]
...

Ponovimo select po 60+ sekundah (štejemo od začetka vstavljanja prvega tupla) in videli bomo, da je modul capped expirationd že deloval:

tarantool> box.space.tester.index.primary:select()
---
  - []
...

Ustavimo nalogo:

capped_connection:call('libcapped-expirationd.kill', {'non-indexed'})

Poglejmo drug primer, kjer se uporablja ločen indeks za iskanje:

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)

Tukaj je vse enako kot v prvem primeru, z majhno razliko. Na tretjem polju zgradimo tree indeks in ga poimenujemo exp. Ta indeks ne sme biti edinstven, za razliko od indeksa, imenovanega primary. Iskanje bo potekalo preko exp indeksa, medtem ko bo brisanje potekalo preko primary. Spomnimo se, da sta bila prej oba postopka izvedena le z uporabo primary indeksa.

Po pripravljalnih delih zagnamo funkcijo start z novimi argumenti:

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 bomo v prostor inserirali več tuple z življenjsko dobo 60 sekund:

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}

Čez 30 sekund bomo podobno dodali še nekaj 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}

Preverimo, da je vstavljanje uspelo:

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 po 60+ sekundah (štejemo od začetka vstavljanja prvega tupla) in videli bomo, da je modul capped expirationd že deloval:

tarantool> box.space.tester.index.primary:select()
---
- - [3, 'user3@tarantool.io', 1576421287]
  - [4, 'user4@tarantool.io', 1576421287]
  - [5, 'user5@tarantool.io', 1576421287]
...

V prostoru so ostali tuple, ki bodo živeli še približno 30 sekund. Poleg tega se je skeniranje ustavilo pri prehodu od tuple z identifikatorjem 2 in življenjsko dobo 1576421257 do tuple z identifikatorjem 3 in življenjsko dobo 1576421287. Tuple z življenjsko dobo 1576421287 in več niso bile pregledane zaradi urejenosti ključev exp indeksa. To je prihranek, ki smo ga želeli doseči na začetku.

Ustavimo nalogo:

capped_connection:call('libcapped-expirationd.kill', {'indexed'})

Implementacija

Najbolje o vseh posebnostih projekta vedno pove njegovo izhodišče kodo! V okviru publikacije se bomo ustavili le na najpomembnejših točkah, in sicer na algoritmih iskanja v prostoru.

Argumenti, ki jih posredujemo metodi start, se shranijo v strukturi, imenovani 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 — to je ime zadatka. Atribut space_id — identifikator prostora. Atribut rm_index_id — identifikator jedinstvenog indeksa, prema kojem će se vršiti brisanje tuplji. Atribut it_index_id — identifikator indeksa, prema kojem će se vršiti pretraga tuplji. Atribut it_index_type — tip indeksa, prema kojem će se vršiti pretraga tuplji. Atribut field_no — broj polja tuplje s vremenom života. Atribut scan_size — maksimalan broj tuplji koje se pregledaju u okviru jedne transakcije. Atribut scan_time — vreme potpunog skeniranja u sekundama.

Parsiranje argumenata razmatrati nećemo. To je mukotrpan, ali ne i složen posao, u kojem će vam pomoći biblioteka msgpuck. Teškoće mogu nastati samo s indeksima, koji se šalju iz Lua u obliku složene strukture podataka tipa mp_map, a ne s pomoć jednostavnih tipova mp_bool, mp_double, mp_int, mp_uint i mp_array. No, ceo indeks ne treba parsirati. Dovoljno je samo proveriti njegovu jedinstvenost, izračunati tip i izvući identifikator.

Nabrojaćemo prototipove 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);

Sedaj pa preidimo k najpomembnejšemu - logiki prečkanja prostora in brisanja tupljev. Vsak blok tupljev, velikosti do scan_size, se obravnava in spreminja v okviru enega transakcijskega bloka. V primeru uspeha se ta transakcija izvrši, v primeru napake pa se vrne nazaj. Zadnji argument v funkciji expirationd_iterate preda kazalec na iterator, s katerega se prične ali nadaljuje skeniranje. Ta iterator se povečuje v notranjosti, dokler ne pride do napake, ne zmanjka prostora ali ne nastane možnost, da se postopek predčasno ustavi. Funkcija expirationd_expired preverja življenjsko dobo tuplja, expirationd_delete pa izbriše tuplj, expirationd_breakable pa preverja, ali moramo iti naprej.

Koda 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;
}

Koda 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;
}

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

Koda 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

Izvorno kodo si lahko ogledate na tukaj!

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster