Привет, Хабр! Меня зовут Семен Попов, я отвечаю за разработку хранилища клиентских данных в Сбере. Расскажу, как мы в эксплуатации перешли с одной базы данных на другую — а именно, с Oracle на Platform V Pangolin, разработку СберТеха на основе PostgreSQL, доработанного в области безопасности, надёжности и производительности.
Что мы переносим
В децентрализованном хранилище находятся данные клиентов. С точки зрения слоя хранения это выглядит примерно так:
У нас есть Ignite, который мы используем как кеш для быстрых поисков по актуальным клиентским данным, несколько баз данных, в которых храним историю всех изменений, а также Kafka и ZooKeeper.
Я неспроста сказал «несколько баз данных»: когда возникла необходимость перейти с Oracle на Platform V Pangolin, наше решение уже было шардированным. У нас было 4 шарда суммарным объёмом 30 Тб, примерно столько же в Ignite (с учётом трёхкратного резервирования). Нагрузка, в основном, на чтение, и она ложится на Ignite, но мы обязаны поддержать такую же нагрузку и на БД, а это около 30 000 операций в секунду на чтение и ещё 5000 операций в секунду на запись.
Для распределения данных по шардам мы выбрали «самый главный» параметр — идентификатор клиента. Он хорош сразу по двум причинам: не меняется, и основное количество операций чтения происходят именно по нему. Затем мы считаем от него хеш, по нему идентифицируем партицию, а по партиции определяем шарды, на которые должна произойти запись или чтение.
Greenfield-шард
Один из наших шардов отличается от остальных: он существенно меньше по размеру, поэтому на него приходит меньшая нагрузка и на чтение, и на запись. Логично начать наши эксперименты именно с него.
Мы настроили систему так, чтобы данные, которые пишутся на наш greenfield-шард, реплицировались также и на новый шард на PostgreSQL. У нас есть механизмы для обеспечения гарантированной доставки этой асинхронной реплики: мы отправляем квитки о записи обратно на шард-источник и анализируем ошибки и отсутствие ответов.
Для того, чтобы асинхронная реплика поддерживала наш новый шард в актуальном состоянии, нам необходимо сначала до этого актуального состояния шард довести, то есть выполнить первоначальную миграцию данных. И в этом нам пригодились заранее подготовленные механизмы массовых операций.
Поддержка нового диалекта
«А как вы поддержали работу на новой БД на вашем шарде?», — спросите вы. И будете абсолютно правы: самостоятельно поддержать хранение в новой БД наше решение не может: мы не используем ORM, но у нас уже была подготовленная инфраструктура под несколько диалектов. Мы переписали около 20 небольших хранимых процедур и провели косметические правки порядка 50 SQL‑запросов. В эти моменты мы были рады, что у нас есть тесты, автотесты и процесс нагрузочного тестирования.
Всё готово, приступаем!
План по переходу на PostgreSQL-шард у нас выглядел вот так:
Настраиваем новый шард как асинхронную реплику старого.
Массово мигрируем данные в новый шард.
Меняем шарды местами.
Отключаем старый шард насовсем.
…
Profit!
Подмена «маленького» greenfield-шарда прошла быстро и без проблем, а вот с тремя другими мы получили разнообразный «опыт» при проведении нагрузочного тестирования и эксплуатации. Им я с вами сейчас и поделюсь.
Партиционирование
Если вы задаётесь вопросом «Что делать с таблицей в 1 Тб?», на ум приходит: партиционировать. В идеале, с точки зрения изменения прикладного кода, вам даже SQL переписывать не придётся. База данных сама разберётся, как оптимально выполнить запрос, а вы получите все преимущества от того, что таблицы, с которыми вы будете работать, стали меньше: их проще обслужить, нужно прочитать меньше данных и т. д.
В Oracle мы использовали партиционированные таблицы и индексы, поэтому знали, чего примерно ожидать в планах исполнения запросов. Но вот что мы не планировали увидеть:
Такая ситуация — не что‑то исключительное, она будет случаться всегда, когда в запросе к партиционированной таблице нет критериев, по которым эта таблица партиционируется.
В случае из примера у нас есть таблица, которая партиционируется по колонке id
, а в запросе этого идентификатора нет. Поэтому, чтобы исполнить этот запрос, базе данных придётся пробежаться по всем партициям и поискать внутри них, «локально».
Но если можно поискать «локально», то, наверно, и «глобально» тоже? Можно, но если ваша конкретная инсталляция PostgreSQL это поддерживает. Например, Platform V Pangolin поддерживает в том числе и глобальные индексы.
Подытоживая тему партиционирования: это эффективная и полезная технология, которую можно и нужно применять, но с вниманием к деталям.
Автоочистка
В этой главе я дам рекомендации по настройке базы данных. Некоторые опции (скорее всего, все) вам в любом случае придётся сконфигурировать, если вы планируете добиться от базы данных приемлемой производительности. Но помните, что любые изменения настроек необходимо проводить через ваше нагрузочное тестирование.
На иллюстрации выше вы видите два письма, которые я получил с разницей в пару дней. Одно из них c прода, другое — со стенда нагрузочного тестирования. В обоих говорится, что у нас «случается какая‑то автоочистка», которая вызывает «какие‑то проблемы» и их решение не стоит откладывать в долгий ящик. Откуда вообще возникает необходимость работы этой автоочистки? Коротко и ёмко ответить вряд ли получится, но, в основном, это из‑за MVCC.
Аббревиатура MVCC (multiversion concurrency control) дословно расшифровывается как «контроль конкурентного доступа на основе механизма многоверсионности записей в базе данных». Уф. То есть запись одна, а версий много. Когда база данных решает, что какая‑то версия ей больше не нужна, эту версию можно почистить. Кто это сделает? Автоочистка!
Если такого беглого объяснения вам не хватило, то крайне рекомендую почитать вот эту серию статей. Я и дальше буду ссылаться на некоторые выдержки из этого цикла.
Автоочистка, приходи
А когда эта самая автоочистка к нам придёт? Тогда, когда база данных решит, что на основании собранной статистики в таблице накопился достаточный объём данных для чистки. Получается, сначала нам нужна эта статистика.
Статистика, соберись
За сбор статистики отвечает процесс, который называется автоанализом. Есть две основных настройки:
autovacuum_analyze_threshold
— минимальное количество модификаций в таблице;autovacuum_analyze_scale_factor
— доля модифицированных строк в таблице.
Доля строк пересчитывается в абсолютное значение (ведь мы знаем, сколько всего записей в таблице). Затем прибавляем минимальное значение, и если сумма получилась не меньше, чем количество модификаций в таблице с последнего автоанализа, значит, пора.
autovacuum_analyze_threshold + autovacuum_analyze_scale_factor * количество записей >= количество модификаций в таблице
Количество модификаций (n_mod_since_analyze
) можно посмотреть по представлению pg_stat_all_tables
, а настройки по автоанализу можно задавать в том числе и для конкретной таблицы. Больше полезной информации можно найти в этой статье.
Например, в наших инсталляциях для больших таблиц с миллионами значений и редким обновлением мы установили значение параметра autovacuum_analyze_scale_factor
равным 0,01. То есть, когда мы модифицируем 1 % строк, в таблицу сможет прийти автоанализ.
Теперь-то приходи, автоочистка
Настройки для автоочистки вызывают ощущение дежавю:
autovacuum_vacuum_threshold
— минимальное количество модификаций в таблице;autovacuum_vacuum_scale_factor
— доля модифицированных строк в таблице.
Логика та же: долю пересчитываем в абсолютное количество строк, прибавляем минимальное количество, сравниваем с количеством строк, которые уже не видны ни в одной транзакции и считаются ненужными.
autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * количество записей >= количество ненужных строк в таблице
Количество строк, которые считаются ненужными (n_dead_tup
), находится там же — в представлении со статистикой pg_stat_all_tables
. Задавать настройки можно для конкретных таблиц, а подробнее можно узнать всё в той же статье.
Для тех же самых крупных и редко модифицируемых таблиц мы установили долю обновлений в 2 %, то есть autovacuum_vacuum_scale_factor = 0,02
.
Надо быть более эффективными, говорили они…
Вроде бы разобрались, когда запускается автоочистка. Давайте сделаем ее максимально эффективной. Наверняка есть какая‑то магическая настройка, которую в два раза увеличил — получил в два раза более мощную автоочистку. Есть: это количество «работников» (autovacuum_max_workers
). Может быть, сделаем работников как можно больше и забудем о проблемах с автоочисткой навсегда? Скорее всего, такое решение не решит проблем, а добавит новых. Например, все ресурсы системы будут отданы под «очень эффективное» выполнение автоочистки, а на ваши транзакции ресурсов уже не останется. Имеет смысл установить значение, опираясь на количество доступных ядер в системе, исходя из того, сколько ресурсов от общего количества вы готовы отдать на это доброе дело. За отправную точку можно считать четверть доступных ядер.
Тогда давайте сделаем, чтобы каждый работник был эффективнее.
Что нужно каждому работяге? Побольше отдыхать и поменьше работать. Типичный жизненный цикл работника: отдых, немного работы, передышка, немного работы, передышка, и так до тех пор, пока работник не решит, что он сделал достаточно работы для того, чтобы снова отдохнуть.
Тут к нам на помощь приходят рекомендации от сопровождения Platform V Pangolin. Для начала, давайте уменьшим у работников время на отдых:
autovacuum_naptime = 10sec
Сократим время передышки:
autovacuum_vacuum_cost_delay = 1ms
И дадим задание — работать побольше:
autovacuum_vacuum_cost_limit = 4000
Любая операция, которую делает PostgreSQL, — просканировать индекс, прочитать данные из буферного кеша и т. д. — чего‑то стоит. Операции, которые делает в процессе своей работы автоочистка, — не исключение. Когда сумма всех выполненных работ с предыдущего отдыха достигает значения autovacuum_vacuum_cost_limit
, работник уходит на отдых, а затем и на очередной круг работ. Стоимость разных работ можно настраивать, обратите внимание на настройки, которые заканчиваются на _cost
. Стоимость работ учитывается не только при работе автоочистки, но и при планировании выполнения запросов. Особенно любознательные могут посмотреть это видео.
Если вы всё настроили правильно, то автоочистка у вас запускается периодически, выполняет всю необходимую работу и не оказывает существенного влияния на выполнение полезной нагрузки.
Вернёмся к письму
В нем каким-то образом завязан уже известный нам автовакуум и какой-то счетчик транзакций. Давайте разбираться, что это и зачем это нужно!
Но сначала давайте поговорим про время. Вроде бы, всё просто: есть прошлое, настоящее и будущее. Но они могут быть разными в зависимости от того, в каком моменте времени мы находимся.
Если взглянуть на эту концепцию с точки зрения базы данных и MVCC, каждая транзакция видит свою версию окружающего мира: в зависимости от времени начала этой транзакции и одного из её свойств — уровня изоляции. Уровень изоляции позволяет ограничивать в базе данных видимость изменений, которые произошли после начала транзакции.
Давайте попробуем организовать наивное ограничение видимости данных. Если фиксировать время события, то можно определить, какие изменения произошли до начала транзакции (и они, например, видны), а какие — после (и они не видны). А вместо времени можно использовать счётчик. Подробнее — всё в том же цикле статей.
Счётчик транзакций
В PostgreSQL для каждой записи есть значение этого счётчика, а иногда и по две записи. Счётчик — очень хорошее решение, пока мы не достигнем максимального значения. В некоторых реализациях PostgreSQL счётчик может быть 64-битным, в том числе и в Platform V Pangolin, начиная с 6-й версии. Но в нашей 5-й версии он 32-битный (как и в ванильном PostgreSQL), а это чуть более 4 миллиардов значений. Этого количества транзакций легко достичь при сколь бы то ни было адекватной нагрузке.
Что можно сделать с таким счётчиком? Если разрешить ему обнуляться, то можно сделать его бесконечным.
Циклический счетчик транзакций
Такой подход хорош всем, кроме одного: когда мы двигаем счётчик вперед, какой-то процесс должен позаботиться о том, чтобы предыдущие значения счётчика уже не использовались. Иначе какие-то данные внезапно окажутся в будущем и станут не видны пользователю.
Тут есть две новости. Первая — PostgreSQL такого не допустит. Но нам точно не понравится, как: он аварийно завершит работу. Вторая — есть достаточное количество механизмов защиты, чтобы такого не случилось.
Итак, нам нужно освобождать номера транзакций из прошлого, чтобы их можно было переиспользовать в будущем. Этот механизм в PostgreSQL называется заморозкой. Как вы можете предположить, имеет смысл совместить заморозку с другим процессом, который ходит и наводит порядок в таблицах. То есть с автоочисткой.
Когда в таблицу приходит автоочистка, он проходит только по страницам, в которых есть данные для очистки. Если повезёт, и в этой же странице есть записи, номер транзакции которых можно заморозить, то автоочистка это сделает.
Процесс заморозки жесток, он замораживает возрастные записи, давая дорогу молодым 😊
Как можно понять, нужно ли замораживать номер транзакции или нет и что такое возраст этого номера? Это разница между текущей позицией счётчика и номером транзакции записи. За значение возраста, превышение которого будет приводить к заморозке значения счётчиков в процессе автоочистки, отвечает настройка vacuum_freeze_min_age
.
Но на один этот механизм полагаться нельзя, ведь мы прошлись только по страницам с данными на удаление. И у PostgreSQL есть ещё один туз в рукаве — настройка vacuum_freeze_table_age
. Если в таблице будут записи старше этого возраста, то «ледникового периода» не избежать: автоочистке придётся пробежаться по всем возрастным записям и заморозить их номера транзакций.
И последний бастион — это настройка autovacuum_freeze_max_age
. Если автоочистка так и не зашла в нашу таблицу, а между тем возраст самой старой записи уже приблизился к значению этой настройки, то наступило время для агрессивного срабатывания. Именно это событие и послужило причиной описанных выше писем.
А вот дальше — только аварийное завершение работы.
Подробнее — в этой статье.
А теперь коротко рассмотрим настройки, на которые вам точно нужно обратить внимание. Сразу оговорюсь: всё применяйте на свой страх и риск. Лучше всего, через нагрузочное тестирование.
Начнём с увеличения.
shared_buffers
Например, до ¼ от всего RAM. В таком случае будет закешировано больше данных, и потенциально придётся читать с диска меньше информации и реже висеть в блокировке в ожидании освобождения буферов.
Следующая секция настроек предназначена для сброса буферов на диск как можно раньше. И чтобы ситуация, когда вам нужен свободный буфер, а они все «грязные», не случилась, по возможности, никогда.
bgwriter_delay = 10ms
bgwriter_flush_after = 0
bgwriter_lru_maxpages = 4000
bgwriter_lru_multiplier = 10
Теперь нам и чекпоинты особо не нужны, сделаем их максимально неинтенсивными:
checkpoint_timeout = 30min
checkpoint_completion_target = 0.9
Стоит уменьшить размер генерируемого объёма WAL с помощью сжатия и сделать их смену пореже с помощью увеличения размера:
wal_compression = ON
max_wal_size = 32GB
Помните, что сжатие требует дополнительных затрат CPU. Поэтому, выбирая, включать сжатие или не включать, необходимо исходить из потребления ресурсов системы.
Последняя настройка в моём списке — max_parallel_workers_per_gather
. Если упростить, то её смысл в том, насколько один запрос может распараллелиться на определённых шагах исполнения. После экспериментов на стенде нагрузочного тестирования мы пришли к выводу, что наибольшую пропускную способность мы получаем, если установить значение этой настройки в 0, то есть выключить распараллеливание. Скорее всего, это стоит сделать и вам, если ваша система — какая‑нибудь OLTP, как и наша.
Эту и другую полезную информацию лично я утащил почерпнул из этого доклада.
Вместо заключения
При переходе на новую базу данных хочется быть уверенными, что она сопровождается: мониторится, есть инструменты для управления и достаточная экспертиза для недопущения или оперативного решения проблем. И всё это есть у Platform V Pangolin — реляционной СУБД промышленного уровня.
Она соответствует высоким стандартам хранения персональных и критичных данных, обеспечивает лёгкое масштабирование благодаря репликации данных и уровень отказоустойчивости 99,99 % по опыту эксплуатации. Подойдёт как для масштабных корпоративных приложений с большим объёмом структурированных данных, так и для мобильных и десктопных приложений, где важна высокая пропускная способность и доступность баз данных.