Izstrādājam savu capped expirationd moduļa tarantool.

Izstrādājam savu capped expirationd moduļa tarantool.

Pirms kāda laika pie mums radās problēma ar tuplēm telpās. tarantool. Tīrīšana bija jāuzsāk ne tad, kad tarantool jau bija beidzies atmiņa, bet iepriekš un noteiktos intervālos. Šim uzdevumam tarantool ir modulis, kas uzrakstīts Lua valodā, ar nosaukumu expirationd. Pēc neilgas šī moduļa lietošanas mēs sapratām, ka tas mums neder: patstāvīgi tīrot lielus datu apjomus, Lua piekārās GC. Tāpēc mēs izvērtējām iespēju izstrādāt savu capped expirationd moduli, cerot, ka nativā programmēšanas valodā rakstīts kods vislabāk atrisinās mūsu uzdevumus.

Labs piemērs mums ir tarantool modulis ar nosaukumu memcached. Izmantotā pieeja balstās uz to, ka telpā izveido atsevišķu lauku, kurā norādīts tuplē dzīves ilgums, citiem vārdiem sakot, ttl. Modulis fonā skenē telpu, salīdzina ttl ar pašreizējo laiku un pieņem lēmumu, vai dzēst tupli vai nē. Memcached moduļa kods ir vienkāršs un stilīgs, bet pārlieku vispārīgs. Pirmkārt, tas neņem vērā indeksu veidu, pēc kura tiek veikta pārgājiena un dzēšanas procedūra. Otrkārt, katrā pārgājienā tiek skenētas visas tuples, kuras var būt diezgan plašas. Un, ja expirationd modulī pirmā problēma bija atrisināta (tree-indekss izcelts atsevišķā klasē), tad otrajai joprojām nebija pievērsta uzmanība. Šie trīs punkti noteica izvēli par labu savas kodēšanas rakstīšanai.

Apraksts

Tarantool dokumentācijā ir ļoti labs , par to, kā rakstīt savas saglabātās procedūras C valodā. Pirmkārt, iesaku ar to iepazīties, lai saprastu ievietojumus ar komandām un kodu, kas tiks minēti zemāk. Arī jāpievērš uzmanība atsaucei uz objektiem, kas ir pieejami, rakstot savu capped moduļa, proti, uz box, fiber, index un txn.

Sāksim no tālienes un aplūkosim, kā izskatās capped expirationd modulis no ārpuses:

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)

Lai vienkāršotu uzsākšanu, palaidīsim tarantool tajā katalogā, kur atrodas mūsu bibliotēka libcapped-expirationd.so. No bibliotēkas eksportē divas funkcijas: start un kill. Sākumā ir nepieciešams padarīt šīs funkcijas pieejamas no Lua, izmantojot box.schema.func.create un box.schema.user.grant. Pēc tam jāizveido spaeiss, kura krotuļu struktūrā būs trīs lauki: pirmais — unikālais identifikators, otrais — e-pasts, trešais — krotuļa dzīvotspēja. Pāri pirmajam laukam izveidojam tree-indeksu un nosaukam to par primary. Pēc tam iegūstam piekļuves objektu mūsu natīvajai bibliotēkai.

Pēc sagatavošanas darbu pabeigšanas palaidīsim 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})

Šis piemērs darbosies tieši tāpat kā expirationd modulis, kas uzrakstīts Lua. Pirmais arguments funkcijai start ir unikāls uzdevuma nosaukums. Otrais — spaisa identifikators. Trešais — unikālais indekss, pēc kura tiks dzēsti krotuļi. Ceturtajam — indekss, pēc kura tiks veikta krotuļu skenēšana. Piektajam — krotuļa laika dzīvotspējas lauks (numurācija sākas no 1, nevis 0!). Sestais un septītais — skenēšanas iestatījumi. 1024 — maksimālais krotuļu skaits, ko pārskata vienā darījuma laikā. 3600 — pilnvērtīgas skenēšanas laiks sekundēs.

Lūdzu, ņemiet vērā, ka piemēra skenēšanai un dzēšanai tiek izmantots viens un tas pats indekss. Ja tas ir tree-indekss, tad skenēšana tiek veikta no mazākā atslēgas līdz lielākai. Ja tas ir kāds cits, piemēram, hash-indekss, tad skenēšana parasti notiek nejaušā secībā. Vienas skenēšanas laikā tiek pārskatīti visi spaisa krotuļi.

Izveidosim spaisā vairākus krotuļus ar 60 sekunžu dzīvotspēju:

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}

Pārbaudīsim, vai ievietošana veiksmīgi notika:

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

Atkārtoti izpildīsim select pēc 60+ sekundēm (skaitot no pirmā krotuļa ievietošanas sākuma) un redzēsim, ka capped expirationd modulis jau ir darbojas:

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

Apslāpīsim uzdevumu:

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

Apskatīsim otro piemēru, kad apstrādei tiek izmantots atsevišķs indekss:

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)

Šeit viss ir tāpat kā pirmajā piemērā, izņemot dažus sīkumus. Virs trešā lauka veidojam tree-indeksu un nosaucam to par exp. Šim indeksam nav jābūt unikālam, atšķirībā no indeksa ar nosaukumu primary. Apstrāde tiks veikta, izmantojot exp indeksu, bet dzēšana — ar primary. Mēs atceramies, ka iepriekš gan vienu, gan otru veica tikai ar primary indeksa palīdzību.

Pēc sagatavošanas darbu pabeigšanas izsaucam funkciju start ar jaunām argumentiem:

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

Atkal veiksim ievadi telpā ar vairākiem tuple ar dzīves ilgumu 60 sekundes:

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}

Pusstundā, analogi, pievienosim vēl dažus 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}

Pārbaudīsim, vai ievietošana veiksmīgi notika:

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

Atkārtoti izpildīsim select pēc 60+ sekundēm (skaitot no pirmā krotuļa ievietošanas sākuma) un redzēsim, ka capped expirationd modulis jau ir darbojas:

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

Telpā ir atlikušas tuple, kurām dzīves ilgums ir vēl aptuveni 30 sekundes. Turklāt skanējums apstājās, pārejot no tuple ar identifikatoru 2 un dzīves ilgumu 1576421257 uz tuple ar identifikatoru 3 un dzīves ilgumu 1576421287. Tuple ar dzīves ilgumu 1576421287 un vairāk netika skatīti, pateicoties exp indeksa atslēgu secības kārtībai. Tas ir tas ietaupījums, kuru sākotnēji mēs vēlējāmies sasniegt.

Apslāpīsim uzdevumu:

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

Realizācija

Vislabāk par visām projekta īpatnībām vienmēr pastāstīs tā avots quiche bibliotēkas kods, pēc tam jāpārbūvē nginx ar opcijām "—with-http_v3_module —with-quiche=..../quiche". Izstrādājot, TLS atbalstam jābalstās uz BoringSSL bibliotēku ("—with-openssl=..../quiche/deps/boringssl"), OpenSSL izmantošana pagaidām nav atbalstīta. Lai pieņemtu savienojumus, iestatījumos jāiekļauj direktīva listen ar flagu "quic" (piemēram, "listen 443 quic reuseport").! Publicēšanas ietvaros mēs apstāsies tikai pie vissvarīgākajiem aspektiem, proti, pie telpas apstrādes algoritmiem.

Argumenti, kurus mēs nododam metodei start, tiek saglabāti struktūrā, kas saucās 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ūts name ir uzdevuma nosaukums. Atribūts space_id ir telpas identifikators. Atribūts rm_index_id ir unikālā indeksa identifikators, pēc kura tiks izdzēsti ieraksti. Atribūts it_index_id ir indeksa identifikators, pēc kura tiks veikta ierakstu apstrāde. Atribūts it_index_type ir indeksa veids, pēc kura tiks veikta ierakstu apstrāde. Atribūts field_no ir lauka numurs ierakstā ar mūža ilgumu. Atribūts scan_size ir maksimālais ierakstu skaits, kas tiek skatīts vienas transakcijas laikā. Atribūts scan_time ir pilnīgas skenēšanas laiks sekundēs.

Argumentu parsēšanu neapskatīsim. Tas ir laikietilpīgs, bet negrūts darbs, kurā jums palīdzēs bibliotēka msgpuck. Grūtības var rasties tikai ar indeksi, kas tiek nodoti no Lua kā sarežģītu datu struktūra ar tipu mp_map, nevis ar vienkāršajiem tipiem mp_bool, mp_double, mp_int, mp_uint un mp_array. Bet visu indeksu parsēt nav nepieciešams. Pietiek tikai pārbaudīt tā unikālumu, aprēķināt tipu un izvilkt identifikatoru.

Apskatīsim visu funkciju prototipus, kas tiek izmantoti parsēšanai:

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

Tagad pāriesim pie svarīgākā — loģikas par telpas esošo pārbaudi un tuplu dzēšanu. Katrs tuplu bloks, kura izmērs nepārsniedz scan_size, tiek skatīts un modificēts vienā transakcijā. Ja transakcija ir izdevusies, tā tiek apstiprināta, bet kļūdas gadījumā — atcelta. Pēdējais arguments funkcijā expirationd_iterate ir iteratora rādītājs, no kura sākas vai turpinās skenēšana. Šis iterators tiek inkrementēts līdz brīdim, kad notiek kļūda, līdz telpa beidzas vai kad rodas iespēja procesu apturēt iepriekš. Funkcija expirationd_expired pārbauda tupla mūža laiku, expirationd_delete — dzēš tupla, expirationd_breakable — pārbauda, vai mums jāturpina tālāk.

Funkcijas expirationd_iterate kods:

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

Funkcijas expirationd_expired kods:

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

Funkcijas expirationd_delete kods:

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

Funkcijas expirationd_breakable kods:

static bool
expirationd_breakable(struct expirationd_task *task)
{
  return task->it_index_id != task->rm_index_id && task->it_index_type == ITER_GT;
}

Pielietojums

Izvērtēt avota kodu var šeit!

Avots: habr.com

Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem 🔥 Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem | ProHoster