Как стать автором
Обновить
48.4

Как сделать alter table в высоконагруженной таблице

Время на прочтение3 мин
Количество просмотров16K

Ещё лет 5 назад на собеседованиях с backend-разработчиками был популярен вопрос: как переименовать колонку в высоконагруженной таблице (возможны вариации: как сделать любой другой alter table)? Ответ мог быть примерно следующий:

Для начала создаём новую колонку (или даже таблицу с нужной структурой). Далее делаем триггеры на старую колонку, которые вносят все поступающие изменения на новую колонку. Параллельно с этим синхронизируем уже добавленные данные из старой колонки в новую. Через какое-то время данные актуализируются, и задача будет решена.

Вопрос всё ещё часто звучит на собесах. И актуальность его выросла, так как за последние 5 лет стало намного больше масштабных проектов с высокой нагрузкой.

В MySQL существуют специальные инструменты для решения подобных задач: pt-online-schema-change и gh-ost. Принцип их действия практически такой же, как описанный выше.

Хочу рассказать, как мы делали alter table в нашем микросервисе с высоконагруженной таблицей (golang + postgres, ~1500rps, ~15 млн. записей).

Постановка задачи

В одном из проектов, над которым мы работаем в Каруне, задача стояла немного сложнее, чем просто "переименовать колонку": необходимо было захешировать данные одной колонки. Мы решили сделать это через создание новой колонки и перенести функционал на неё.

Шаг 1. Добавляем колонку

Для начала мы создали колонку и повесили на неё индекс:

ALTER TABLE my_table ADD COLUMN IF NOT EXISTS field_encrypted TEXT;

CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS field_encrypted_unique_idx 
	ON my_table (field_encrypted);

Выполненные операции не создавали никаких блокировок.

Шаг 2. Переливаем старые данные

Перезалив данных из старой колонки в новую мы реализовали через cron библиотеку языка golang. Такое решение приняли из-за удобного логирования возникающих проблем и возможности быстрого реагирования на них. Да и сторонников делать через логику базы данных у нас не нашлось.

Конечно, сразу же возникли трудности следующего вида. В принципе, аффектить базу данных сильно нельзя — это увеличивает время ответа сервиса (>1 sec — это уже плохо) и количество блокировок. Зависимые сервисы могут не обработать ответы, появится неконсистентность. Или может спровоцироваться ещё более сильная нагрузка на базу данных, и сервис может не выдержать.

Среднее время ответа сервиса
Среднее время ответа сервиса

Поэтому:

  • Все изменения для новой колонки делались батчами, а не по одной записи (batch insert, batch update).

  • У нас запущено несколько инстансов этого микросервиса. Поэтому необходимо было не допустить многократный запуск этого скрипта в одно время. Тут нам помогла advisory lock для postgres (каждый инстанс проверял блокировку и только 1 выполнял джобу):

SELECT pg_try_advisory_xact_lock(1);
  • Периодически для базы запускался достаточно тяжелый VACUUM:

Время работы vacum'a
Время работы vacum'a

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

SELECT check_vacuum('my_table');

Сама функция check_vacuum:

CREATE FUNCTION check_vacuum(name text) RETURNS boolean
    LANGUAGE sql SECURITY DEFINER
AS $_$
SELECT count(*)::int > 0
FROM pg_stat_progress_vacuum
WHERE relid::regclass = $1::regclass OR
        relid::regclass::text in (
        SELECT reltoastrelid::regclass::text FROM pg_class WHERE relname = $1
    );
$_$;

После того, как мы перегнали все данные, необходимостью стало переключение на работу с новой колонкой.

ШАГ 3. Вешаем constraint

Для новой колонки требовался constraint not null. Но напрямую сделать этого нельзя, иначе можно надолго заблокировать таблицу, что недопустимо. Поэтому в postgres существует следующий механизм:

ALTER TABLE my_table ADD CONSTRAINT field_ecnrypted_not_null
    CHECK (field_encrypted IS NOT NULL) NOT VALID;

Он позволяет повесить ограничение not null, но не валидировать существующие записи сразу, а сделать это следующим шагом и без блокировок:

ALTER TABLE my_table VALIDATE CONSTRAINT field_ecnrypted_not_null;

Ну и в заключение мы сделали то, к чему шли:

ALTER TABLE my_table DROP COLUMN old_field;

Заключение

Данные шаги позволили бесшовно, незаметно для пользователей нашего микросервиса переименовать колонку и добавить хеширование к ней. Данный подход мы реализовали примерно за неделю — у нас не было дедлайна. Но если вдруг вы едете на горящем велосипеде, и всё вокруг горит, то можно справиться и за пару дней.  

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

Теги:
Хабы:
+23
Комментарии29

Публикации

Информация

Сайт
karuna.group
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия