We schrijven onze capped expirationd module voor Tarantool.

We schrijven onze capped expirationd module voor Tarantool.

Een tijdje geleden stonden we voor het probleem van het opruimen van tuples in spaces tarantool. Opruiming moest niet plaatsvinden wanneer tarantool al zonder geheugen zat, maar voorafgaand en met een bepaalde periodiciteit. Voor deze taak heeft tarantool een module geschreven in Lua, genaamd expirationd. Na kort gebruik van deze module realiseerden we ons dat deze niet geschikt voor ons was: bij het constant opruimen van grote datavolumes kwam Lua in de GC vast te zitten. Daarom dachten we na over het ontwikkelen van onze eigen capped expirationd-module, in de hoop dat de code, geschreven in een native programmeertaal, onze taken op de beste manier zou oplossen.

Een goed voorbeeld voor ons was de tarantool-module met de naam redis. De gebruikte benadering is gebaseerd op het idee dat er in de space een apart veld wordt aangemaakt waarin de levensduur van de tuple wordt aangegeven, oftewel, ttl. De module scant op de achtergrond de space, vergelijkt ttl met de huidige tijd en neemt de beslissing of de tuple moet worden verwijderd of niet. De code van de memcached-module is eenvoudig en elegant, maar te algemeen. Ten eerste houdt het geen rekening met het type index waarmee de traverse en verwijdering plaatsvinden. Ten tweede worden bij elke doorloop alle tuples gescand, waarvan het aantal behoorlijk groot kan zijn. En hoewel in de expirationd-module het eerste probleem werd opgelost (de tree-index werd in een aparte klasse geplaatst), kreeg het tweede nog steeds geen aandacht. Deze drie punten bepaalden de keuze voor het schrijven van onze eigen code.

Omschrijving

In de documentatie van tarantool is er een zeer goede tutorial over hoe je je eigen opgeslagen procedures in de C-taal kunt schrijven. Ik raad in de eerste plaats aan deze te bekijken, zodat je de invoegingen met commando's en code die hieronder zullen verschijnen begrijpt. Ook is het de moeite waard om aandacht te besteden aan de referentie naar de objecten die beschikbaar zijn bij het schrijven van je eigen capped-module, met name naar box, fiber, index en txn.

Laten we van ver komen en kijken hoe de capped expirationd-module er van buiten uitziet:

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)

Voor de eenvoud starten we Tarantool in de directory waar onze bibliotheek libcapped-expirationd.so zich bevindt. Twee functies worden uit de bibliotheek geƫxporteerd: start en kill. In eerste instantie moeten we deze functies beschikbaar maken vanuit Lua met behulp van box.schema.func.create en box.schema.user.grant. Vervolgens creƫren we een space, waarvan de tuples drie velden bevatten: het eerste is een unieke identifier, het tweede is een e-mailadres, en het derde is de levensduur van de tuple. We bouwen een tree-index boven het eerste veld en noemen het primary. Daarna verkrijgen we een objectverbinding met onze native bibliotheek.

Na de voorbereidende werkzaamheden starten we de functie 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})

Dit voorbeeld zal functioneren bij het scannen op dezelfde manier als het Lua-module expirationd. De eerste argument voor de functie start is een unieke naam voor de taak. De tweede is de identifier van de space. De derde is de unieke index volgens welke tuples verwijderd zullen worden. De vierde is de index volgens welke tuples doorlopen worden. De vijfde is het veldnummer van de tuple met een levensduur (de telling begint bij 1 en niet bij 0!). De zesde en zevende zijn de scaninstellingen. 1024 is het maximale aantal tuples dat in ƩƩn transactie wordt bekeken. 3600 is de totale scanduur in seconden.

Let op dat in het voorbeeld dezelfde index wordt gebruikt voor zowel het doorlopen als verwijderen. Als dit een tree-index is, wordt er van een kleinere sleutel naar een grotere gelopen. Als het een andere, bijvoorbeeld een hash-index is, gebeurt het doorlopen doorgaans in willekeurige volgorde. Bij ƩƩn scan worden alle tuples van de space bekeken.

Laten we enkele tuples met een levensduur van 60 seconden in de space invoegen:

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}

Laten we controleren of de invoeging succesvol was:

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

Laten we de select opnieuw uitvoeren na 60+ seconden (berekend vanaf het begin van de invoeging van de eerste tuple) en we zullen zien dat de module capped expirationd al heeft gewerkt:

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

Laten we de taak stoppen:

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

Laten we het tweede voorbeeld bekijken, waarbij een aparte index voor het doorlopen wordt gebruikt:

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)

Hier is alles hetzelfde als in het eerste voorbeeld, met ƩƩn klein verschil. We bouwen een tree-index bovenop het derde veld en noemen het exp. Deze index hoeft niet uniek te zijn, in tegenstelling tot de index genaamd primary. Het doorlopen gebeurt via de exp-index, terwijl verwijdering via primary geschiedt. We herinneren ons dat eerder beide alleen met de primary-index werden gedaan.

Na de voorbereidende werkzaamheden starten we de functie start met nieuwe argumenten:

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

Laten we opnieuw een aantal tuples invoegen met een levensduur van 60 seconden:

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}

Na 30 seconden voegen we analogisch nog enkele tuples toe:

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}

Laten we controleren of de invoeging succesvol was:

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

Laten we de select opnieuw uitvoeren na 60+ seconden (berekend vanaf het begin van de invoeging van de eerste tuple) en we zullen zien dat de module capped expirationd al heeft gewerkt:

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

In de space zijn er nog tuples over die nog ongeveer 30 seconden meegaan. Bovendien is het doorzoeken gestopt bij de overgang van de tuple met identificatienummer 2 en levensduur 1576421257 naar de tuple met identificatienummer 3 en levensduur 1576421287. Tuples met een levensduur van 1576421287 en hoger zijn niet bekeken wegens de ordening van de exp-index sleutels. Dit is de besparing die we in het begin wilden bereiken.

Laten we de taak stoppen:

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

Implementatie

Het beste verhaal over alle bijzonderheden van het project komt altijd van de oorspronkelijke code! In deze publicatie beperken we ons tot de belangrijkste momenten, namelijk de algoritmes voor het doorlopen van de space.

De argumenten die we aan de methode start doorgeven, worden opgeslagen in een structuur genaamd 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;
};

Het attribuut name is de naam van de taak. Het attribuut space_id is de identificator van de ruimte. Het attribuut rm_index_id is de identificator van de unieke index, waarvoor tuples zullen worden verwijderd. Het attribuut it_index_id is de identificator van de index, waarvoor tuples zullen worden doorlopen. Het attribuut it_index_type is het type index, waarvoor tuples zullen worden doorlopen. Het attribuut filed_no is het nummer van het tuple-veld met de levensduur. Het attribuut scan_size is het maximale aantal tuples dat in ƩƩn transactie wordt bekeken. Het attribuut scan_time is de tijd voor een volledige scan in seconden.

We zullen de argumentparse niet overwegen. Dit is een nauwkeurige, maar eenvoudige taak, waarbij u de bibliotheek msgpuck. Moeilijkheden kunnen zich slechts voordoen met de indexen die uit Lua worden doorgegeven als een complexe gegevensstructuur met het type mp_map, en niet met eenvoudige types zoals mp_bool, mp_double, mp_int, mp_uint en mp_array. Maar het is niet nodig om de hele index te parseren. Het is voldoende om alleen de uniciteit te controleren, het type te berekenen en de identificator te extraheren.

Laten we de prototypes van alle functies opsommen die worden gebruikt voor 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);

Laten we nu overgaan tot het belangrijkste — de logica van het doorlopen van de ruimte en het verwijderen van tuples. Elke blok tuples van maximaal scan_size wordt doorzocht en gewijzigd binnen ƩƩn transactie. Bij succes wordt deze transactie vastgelegd, bij een fout wordt deze teruggedraaid. De laatste parameter in de functie expirationd_iterate is een pointer naar de iterator, waarmee het scannen begint of voortgaat. Deze iterator wordt geĆÆncrementeerd totdat er een fout optreedt, de ruimte is opgebruikt, of er een mogelijkheid is om het proces vooraf te stoppen. De functie expirationd_expired controleert de levensduur van de tuple, expirationd_delete verwijdert de tuple, expirationd_breakable controleert of we verder moeten gaan.

Code van de functie 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;
}

Code van de functie 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;
}

Code van de functie 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);
}

Code van de functie 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;
}

Toepassing

De broncode is te vinden op here!

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster