Skriver vores capped expirationd modul til tarantool

Skriver vores capped expirationd modul til tarantool

For nogle tid siden stod vi over for problemet med at rense tuples i spaces. tarantool. Rensningen skulle ikke startes, nÄr tarantool allerede var lÞbet tÞr for hukommelse, men i stedet pÄ forhÄnd og med bestemte intervaller. Til denne opgave findes der i tarantool et modul, skrevet i Lua, kaldet expirationd. Efter kort tids brug af dette modul indsÄ vi, at det ikke var egnet til vores behov: ved udfÞrelse af regelmÊssige rensninger af store datamÊngder, hang Lua i GC. Derfor overvejede vi at udvikle vores eget capped expirationd modul, i hÄbet om at kode skrevet i et nativesprog ville lÞse vores opgaver pÄ bedste vis.

Et godt eksempel for os var modulen fra tarantool kaldet memcached. Den anvendte tilgang bygger pÄ, at der i spacet oprettes et separat felt, hvor levetiden for tuplet angives, med andre ord, ttl. Modulet scanner i baggrunden spacet, sammenligner ttl med den aktuelle tid og trÊffer beslutning om, hvorvidt tuplet skal slettes eller ej. Koden til memcached-modulet er enkel og elegant, men for generelt. For det fÞrste tager det ikke hÞjde for typen af indeks, som bruges til gennemgang og sletning. For det andet bliver alle tuples scannet ved hver gennemgang, hvor antallet kan vÊre ret stort. Og mens det i expirationd-modulet blev lÞst, at det fÞrste problem (tree-indekset er udskilt i en separat klasse), blev der stadig ikke givet opmÊrksomhed til det andet. Disse tre punkter udgjorde grundlaget for beslutningen om at skrive vores egen kode.

Description

I dokumentationen til tarantool findes der en meget god tutorial om hvordan man skriver sine egne lagrede procedurer i sprog C. Jeg anbefaler fÞrst at sÊtte sig ind i det, sÄ man forstÄr de indstik med kommandoer og kode, der vil blive prÊsenteret nedenfor. Det er ogsÄ vÊrd at lÊgge mÊrke til referencen til objekterne, der er tilgÊngelige ved skrivning af sin egen capped modul, nemlig til box, fiber, index og txn.

Lad os starte fra starten og se pÄ, hvordan capped expirationd-modulet ser ud udenfor:

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)

For simplicity, we launch Tarantool in the directory where our library libcapped-expirationd.so is located. The library exports two functions: start and kill. First, we need to make these functions accessible from Lua using box.schema.func.create and box.schema.user.grant. Then, we create a space, the tuples of which will contain only three fields: the first is a unique identifier, the second is an email address, and the third is the lifetime of the tuple. We build a tree index on the first field and name it primary. Next, we obtain the connection object to our native library.

After the preparatory work, we launch the start function:

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

This example will work when scanning just like the expirationd module written in Lua. The first argument passed to the start function is a unique task name. The second is the identifier of the space. The third is the unique index by which the tuples will be removed. The fourth is the index for iterating over the tuples. The fifth is the number of the field of the tuple with the lifetime (numbering starts from 1, not 0!). The sixth and seventh are scanning settings. 1024 is the maximum number of tuples that are viewed within one transaction. 3600 is the total scanning time in seconds.

Note that in the example, the same index is used for iteration and deletion. If it is a tree index, iteration is performed from the smaller key to the larger one. If it is some other, for example, a hash index, then iteration usually occurs in arbitrary order. During one scan, all tuples of the space are examined.

Let’s insert several tuples with a lifetime of 60 seconds into the space:

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}

Let’s check that the insertion was successful:

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

Let’s repeat the select after 60+ seconds (counting from the start of the insertion of the first tuple) and we will see that the capped expirationd module has already worked:

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

We will stop the task:

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

Lad os tage et kig pÄ det andet eksempel, hvor der bruges et separat indeks til traversal:

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)

Her er alt det samme som i det fĂžrste eksempel, med undtagelse af en lille detalje. Vi opretter et tree-indeks over det tredje felt og kalder det exp. Dette indeks behĂžver ikke at vĂŠre unikt, i modsĂŠtning til indekset kaldet primary. Traverseringen vil finde sted via exp-indekset, mens sletningen vil ske via primary. Vi husker, at tidligere blev begge dele kun udfĂžrt med brug af primary-indekset.

Efter forberedelserne starter vi funktionen start med nye argumenter:

capped_connection:call('libcapped-expirationd.start', {'indexed', box.space.tester.id, box.space.tester.index.primary, box.space.tester.index.exp, 3, 1024, 3600})

Lad os igen indsÊtte flere tupler i rummet med en livstid pÄ 60 sekunder:

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}

Efter 30 sekunder tilfĂžjer vi analogt endnu et par tupler:

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}

Let’s check that the insertion was successful:

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]
...

Let’s repeat the select after 60+ seconds (counting from the start of the insertion of the first tuple) and we will see that the capped expirationd module has already worked:

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

I rummet er der stadig tupler, der har cirka 30 sekunder tilbage at leve. Desuden stoppede scanningen, da den gik fra tuplet med ID 2 og livstid 1576421257 til tuplet med ID 3 og livstid 1576421287. Tupler med livstid 1576421287 og derover blev ikke set pÄ grund af nÞglernes orden i exp-indekset. Dette er den besparelse, vi Þnskede at opnÄ fra starten.

We will stop the task:

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

Implementering

Den bedste forklaring af alle projektets funktioner findes altid i sin kilde. koden! I denne publikation vil vi kun fokusere pÄ de vigtigste aspekter, nemlig algoritmerne for traversal i rummet.

De argumenter, vi sender til metoden start, gemmes i en struktur kaldet 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;
};

Attributten name er navnet pÄ opgaven. Attributten space_id er identifikatoren for rummet. Attributten rm_index_id er identifikatoren for den unikke indeks, som tuple- sletning vil blive udfÞrt pÄ. Attributten it_index_id er identifikatoren for den indeks, som traversal af tuples vil blive udfÞrt pÄ. Attributten it_index_type er typen af indeks, som traversal af tuples vil blive udfÞrt pÄ. Attributten field_no er tuple-feltet med levetid. Attributten scan_size er det maksimale antal tuples, der gennemgÄs under en enkelt transaktion. Attributten scan_time er tiden for en fuld scanning i sekunder.

Vi vil ikke overveje parsing af argumenter. Det er et omhyggeligt, men ikke vanskeligt arbejde, hvor du vil fÄ hjÊlp af biblioteket msgpuck. Vanskeligheder kan kun opstÄ med indekserne, som overfÞres fra Lua i form af en kompleks datstruktur af typen mp_map, og ikke ved hjÊlp af simple typer som mp_bool, mp_double, mp_int, mp_uint og mp_array. Men det er ikke nÞdvendigt at parse hele indekset. Det er tilstrÊkkeligt kun at kontrollere dets unikhed, beregne typen og udtrÊkke identifikatoren.

Lad os opremse prototyperne for alle de funktioner, der bruges til parsing:

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

Lad os nu gĂ„ videre til det vigtigste — logikken bag scanning af plads og sletning af tuples. Hver blok af tuples, som ikke overstiger scan_size, gennemgĂ„s og ĂŠndres under Ă©n transaktion. Ved succes commit'en denne transaktion, ved fejl bliver den rullet tilbage. Den sidste argument i funktionen expirationd_iterate modtager en peger til iteratoren, hvor scanning enten starter eller fortsĂŠtter fra. Denne iterator inkrementeres indeni, indtil der opstĂ„r en fejl, pladsen er udtĂžmt, eller der gives mulighed for at stoppe processen pĂ„ forhĂ„nd. Funktionen expirationd_expired tjekker tuple's levetid, expirationd_delete — sletter tuple, expirationd_breakable — tjekker, om vi skal fortsĂŠtte videre.

Koden for funktionen 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;
}

Koden for funktionen 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;
}

Koden for funktionen 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);
}

Koden for funktionen 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;
}

Applikation

Du kan finde kildekoden pÄ her!

Kilde: habr.com

KĂžb pĂ„lidelig webhosting med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KĂžb pĂ„lidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster