[Перевод] Пока-пока, MongoDB: почему компании переходят на PostgreSQL

За последние несколько лет многие компании перешли с MongoDB на PostgreSQL, в том числе известное онлайн-издание The Guardian. В статье говорим о причинах перехода и разбираемся, действительно ли PostgreSQL лучше MongoDB.

93887fb99bfa7aaabf62154ef1ddba6f.jpg

Примечание: дальнейшее повествование ведётся от лица команды 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.

«PostgreSQL База»

Перенос контента за два десятилетия без простоев

План

Большинство миграций баз данных включают одни и те же шаги. Эта не стала исключением. Вот шаги, которые мы предприняли для переноса:

  • создать новую базу данных;

  • создать способ записи в новую базу данных (новый 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.

Прокси и работа в продакшене

30d11410c3816278b8a2d7624c005e8e.png

Прокси

Когда новый 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. Но перед этим был момент, которого все так долго ждали: предстояло убить прокси.

675044e6d385a49630baeafceb035eb1.png

Небольшая часть программного обеспечения вызвала столько проблем, что хотелось поскорее отключить её. А для этого требовалось одно — обновить запись 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 делает сравнение и синхронизацию данных более эффективными. 

e013451c8eaed0dd3c9ce4f8ff1a2e80.jpg

«PostgreSQL База»

© Habrahabr.ru