Всем привет! Меня зовут Григорий Тарасенко, я работаю DBA в Авито с 2022 года. В этой статье я расскажу про High Availability (HA) в PostgreSQL. Сначала мы выясним, что такое HA и зачем оно нужно, потом посмотрим, как можно построить отказоустойчивость без погружения в PostgreSQL. А затем я покажу готовые решения для организации High Availability и расскажу, как это всё выглядит у нас в Авито.

С этим материалом я выступал на митапе «PostgreSQL: HA в Avito». Здесь — адаптированная версия доклада.

Основные понятия: HA, RTO, RPO

High Availability (HA) — свойство системы обеспечивать максимально продолжительную доступность даже при отказе компонентов из-за аварии. Когда происходит авария, бизнес хочет мгновенно восстановиться. Вопрос в том, сколько занимает это «мгновенно» — об этом говорит метрика RTO. 

Recovery Time Objective (RTO) — это максимально допустимое время простоя или недоступности системы, после которого она должна восстановиться и снова работать. Чем меньше есть времени на восстановление, тем меньше данных мы можем восстановить. О том, какой объём данных мы готовы потерять, говорит метрика RPO. 

Recovery Point Objective (RPO) — это максимально допустимая потеря данных или период времени, в течение которого система может быть восстановлена до последнего сохраненного состояния.

Чем меньшее время простоя сервиса обеспечивает решение по защите, тем дороже такая защита стоит. Источник (Хабр)
Чем меньшее время простоя сервиса обеспечивает решение по защите, тем дороже такая защита стоит. Источник (Хабр)

RTO и RPO обычно определяются как золотая середина между стоимостью потери данных или простоя и доступности сервиса. Чем меньшее время простоя сервиса обеспечивает защита, тем дороже она стоит. 

Простой в 1 минуту из-за падения не будет стоить дорого для бизнеса. А вот чтобы восстановить БД за 1 минуту, нужны космические средства: поддержка инфраструктуры, планов восстановления, людей. Аналогично — с RPO.

Возможные решения для организации High Availability

Поговорим про 3 возможных решения по организации High Availability, которые первыми приходят в голову. Для них не нужно погружаться в PostgreSQL, но у них много минусов.

Виртуализация. Первое решение — настроить кластер виртуализации с общей СХД. Если сервер с виртуальной машиной базы откажет, ВМ перезапустится на другом сервере.

Происходит авария, отказывает сервер с ВМ базы. ВМ с БД перезапускается на другом сервере
Происходит авария, отказывает сервер с ВМ базы. ВМ с БД перезапускается на другом сервере

Минусы такого решения:

  • кто-то может утилизировать все IO СХД (эффект «шумного соседа»),

  • не защитит при потере ДЦ,

  • не страхует при ошибках ОС и БД,

  • может быть дорогой: и по деньгам, и по вычислительным ресурсам.

Преднастроенная база. Другое решение — заранее купить и настроить сервера, чтобы база восстанавливалась по расписанию: например, два раза в день. Из плюсов — в среднем случае это позволит сэкономить RTO.

Из минусов:

  • не подойдёт для больших баз с высокой активностью,

  • в худшем случае — большее RPO, чем при виртуализации.

Множество копий базы. Третье решение, которое может прийти в голову, — писать сразу в несколько баз. Так мы получим Multi-Master, масштабирование по чтению и крайне низкие RTO/RPO — за счёт того, что все БД онлайн.

Происходит авария, одна копия БД отваливается. Сервис перестаёт обращаться к этой копии и забирает данные из других
Происходит авария, одна копия БД отваливается. Сервис перестаёт обращаться к этой копии и забирает данные из других

Минусы этого решения:

  • непонятно, что делать, если база перегружена, но не мертва;

  • реализовать и администрировать собственное решение, чтобы согласовать не дошедшие изменения;

  • плохо масштабируется при решении в лоб;

  • трудно поддерживать консистентность.

High Availability в PostgreSQL с помощью WAL-репликации 

Мы рассмотрели решения организации High Availability без погружения в особенности БД. Но лучше сделать отказоустойчивое решение на уровне базы. В PostgreSQL есть журнал предзаписи — Write-Ahead Log, мы можем попробовать этим воспользоваться.

Write-Ahead Logging (WAL) — это механизм последовательной фиксации изменений в данных. Журнал предварительной записи позволяет восстановить БД до последнего сохранённого состояния, если произойдёт сбой. При записи операции в БД она сначала записывается в WAL. Только после этого происходит фактическое выполнение операции. Поскольку WAL пишется последовательно, мы можем воспроизвести все операции, пройдясь по записям в журнале. 

Подробнее про WAL в документации PostgreSQL →
Статья WAL в PostgreSQL: Журнал предзаписи →

В PostgreSQL 9.0+ есть механизм потоковой репликации WAL. Он позволяет не ждать создания целого WAL-файла со всеми изменениями, а применять изменения, как только они появляются. С потоковой репликацией WAL-записи отправляются с Primary-сервера на одну или несколько реплик.

Настройка потоковой репликации в PostgreSQL требует конфигурации параметров на обоих серверах — primary и реплике, при необходимости, дополнительной настройки слотов репликации. После настройки репликации primary будет непрерывно передавать WAL-записи на реплику, обеспечивая постоянную синхронизацию данных между системами.

Подробнее про потоковую WAL-репликацию →

Готовые решения для High Availability в PostgreSQL

В PostgreSQL есть множество готовых решений для организации High Availability. 

  1. repmgr (Replication Manager for PostgreSQL) — пионер в High Availability для PostgreSQL. Написан на C как расширение и как внешняя программа. Поддерживает Witness-ноды.
    repmgr на GitHub → 

  2. Patroni — внешний сервис на Python. Работает через DCS. Есть REST API и поддержка k8s. Можно настраивать PostgreSQL в одном месте.
    Patroni на GitHub → 

  3. Stolon — решение на Golang с поддержкой k8s. Состоит из нескольких компонентов: 

  • sentinel следит за состоянием кластера,

  • keeper разворачивается рядом с PostgreSQL и выполняет настройку,

  • proxy указывает, к кому подключаться для чтения/записи.

Stolon на GitHub →

  1. Corosync + Pacemaker. Шаблон HA для кластеризации любых ресурсов через написание своих скриптов.
    Corosync на GitHub →
    Pacemaker на GitHub →

  2. PAF (PostgreSQL Auto Failover). Готовые скрипты для Corosync + Pacemaker.
    PAF на GitHub →

  3. pg_auto_failover. Расширение и программа на C. По духу похож на repmgr.
    pg_auto_failover на GitHub →

  4. Bucardo. Perl-программа, которая обеспечивает логическую Multi-Master репликацию.
    Bucardo на GitHub →

  5. EDB (aka 2ndQuandrant BDR). Enterprise-решение, обещающее доступность 99,99%+ за кучу денег.
    Официальный сайт EDB →

High Availability в Авито: организация

Вот стек, который мы используем для организации High Availability:

  1. PostgreSQL 15 с асинхронной потоковой репликацией.

  2. Patroni для управления High Availability и конфигурацией кластера. 

  3. Consul в качестве  распределенного хранилища конфигурации (Distributed Configuration Store, DCS) для Patroni. 

  4. Ceph в качестве хранилища резервных копий.

  5. WAL-G для работы с бэкапами.

Так выглядит наша High Availability
Так выглядит наша High Availability

Дополнительно мы обеспечиваем отказоустойчивость на уровне железа. В Авито несколько БД и сетевых карт, кластер PostgreSQL распределён на нескольких датацентрах, а ещё мы используем RAID. 

Для управления RTO и RPO мы используем следующие настройки: 

  • loop_wait = 10 секунд — для управления запросами в DCS;

  • retry_timeout = 55 секунд — для управления запросами в DCS;

  • tll = 2 минуты — сколько живёт информация о текущем лидере;

  • master_start_timeout = 2 минуты — сколько ждать лидера, если он не отзывается;

  • maximum_lag_on_failover = 160 Мб — сколько данных разрешено потерять.

При такой организации High Availability гарантируются такие показатели: 

  • RTO в худшем случае: 2 минуты,

  • RPO в худшем случае: 160 Мб.

Больше деталей про управление высокодоступными PostgreSQL-кластерами →

Защита от Split Brain

Если участники High Availability заизолировались, то без должной обработки возникнет несколько лидеров: primary, которые выиграли гонку за ключ. Они не будут синхронизироваться и в каждого из них будут писаться разные данные. Это может случиться, например, если ляжет сеть. Такая проблема называется Split Brain, потому что мозг как бы поделился, и каждая часть знает только что-то своё.

Проблема Split Brain возникает, когда единый кластер разделяется на несколько изолированных частей, каждая из которых продолжает работу без синхронизации. 

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

Для решения Split Brain мы используем Patroni в связке с Consul (в качестве DCS):

  • пока в DCS TTL лидера обновляется, реплики не могут начать голосование, даже если они не получают WAL;

  • если возникают проблемы с DCS, то и лидер, и реплика перестают обрабатывать запросы;

  • если живых узлов Consul меньше половины, то он приостанавливает обслуживание (согласно алгоритму Raft).

Raft — это алгоритм консенсуса, который гарантирует согласованность данных в распределённой системе. Он позволяет однозначно и явно выбрать лидера и реплицировать данные в ненадежных условиях.

Подробнее про протокол Raft → 

Добавление и удаление реплики

Добавление реплики происходит в три этапа: 

  1. Patroni узнает о лидере при помощи единого источника правды — Consul. 

  2. Реплика PostgreSQL создаётся из бэкапа в Ceph.

  3. Реплика PostgreSQL догоняет лидера при помощи бэкапов WAL.

Когда реплика догоняет лидера, включается потоковая репликация.


Так происходит добавление реплики в системе High Availability в Авито

Удаление реплики происходит сильно проще. Достаточно остановить сервис Patroni на реплике и выключить/удалить/забыть её.

Отказ лидера

Вот, как происходит смена лидера в случае его отказа в нашей High Availability инфраструктуре:

  • Реплики опрашивают DCS с интервалом loop_wait (10 секунд), ожидая ответ в течение retry_timeout (55 секунд).

  • Если лидер перестал отмечаться в Consul спустя ttl (2 минуты) или PostgreSQL не работает спустя master_start_timeout (2 минуты), мы считаем его недоступным.

  • Если лидер не объявился, начинаются выборы с учётом maximum_lag_on_failover (160 Мб) и времени, на которое отстала  физическая реплика (при прочих равных, выиграет та реплика, которая имеет наименьшее отставание).

Так происходит отказ от лидера в системе High Availability в Авито
Так происходит отказ от лидера в системе High Availability в Авито

Полезные ссылки

Материалы по теме

Готовые решения для High Availability в PostgreSQL

Решения, которые мы используем в Авито для организации High Availability:

Ceph на GitHub.

Предыдущая статья: Как дизайнеру и редактору работать вместе: опыт Авито

Комментарии (15)


  1. maxim_ge
    24.11.2023 11:20

    RPO в худшем случае: 160 Мб.

    Можно подробнее, в каких ситуациях могут быть потеряны данные в такой архитектуре?


    1. gle4er Автор
      24.11.2023 11:20
      +1

      В статье это опущено, но есть параметр synchronous_commit. У нас он выставлен в off. Плюс используется асинхронная потоковая репликация, поэтому некоторые данные до реплик могут не доехать, особенно если лидер будет перегружен.


      1. rzerda
        24.11.2023 11:20

        Поясните, пожалуйста, каким образом настройка maximum_lag_on_failover оный RPO гарантирует? Я даже не о плюс-минуc loop_wait/2 seconds из документации, а как вообще оно механически работает?


      1. maxim_ge
        24.11.2023 11:20

        Понятно, спасибо. Если можно, приведите примеры, для каких операций в avito используется описанный "асинхронный подход", а для каких этого недостаточно и приходится "синхронизировать".


  1. nin-jin
    24.11.2023 11:20
    +1

    RTO и RPO обычно определяются как золотая середина между стоимостью потери данных или простоя и доступности сервиса. Чем меньшее время простоя сервиса обеспечивает защита, тем дороже она стоит. 

    В local-first архитектурах потери данных в принципе не происходит и это ничего не стоит. На вашей картинке это будет в центре координат:

    :


    1. maxim_ge
      24.11.2023 11:20

      А где предполагается хранить local-first базы для случая avito?


      1. nin-jin
        24.11.2023 11:20

        Там же, где и remote-first.


        1. maxim_ge
          24.11.2023 11:20

          В облаке, что ли? Можете схемку набросать, допустим, тут https://mermaid.live?


          1. nin-jin
            24.11.2023 11:20

            Тут есть куча схемок например: https://page.hyoo.ru/#!=aqsy4f_sh755c


  1. dbax
    24.11.2023 11:20
    +4

    Интересно, я один не понимаю смысла писать 100500ю статью на тему "Мы используем Patroni, он классный"?


  1. ErgoZru
    24.11.2023 11:20

    А как вы решаете проблему, когда нужно записать в базу и сразу сделать чтение данных с учетом записанных? Если писать только в мастер, а читать из слейвов, то есть риск не получить данные. Если и писать и читать из мастера - то не получится подразмазать нагрузку. Как вы решаете такие проблемы, особенно с учетом синхрониус коммит офф? Буду благодарен за инфу :)


    1. fr0z
      24.11.2023 11:20

      Никак не решают, читания-писания уровнем выше скорее всего реализованы.


  1. schernolyas
    24.11.2023 11:20

    Это active - active cluster?


  1. NawiLan
    24.11.2023 11:20

    используете ли pg_rewind и если происходит переключение, то занимаетесь ли восстановлением данных, если на момент переключения был replication lag?


  1. 1ars
    24.11.2023 11:20

    а ещё мы используем RAID

    Интересно было бы узнать поподробнее, какая реализация используется? Программный/аппаратный, какая конфигурация себя зарекомендовала?