Как при помощи Node.js + MySQL упорядочить 100 Гбайт данных
6К открытий6К показов
Эта статья поведает о том, как можно эффективно обрабатывать миллиарды строк, которые занимают сотни гигабайт памяти, при помощи Node.js и MySQL. Вторичная цель статьи — это помочь определиться, подходит ли такое решение для ваших нужд. Код, который будет использоваться в этом примере, можно найти на GitHub.
Почему Node.js и MySQL?
MySQL будет использоваться для хранения информации из Trace — инструмента Node.js для отладки и мониторинга производительности.
Выбор пал на MySQL, потому что на момент принятия решения Postgres не очень хорошо обновлял строки и обновление неизменяемых данных было бы неоправданно сложным. К сожалению, эти решения не соответствуют требованиям ACID, что затрудняет их использование.
Однако при хорошем индексировании и правильном планировании MySQL, это может быть подходящим решением для поставленной задачи.
MySQL имеет несколько модулей хранения. InnoDB является стандартным, в котором есть большинство функций. Однако следует учитывать, что таблицы InnoDB не изменяемы, что означает, что каждый оператор ALTER TABLE
скопирует все данные в новую таблицу. Это ухудшит ситуацию, когда возникнет необходимость в переносе уже существующей базы данных.
Если имеются номинальные значения, каждое из которых имеет множество связанных данных, например, у каждого из пользователей есть множество продуктов, а пользователей довольно много — пожалуй, наиболее оптимальным будет создание отдельных таблиц с именем <user_id>_<entity_name>
для каждой из сущностей. Таким образом можно значительно уменьшить размер конкретной таблицы.
Кроме того, если не разделить пользователей по таблицам, очистка данных пользователя в случае удаления учетной записи будет являться операцией с постоянной временной сложностью алгоритма O(1), когда время выполнения не зависит от размера обрабатываемых данных. Это очень важно, потому что, если нужно удалить большое количество значений из больших таблиц, MySQL может решить использовать неверный индекс или вообще не использовать индексы. Если не создавать отдельных таблиц для каждой сущности, вы не сможете использовать подсказки для DELETE
, чтобы указать MySQL, какие именно индексы нужно обрабатывать. Вы также можете захотеть использовать ALTER
для таблицы, чтобы удалить данные, но это приведет к копированию каждой строки в новую таблицу, что не является рациональным при использовании одной большой таблицы для всех пользователей.
Создание таблиц для каждого пользователя, на первый взгляд, усложняет задачу, но это может быть единственным решением, когда дело касается удаления пользователей или подобных объектов с огромным количеством связанных данных.
Однако перед тем как использовать динамически созданные таблицы, стоит попробовать удалить строки в чанках (англ. chunks). Такой вариант также может сработать и в этом случае упростит весь процесс. Конечно, если данные появляются быстрее, чем происходит их удаление, решение с чанками не подойдет.
Но что если таблицы все еще остались огромными даже после их разделения по пользователями и также нужно удалить устаревшие строки? А данные все еще появляются быстрее, чем происходит их удаление? В этом случае стоит попробовать встроенный в MySQL инструмент для секционирования таблиц. Это удобно, когда нужно разделить данные по постоянным или переменным значениям, например, по времени создания.
Секционирование таблиц с помощью MySQL
С MySQL секционированная таблица будет работать так, как если бы она была несколькими таблицами, но можно использовать тот же привычный интерфейс без изменений в логике приложения. Это также означает, что можно удалять разделы, как если бы происходило удаление таблиц.
Документация хорошая, но довольно многословная (в конце концов, это не простая тема), поэтому просто создадим секционированную таблицу.
В коде нет ничего необычного до PARTITION BY RANGE
.
В MySQL таблицы можно разделить на RANGE
, LIST
, COLUMN
, HASH
и KEY
, о которых можно прочитать в документации. Стоит обратить внимание, что ключ секционирования должен быть частью первичного ключа или любых уникальных индексов.
Все, что начинается с <date>
, должно быть понятным. Каждый раздел содержит значения, для которых столбец created_at
меньше даты следующего дня. Это также означает, что from20120414
содержит все данные, которые старше 2012-04-15, так что этот раздел будет удален при выполнении очистки.
Разделы start
и future
нуждаются в некотором разъяснении: future
содержит данные для дат, которые еще не включены в правила секционирования. Если невозможно выполнить секционирование вовремя, все данные, поступившие 2017-05-17 и позже, будут в этом разделе, и мы ничего не потеряем. Раздел start
является неким защитным механизмом. Ожидается, что все строки имеют значение DATETIME
created_at
, однако нужно подготовиться к возможным ошибкам. Если по какой-то причине в строке в этом поле будет NULL
, она окажется в разделеstart
. Это укажет нам на ошибки, и, значит, нужно разбираться.
При использовании секционирования MySQL сохранит эти данные на отдельных частях диска, как если бы они были отдельными таблицами, и автоматически упорядочит все данные на основании ключа секционирования.
Однако есть некоторые ограничения при использовании секционирования, которые необходимо принять во внимание:
- кэш запросов не поддерживается;
- внешние ключи не поддерживаются для секционированных таблиц InnoDB;
- секционированные таблицы не поддерживают индексы или поисковые запросы
FULLTEXT
.
Ограничений гораздо больше, это лишь основные.
Если нужно создать новый раздел, то необходимо реорганизовать существующий и разбить его в соответствии с потребностями:
Удаление разделов происходит с помощью ALTER TABLE
, поскольку выполняется аналогично удалению таблиц:
Как можно увидеть, нужно включать фактические имена и описания разделов в операторах. Они не могут быть динамически генерированы MySQL, поэтому придется обрабатывать их в логике приложения. Это будет рассмотрено дальше.
Пример секционирования при помощи Node.js и MySQL
Рассмотрим практическое решение. Для примера здесь используется knex — инструмент запросов для JavaScript. Для тех, кто знаком с SQL, понимание кода не составит труда.
Сначала создадим таблицу:
Это практически то же выражение, что было выше, но в задаче необходимо динамически создавать имена и описания разделов. Вот почему был создан метод getPartitionStrings
.
Первая строка:
MAX_DATA_RETENTION - 2 = 5
создает последовательность от 5 до -2 (последнее число не входит в последовательность) → [ 5, 4, 3, 2, 1, 0, -1 ]
, после этого вычитаются значения из текущего времени и создается раздел today
и его предел tomorrow
. Порядок очень важен, поскольку MySQL выдаст ошибку, если значения для разделения находятся не в возрастающем порядке.
Пример удаления больших масштабов данных при помощи MySQL и Node.js
Теперь можно шаг за шагом разобраться с удалением данных. Полный код можно посмотреть здесь.
Первый метод, removeExpired
, получает список текущих разделов, затем передает его в repartition
.
Сначала выбираются все существующие в настоящее время разделы из таблицы information_schema.partitions
, которая управляется MySQL.
Затем создаются все разделы, которые должны существовать для таблицы. Если A
— это множество разделов, которые существуют, а B
— множество разделов, которые должны существовать, тогда:
partitionsToBeCreated = B \ A
partitionsToBeDropped = A \ B
getPartitionsThatShouldExist creates set B
.
Создание объектов разделов очень похоже на создание выражения CREATE TABLE ... PARTITION BY RANGE
. Также важно проверить, является ли раздел, который будет создан, старше текущего самого старого раздела, ведь со временем может понадобиться изменить dataRetention
.
Пример одного из возможных сценариев:
Пользователи могут хранить данные в течение 7 дней, но также существует возможность расширить этот диапазон до 10 дней. Сначала у пользователя есть разделы, которые охватывают дни в следующем порядке: [ start, -7, -6, -5, -4, -3, -2, -1, future ]. Вскоре пользователь решает увеличить срок хранения данных. Отсутствующие разделы в этом случае: [ -10, -9, -8, 0 ].При очистке текущий скрипт попытается реорганизовать раздел future для отсутствующих разделов, добавляя их после текущих.Создание дополнительных разделов, которые старше 7 дней, не имеет смысла, потому что эти данные будут заброшены так далеко, что это создаст список разделов, который выглядит так: [ start, -7, -6, -5, -4, -3, -2, -1, -10, -9, -8, 0, future ]. Непоследовательно возрастающий список приведет к ошибке MySQL, и вся операция завершится неудачей.
Функция MySQL TO_DAYS(date)
вычисляет количество дней, прошедших с 1 января 2000 года, поэтому мы повторяем это в JavaScript.
Теперь, когда созданы разделы, которые нужно удалить, и разделы, которые должны быть созданы, нужно создать новый раздел для нового дня.
Здесь просто выполняется код для создания новых разделов.
Запуская этот скрипт ежечасно, можно убедиться в том, что все идет по плану. Это также поможет выполнять очистку должным образом, по крайней мере, один раз в день.
Итак, первое, что нужно проверить — это создать любой раздел. Это должно произойти только при первом прогоне, после чего функция 23 раза в день будет лишь выполнять проверку.
Также необходимо удалить устаревшие разделы.
Этот код создает тот же самый ALTER TABLE ... DROP PARTITION
, который был показан выше.
И, наконец, все готово к реорганизации.
Заключение
Как можно видеть, вопреки распространенному мнению, совместимые с ACID решения СУБД, такие как MySQL, могут использоваться, когда обрабатываются большие объемы данных, поэтому необязательно отказываться от функций транзакционных баз данных.
Тем не менее, разделение таблиц имеет довольно много ограничений, а это означает, что невозможно будет использовать все возможности, которые InnoDB предоставляет для обеспечения гарантированного сохранения данных. Также может потребоваться обработать в логике приложения то, что без секционирования было бы реализовано средствами СУБД. Например, ограничения внешнего ключа или полнотекстовый поиск.
6К открытий6К показов