การเขียนโมดูลหมดอายุต่อยอดของเราเองสำหรับ tarantool

การเขียนโมดูลหมดอายุต่อยอดของเราเองสำหรับ tarantool

ก่อนหน้านี้เราประสบปัญหาในการทำความสะอาดสิ่งอันดับในอวกาศ ทารันทูล. การทำความสะอาดจะต้องเริ่มต้นไม่ใช่เมื่อ tarantool หน่วยความจำไม่เพียงพอ แต่ต้องเริ่มต้นล่วงหน้าและด้วยความถี่ที่แน่นอน สำหรับงานนี้ tarantool มีโมดูลที่เขียนด้วยภาษา Lua เรียกว่า การหมดอายุ. หลังจากใช้โมดูลนี้ในช่วงเวลาสั้นๆ เราก็พบว่ามันไม่เหมาะกับเรา เนื่องจากมีการล้างข้อมูลจำนวนมากอย่างต่อเนื่อง Lua จึงหยุดทำงานใน GC ดังนั้นเราจึงคิดที่จะพัฒนาโมดูลหมดอายุแบบต่อยอดของเราเอง โดยหวังว่าโค้ดที่เขียนด้วยภาษาการเขียนโปรแกรมแบบเนทีฟจะช่วยแก้ปัญหาของเราในวิธีที่ดีที่สุดเท่าที่จะเป็นไปได้

ตัวอย่างที่ดีสำหรับเราคือโมดูล tarantool ที่เรียกว่า memcached. วิธีการที่ใช้นั้นขึ้นอยู่กับข้อเท็จจริงที่ว่าฟิลด์แยกต่างหากถูกสร้างขึ้นในพื้นที่ ซึ่งระบุอายุการใช้งานของทูเพิล หรืออีกนัยหนึ่งคือ ttl โมดูลในพื้นหลังจะสแกนพื้นที่ เปรียบเทียบ TTL กับเวลาปัจจุบัน และตัดสินใจว่าจะลบทูเพิลหรือไม่ โค้ดโมดูล memcached นั้นเรียบง่ายและสวยงาม แต่กว้างเกินไป ประการแรก จะไม่คำนึงถึงประเภทของดัชนีที่กำลังรวบรวมข้อมูลและลบ ประการที่สอง ในแต่ละรอบจะสแกนสิ่งอันดับทั้งหมด ซึ่งมีจำนวนค่อนข้างมาก และหากปัญหาแรกได้รับการแก้ไขในโมดูลที่หมดอายุแล้ว (ดัชนีแผนผังถูกแยกออกเป็นคลาสแยกต่างหาก) จากนั้นปัญหาที่สองก็ยังไม่ได้รับความสนใจใดๆ ประเด็นทั้งสามนี้ได้กำหนดไว้ล่วงหน้าว่าจะเลือกเขียนโค้ดของตัวเองได้อย่างไร

ลักษณะ

เอกสารประกอบสำหรับ tarantool นั้นดีมาก กวดวิชา เกี่ยวกับวิธีการเขียน Stored Procedure ของคุณใน C ก่อนอื่น ฉันขอแนะนำให้คุณทำความคุ้นเคยกับมันเพื่อทำความเข้าใจส่วนแทรกเหล่านั้นด้วยคำสั่งและโค้ดที่จะปรากฏด้านล่าง นอกจากนี้ยังควรค่าแก่การใส่ใจด้วย อ้างอิง ไปยังอ็อบเจ็กต์ที่มีอยู่เมื่อเขียนโมดูลต่อยอดของคุณเองกล่าวคือ กล่อง, ไฟเบอร์, ดัชนี и เท็กซัส.

มาเริ่มต้นจากระยะไกลแล้วดูว่าโมดูลที่หมดอายุต่อยอดมีลักษณะอย่างไรจากภายนอก:

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 จากนั้นสร้างช่องว่างที่ทูเพิลจะมีเพียงสามฟิลด์ ช่องแรกคือตัวระบุที่ไม่ซ้ำกัน ช่องที่สองคืออีเมล และช่องที่สามคืออายุการใช้งานของทูเพิล เราสร้างดัชนีแบบต้นไม้ที่ด้านบนของฟิลด์แรกและเรียกว่าดัชนีหลัก ต่อไปเราจะได้วัตถุการเชื่อมต่อกับไลบรารี่ของเรา

หลังจากงานเตรียมการให้รันฟังก์ชัน 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})

ตัวอย่างนี้จะทำงานในระหว่างการสแกนเหมือนกับโมดูลที่หมดอายุซึ่งเขียนด้วยภาษา Lua ทุกประการ อาร์กิวเมนต์แรกของฟังก์ชัน start คือชื่อเฉพาะของงาน ประการที่สองคือตัวระบุพื้นที่ ที่สามคือดัชนีเฉพาะที่สิ่งอันดับจะถูกลบ ประการที่สี่คือดัชนีที่จะใช้สำรวจสิ่งอันดับ อันที่ห้าคือจำนวนฟิลด์ทูเพิลที่มีอายุการใช้งาน (การนับเริ่มจาก 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 ดัชนีนี้ไม่จำเป็นต้องมีเอกลักษณ์ ซึ่งแตกต่างจากดัชนีที่เรียกว่าหลัก การข้ามผ่านจะดำเนินการตามดัชนีประสบการณ์ และการลบออกตามหลัก เราจำได้ว่าก่อนหน้านี้ทั้งสองทำโดยใช้ดัชนีหลักเท่านั้น

หลังจากงานเตรียมการ เราจะรันฟังก์ชัน start ด้วยอาร์กิวเมนต์ใหม่:

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 วินาที ยิ่งไปกว่านั้น การสแกนหยุดลงเมื่อย้ายจากทูเปิลที่มี ID 2 และอายุการใช้งาน 1576421257 ไปเป็น tuple ที่มี ID 3 และอายุการใช้งาน 1576421287 Tuple ที่มีอายุการใช้งาน 1576421287 ขึ้นไปจะไม่ถูกสแกนเนื่องจากการเรียงลำดับของ ปุ่มดัชนีประสบการณ์ นี่คือการออมที่เราต้องการบรรลุตั้งแต่เริ่มต้น

มาหยุดงานกันเถอะ:

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 ตรวจสอบอายุการใช้งานของ tuple, expirationd_delete ลบ tuple, 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;
}

ใบสมัคร

คุณสามารถดูซอร์สโค้ดได้ที่ ที่นี่!

ที่มา: will.com

เพิ่มความคิดเห็น