Переезжаем с Oracle на Platform V Pangolin в промышленной эксплуатации

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет, Хабр! Меня зовут Семен Попов, я отвечаю за разработку хранилища клиентских данных в Сбере. Расскажу, как мы в эксплуатации перешли с одной базы данных на другую — а именно, с 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-шард у нас выглядел вот так:

  1. Настраиваем новый шард как асинхронную реплику старого.

  2. Массово мигрируем данные в новый шард.

  3. Меняем шарды местами.

  4. Отключаем старый шард насовсем.

  5. …

  6. Profit!

Подмена «маленького» greenfield-шарда прошла быстро и без проблем, а вот с тремя другими мы получили разнообразный «опыт» при проведении нагрузочного тестирования и эксплуатации. Им я с вами сейчас и поделюсь.

Партиционирование

Если вы задаётесь вопросом «Что делать с таблицей в 1 Тб?», на ум приходит: партиционировать. В идеале, с точки зрения изменения прикладного кода, вам даже SQL переписывать не придётся. База данных сама разберётся, как оптимально выполнить запрос, а вы получите все преимущества от того, что таблицы, с которыми вы будете работать, стали меньше: их проще обслужить, нужно прочитать меньше данных и т. д.

В Oracle мы использовали партиционированные таблицы и индексы, поэтому знали, чего примерно ожидать в планах исполнения запросов. Но вот что мы не планировали увидеть:

Такая ситуация — не что‑то исключительное, она будет случаться всегда, когда в запросе к партиционированной таблице нет критериев, по которым эта таблица партиционируется.

В случае из примера у нас есть таблица, которая партиционируется по колонке id, а в запросе этого идентификатора нет. Поэтому, чтобы исполнить этот запрос, базе данных придётся пробежаться по всем партициям и поискать внутри них, «локально».

Но если можно поискать «локально», то, наверно, и «глобально» тоже? Можно, но если ваша конкретная инсталляция PostgreSQL это поддерживает. Например, Platform V Pangolin поддерживает в том числе и глобальные индексы.

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

Автопылесос

В этой главе я дам рекомендации по настройке базы данных. Некоторые опции (скорее всего, все) вам в любом случае придётся сконфигурировать, если вы планируете добиться от базы данных приемлемой производительности. Но помните, что любые изменения настроек необходимо проводить через ваше нагрузочное тестирование.

На иллюстрации выше вы видите два письма, которые я получил с разницей в пару дней. Одно из них из эксплуатационной среды, другое — со стенда нагрузочного тестирования. В обоих говорится, что у нас «случается какой‑то автопылесос», который вызывает «какие‑то проблемы» и их решение не стоит откладывать в долгий ящик. Откуда вообще возникает необходимость работы этого автопылесоса? Коротко и ёмко ответить вряд ли получится, но, в основном, это из‑за 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 называется заморозкой. Как вы можете предположить, имеет смысл совместить заморозку с другим процессом, который ходит и наводит порядок в таблицах. То есть с автопылесосом.

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

Процесс заморозки жесток, он замораживает возрастные записи, давая дорогу молодым

Источник: https://habr.com/ru/companies/sberbank/articles/836084/


Интересные статьи

Интересные статьи

Не так давно я опубликовал заметку о некотором варианте иерархического запроса, который выполняется очень долго и неограниченно и не потребляет иных ресурсов базы данных кроме CPU.Возникла довольно ес...
Российская «дочка» американской компании — разработчика систем управления базами данных Oracle «Оракл компьютерное оборудование» так и не выплатила 1,43 млрд рублей долгов российским ко...
Пример использования триггеров, при взаимном обновлении таблиц. При таких задачах возникает ошибка мутирования таблиц ORA-04091: table is mutating.
Факт: за годы ВОВ около 600 таких полуторок (ГАЗ-ММ), что перед заградительным аэростатом, были переведены на водород.Мало кто знает, что впервые водород начали массово применять в автомобильных двига...
Недавно я с удивлением обнаружил, что, оказывается, толковых туториалов по Oracle Application Express (он же APEX, он же апекс, но не путать с языком apex, который используется в Salesforce) в пр...