كتابة وحدة انتهاء الصلاحية الخاصة بنا لـ 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 الخاصة بنا. يتم تصدير وظيفتين من المكتبة: البدء والقتل. الخطوة الأولى هي إتاحة هذه الوظائف من Lua باستخدام box.schema.func.create وbox.schema.user.grant. ثم قم بإنشاء مساحة تحتوي صفوفها على ثلاثة حقول فقط: الأول هو معرف فريد، والثاني هو البريد الإلكتروني، والثالث هو عمر الصف. نقوم ببناء فهرس شجرة أعلى الحقل الأول ونطلق عليه اسم الأساسي. بعد ذلك نحصل على كائن الاتصال بمكتبتنا الأصلية.

بعد الانتهاء من الأعمال التحضيرية، قم بتشغيل وظيفة البداية:

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

سمة الاسم هي اسم المهمة. السمة space_id هي معرف المساحة. السمة rm_index_id هي معرف الفهرس الفريد الذي سيتم من خلاله حذف المجموعات. السمة it_index_id هي معرف الفهرس الذي سيتم من خلاله اجتياز الصف. السمة it_index_type هي نوع الفهرس الذي سيتم من خلاله اجتياز الصف. السمة file_no هي رقم حقل الصف مع العمر. السمة scan_size هي الحد الأقصى لعدد المجموعات التي يتم فحصها في معاملة واحدة. السمة scan_time هي وقت الفحص الكامل بالثواني.

لن نفكر في تحليل الحجج. هذه مهمة شاقة ولكنها بسيطة، وستساعدك المكتبة فيها com.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;
}

تطبيق

يمكنك عرض الكود المصدري على هنا!

المصدر: www.habr.com

إضافة تعليق