OPTIMIZE огромных таблиц в условиях ограниченных ресурсов или закат солнца вручную

Предыстория


Есть проект, в рамках которого приходится работать с большим объем данных. В частности есть одна денормализованная таблица, в которой хранятся все актуальные предложения существующих клиентов, а также устаревшие предложения, помеченные is_deleted = 1, ожидающие удаления.

Количество записей в данной таблице до недавнего времени колебалось от 30 до 50 миллионов. Обычный OPTIMIZE даже при таких условиях не всегда срабатывал. Поэтому отец-основатель (Евгений Васильевич) придумал пересобирать таблицу таким образом: все актуальные (is_deleted = 0) копировались в таблицу с идентичной структурой с добавлением префикса по дате и времени, а когда копирование завершалось, оставалось только удалить исходную таблицу, а новую переименовать в исходную.

Такой подход работал надежно, пока не потребовалось повысить скорость поиска предложений. И тут начинается наша небольшая история.

Ветер перемен


Для повышения скорости поиска, как ни странно, было решено использовать поисовик. Мы выбрали Solr. Почему? Потому что для наших целей он хорош. Да и не только для наших целей. Если будет время, обязательно напишу статью, посвященную этому поисковому движку.

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

Проблема заключалась в том, что Solr располагался на том же серваке, что и мускуль. Для поиска предложений по сайту это было нормально. Но Solr хоть и шустрый зверь, зато требует для своей работы немало ресурсов. Впрочем, любое аналогичное решение делило бы ресурсы сервера пополам между мускулем и поисковым движком. Соответственно, скрипт пересбора предложений падал с ошибкой о нехватке места в tmpfs.

Поиск решения


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

Первый вариант. Заключался он в попытке как-то оптимизировать уже существующий скрипт. Но успеха мы не добились, и практически сразу от него отказались.

Второй вариант — использовать HandlerSocket. HS — это быстро, это надежно, и это в конце концов модно. Впрочем, оказалось, что HS не подходит для считывания огромного массива данных. HS отлично справляется с быстрым поиском хаотично расположенных отдельных записей. А при последовательном считывании большого массива данных частями происходит замедление на каждом следующем шаге, если использовать limit, offset. Но это не самая большая проблема при использовании HS, — нам нужно было делать выборку по условию is_deleted = 0, а это поле не являлось индексным. И вообще говоря, делать его таковым бессмысленно. Поэтому бравый HandlerSocket, который уже хорошо зарекомендовал себя для других задач, в этот раз надежд не оправдал.

К счастью, был еще третий вариант, которым лично я до этого случая никогда не пользовался. Это — нативный мускульный HANDLER. Что он позволяет делать и чем он хорош? Он позволяет осуществлять последовательное считывание записей по определенному условию (даже не по индексному полю) без потери скорости, которая происходит обычно за счет высчитывания limit, offset или between. Все, что вам нужно сделать, — это открыть handler, считать первую порцию данных с определенным условием (READ FIRST) и дальше, не меняя условия, осуществлять READ NEXT, пока есть хоть какие-то данные. Последовательность действий вызывает ассоциацию с C-шным подходом, например, к сканированию директории. И самое радостное здесь в том, что указатель остается на том месте, где мы его оставили в последний раз.

В итоге мы имеем постоянную высокую скорость считывания и разумное использование памяти при считывании данных по всей таблице, даже если в таблице скопилось 270 миллионов записей. Ровно столько было записей к тому моменту, как мы нашли данное решение. Много это или мало? Вопрос относительный. Но если сервис из-за такого объема начинает давать сбои, значит много.

В условиях ограниченных ресурсов это решение оказалось наиболее выигрышным по скорости и по надежности. Я хочу сказать, что если вы смогли считать первые 10000 записей за одну итерацию, значит, вы считаете и все последующие записи, сколько бы их ни было.

PS


Думаю, что данное решение кому-нибудь обязательно пригодится.

Благодарности


Хочу сказать спасибо Юрию Масленникову, который собственно и предложил идею про handler.
Поделиться публикацией

Комментарии 7

    +1
    Если я не ошибаюсь, HANDLER работает только с таблицами MyISAM.
      +2
      Как оказалось, нет. По InnoDB прекрасно прошёлся. Хотя в документации сказано только про MyISAM.
      0
      А просто /tmp/ вынести из tmpfs на жесткий диск, не?
      Вместо фиксированной памяти под /tmp/ ограничиться дисковым кешем.
        +1
        Во-первых, работа с диском априори медленнее. Во-вторых, вы предлагаете по сути перенести проблему из одного места в другое, и в какой-то момент всё равно начнутся падения.
        Предложенный же способ гарантирует быструю, надёжную работу скрипта.
        0
        А почему место в tmpfs кончалось? INSERT FROM SELECT использовали?
        Вручную 1 -> SELECT без лимита -> читаем 1000 строк -> INSERT -> 1 не пробовали?
        Для INNODB ещё можно было бы в вашем случае заменить OFFSET на id>last_processed_id + ORDER BY id если id-первичный ключ сортировка по которому бесплатна
          0
          INSERT FROM SELECT, конечно, удобная конструкция. Но вопрос не в этом. В данной статье я постарался осветить вопрос последовательного считывания большого количества данных. И основная загвоздка в том, что как бы вы не задавали условие в WHERE, происходит замедление на каждой следующей итерации цикла. Ни id>last_processed_id limit 1000, ни id between last_processed_id and last_processed_id+1000 не дают такую скорость, как HANDLER, потому что HANDLER'у не нужно тратить время на позиционирование указателя на первую запись.
            0
            тут можно использовать даже не insert from select, а сразу create from select.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.