За последние несколько лет многие компании перешли с MongoDB на PostgreSQL, в том числе известное онлайн-издание The Guardian. В статье говорим о причинах перехода и разбираемся, действительно ли PostgreSQL лучше MongoDB.
Примечание: дальнейшее повествование ведётся от лица команды The Guardian.
Погружаемся в контекст
В The Guardian большая часть контента, включая статьи, блоги, галереи и видеоконтент, создаётся с помощью собственной CMS-системы Composer. Изначально она принадлежала Guardian Cloud — центру обработки данных, находящемся в подвале офиса недалеко от Кингс-Кросс с аварийным переключением в другом месте Лондона. Одним жарким июльским днём процедуры аварийного переключения подверглись довольно суровому испытанию, после чего вопрос миграции Guardian на AWS стал особенно актуальным.
Мы решили приобрести OpsManager — программное обеспечение Mongo для управления базами данных. Из-за редакционных требований запустить кластер базы данных и OpsManager нужно было в собственной инфраструктуре в AWS, а не использовать предложение Mongo. Однако никаких инструментов для настройки на AWS предоставлено не было. Нам пришлось вручную написать cloudformation и вдобавок к этому подготовить сотни строк ruby-скриптов для обработки установки агентов мониторинга и автоматизации, оркестровки новых экземпляров БД.
С момента перехода на AWS у нас было два серьезных сбоя из-за проблем с базой данных, каждый из которых препятствовал публикации на theguardian.com. В обоих случаях ни OpsManager, ни агенты поддержки Mongo не смогли помочь, и мы решали проблемы самостоятельно. Каждая из проблем сама по себе могла бы стать поводом для отдельной публикации в блоге, но общие выводы следующие:
часы важны — не блокируйте VPC настолько, чтобы NTP перестал работать.
автоматически генерировать индексы базы данных при запуске приложения — плохая идея;
управление базой данных важно и сложно, желательно делать это самостоятельно.
В действительности OpsManager не подходил для «простого управления базой данных». Даже фактическое управление самим OpsManager (скажем, обновление с OpsManager 1 до 2) требовало много времени и знаний. Об «обновлении в один клик» тоже не могло идти и речи из-за изменений в схеме аутентификации между различными версиями Mongo DB. Все эти проблемы в сочетании с огромными счетами заставили нас искать альтернативный вариант со следующими требованиями:
минимальное управление базой данных;
возможность шифрования в состоянии покоя;
возможность миграции из Mongo DB.
Поскольку остальные сервисы работали в AWS, очевидным выбором стала DynamoDB — предложение Amazon на базе данных NoSQL. Однако в то время Dynamo не поддерживало шифрование в состоянии покоя. Прождав около девяти месяцев, пока эта функция появится, мы сдались и начали искать что-то другое. В конечном счёте выбор пал на Postgres на AWS RDS.
«Но postgres — это не хранилище документов!» — это не совсем так. У Postgres есть тип столбца JSONB с поддержкой индексов для полей в большом двоичном объекте JSON. И мы надеялись, что с помощью JSONB сможем перейти с Mongo на Postgres с минимальными изменениями в модели данных. Кроме того, у Postgres была замечательная особенность — зрелость. На каждый вопрос в большинстве случаев уже был ответ на Stack Overflow.
Перенос контента за два десятилетия без простоев
План
Большинство миграций баз данных включают одни и те же шаги. Эта не стала исключением. Вот шаги, которые мы предприняли для переноса:
создать новую базу данных;
создать способ записи в новую базу данных (новый API);
создать прокси, который будет отправлять трафик как в старую, так и в новую базу данных, используя старую в качестве основной;
перенести записи из старой базы данных в новую;
сделать новую базу данных основной;
удалить старую базу данных;
Было важно, чтобы миграция вызвала как можно меньше неудобств для журналистов Guardian, ведь поток новостей никогда не останавливается.
Новый API
Упрощенная архитектура CMS была примерно такой: база данных, API и несколько взаимодействующих с ней приложений. Стек был (и остаётся) построен с использованием Scala, Scalatra Framework и Angular.js — ему около четырех лет.
После небольшого исследования мы пришли к выводу: прежде чем переносить существующий контент, нужно разработать способ взаимодействия с новой базой данных PostgreSQL. Причём старый API должен работать как обычно, ведь база данных Mongo — основной источник информации. Это дало подушку безопасности во время экспериментов с новым API.
В исходном API было очень мало разделения ответственности, и особенности MongoDB можно было найти даже на уровне контроллера. В результате задача добавления другого типа базы данных в существующий API была слишком рискованной.
Мы решили идти другим путём и продублировали старый API. Так родился APIV2 — более-менее точная копия Mongo с теми же конечными точками и функциями. Мы использовали doobie, функциональный слой JDBC для Scala, добавили Docker для локального запуска и тестирования, а также улучшили логирование и разделение задач.
В итоге был развёрнут новый API, который использовал PostgreSQL в качестве базы данных. Но это было только начало. В базе данных Mongo есть статьи, впервые созданные более двух десятилетий назад, — их тоже нужно было переместить в базу данных Postgres.
Миграция
Прежде чем отключать Mongo, нужно было убедиться, что один и тот же запрос к API на основе Postgres и API на основе Mongo вернёт идентичные ответы. Для этого предстояло скопировать весь контент в новую базу данных Postgres. Для этого мы воспользовались скриптом, который общался напрямую к старым и новым API.
План миграции:
получить контент от Mongo;
опубликовать контент в Postgres;
получить контент от Postgres;
убедиться, что ответы идентичны.
Можно сказать, что миграция базы данных прошла успешно только в том случае, если конечные пользователи ничего об этом не узнали. Держа это в голове, мы искали скрипт, который бы мог:
делать HTTP-запросы;
проверять, что после переноса части контента ответ от обоих API совпал;
останавливаться в случае ошибки;
создавать подробные логи, чтобы помочь диагностировать проблемы;
перезапускаться с правильной точки после ошибки.
Мы начали с Ammonite — скрипта, позволяющего писать сценарии на языке Scala (он считается основным в команде). Хотя Ammonite позволил использовать знакомый язык, у него были и недостатки. Сейчас Intellij поддерживает Ammonite, но в то время это было не так, что означало потерю автозаполнения и автоматического импорта. Также было невозможно запускать скрипт Ammonite в течение длительного времени.
Постепенно выяснилось, что Ammonite — не самый подходящий инструмент, и мы решили попробовать для выполнения миграции sbt. Это позволило работать на языке, в котором мы были уверены, и выполнять «тестовые миграции».
Перенесёмся в то время, когда требовалось протестировать полную миграцию в нашу тестовую среду CODE.
Единственное сходство между CODE и PROD — версия приложения, которое они запускают. Инфраструктура AWS, поддерживающая среду CODE, была менее мощной, чем та, что поддерживала PROD, просто потому что использовалась гораздо реже.
Запуск миграции на CODE помог нам:
оценить, сколько времени займёт миграция в PROD;
оценить, какое влияние окажет миграция на производительность.
Чтобы получить точные значения этих показателей, нужно было сопоставить две среды. Это включало восстановление резервной копии базы данных PROD mongo в CODE и обновление поддерживаемой AWS инфраструктуры.
Чтобы оценить ход миграции, мы отправили структурированные логи с использованием маркеров в стек ELK. Отсюда мы могли бы создавать подробные информационные панели, отслеживая количество успешно перенесенных статей и сбоев, а также общий прогресс.
После завершения миграции мы использовали те же методы для проверки каждого документа в Postgres на соответствие Mongo.
Прокси и работа в продакшене
Прокси
Когда новый API на основе Postgres заработал, нужно было протестировать его с реальным трафиком и шаблонами доступа к данным, чтобы убедиться, что он надёжный и стабильный. Этого можно было добиться двумя способами. Первый — обновить каждый клиент, взаимодействующий с Mongo API, чтобы он взаимодействовал с обоими API. Второй — запустить прокси, который сделает это.
Мы написали прокси на Scala с помощью Akka Streams. Прокси был довольно прост в своей работе:
принимал трафик от балансировщика нагрузки;
направлял трафик на основной API и возвращал;
асинхронно перенаправлял тот же трафик на вторичный API;
вычислял разницу между ответами и записывал её.
Вначале прокси-сервер регистрировал много различий между ответами двух API, выявляя важные поведенческие проблемы, которые необходимо было исправить.
Структурированное логирование
Всё логирование ведётся с помощью стека ELK. Изначально мы работали с Kibana, поскольку инструмент использовал синтаксис запросов lucene, который довольно легко освоить. Однако вскоре стало понятно, что отфильтровать логи или сгруппировать их в текущей настройке невозможно. Например, не удавалось отфильтровать логи, отправленные в результате GET-запросов.
Наше решение состояло в том, чтобы отправлять в Kibana более структурированные логи, а не только сообщение. Для каждого запроса извлекли полезную информацию (например, путь, метод, код состояния) и создали карту с дополнительной информацией, необходимой для регистрации:
import akka.http.scaladsl.model.HttpRequest
import ch.qos.logback.classic.{Logger => LogbackLogger}
import net.logstash.logback.marker.Markers
import org.slf4j.{LoggerFactory, Logger => SLFLogger}
import scala.collection.JavaConverters._
object Logging {
val rootLogger: LogbackLogger = LoggerFactory.getLogger(SLFLogger.ROOT_LOGGER_NAME).asInstanceOf[LogbackLogger]
private def setMarkers(request: HttpRequest) = {
val markers = Map(
"path" -> request.uri.path.toString(),
"method" -> request.method.value
)
Markers.appendEntries(markers.asJava)
}
def infoWithMarkers(message: String, akkaRequest: HttpRequest) =
rootLogger.info(setMarkers(akkaRequest), message)
}
Дополнительная структура в логах позволила создать полезные информационные панели и добавить больше контекста для различий. Это также помогло при выявлении несоответствий между API.
Репликация трафика и рефакторинг прокси
Перенеся контент в базу данных CODE, мы получили почти точную копию базы данных PROD. Основное отличие заключалось в том, что у CODE не было трафика. Для репликации реального трафика в среду CODE мы использовали GoReplay (gor) — инструмент с открытым исходным кодом, который легко настраивался под наши требования.
Поскольку весь трафик, поступающий в API, сначала попадал на прокси, имело смысл установить gor на прокси-боксы. Как скачать gor на бокс и как перехватывать трафик порта 80 и отправлять его на другой сервер:
wget https://github.com/buger/goreplay/releases/download/v0.16.0.2/gor_0.16.0_x64.tar.gz
tar -xzf gor_0.16.0_x64.tar.gz gor
sudo gor --input-raw :80 --output-http http://apiv2.code.co.uk
Какое-то время всё работало нормально, но очень скоро произошел сбой, и прокси стал недоступен на пару минут. Оказалось, что все три ящика, на которых работал прокси-сервер, зациклились одновременно. Подозрение пало на gor — возможно, он использует слишком много ресурсов и вызывает падение прокси. При дальнейшем расследовании обнаружили в консоли AWS, что ящики регулярно переключались, но не в одно и то же время.
Мы попытались найти способ запустить gor, но на этот раз без давления на прокси. Решение пришло из вторичного стека для Composer, который используется только в экстренных случаях. Мы воспроизвели трафик из стека в CODE с удвоенной скоростью, и на этот раз всё заработало без проблем.
Новые данные вызвали много вопросов. Мы создавали прокси с мыслью, что он будет существовать временно, поэтому не нуждается в такой тщательной разработке, как другие приложения. Кроме того, он был построен с использованием Akka Http, который никто из членов команды ранее не использовал. Код получался беспорядочным и полным быстрых исправлений. Мы решили начать большую работу по рефакторингу, чтобы улучшить читаемость.
Предполагали, что, упростив логику, мы сможем предотвратить зацикливание блоков. Но это не сработало. Примерно через две недели попыток сделать прокси более надёжным мы начали чувствовать, что всё глубже и глубже проваливаемся в кроличью нору. Тогда мы решили, что лучше потратить время на фактическую миграцию, чем пытаться исправить часть программного обеспечения, которое исчезнет через месяц. И оставили попытки исправить прокси. За это мы заплатили ещё двумя перерывами в работе, каждый из которых длился около двух минут, но в целом решение было правильным.
Перенесемся в то время, когда мы завершили миграцию CODE без негативных последствий для производительности API или взаимодействия с пользователем в CMS. Предстоял вывод из эксплуатации прокси-сервера в CODE.
Для начала требовалось изменить приоритеты API, чтобы прокси-сервер сначала общался с Postgres. Как упоминалось ранее, это основывалось на конфигурации, но была одна сложность.
Composer отправляет сообщения в поток Kinesis при обновлении документа. Во избежание дублирования эти сообщения должен отправлять только один API. Для этого у API есть флаг в конфигурации: значение true для API, поддерживаемого Mongo; и false для API, поддерживаемого Postgres. Просто изменить прокси-сервер, чтобы сначала обратиться к Postgres, недостаточно, поскольку сообщение не будет отправлено в потоке Kinesis, пока запрос не достигнет Mongo.
Для решений этой проблемы мы создали конечные точки HTTP для мгновенного изменения конфигурации в памяти для всех инстансов балансировщика нагрузки. Это позволило быстро переключать API на первичный без необходимости редактирования файла конфигурации и повторного развёртывания. Кроме того, это можно было запрограммировать, что снижало влияние человеческого фактора и количество возможных ошибок.
Теперь все запросы сначала направлялись к Postgres, а API2 общался с Kinesis.
Следующий шаг — полностью удалить прокси и заставить клиентов общаться исключительно с API Postgres. Поскольку клиентов много, обновлять каждого из них по отдельности нецелесообразно, поэтому мы передали это DNS. Мы создали CNAME в DNS, который сначала указывал на ELB прокси, а затем менялся, чтобы указать на ELB API.
Пришло время переноса на PROD. Процесс был относительно простым, так как всё было основано на конфигурации.
Отключение прокси и MongoDB
Спустя десять месяцев и 2,4 млн перенесенных статей мы отключили всю инфраструктуру, связанную с Mongo. Но перед этим был момент, которого все так долго ждали: предстояло убить прокси.
Небольшая часть программного обеспечения вызвала столько проблем, что хотелось поскорее отключить её. А для этого требовалось одно — обновить запись CNAME, чтобы она указывала непосредственно на балансировщик нагрузки APIV2. Когда мы сделали это, и ничего не сломалось, все, наконец, расслабились.
Но удаление старого API MongoDB все же привело к некоторым трудностям. Удаляя старый код, мы обнаружили, что интеграционные тесты никогда не менялись для использования нового API. Всё быстро покраснело. К счастью, большинство проблем оказались связаны с конфигурацией, и их можно было легко исправить.
Дальше всё работало без сбоев. Мы отсоединили инстансы Mongo от OpsManager, а затем уничтожили их. Оставалось только праздновать. И выспаться.
Коротко о главном
Вместо заключения перечислим основные тезисы, почему PostgreSQL с течением времени оказывается более оптимальным вариантом, нежели MongoDB.
Более структурированная система управления. Если вы планируете перейти на сочетание структурированных и неструктурированных данных или считаете, что соответствие требованиям ACID важно для вашего продукта в будущем, PostgreSQL — отличный выбор.
Лучшая производительность. Распространено мнение, что одним из лучших качеств систем управления базами данных NoSQL является их производительность. Однако для всех стало неожиданностью, когда PostgreSQL превзошел MongoDB в рейтинге производительности EnterpriseDB.com в 2014 году. В тестах, основанных на выборе, загрузке и вставке сложных данных документа объемом 50 миллионов записей, PostgreSQL был примерно в два раза быстрее при приеме данных, в 2,5 раза быстрее при выборе данных и в 3 раза быстрее при вставке данных. И всё это при потреблении на 25% меньше места на диске.
Совместимость со сторонними инструментами. Системы управления базами данных PostgreSQL обладают мощной поддержкой сторонних инструментов, включающих расширения для улучшения многих аспектов. Например, ClusterControl помогает в управлении, мониторинге и масштабировании баз данных SQL и NoSQL с открытым исходным кодом. А DB Data Difftective делает сравнение и синхронизацию данных более эффективными.
Комментарии (21)
SWATOPLUS
03.02.2023 11:36+31Суть статьи: не смогли перенести MongoDB в AWS, поэтому перешли на PgSQL, потому что с ним деплой проще. И это было не сложно, так как в PgSQL есть json колонки в которых можно хранить json-документы без схемы.
Ivan22
03.02.2023 12:55+30ну да, оказалось что администрирование это критично, а в монго это боль, и оказалось что одна колонка в постгресе бьет всю документоориентированность всей монге просто в разы. А так все верно
vitaly_il1
03.02.2023 15:38Вот и у меня такое же впечатление. Очень странная статья.
MongoDB Atlas прекрасно живет в AWS.
Плюс метаться от DynamoDB к Postgres - это выглядит очень-очень необычно, мягко говоря.ef_end_y
04.02.2023 10:32А мне показалось, что вы отвечаете на комментарий, который говорит о правильности решения
MentalBlood
03.02.2023 12:53+1
velipre_xella
03.02.2023 12:55+12Была же уже 4 года назад https://habr.com/ru/company/itsumma/blog/436416/, зачем опять?
fireSparrow
03.02.2023 13:46+9А это нормально вообще — плашку с рекламой вставлять прямо в середину статьи?
php7
03.02.2023 16:05+4в том числе известное онлайн-издание The Guardian
Ну все. Тогда нужно и мне переходить.
kpmy
03.02.2023 19:55-1Подобные статьи льют воду на мельницу сегодняшних импортозаместителей, которые ради удобства всех причастных (всех, кроме разработчиков), почему-то решили, что всем PostgreSQL.
Neveil
03.02.2023 22:44+6Краткий пересказ мотивации.
Мы приобрели продукт, который не работает с AWS, поэтому написали тонну костылей. Также не осилили прочитать доку к монге. В итоге костыли стало накладно поддерживать, вывод мигрируем на постгрес.
Еще одна бесполезная статья написанная за деньги.
caballero
04.02.2023 01:39+8Ничего удивительного - хайп вокруг nosql (очередной "серебряной пули") прошел и все вернулись к нормальным БД
RuslanHamhoev
05.02.2023 03:35Не совсем так. История вкратце была такая:
1) ой смотрите у нас тут новая быстрая база, insert просто бомбически быстрый и нет этих дурацких индексов, которые тормозят всё и надо думать как их создавать;
2) база выросла до нескольких гигабайт, есть проблемы со скоростью чтения данных, думаем, решаем что делать;
3) ура, мы изобрели
колесоиндексы, теперь NoSQL означает NotOnlySQL;4) хм, а в Postgres есть json и concurrent index, интересно;
5) да ну её эту монгу с этими пайплайнами, хотим как в oldSQL, join/union.
Sleuthhound
04.02.2023 13:52Можно сколько угодно ругать MongoDB, но в плане запуска реплик у MongoDB просто чудесно все сделано, никакого гемороя, все запускается и наливается вводом одной команды. В этом плане PostgreSQL и MySQL просто рядом не стояли.
P.S. Для MySQL 8 конечно уже есть clone plugin, который так же все делает одной командой, но у PostgreSQL (ванилы) ничего подобного и в помине нет.
RuslanHamhoev
05.02.2023 14:27Пришёл год назад в компанию, 7 лет используют Монго реплику, за 7 лет проблем не было ни разу. В случае проблем в регионе поднять новую БД из бэкапа можно за час. Зачем платить за Secondary и Arbiter?
nik_savchenko
Небольшое дополнение к производительности и нюансам MongoDB
https://stroppy.io/ru/reports/mongodb-report.html
Shark13
Дополню про постгрес, в котором есть серьезное замедление работы при размере документов превышающим 2кб. Для нас это оказалось решающим фактором в пользу монги. Мы переехали но поняли что стало значительно медленнее, в десятки раз, для наших запросов. Будьте внимательны https://www.google.com/url?sa=t&source=web&rct=j&url=https://habr.com/ru/amp/post/646987/&ved=2ahUKEwiL09nZ8f38AhW1mFwKHfu4B7wQtwJ6BAgXEAE&usg=AOvVaw0ktgonh2RwmJRz_3scjVh5