Хабрахабр

Отказоустойчивый кластер PostgreSQL + Patroni. Опыт внедрения

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

Сервера находятся в Amazone в одном регионе Ирландии: в работе постоянно 100+ различных серверов, из них почти 50 — с базами данных. У нас высоконагруженный сервис: 2,5 млн пользователей по всему миру, 50К+ активных пользователей каждый день.

При одновременной работе нескольких пользователей на одной доске все они видят изменения в режиме реального времени, потому что каждое изменение мы записываем в базу. Весь backend — большое монолитное stateful-приложение на Java, которое держит постоянное websocket соединение с клиентом. В пиковой нагрузке в Redis мы пишем по 80-100К запросов в секунду.

У нас примерно 10К запросов в секунду к нашим базам.

Почему мы перешли с Redis на PostgreSQL

Изначально наш сервис работал с Redis, key-value хранилищем, которое хранит все данные в оперативной памяти сервера.

Плюсы Redis:

  1. Высокая скорость ответа, т.к. всё хранится в памяти;
  2. Удобство бэкапа и репликации.

Минусы Redis для нас:

  1. Нет настоящих транзакций. Мы пытались имитировать их на уровне нашего приложения. К сожалению, это не всегда хорошо работало и требовало написания очень сложного кода.
  2. Объём данных ограничен количеством памяти. При увеличении количества данных память будет расти, и, в конце концов, мы упрёмся в характеристики выбранного инстанса, что в AWS требует остановки нашего сервиса для изменения типа инстанса.
  3. Необходимо постоянно поддерживать уровень низкого latency, т.к. у нас очень большое количество запросов. Оптимальный для нас уровень задержки — 17-20 ms. При уровне 30-40 ms мы получаем долгие ответы на запросы нашего приложения и деградацию сервиса. К сожалению, у нас это случилось в сентябре 2018 года, когда один из инстансов с Redis почему-то получил latency в 2 раза больше обычного. Для решения проблемы мы остановили сервис в середине рабочего дня для внепланового maintenance и заменили проблемный инстанс Redis.
  4. Легко получить неконсинстентность данных даже при незначительных ошибках в коде и потом потратить много времени на написание кода для исправления этих данных.

Мы учли минусы и поняли, что нам необходимо переехать на что-то более удобное, с нормальными транзакциями и меньшей зависимостью от latency. Провели исследование, проанализировали множество вариантов и выбрали PostgreSQL.

Подробнее об этапах переезда и переключении данных между БД написано в статье моего коллеги. На новую БД мы переезжаем уже 1,5 года и перевезли только небольшую часть данных, поэтому сейчас работаем одновременно с Redis и PostgreSQL.

Кластер PostgreSQL состоял из мастера и реплики с асинхронной репликацией. Когда мы только начинали переезжать, наше приложение работало напрямую с БД и обращалось к мастеру Redis и PostgreSQL. Так выглядела схема работы с базами:

Внедрение PgBouncer

Пока мы переезжали, продукт тоже развивался: увеличивалось количество пользователей и количество серверов, которые работали с PostgreSQL, и нам стало не хватать соединений. PostgreSQL на каждое соединение создаёт отдельный процесс и потребляет ресурсы. Увеличивать число коннектов можно до определённого момента, иначе есть шанс получить неоптимальную работу БД. Идеальным вариантом в такой ситуации будет выбор менеджера коннектов, который встанет перед базой.

Но первый не поддерживает транзакционный режим работы с базой, поэтому мы выбрали PgBouncer. У нас было два варианта для менеджера соединений: Pgpool и PgBouncer.

Мы настроили следующую схему работы: наше приложение обращается к одному PgBouncer, за которым находятся masters PostgreSQL, а за каждым мастером — одна реплика с асинхронной репликацией.

Описанная выше схема является для этого относительно удобной: при добавлении нового шарда PostgreSQL достаточно обновить конфигурацию PgBouncer и приложение может сразу работать с новым шардом. При этом мы не могли хранить весь объём данных в PostgreSQL и для нас была важна скорость работы с базой, поэтому мы начали шардировать PostgreSQL на прикладном уровне.

Отказоустойчивость PgBouncer

Эта схема проработала до момента, пока единственный инстанс PgBouncer не умер. Мы находимся в AWS, где все инстансы запущены на железе, которое периодически умирает. В таких случаях инстанс просто переезжает на новое железо и снова работает. Так произошло и с PgBouncer, однако он стал недоступен. Результатом этого падения стала недоступность нашего сервиса в течение 25 минут. AWS для таких ситуаций рекомендует использовать избыточность на стороне пользователя, что не было реализовано у нас на тот момент.

После этого мы всерьёз задумались об отказоустойчивости PgBouncer и кластеров PostgreSQL, потому что подобная ситуация могла повториться с любым инстансом в нашем AWS аккаунте.

Каждый из PgBouncer смотрит на одни и те же master PostgreSQL каждого шарда. Схему отказоустойчивости PgBouncer мы построили следующим образом: все сервера приложения обращаются к Network Load Balancer, за которым стоят два PgBouncer. Отказоустойчивость Network Load Balancer обеспечивает AWS. В случае повторения ситуации с падением инстанса AWS, весь трафик перенавравляется через другой PgBouncer.

Такая схема позволяет без проблем добавлять новые сервера PgBouncer.

Создание отказоустойчивого кластера PostgreSQL

При решении этой задачи мы рассматривали разные варианты: самописный failover, repmgr, AWS RDS, Patroni.

Самописные скрипты

Могут мониторить работу мастера и, в случае его падения, продвигать реплику до мастера и обновлять конфигурацию PgBouncer.

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

Минусы:

  • Мастер мог не умереть, вместо этого мог произойти сетевой сбой. Failover, не зная об этом, продвинет реплику до мастера, а старый мастер будет продолжать работать. В результате мы получим два сервера в роли master и не будем знать, на каком из них последние актуальные данные. Такую ситуацию называют ещё split-brain;
  • Мы остались без реплики. В нашей конфигурации мастер и одна реплика, после переключения реплика продвигается до мастера и у нас больше нет реплик, поэтому приходится в ручном режиме добавлять новую реплику;
  • Нужен дополнительный мониторинг работы failover, при этом у нас 12 шардов PostgreSQL, а значит мы должны мониторить 12 кластеров. При увеличении количества шардов надо ещё не забыть обновить failover.

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

Repmgr

Replication Manager for PostgreSQL clusters, который умеет управлять работой кластера PostgreSQL. При этом в нём нет автоматического failover “из коробки”, поэтому для работы потребуется писать свою “обёртку” поверх готового решения. Так что всё может получится даже сложнее, чем с самописными скриптами, поэтому Repmgr мы даже не стали пробовать.

AWS RDS

Поддерживает всё необходимое для нас, умеет делать бэкапы и поддерживает пул коннектов. Имеет автоматическое переключение: при смерти мастера реплика становится новым мастером, а AWS меняет dns запись на нового мастера, при этом реплики могут находится в разных AZ.

Как пример тонких настроек: на наших инстансах стоят ограничения для tcp коннектов, чего, к сожалению, нельзя сделать в RDS: К минусам можно отнести отсутствие тонких настроек.

net.ipv4.tcp_keepalive_time=10
net.ipv4.tcp_keepalive_intvl=1
net.ipv4.tcp_keepalive_probes=5
net.ipv4.tcp_retries2=3

Кроме того у AWS RDS цена почти в два раза дороже обычной цены instance, что и послужило главной причиной отказа от этого решения.

Patroni

Это шаблон на python для управления PostgreSQL с хорошей документацией, автоматическим failover и исходным кодом на github.

Плюсы Patroni:

  • Расписан каждый параметр конфигурации, понятно как что работает;
  • Автоматический failover работает из коробки;
  • Написан на python, а так как мы сами много пишем на python, то нам будет проще разбираться с проблемами и, возможно, даже помочь развитию проекта;
  • Полностью управляет PostgreSQL, позволяет менять конфигурацию сразу на всех нодах кластера, а если для применения новой конфигурации требуется перезапуск кластера, то это можно сделать опять же с помощью Patroni.

Минусы:

  • Из документации непонятно, как правильно работать с PgBouncer. Хотя минусом это назвать сложно, потому что задача Patroni — управлять PostgreSQL, а как будут ходить подключения к Patroni — уже наша проблема;
  • Мало примеров внедрения Patroni на больших объёмах, при этом много примеров внедрения с нуля.

В итоге для создания отказоустойчивого кластера мы выбрали именно Patroni.

Процесс внедрения Patroni

До Patroni у нас было 12 шардов PostgreSQL в конфигурации один мастер и одна реплика с асинхронной репликацией. Сервера приложения обращались к базам данных через Network Load Balancer, за которым стояли два instance с PgBouncer, а за ними находились все PostgreSQL сервера.

Patroni работает с распределёнными системами хранения конфигураций, такими как etcd, Zookeeper, Сonsul. Для внедрения Patroni нам нужно было выбрать распределенное хранилище конфигурации кластера. Отличный повод начать использовать Consul по назначению. У нас как раз на проде есть полноценный кластер Consul, который работает в связке с Vault и больше мы его никак не используем.

Как работает Patroni с Consul

У нас есть кластер Сonsul, который состоит из трёх нод и кластер Patroni, который состоит из лидера и реплики (в Patroni мастер называется лидером кластера, а слейвы — репликами). Каждый инстанс кластера Patroni постоянно посылает в Consul информацию о состоянии кластера. Поэтому из Сonsul всегда можно узнать текущую конфигурацию кластера Patroni и того, кто является лидером в данный момент.

Для подключения Patroni к Сonsul достаточно изучить официальную документацию, в которой написано, что необходимо указать хост в формате http или https в зависимости от того, как мы работаем с Сonsul, и схему подключения, опционально:

host: the host:port for the Consul endpoint, in format: http(s)://host:port
scheme: (optional) http or https, defaults to http

Выглядит просто, но тут начинаются подводные камни. С Сonsul мы работаем по защищённому соединению через https и наш конфиг подключения будет выглядеть следующим образом:

consul: host: https://server.production.consul:8080 verify: true cacert: } cert: {{ consul_cert }} key: {{ consul_key }}

Но так не работает. При старте Patroni не может подключиться к Сonsul, потому что пытается всё равно идти по http.

Хорошо, что он написан на python. Разобраться с проблемой помог исходный код Patroni. Вот так выглядит работающий блок конфигурации для работы с Сonsul у нас: Оказывается параметр host никак не парсится, а протокол необходимо указать в scheme.

consul: host: server.production.consul:8080 <b>scheme: https</b> verify: true cacert: {{ consul_cacert }} cert: {{ consul_cert }} key: {{ consul_key }}

Consul-template

Итак, хранилище для конфигурации мы выбрали. Теперь нужно понять, как PgBouncer будет переключать свою конфигурацию при смене лидера в кластере Patroni. В документации на этот вопрос ответа нет, т.к. там в принципе не описана работа с PgBouncer.

Это подтолкнуло нас на исследование работы Consul-template. В поисках решения мы нашли статью (название, к сожалению, не помню), где было написано, что Сonsul-template очень помог в связке PgBouncer и Patroni.

При смене лидера он обновляет конфигурацию PgBouncer и отправляет команду на её перезагрузку. Оказалось, что Сonsul-template постоянно мониторит конфигурацию кластера PostgreSQL в Сonsul.

Большой плюс template в том, что он хранится в виде кода, поэтому при добавлении нового шарда достаточно сделать новый коммит и обновить template в автоматическом режиме, поддерживая принцип Infrastructure as code.

Новая архитектура с Patroni

В результате мы получили такую схему работы:

Все сервера приложения обращаются к балансировщику → за ним стоят два instance PgBouncer → на каждом instance запущен Сonsul-template, который мониторит состояние каждого кластера Patroni и следит за актуальностью конфига PgBouncer, который направляет запросы на текущего лидера каждого кластера.

Ручное тестирование

Эту схему перед выводом на прод мы запустили на небольшой тестовой среде и проверили работу автоматического переключения. Открывали доску, передвигали стикер и в этот момент “убивали” лидера кластера. В AWS для этого достаточно выключить инстанс через консоль.

Значит, кластер Patroni сработал верно: сменил лидера, отправил информацию в Сonsul, а Сonsul-template сразу подхватил эту информацию, заменил конфигурацию PgBouncer и отправил команду на reload. Стикер в течение 10-20 секунд возвращался назад, а потом вновь начинал нормально перемещаться.

Как выжить под высокой нагрузкой и сохранить минимальный даунтайм?

Всё работает отлично! Но появляются новые вопросы: Как это сработает под высокой нагрузкой? Как быстро и безопасно раскатать всё на production?

Она полностью идентична production по архитектуре и имеет сгенерированные тестовые данные, которые по объёму примерно равны production. Ответить на первый вопрос нам помогает тестовая среда, на которой мы проводим нагрузочное тестирование. Но перед этим важно проверить автоматическую раскатку, ведь на этой среде у нас есть несколько шардов PostgreSQL, так что мы получим отличное тестирование конфигурационных скриптов перед продом. Мы решаем просто “убить” один из мастеров PostgreSQL во время теста и посмотреть, что будет.

6. Обе задачи выглядят амбициозно, но у нас PostgreSQL 9. 2 обновимся? Может мы сразу на 11.

2, потом запустить Patroni. Мы решаем сделать это в 2 этапа: сначала обновить версию до 11.

Обновление PostgreSQL

Для быстрого обновления версии PostgreSQL необходимо использовать опцию -k, в которой создаются hard link на диске и нет необходимости в копировании ваших данных. На базах в 300-400 ГБ обновление занимает 1 секунду.

Для этого мы написали Ansible playbook, который выполняет весь процесс обновления за нас: У нас много шардов, поэтому обновление нужно сделать в автоматическом режиме.

/usr/lib/postgresql/11/bin/pg_upgrade \
<b>--link \</b>
--old-datadir='' --new-datadir='' \ --old-bindir='' --new-bindir='' \ --old-options=' -c config_file=' \ --new-options=' -c config_file='

Здесь важно отметить, что перед запуском апгрейда необходимо выполнить его с параметром --check, чтобы быть уверенным в возможности апгрейда. Так же наш сценарий делает подмену конфигов на время апгрейда. Сценарий у нас выполнился за 30 секунд, это отличный результат.

Запуск Patroni

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

Patroni ничего не знает о раннем состоянии кластера и пытается запустить оба сервера как два отдельных кластера с одинаковым именем. Когда мы начали ставить Patroni уже на готовый кластер PostgreSQL и запускать его, то столкнулись с новой проблемой: оба сервера запускались как leader. Для решения этой проблемы необходимо удалить директорию с данными на slave:

rm -rf /var/lib/postgresql/

Это необходимо сделать только на slave!

При подключении чистой реплики Patroni делает basebackup leader и восстанавливает его на реплику, а затем догоняет актуальное состояние по wal-логам.

Когда каждый кластер ничего не знает про другой — это нормально. Ещё одна сложность, с которой мы столкнулись, — все кластеры PostgreSQL по умолчанию называются main. Решение — поменять имя кластера в конфигурации PostgreSQL. Но когда вы хотите использовать Patroni, то все кластера должны иметь уникальное имя.

Нагрузочный тест

Мы запустили тест, который имитирует работу пользователей на досках. Когда нагрузка достигла нашего среднего дневного значения, мы повторили точно такой же тест, мы выключили один instance с leader PostgreSQL. Автоматический failover сработал так, как мы ожидали: Patroni сменил лидера, Сonsul-template обновил конфигурацию PgBouncer и отправил команду на reload. По нашим графиками в Grafana было видно, что есть задержки на 20-30 секунд и небольшой объём ошибок с серверов, связанных с соединением к базе. Это нормальная ситуация, такие значения допустимы для нашего failover и точно лучше, чем даунтайм сервиса.

Вывод Patroni на production

В итоге у нас получился следующий план:

  • Деплой Сonsul-template на сервера PgBouncer и запуск;
  • Обновления PostgreSQL до версии 11.2;
  • Смена имени кластера;
  • Запуск кластера Patroni.

При этом наша схема позволяет сделать первый пункт практически в любое время, мы можем по очереди убрать каждый PgBouncer из работы и выполнить на него деплой и запуск consul-template. Так мы и сделали.

Мы могли всё выкатить поочередно на каждый шард без остановки нашего сервиса, но нам пришлось бы на несколько минут выключать каждый PostgreSQL. Для быстрой раскатки мы использовали Ansible, так как все playbook мы уже проверили на тестовой среде, а время выполнения полного сценария было от 1,5 до 2 минут для каждого шарда. В этом случае пользователи, чьи данные есть на этом шарде, не могли бы полноценно работать в это время, а это для нас неприемлемо.

Это окно для плановых работ, когда мы полностью выключаем наш сервис и обновляем инстансы баз данных. Выходом из этой ситуации стал плановый maintenance, который проходит у нас каждые 3 месяца. За время ожидания мы дополнительно подстраховались: для каждого шарда PostgreSQL подняли по запасной реплике на случай неудачи, чтобы сохранить самые последние данные, и добавили по новому инстансу для каждого шарда, который должен стать новой репликой в кластере Patroni, чтоб не выполнять команду для удаления данных. До очередного окна оставалась одна неделя, и мы решили просто подождать и дополнительно подготовиться. Всё это помогло максимально снизить риск ошибки.

Мы перезапустили наш сервис, все заработало как надо, пользователи продолжили работать, но на графиках мы заметили аномально высокую нагрузку на Сonsul-сервера.

Эта проблема очень хорошо иллюстрирует, что необходимо следовать принципу Infrastructure as code и дорабатывать всю инфраструктуру, начиная с тестовых сред и заканчивая production. Почему мы не увидели это на тестовой среде? Что произошло? Иначе очень легко получить такую проблему, которую получили мы. Как раз в одном из релизов была решена утечка CPU при работе с consul-template. Сonsul сначала появился на production, а потом на тестовых средах, в итоге на тестовых средах версия Consul была выше, чем на production. Поэтому мы просто обновили Consul, решив таким образом проблему.

Restart Patroni cluster

Однако мы получили новую проблему, о которой даже не подозревали. При обновлении Consul мы просто удаляем ноду Consul из кластера с помощью команды consul leave → Patroni подключается к другому Consul серверу → всё работает. Но когда мы дошли до последнего инстанса кластера Consul и отправили ему команду consul leave, все кластеры Patroni просто перезапустились, а в логах мы увидели следующую ошибку:

ERROR: get_cluster
Traceback (most recent call last):
...
RetryFailedError: 'Exceeded retry deadline'
ERROR: Error communicating with DCS
<b>LOG: database system is shut down</b>

Кластер Patroni не смог получить информацию о своём кластере и перезапустился.

Они предложили улучшения наших конфигурационных файлов: Для поиска решения мы обратились к авторам Patroni через issue на github.

consul: consul.checks: []
bootstrap: dcs: retry_timeout: 8

Мы смогли повторить проблему на тестовой среде и протестировали там эти параметры, но, к сожалению, они не сработали.

Мы планируем попробовать следующие варианты решения: Проблема до сих пор остаётся нерешённой.

  • Использовать Сonsul-agent на каждом инстансе кластера Patroni;
  • Исправить проблему в коде.

Нам понятно место возникновения ошибки: вероятно, проблема в использовании default timeout, который не переопределяется через файл конфигурации. При удалении последнего сервера Сonsul из кластера происходит зависание всего Сonsul-кластера, которое длится дольше секунды, из-за этого Patroni не может получить состояние кластера и полностью перезапускает весь кластер.

К счастью, больше никаких ошибок мы не встретили.

Итоги использования Patroni

После успешного запуска Patroni мы добавили по дополнительной реплике в каждом кластере. Теперь в каждом кластере есть подобие кворума: один лидер и две реплики, — для подстраховки на случай split-brain при переключении.

За это время он уже успел нас выручить. На production Patroni работает более трёх месяцев. Patroni выполнил свою главную задачу. Недавно в AWS умер лидер одного из кластеров, автоматический failover сработал и пользователи продолжили работать.

Небольшой итог использования Patroni:

  • Удобство изменения конфигурации. Достаточно изменить конфигурацию на одном инстансе и она подтянется на весь кластер. Если требуется перезагрузка для применения новой конфигурации, то Patroni об этом сообщит. Patroni может перезапустить весь кластер с помощью одной команды, что тоже очень удобно.
  • Автоматический failover работает и уже успел нас выручить.
  • Обновление PostgreSQL без даунтайма приложения. Необходимо сначала обновить реплики на новую версию, затем сменить лидера в кластере Patroni и обновить старого лидера. При этом происходит необходимое тестирование автоматического failover.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть