В статията ще обясня как подходим към проблема с отказоустойчивостта на PostgreSQL, защо това стана важно за нас и какво постигнахме в крайна сметка.
Имаме високонагънат сервис: 2,5 милиона потребители по целия свят, над 50К активни потребители всеки ден. Сървърите са разположени в AWS в един регион в Ирландия: в работа постоянно са над 100 различни сървъра, от които почти 50 са с бази данни.
Целият backend представлява голямо монолитно stateful приложение на Java, което поддържа постоянно websocket свързване с клиента. При едновременната работа на няколко потребители на една дъска, те виждат промените в реално време, тъй като всяка промяна записваме в базата. Имаме около 10К заявки в секунда към нашите бази. При пикова натовареност в Redis записваме 80-100К заявки в секунда.

Защо преминахме от Redis на PostgreSQL
Първоначално нашия сервис работеше с Redis, key-value хранилище, което съхранява всички данни в оперативната памет. сървър.
Предимства на Redis:
- Висока скорост на отговор, тъй като всичко се съхранява в паметта;
- Удобство при резервното копиране и репликацията.
Недостатъци на Redis за нас:
- Нямаше истински транзакции. Опитахме се да ги имитираме на ниво приложение. За съжаление, това не винаги проработваше добре и изискваше написването на много сложен код.
- Обемът данни е ограничен от количеството памет. При увеличаване на данните паметта ще нараства и в крайна сметка ще се натъкнем на характеристиките на избраната инстанция, което в AWS дава нужда от спиране на нашия сервис за смяна на типа инстанция.
- Необходимо е постоянно да поддържаме ниво на ниска латентност, тъй като имаме много заявки. Оптималното ниво за нас е 17-20 ms. При ниво от 30-40 ms получаваме дълги отговори на нашите приложения и деградация на услугата. За съжаление, това се случи през септември 2018 година, когато една от инстанциите с Redis по някаква причина получи латентност два пъти повече от обичайното. За да решим проблема, спряхме сервиса в средата на работния ден за непланирана поддръжка и заменихме проблемната инстанция Redis.
- Лесно е да се получи неконсистентност на данните дори при незначителни грешки в кода и след това да прекарате много време в написването на код за коригиране на тези данни.
Ние взехме предвид недостатъците и осъзнахме, че е необходимо да преминем към нещо по-удобно, с нормални транзакции и по-малка зависимост от латентността. Проведохме проучване, анализирахме множество опции и избрахме PostgreSQL.
Точим новата база данни вече 1,5 години и сме преместили само малка част от данните, така че в момента работим едновременно с Redis и PostgreSQL. Подробности за етапите на миграцията и прехвърлянето на данни между базите са описани в статия на моя колега.
Когато започнахме миграцията, нашето приложение работеше директно с базата данни и се свързваше с мастера на Redis и PostgreSQL. Клъстерът PostgreSQL се състоеше от мастър и реплика с асинхронна репликация. Ето как изглеждаше схемата на работа с базите:

Внедряване на PgBouncer
Докато мигрирахме, продуктът също се развиваше: увеличаваше се броят на потребителите и числото на сървърите, които работеха с PostgreSQL, и ни липсваха връзки. PostgreSQL създава отделен процес за всяка връзка и консумира ресурси. Броят на свързванията може да бъде увеличаван до определен момент, в противен случай има риск от неефективна работа на БД. Идеалният вариант в такава ситуация е изборът на мениджър на връзки, който да застане пред базата.
Имахме два варианта за мениджър на връзки: Pgpool и PgBouncer. Но първият не поддържа транзакционен режим работа с базата, затова избрахме PgBouncer.
Настроихме следната схема на работа: нашето приложение се свързва с един PgBouncer, зад който се намират мастърите на PostgreSQL, а зад всеки мастър - една реплика с асинхронна репликация.

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

Създаване на отказоустойчив кластер PostgreSQL
При решаването на този проблем разгледахме различни варианти: самостоятелно написан failover, repmgr, AWS RDS, Patroni.
Самостоятелно написани скриптове
Могат да следят работата на мастера и в случай на неговото падане, да повишат репликата до майстор и да актуализират конфигурацията на PgBouncer.
Плюсовете на този подход са в максималната простота, тъй като вие сами пишете скриптовете и точно разбирате как работят.
Минуси:
- Майсторът може да не е умрял, вместо това може да е възникнала мрежова повреда. Failover, без да знае за това, ще повиши репликата до мастер, а старият мастер ще продължи да работи. В резултат получаваме два сървъра в ролята на мастер и не знаем на кой от тях са последните актуални данни. Тази ситуация се нарича split-brain;
- Останахме без реплика. В нашата конфигурация имаме мастер и една реплика, след превключването репликата се повишава до мастер и нямаме повече реплики, затова се налага ръчно да добавим нова реплика;
- Нужен е допълнителен мониторинг на работата на failover, при което имаме 12 шарда PostgreSQL, а значи трябва да следим 12 клъстера. При увеличение на броя на шардовете, не бива да забравяме да обновим и failover.
Собствената система за failover изглежда много сложна и изисква значителна поддръжка. С един PostgreSQL клъстер това ще бъде най-простият вариант, но той не е мащабируем, така че не е подходящ за нас.
Repmgr
Replication Manager за PostgreSQL клъстери, който управлява работата на 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 е почти два пъти по-висока от стандартната цена на инстанцията, което беше основната причина да откажем това решение.
Patroni
Това е шаблон на python за управление на PostgreSQL с добра документация, автоматичен failover и изходен код в GitHub.
Плюсове на Patroni:
- Всеки параметър на конфигурацията е описан, ясно е как работи;
- Автоматичният failover работи от кутийката;
- Написан е на python, а тъй като и ние много пишем на python, ще ни е по-лесно да се справим с проблемите и, възможно, да помогнем в развитието на проекта;
- Изцяло управлява PostgreSQL, позволява промяна на конфигурацията веднага на всички възли на клъстера, а ако за прилагането на новата конфигурация е необходимо презареждане на клъстера, това може да се направи отново с помощта на Patroni.
Минуси:
- От документацията не е ясно как да работим правилно с PgBouncer. Въпреки това, трудно е да се нарече недостатък, тъй като задачата на Patroni е да управлява PostgreSQL, а как ще се свързват клиентите към Patroni е наш проблем;
- Има малко примери за внедряване на Patroni при големи обеми, но много примери за внедряване от нулата.
В крайна сметка, за създаването на отказоустойчив клъстер избрахме именно Patroni.
Процесът на внедряване на Patroni
Преди внедряването на Patroni имахме 12 шарда PostgreSQL в конфигурация с един майстор и една реплика с асинхронна репликация. Приложните сървъри се свързваха с базите данни чрез Network Load Balancer, зад който стояха два инстанса с PgBouncer, а след тях се намираха всичките PostgreSQL сървъри.

За внедряване на Patroni трябваше да изберем разпределено хранилище на конфигурацията на клъстера. Patroni работи с разпределени системи за съхранение на конфигурации, като etcd, Zookeeper, Consul. В продъкшъна имаме пълен клъстер Consul, който работи в комбинация с Vault и по друг начин не го използваме. Отлична възможност да започнем да използваме Consul по назначение.
Как работи Patroni с Consul
Имаме клъстер Consul, който се състои от три нода, и клъстер Patroni, който се състои от лидер и реплика (в Patroni, майсторът се нарича лидер на клъстера, а слейвовете — реплики). Всеки инстанс на клъстера Patroni постоянно изпраща информация за състоянието на клъстера до Consul. Поради това от Consul винаги може да се узнае текущата конфигурация на клъстера Patroni и кой е настоящият лидер.

За свързване на Patroni с Consul е достатъчно да се проучи официалната документация, в която е посочено, че е необходимо да се укаже хост в формат http или https в зависимост от начина, по който работим с Consul, и схемата на свързване, по избор:
host: хост:порт за Consul ендпойнта, в формат: http(s)://host:port
scheme: (по избор) http или https, по подразбиране е httpИзглежда просто, но тук започват подводните камъни. С Consul работим по защитено съединение през https и нашата конфигурация за свързване ще изглежда по следния начин:
consul:
host: https://server.production.consul:8080
verify: true
cacert: {{ consul_cacert }}
cert: {{ consul_cert }}
key: {{ consul_key }}Но така не работи. При стартиране Patroni не може да се свърже с Consul, защото все пак опитва да се свърже през http.
Разглеждането на проблема помогна изходният код на Patroni. Добре е, че е написан на python. Оказва се, че параметърът host не се парсира, а протоколът трябва да бъде указан в scheme. Ето как изглежда работещият блок на конфигурацията за работа с Consul при нас:
consul:
host: server.production.consul:8080
scheme: https
verify: true
cacert: {{ consul_cacert }}
cert: {{ consul_cert }}
key: {{ consul_key }}Consul-template
И така, избрахме хранилище за конфигурацията. Сега трябва да разберем как PgBouncer ще превключва конфигурацията си при смяна на водача в клъстера Patroni. В документацията няма отговор на този въпрос, тъй като там не е описана работата с PgBouncer.
В търсене на решение намерихме статия (за съжаление, не помня заглавието), в която беше споменато, че Consul-template е много полезен за свързването на PgBouncer и Patroni. Това ни подтикна да проучим работата на Consul-template.
Оказа се, че Consul-template постоянно следи конфигурацията на PostgreSQL клъстера в Consul. При смяна на водача, той обновява конфигурацията на PgBouncer и изпраща команда за нейното презареждане.

Голямо предимство на шаблона е, че той се съхранява под формата на код, така че при добавяне на нов шард, е достатъчно да направим нов комит и да обновим шаблона в автоматичен режим, поддържайки принципа Infrastructure as code.
Нова архитектура с Patroni
В резултат получихме схемата на работа:

Всички сървъри на приложението се свързват с балансировчика → зад него стоят два инстанса PgBouncer → на всеки инстанс е стартиран Consul-template, който следи състоянието на всеки клъстер Patroni и следи актуалността на конфигурацията на PgBouncer, която насочва заявките към текущия водач на всеки клъстер.
Ръчно тестване
Тази схема преди да бъде пусната на продукция, я стартирахме в малка тестова среда и проверихме работата на автоматичното превключване. Отворихме дъската, преместихме стикера и в този момент "убихме" водача на клъстера. В AWS за това е достатъчно да изключите инстанса чрез конзолата.

Стикерът се връщаше обратно в рамките на 10-20 секунди, а след това отново започваше да се движи нормално. Значи, клъстерът Patroni работи правилно: смени водача, изпрати информация в Consul, а Consul-template веднага подхвана тази информация, замени конфигурацията на PgBouncer и изпрати команда за reload.
Как да преживеем при високо натоварване и да запазим минимален даунтайм?
Всичко работи отлично! Но възникват нови въпроси: Как ще проработи под високо натоварване? Как бързо и безопасно да внедрим всичко на продукция?
На първия въпрос ни помага тестова среда, на която провеждаме натоварващо тестване. Тя е напълно идентична на продукционната среда по архитектура и разполага със сгенерирани тестови данни, които по обем са приблизително равни на продукционната среда. Ние просто решаваме да "убием" един от кластерите на PostgreSQL по време на теста и да видим какво ще се случи. Но преди това е важно да проверим автоматичното разгръщане, защото на тази среда имаме няколко шарда на PostgreSQL, така че ще получим отлично тестване на конфигурационните скриптове преди продукцията.
И двете задачи изглеждат амбициозно, но ние използваме PostgreSQL 9.6. Може би веднага да обновим до 11.2?
Ние решаваме да направим това в два етапа: първо обновяваме версията до 11.2, след това стартираме Patroni.
Обновяване на PostgreSQL
За бързото обновяване на версията на PostgreSQL е необходимо да се използва опцията -k, при която се създават hard link на диска и не е необходимо копиране на вашите данни. За бази от 300-400 ГБ обновяването отнема 1 секунда.
Имаме много шардове, затова обновяването трябва да се извърши в автоматичен режим. За това написахме Ansible playbook, който изпълнява целия процес на обновяване вместо нас:
/usr/lib/postgresql/11/bin/pg_upgrade
<b>--връзка </b>
--стара-директория-данни='' --нова-директория-данни=''
--стара-директория-бинари='' --нова-директория-бинари=''
--стари-опции=' -c конфигурационен_файл='
--нови-опции=' -c конфигурационен_файл='Тук е важно да се отбележи, че преди стартиране на обновлението е необходимо да се изпълни с параметъра --check, за да се уверим в възможността за обновление. Също така нашият сценарий заменя конфигурациите за времето на обновлението. Нашият сценарий се изпълни за 30 секунди, което е отличен резултат.
Стартиране на Patroni
За решаването на втория проблем е достатъчно да погледнем конфигурацията на Patroni. В официалното хранилище има пример за конфигурация с initdb, който отговаря за инициализацията на новата база при първото стартиране на Patroni. Но тъй като вече имаме готова база, просто премахнахме тази част от конфигурацията.
Когато започнахме да инсталираме Patroni на готовия кластер PostgreSQL и да го стартираме, се срещнахме с нов проблем: и двата сървъра се стартираха като лидер. Patroni не знае за ранното състояние на клъстера и се опитва да стартира и двата сървъра като два отделни клъстера с едно и също име. За да решим този проблем, е необходимо да изтрием директорията с данни на slave:
rm -rf /var/lib/postgresql/Това трябва да се направи само на slave!
При свързване на чиста реплика, Patroni прави basebackup от лидера и го възстановява на репликата, а след това актуализира текущото състояние по wal-логовете.
Още една трудност, с която се сблъскахме, е, че всички PostgreSQL клъстери по подразбиране се наричат main. Когато всеки клъстер не знае нищо за друг – това е нормално. Но когато искате да използвате Patroni, всички клъстери трябва да имат уникално име. Решението е да промените името на клъстера в конфигурацията на PostgreSQL.
Тест за натоварване
Стартирахме тест, който имитира дейността на потребителите на таблата. Когато натоварването достигна нашето средно дневно значение, повторихме точно същия тест и изключихме един инстанс с лидер PostgreSQL. Автоматичното прехвърляне на лидер работи, както очаквахме: Patroni смени лидера, Consul-template обнови конфигурацията на PgBouncer и изпрати команда за презареждане. В нашите графики в Grafana можеше да се види, че има закъснения от 20-30 секунди и малък обем грешки от сървъри, свързани с връзката с базата. Това е нормална ситуация, такива стойности са допустими за нашето прехвърляне на лидер и определено са по-добри от времето на недостъпност на услугата.
Изпълнение на Patroni в продукция
В крайна сметка получихме следния план:
- Деплой на Consul-template на сървърите PgBouncer и стартиране;
- Актуализация на PostgreSQL до версия 11.2;
- Смяна на името на клъстера;
- Стартиране на клъстера Patroni.
В същото време нашата схема позволява да изпълним първата точка почти по всяко време, можем последователно да извлечем всеки PgBouncer от работа и да извършим деплой и стартиране на consul-template. Така и направихме.
За бързо разгръщане използвахме Ansible, тъй като всички playbook вече бяха проверени в тестовата среда, а времето за изпълнение на целия сценарий беше от 1.5 до 2 минути за всеки шард. Можехме да разгръщаме поетапно на всеки шард без спиране на нашата услуга, но щяхме да трябва да изключим всеки PostgreSQL за няколко минути. В този случай потребителите, чиито данни са на този шард, не можеха да работят пълноценно в това време, а това е неприемливо за нас.
Решение на тази ситуация беше планирано поддръжка, която провеждаме на всеки 3 месеца. Това е период за планирани работи, когато напълно спираме нашия сервис и обновяваме инстанциите на базите данни. Оставаше една седмица до следващия период, и решихме просто да изчакаме и да се подготвим допълнително. През времето на изчакване ние допълнително се застраховахме: за всеки шард PostgreSQL разположихме по една резервна реплика в случай на провал, за да запазим най-новите данни, и добавихме по нова инстанция за всеки шард, която трябваше да стане нова реплика в кластера Patroni, за да не изпълняваме команда за изтриване на данни. Всичко това помогна да се сведе до минимум рискът от грешки.

Рестартирахме нашия сервис, всичко заработи както трябва, потребителите продължиха да работят, но в графиките забелязахме аномално висока натовареност на сървърите Consul.

Защо не забелязахме това в тестовата среда? Проблемът много добре илюстрира необходимостта от следване на принципа Infrastructure as code и усъвършенстване на цялата инфраструктура, започвайки от тестовите среди и завършвайки с продукцията. Иначе е много лесно да се получи такъв проблем, какъвто получихме ние. Какво се случи? Consul първо се появи в продукцията, а след това в тестовите среди, в резултат на което в тестовите среди версията на Consul беше по-висока от тази в продукцията. В един от релизите беше решен проблем с изтичането на CPU при работа с consul-template. Затова просто обновихме Consul, по този начин разрешавайки проблема.
Рестартирайте кластера Patroni
Но получихме нов проблем, за който дори не подозирахме. При обновяването на Consul просто изтриваме нодата Consul от клъстера с помощта на командата consul leave → Patroni се свързва с друг Consul сървър → всичко работи. Но когато достигнахме до последния инстанс на кластера Consul и му изпратихме командата consul leave, всички клъстери Patroni просто се рестартираха, а в логовете видяхме следната грешка:
ГРЕШКА: get_cluster
Следа (последно извикване последно):
...
RetryFailedError: 'Надвишен срок за повторно опити'
ГРЕШКА: Грешка при комуникация с DCS
<b>ЛОГ: системата на базата данни е спряна</b>Клъстерът Patroni не можа да получи информация за своя клъстер и се рестартира.
За да намерим решение, се обърнахме към авторите на Patroni чрез issue в github. Те предложиха подобрения в нашите конфигурационни файлове:
consul:
consul.checks: []
bootstrap:
dcs:
retry_timeout: 8Успяхме да повторим проблема в тестовата среда и там тестваме тези параметри, но, за съжаление, те не сработиха.
Проблемата все още остава нерешена. Плануваме да опитаме следните варианти за решение:
- Да използваме Consul-agent на всеки инстанс от кластера Patroni;
- Да поправим проблема в кода.
Ясно ни е мястото на възникване на грешката: вероятно, проблемът е в използването на default timeout, който не се преопределя чрез конфигурационния файл. При премахването на последния Consul сървър от кластера, целият Consul-класер засяда за повече от секунда, поради което Patroni не може да получи състоянието на кластера и напълно рестартира целия кластер.
За щастие, не срещнахме други грешки.
Резултати от използването на Patroni
След успешното стартиране на Patroni добавихме по една допълнителна реплика във всеки кластер. Сега във всеки кластер има подобие на кворум: един лидер и две реплики, — за застраховка в случай на split-brain при превключване.

На production Patroni работи над три месеца. През това време вече успя да ни помогне. Наскоро в AWS умря лидер на един от кластерите, автоматичният failover сработи и потребителите продължиха да работят. Patroni изпълни главната си задача.
Небольшой итог использования Patroni:
- Удобство на промяна на конфигурацията. Достатъчно е да промените конфигурацията на един инстанс и тя ще се обнови за целия кластер. Ако е необходимо рестартиране за прилагане на новата конфигурация, Patroni ще уведомява за това. Patroni може да рестартира целия кластер с една команда, което също е много удобно.
- Автоматичният failover работи и вече успя да ни помогне.
- Обновление на PostgreSQL без даунтайм на приложението. Първо е необходимо да се обновят репликите на новата версия, след което да се смени лидерът в кластер Patroni и да се обнови старият лидер. При това се извършва необходимо тестиране на автоматичния failover.
Източник: habr.com
