Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

У статті я розповім, як ми підійшли до питання стійкості до відмови PostgreSQL, чому це стало для нас важливо і що в результаті вийшло.

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

Весь backend - великий монолітний stateful-додаток на Java, який тримає постійне websocket з'єднання з клієнтом. При одночасної роботі кількох користувачів на одній дошці всі вони бачать зміни в режимі реального часу, тому що кожну зміну записуємо в базу. У нас приблизно 10К запитів за секунду до наших баз. У піковому навантаженні в Redis ми пишемо по 80-100К запитів на секунду.
Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

Чому ми перейшли з 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. Докладніше про етапи переїзду та перемикання даних між БД написано в статті мого колеги.

Коли ми тільки починали переїжджати, наша програма працювала безпосередньо з БД і зверталася до майстра Redis і PostgreSQL. Кластер PostgreSQL складався з майстра та репліки з асинхронною реплікацією. Так виглядала схема роботи з базами:
Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

Використання PgBouncer

Поки ми переїжджали, продукт теж розвивався: збільшувалася кількість користувачів і кількість серверів, які працювали з PostgreSQL, і нам не вистачало з'єднань. PostgreSQL на кожне з'єднання створює окремий процес та споживає ресурси. Збільшувати кількість коннектів можна до певного моменту, інакше є шанс отримати неоптимальну роботу БД. Ідеальним варіантом у такій ситуації буде вибір менеджера коннектів, який постане перед базою.

У нас було два варіанти для менеджера з'єднань: Pgpool та PgBouncer. Але перший не підтримує транзакційний режим роботи з базою, тому ми вибрали PgBouncer.

Ми налаштували наступну схему роботи: наш додаток звертається до одного PgBouncer, за яким знаходяться masters PostgreSQL, а за кожним майстром одна репліка з асинхронною реплікацією.
Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

При цьому ми не могли зберігати весь обсяг даних у 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 + Patroni. Досвід впровадження

Створення відмовостійкого кластера 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, що і послужило головною причиною відмови від цього рішення.

Патрони

Це шаблон на 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 сервери.
Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

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

Як працює Patroni з Consul

У нас є кластер Сonsul, який складається з трьох нід та кластер Patroni, який складається з лідера та репліки (в Patroni майстер називається лідером кластера, а слейви – репліками). Кожен інстанс кластера Patroni постійно надсилає Consul інформацію про стан кластера. Тому із Сonsul завжди можна дізнатися про поточну конфігурацію кластера Patroni і того, хто є лідером на даний момент.

Відмовостійкий кластер PostgreSQL + 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: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

Але так не працює. При старті Patroni не може підключитися до Сonsul, тому що намагається все одно йти http.

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

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

Consul-template

Отже, сховище конфігурації ми вибрали. Тепер потрібно зрозуміти, як PgBouncer перемикатиме свою конфігурацію при зміні лідера в кластері Patroni. У документації це питання відповіді немає, т.к. там у принципі не описано роботу з PgBouncer.

У пошуках рішення ми знайшли статтю (назву, на жаль, не пам'ятаю), де було написано, що Сonsul-template дуже допоміг у зв'язці PgBouncer та Patroni. Це спонукало нас до дослідження роботи Consul-template.

Виявилося, що Сonsul-template постійно моніторить конфігурацію кластера PostgreSQL у Сonsul. При зміні лідера він оновлює конфігурацію PgBouncer та відправляє команду на її перезавантаження.

Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

Великий плюс template в тому, що він зберігається у вигляді коду, тому при додаванні нового шарда достатньо зробити новий коміт і оновити template в автоматичному режимі, підтримуючи принцип Infrastructure as code.

Нова архітектура з Patroni

В результаті ми отримали таку схему роботи:
Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

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

Ручне тестування

Цю схему перед виведенням на прод ми запустили на невеликому тестовому середовищі та перевірили роботу автоматичного перемикання. Відкривали дошку, пересували наклейку і в цей момент “вбивали” лідера кластера. В AWS для цього достатньо вимкнути інстанс через консоль.

Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

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

Як вижити під високим навантаженням та зберегти мінімальний даунтайм?

Все працює чудово! Але постають нові питання: Як це спрацює під високим навантаженням? Як швидко та безпечно розкотити все на production?

Відповісти на перше запитання нам допомагає тестове середовище, на якому ми проводимо тестування навантаження. Вона повністю ідентична production з архітектури та має згенеровані тестові дані, які за обсягом приблизно рівні production. Ми вирішуємо просто “вбити” один із майстрів PostgreSQL під час тесту та подивитися, що буде. Але перед цим важливо перевірити автоматичне розкочування, адже на цьому середовищі у нас є кілька шардів PostgreSQL, тому ми отримаємо відмінне тестування конфігураційних скриптів перед продом.

Обидві завдання виглядають амбітно, але у нас PostgreSQL 9.6. Може ми одразу на 11.2 оновимося?

Ми вирішуємо зробити це в два етапи: спочатку оновити версію до 2, потім запустити Patroni.

Оновлення 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 вже на готовий кластер PostgreSQL і запускати його, зіткнулися з новою проблемою: обидва сервери запускалися як leader. Patroni нічого не знає про ранній стан кластера і намагається запустити обидва сервери як два окремі кластери з однаковим ім'ям. Для вирішення цієї проблеми необхідно видалити директорію з даними на slave:

rm -rf /var/lib/postgresql/

Це потрібно зробити тільки на slave!

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

Ще одна складність, з якою ми зіткнулися, всі кластери PostgreSQL за замовчуванням називаються main. Коли кожен кластер нічого не знає про інший – це нормально. Але коли ви хочете використовувати Patroni, всі кластери повинні мати унікальне ім'я. Рішення - змінити ім'я кластера в конфігурації PostgreSQL.

Навантажувальний тест

Ми запустили тест, який імітує роботу користувачів на дошках. Коли навантаження досягло нашого середнього денного значення, ми повторили такий самий тест, ми вимкнули один 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. Так ми зробили.

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

Виходом із цієї ситуації став плановий maintenance, який проходить у нас кожні 3 місяці. Це вікно для планових робіт, коли ми повністю вимикаємо наш сервіс та оновлюємо інстанси баз даних. До чергового вікна залишався один тиждень, і ми вирішили просто почекати та додатково підготуватися. За час очікування ми додатково підстрахувалися: для кожного шарду PostgreSQL підняли по запасній репліці на випадок невдачі, щоб зберегти останні дані, і додали по новому інстансу для кожного шарда, який повинен стати новою реплікою в кластері Patroni, щоб не виконувати команду для видалення даних . Все це допомогло максимально зменшити ризик помилки.
Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

Ми перезапустили наш сервіс, все запрацювало як слід, користувачі продовжили працювати, але на графіках ми помітили аномально високе навантаження на Сonsul-сервер.
Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

Чому ми не побачили це на тестовому середовищі? Ця проблема дуже добре ілюструє, що необхідно дотримуватись принципу Infrastructure as code та доопрацьовувати всю інфраструктуру, починаючи з тестових середовищ та закінчуючи production. Інакше дуже легко отримати таку проблему, яку ми отримали. Що сталося? Сonsul спочатку з'явився на production, а потім на тестових середовищах, у результаті на тестових середовищах версія Consul була вищою, ніж на production. Саме в одному з релізів було вирішено витік CPU при роботі з consul-template. Тому ми просто оновили 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 під час перемикання.
Відмовостійкий кластер PostgreSQL + Patroni. Досвід впровадження

На production Patroni працює понад три місяці. За цей час він уже встиг нас врятувати. Нещодавно в AWS помер лідер одного із кластерів, автоматичний failover спрацював і користувачі продовжили працювати. Patroni виконав своє головне завдання.

Невеликий результат використання Patroni:

  • Зручність зміни конфігурації. Достатньо змінити конфігурацію на одному інстансі і вона підтягнеться на весь кластер. Якщо потрібно перезавантаження для застосування нової конфігурації, Patroni про це повідомить. Patroni може перезапустити весь кластер за допомогою однієї команди, що також дуже зручно.
  • Автоматичний failover працює і вже встиг нас врятувати.
  • Оновлення PostgreSQL без даунтайму програми. Необхідно спочатку оновити репліки на нову версію, потім змінити лідера у кластері Patroni та оновити старого лідера. При цьому відбувається необхідне тестування автоматичного failover.

Джерело: habr.com

Додати коментар або відгук