Γράψιμο της δικής μας ενότητας που έχει λήξει με περιορισμένο όριο για το tarantool

Γράψιμο της δικής μας ενότητας που έχει λήξει με περιορισμένο όριο για το tarantool

Πριν λίγο καιρό βρεθήκαμε αντιμέτωποι με το πρόβλημα του καθαρισμού πλειάδων σε χώρους ταραντούλι. Ο καθαρισμός έπρεπε να ξεκινήσει όχι όταν το tarantool είχε ήδη εξαντληθεί από τη μνήμη, αλλά εκ των προτέρων και σε μια συγκεκριμένη συχνότητα. Για αυτήν την εργασία, το tarantool έχει μια ενότητα γραμμένη σε Lua που ονομάζεται λήξη. Αφού χρησιμοποιήσαμε αυτή τη μονάδα για σύντομο χρονικό διάστημα, συνειδητοποιήσαμε ότι δεν ήταν κατάλληλη για εμάς: λόγω του συνεχούς καθαρισμού μεγάλων ποσοτήτων δεδομένων, ο Lua κρεμάστηκε στο GC. Ως εκ τούτου, σκεφτήκαμε να αναπτύξουμε τη δική μας ενότητα που έχει λήξει με ανώτατο όριο, ελπίζοντας ότι ο κώδικας που είναι γραμμένος σε μια μητρική γλώσσα προγραμματισμού θα έλυνε τα προβλήματά μας με τον καλύτερο δυνατό τρόπο.

Ένα καλό παράδειγμα για εμάς ήταν η ενότητα tarantool που ονομάζεται μνήμη. Η προσέγγιση που χρησιμοποιείται σε αυτό βασίζεται στο γεγονός ότι δημιουργείται ένα ξεχωριστό πεδίο στο χώρο, το οποίο υποδεικνύει τη διάρκεια ζωής της πλειάδας, με άλλα λόγια, ttl. Η μονάδα στο παρασκήνιο σαρώνει το χώρο, συγκρίνει το TTL με την τρέχουσα ώρα και αποφασίζει εάν θα διαγραφεί η πλειάδα ή όχι. Ο κώδικας της μονάδας memcached είναι απλός και κομψός, αλλά πολύ γενικός. Πρώτον, δεν λαμβάνει υπόψη τον τύπο του ευρετηρίου που ανιχνεύεται και διαγράφεται. Δεύτερον, σε κάθε πέρασμα σαρώνονται όλες οι πλειάδες, ο αριθμός των οποίων μπορεί να είναι αρκετά μεγάλος. Και αν στη μονάδα που έληξε το πρώτο πρόβλημα λύθηκε (ο δείκτης δέντρου χωρίστηκε σε ξεχωριστή κλάση), τότε το δεύτερο εξακολουθεί να μην λαμβάνει καμία προσοχή. Αυτά τα τρία σημεία προκαθόρισαν την επιλογή υπέρ της συγγραφής του δικού μου κώδικα.

Περιγραφή

Η τεκμηρίωση για το tarantool έχει ένα πολύ καλό φροντιστήριο σχετικά με το πώς να γράψετε τις αποθηκευμένες διαδικασίες σας στο C. Πρώτα απ 'όλα, σας προτείνω να εξοικειωθείτε με αυτό για να κατανοήσετε αυτά τα ένθετα με εντολές και κώδικα που θα εμφανιστούν παρακάτω. Αξίζει επίσης να δοθεί προσοχή αναφορά σε αντικείμενα που είναι διαθέσιμα κατά τη σύνταξη της δικής σας ενότητας με περιορισμένο όριο, συγκεκριμένα κουτί, ίνα, ευρετήριο и txn.

Ας ξεκινήσουμε από μακριά και ας δούμε πώς μοιάζει εξωτερικά μια μονάδα που έχει λήξει με ανώτατο όριο:

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)

Για απλότητα, εκκινούμε το tarantool στον κατάλογο όπου βρίσκεται η βιβλιοθήκη μας libcapped-expirationd.so. Δύο λειτουργίες εξάγονται από τη βιβλιοθήκη: start και kill. Το πρώτο βήμα είναι να καταστήσετε αυτές τις λειτουργίες διαθέσιμες από τη Lua χρησιμοποιώντας box.schema.func.create και box.schema.user.grant. Στη συνέχεια, δημιουργήστε ένα χώρο του οποίου οι πλειάδες θα περιέχουν μόνο τρία πεδία: το πρώτο είναι ένα μοναδικό αναγνωριστικό, το δεύτερο είναι το email και το τρίτο είναι η διάρκεια ζωής της πλειάδας. Δημιουργούμε ένα δέντρο ευρετήριο πάνω από το πρώτο πεδίο και το ονομάζουμε πρωτεύον. Στη συνέχεια παίρνουμε το αντικείμενο σύνδεσης στην εγγενή βιβλιοθήκη μας.

Μετά τις προπαρασκευαστικές εργασίες, εκτελέστε τη λειτουργία εκκίνησης:

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

Αυτό το παράδειγμα θα λειτουργεί κατά τη σάρωση ακριβώς το ίδιο με τη μονάδα που έχει λήξει, η οποία είναι γραμμένη σε Lua. Το πρώτο όρισμα στη συνάρτηση έναρξης είναι το μοναδικό όνομα της εργασίας. Το δεύτερο είναι το αναγνωριστικό χώρου. Το τρίτο είναι ένα μοναδικό ευρετήριο με το οποίο θα διαγράφονται οι πλειάδες. Το τέταρτο είναι ο δείκτης με τον οποίο θα διασχιστούν οι πλειάδες. Το πέμπτο είναι ο αριθμός του πεδίου πλειάδας με διάρκεια ζωής (η αρίθμηση ξεκινά από το 1, όχι από το 0!). Η έκτη και η έβδομη είναι οι ρυθμίσεις σάρωσης. 1024 είναι ο μέγιστος αριθμός πλειάδων που μπορούν να προβληθούν σε μία μόνο συναλλαγή. 3600 — πλήρης χρόνος σάρωσης σε δευτερόλεπτα.

Σημειώστε ότι το παράδειγμα χρησιμοποιεί το ίδιο ευρετήριο για ανίχνευση και διαγραφή. Εάν πρόκειται για δείκτη δέντρου, τότε η διέλευση πραγματοποιείται από το μικρότερο κλειδί στο μεγαλύτερο. Εάν υπάρχει κάποιο άλλο, για παράδειγμα, κατακερματισμένος δείκτης, τότε η διέλευση πραγματοποιείται, κατά κανόνα, με τυχαία σειρά. Όλες οι διαστημικές πλειάδες σαρώνονται σε μία σάρωση.

Ας εισάγουμε πολλές πλειάδες στο χώρο με διάρκεια ζωής 60 δευτερολέπτων:

box.space.tester:insert{0, '[email protected]', math.floor(fiber.time()) + 60}
box.space.tester:insert{1, '[email protected]', math.floor(fiber.time()) + 60}
box.space.tester:insert{2, '[email protected]', math.floor(fiber.time()) + 60}

Ας ελέγξουμε ότι η εισαγωγή ήταν επιτυχής:

tarantool> box.space.tester.index.primary:select()
---
- - [0, '[email protected]', 1576418976]
  - [1, '[email protected]', 1576418976]
  - [2, '[email protected]', 1576418976]
...

Ας επαναλάβουμε την επιλογή μετά από 60+ δευτερόλεπτα (μετρώντας από την αρχή της εισαγωγής της πρώτης πλειάδας) και ας δούμε ότι η μονάδα που έχει λήξει έχει ήδη επεξεργαστεί:

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

Ας σταματήσουμε το έργο:

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

Ας δούμε ένα δεύτερο παράδειγμα όπου χρησιμοποιείται ξεχωριστό ευρετήριο για την ανίχνευση:

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)

Όλα εδώ είναι ίδια όπως στο πρώτο παράδειγμα, με λίγες εξαιρέσεις. Δημιουργούμε ένα δέντρο ευρετήριο πάνω από το τρίτο πεδίο και το ονομάζουμε exp. Αυτό το ευρετήριο δεν χρειάζεται να είναι μοναδικό, σε αντίθεση με το ευρετήριο που ονομάζεται πρωτεύον. Η διέλευση θα πραγματοποιηθεί με το ευρετήριο exp και η διαγραφή από το κύριο. Θυμόμαστε ότι προηγουμένως και τα δύο γίνονταν μόνο χρησιμοποιώντας τον πρωτεύοντα δείκτη.

Μετά την προπαρασκευαστική εργασία, εκτελούμε τη συνάρτηση έναρξης με νέα ορίσματα:

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

Ας εισάγουμε ξανά πολλές πλειάδες στο χώρο με διάρκεια ζωής 60 δευτερολέπτων:

box.space.tester:insert{0, '[email protected]', math.floor(fiber.time()) + 60}
box.space.tester:insert{1, '[email protected]', math.floor(fiber.time()) + 60}
box.space.tester:insert{2, '[email protected]', math.floor(fiber.time()) + 60}

Μετά από 30 δευτερόλεπτα, κατ' αναλογία, θα προσθέσουμε μερικές ακόμη πλειάδες:

box.space.tester:insert{3, '[email protected]', math.floor(fiber.time()) + 60}
box.space.tester:insert{4, '[email protected]', math.floor(fiber.time()) + 60}
box.space.tester:insert{5, '[email protected]', math.floor(fiber.time()) + 60}

Ας ελέγξουμε ότι η εισαγωγή ήταν επιτυχής:

tarantool> box.space.tester.index.primary:select()
---
- - [0, '[email protected]', 1576421257]
  - [1, '[email protected]', 1576421257]
  - [2, '[email protected]', 1576421257]
  - [3, '[email protected]', 1576421287]
  - [4, '[email protected]', 1576421287]
  - [5, '[email protected]', 1576421287]
...

Ας επαναλάβουμε την επιλογή μετά από 60+ δευτερόλεπτα (μετρώντας από την αρχή της εισαγωγής της πρώτης πλειάδας) και ας δούμε ότι η μονάδα που έχει λήξει έχει ήδη επεξεργαστεί:

tarantool> box.space.tester.index.primary:select()
---
- - [3, '[email protected]', 1576421287]
  - [4, '[email protected]', 1576421287]
  - [5, '[email protected]', 1576421287]
...

Υπάρχουν ακόμα μερικές πλειάδες στο χώρο που θα έχουν περίπου 30 δευτερόλεπτα ακόμα για να ζήσουν. Επιπλέον, η σάρωση σταμάτησε κατά τη μετάβαση από πλειάδα με αναγνωριστικό 2 και διάρκεια ζωής 1576421257 σε πλειάδα με αναγνωριστικό 3 και διάρκεια ζωής 1576421287. Οι πλειάδες με διάρκεια ζωής 1576421287 ή περισσότερο δεν σαρώθηκαν λόγω της παραγγελίας του τα κλειδιά ευρετηρίου exp. Αυτή είναι η εξοικονόμηση που θέλαμε να πετύχουμε από την αρχή.

Ας σταματήσουμε το έργο:

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

Реализация

Ο καλύτερος τρόπος για να πείτε για όλα τα χαρακτηριστικά ενός έργου είναι η αρχική του πηγή. κώδικας! Ως μέρος της δημοσίευσης, θα επικεντρωθούμε μόνο στα πιο σημαντικά σημεία, δηλαδή στους αλγόριθμους παράκαμψης χώρου.

Τα ορίσματα που μεταβιβάζουμε στη μέθοδο έναρξης αποθηκεύονται σε μια δομή που ονομάζεται 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;
};

Το χαρακτηριστικό name είναι το όνομα της εργασίας. Το χαρακτηριστικό space_id είναι το αναγνωριστικό χώρου. Το χαρακτηριστικό rm_index_id είναι το αναγνωριστικό του μοναδικού ευρετηρίου με το οποίο θα διαγράφονται οι πλειάδες. Το χαρακτηριστικό it_index_id είναι το αναγνωριστικό του ευρετηρίου με το οποίο θα διασχίζονται οι πλειάδες. Το χαρακτηριστικό it_index_type είναι ο τύπος ευρετηρίου με τον οποίο θα διασχίζονται οι πλειάδες. Το χαρακτηριστικό filed_no είναι ο αριθμός του πεδίου πλειάδας με διάρκεια ζωής. Το χαρακτηριστικό scan_size είναι ο μέγιστος αριθμός πλειάδων που σαρώνονται σε μία συναλλαγή. Το χαρακτηριστικό scan_time είναι ο πλήρης χρόνος σάρωσης σε δευτερόλεπτα.

Δεν θα εξετάσουμε τα επιχειρήματα ανάλυσης. Αυτή είναι μια επίπονη αλλά απλή δουλειά, με την οποία θα σας βοηθήσει η βιβλιοθήκη msgpuck. Δυσκολίες μπορούν να προκύψουν μόνο με ευρετήρια που μεταβιβάζονται από το Lua ως σύνθετη δομή δεδομένων με τον τύπο mp_map και όχι χρησιμοποιώντας τους απλούς τύπους mp_bool, mp_double, mp_int, mp_uint και mp_array. Αλλά δεν υπάρχει ανάγκη ανάλυσης ολόκληρου του ευρετηρίου. Απλά πρέπει να ελέγξετε τη μοναδικότητά του, να υπολογίσετε τον τύπο και να εξαγάγετε το αναγνωριστικό.

Παραθέτουμε τα πρωτότυπα όλων των συναρτήσεων που χρησιμοποιούνται για την ανάλυση:

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

Τώρα ας προχωρήσουμε στο πιο σημαντικό πράγμα - τη λογική της παράκαμψης του χώρου και της διαγραφής πλειάδων. Κάθε μπλοκ πλειάδων όχι μεγαλύτερο από το scan_size σαρώνεται και τροποποιείται σε μία μόνο συναλλαγή. Εάν είναι επιτυχής, αυτή η συναλλαγή δεσμεύεται· εάν παρουσιαστεί σφάλμα, επαναφέρεται. Το τελευταίο όρισμα στη συνάρτηση expirationd_iterate είναι ένας δείκτης στον επαναλήπτη από τον οποίο ξεκινά ή συνεχίζεται η σάρωση. Αυτός ο επαναλήπτης αυξάνεται εσωτερικά έως ότου παρουσιαστεί σφάλμα, εξαντληθεί ο χώρος ή δεν είναι δυνατή η διακοπή της διαδικασίας εκ των προτέρων. Η συνάρτηση expirationd_expired ελέγχει τη διάρκεια ζωής μιας πλειάδας, η expirationd_delete διαγράφει μια πλειάδα, η expirationd_breakable ελέγχει αν πρέπει να προχωρήσουμε.

Κωδικός συνάρτησης 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 < task->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;
}

Κωδικός συνάρτησης 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;
}

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

Κωδικός συνάρτησης expiration_breakable:

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

App

Μπορείτε να δείτε τον πηγαίο κώδικα στο εδώ!

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο