Некоторое время назад мы столкнулись с проблемой очистки кортежей в пространствах тарантула. Очистка не могла дождаться, пока тарантулу уже не хватит памяти, это нужно было делать заранее и с определенной периодичностью. В Tarantool уже был просроченный модуль, написанный на Lua. После непродолжительного использования этой модели мы поняли, что она нам не подходит, при очистке больших объемов данных Lua зависает в GC. Поэтому мы решили разработать модуль с ограниченным сроком действия, надеясь, что код, написанный на родном языке программирования, решит наши проблемы.

Хорошим примером ограниченного модуля с функцией истечения срока действия является модуль тарантула под названием memcached. Используемый в нем подход основан на дополнительном поле кортежа ttl. Модуль сканирует пространство в фоновом режиме, сравнивает ttl с текущим временем и решает, удалить ли кортеж или нет. Код простой и элегантный, но слишком общий. Во-первых, он не принимает во внимание тип индекса, используемый для итерации и удаления. Во-вторых, сканируются все кортежи, количество которых может быть довольно большим. В неэкспирированном модуле первая проблема решена, а на вторую не обращают внимания. Это предопределило наш выбор в написании собственного кода.

В документации к tarantool есть очень хороший туториал с информацией о том, как писать свои хранимые процедуры на C. Прежде всего, я предлагаю вам ознакомиться с ним, чтобы понять вставки с командами и кодом, которые вы найдете ниже. . Также стоит обратить внимание на ссылку с объектами, которые доступны при написании собственного модуля с ограничением (box, fiber, index и txn).

Давайте начнем издалека и посмотрим, как модуль с ограниченным сроком действия работает извне:

Для простоты мы запускаем tarantool в каталоге, в котором собрана и расположена наша библиотека libcapped-expirationd.so. Из библиотеки экспортируются две функции: start и kill. Сначала вам нужно сделать эти функции доступными из Lua, используя box.schema.func.create и box.schema.user.grant. Затем создайте пространство, кортежи которого будут содержать только три поля: первое - уникальный идентификатор, второе - электронная почта, третье - ttl кортежа. Поверх первого поля мы строим древовидный индекс и называем его «первичным» индексом. Далее мы подключаемся к нашей собственной библиотеке.

После подготовительных работ приступаем к задаче:

Этот пример будет работать так же, как и официальный модуль с истекшим сроком действия. Первый аргумент - это уникальное имя задачи. Второй аргумент - это идентификатор пробела. Третий аргумент - это уникальный индекс, используемый для удаления. Четвертый аргумент - это уникальный или неуникальный индекс, используемый для итерации. Пятый аргумент - это номер поля кортежа, содержащего ttl (нумерация начинается с 1, а не с 0!). Шестой и седьмой аргументы - это настройки сканирования. 1024 - это максимальное количество кортежей, которые могут быть обработаны за одну транзакцию. 3600 - время полного сканирования в секундах.

Обратите внимание, что один и тот же индекс используется для итерации и удаления. Ключи дерева-указателя отсортированы. Ключи хеш-индекса не сортируются. Все кортежи просматриваются за одно сканирование.

Вставим в пространство несколько кортежей со временем жизни 60 секунд:

Убедитесь, что вставка прошла успешно:

Повторите выбор через 60+ секунд (отсчитайте от начала вставки первого кортежа) и убедитесь, что модуль с ограниченным сроком действия уже сработал:

Остановите задачу:

Давайте посмотрим на второй пример, в котором для итерации используется отдельный индекс:

Здесь все так же, как в первом примере, за некоторыми исключениями. Поверх третьего поля постройте древовидный индекс и назовите его «exp». Этот индекс не обязательно должен быть уникальным, в отличие от индекса, называемого «первичным». Он будет использоваться для итерации. Помните, что ранее итерация и удаление выполнялись только с использованием «первичного» индекса. Теперь это не так!

После подготовительных работ приступаем к задаче с новыми аргументами:

Опять же, вставьте несколько кортежей в пространство со временем жизни 60 секунд:

Через 30 секунд добавьте еще несколько кортежей:

Убедитесь, что вставка прошла успешно:

Повторите выбор через 60+ секунд (отсчитайте от начала вставки первого кортежа) и убедитесь, что модуль с ограниченным сроком действия уже сработал:

В пространстве остались только кортежи с 30-секундным временем жизни. Более того, сканирование было остановлено при переключении с кортежа с id = 2 и ttl = 1576421257 на кортеж с id = 3 и ttl = 1576421287. Кортежи с ttl = 1576421287 и более не просматривались из-за упорядочения ключей индекса «exp». Это оптимизация, которой мы хотели добиться.

Остановите задачу:

В рамках публикации мы остановимся только на самых важных моментах. Вы должны просмотреть код для более подробной информации.

Аргументы, которые мы передаем функции запуска, хранятся в структуре с именем expirationd_task:

Атрибут name - это имя задачи. Атрибут space_id - это идентификатор пространства. Атрибут rm_index_id - это идентификатор уникального индекса, используемого для удаления. Атрибут it_index_id - это идентификатор уникального или неуникального индекса, используемого для итерации. Атрибут it_index_type - это тип индекса, используемый для итерации. Атрибут filed_no - это номер поля кортежа, содержащего ttl. Атрибут scan_size - это максимальное количество кортежей, которые могут быть обработаны за одну транзакцию. Атрибут scan_time - это время полного сканирования в секундах.

Мы не будем рассматривать аргументы парсинга. Это кропотливая, но несложная работа, в которой вам поможет библиотека msgpuck. Сложности могут возникнуть только с индексами, которые передаются из Lua в виде сложной структуры данных с типом MP_MAP. Но весь анализ индекса не нужен. Достаточно проверить его уникальность, извлечь тип и идентификатор.

Перечислим прототипы всех функций, которые используются для парсинга:

Перейдем к самому главному - логике перебора и удаления кортежей. Каждый блок кортежей размером не более scan_size сканируется и изменяется в рамках одной транзакции. В случае успеха эта транзакция фиксируется; в случае ошибки откатывается. Последний аргумент функции expirationd_iterate - это указатель на итератор, с которого начинается или продолжается сканирование. Этот итератор внутренне инкрементируется до тех пор, пока не произойдет ошибка, не закончится пробел или пока не будет возможности остановить процесс заранее. Функция expirationd_expired проверяет ttl кортежа, expirationd_delete - удаляет кортеж, expirationd_breakable - проверяет, нужно ли нам двигаться дальше.

Код функции expirationd_iterate:

Код функции expirationd_expired:

Код функции expirationd_delete:

Код функции expirationd_breakable:

Пожалуйста, посмотрите исходный код на github!