Как Patroni обеспечивает высокую доступность PostgreSQL
Автор: Shaun Thomas, How Patroni Brings High Availability to Postgres
Статьи серии:
- Как Patroni обеспечивает высокую доступность PostgreSQL
- Использование Patroni для создания кластера высокой доступности Postgres — Часть 1: etcd
- Использование Patroni для создания кластера высокой доступности Postgres — Часть 2: Postgres и Patroni
- Использование Patroni для создания кластера высокой доступности Postgres — Часть 3: HAProx
Давайте признаем: существует множество инструментов высокой доступности (High Availability) для управления кластерами Postgres. Этот ландшафт эволюционировал на протяжении десятилетий, достигнув своего нынешнего состояния, и в сообществе царит немалая путаница. Будь то Reddit, списки рассылки Postgres, Slack, Discord, IRC, доклады на конференциях или любые другие площадки, один из самых частых вопросов, которые мне задают: «Как сделать Postgres отказоустойчивым?»
Где-то с 2017 года мой ответ неизменен: «Просто используйте Patroni». Если только в экосистеме Postgres не произойдёт что-то чудесное, этот ответ вряд ли изменится. Но почему? Что делает Patroni «окончательным ответом», когда речь заходит о Postgres и высокой доступности? Это во многом связано с тем, как Patroni выполняет свою работу, и именно это мы и рассмотрим в данной статье.
Слон в посудной лавке
Сам по себе Postgres не является кластером в том смысле, который большинство людей вкладывает в это слово. Они могут представлять себе сложную массу взаимосвязанных серверов, яростно моргающих лампочками друг на друга, осведомлённых о каждом вычислении, совершаемом другими, и готовых взять на себя управление в случае сбоя одного из них. В реальности же «официальное» использование слова «кластер» в мире Postgres означает просто одну или несколько баз данных, связанных с одним экземпляром Postgres. Это прямо указано в документации по Creating a Database Cluster.
«A database cluster is a collection of databases that is managed by a single instance of a running database server».
Концепция взаимодействия нескольких таких экземпляров была настолько чужда Postgres, что даже не существовала до выхода версии 9.0, которая в 2010 году представила горячие резервные серверы (Hot Standbys) и потоковую репликацию. А как работают горячие резервные экземпляры? Так же, как и основной узел: они применяют страницы WAL к файлам кучи. Эти страницы WAL могут поступать из архивированных файлов WAL или передаваться потоком с основного сервера, но по сути это всё ещё непрерывное восстановление после сбоя под другим именем.
Это важно, потому что каждый узел Postgres до сих пор почти ничего не знает о других узлах в этом импровизированном кластере, спустя более чем 15 лет. Само по себе это не обязательно проблема, но это выдает определённую долю умышленного неведения со стороны каждого узла. Почему каждый узел не заботится о существовании других узлов или даже не признаёт их существования?
Конечно же, каждый узел должен заботиться о том, что другие узлы существуют! Именно так и родился каждый инструмент высокой доступности для Postgres. Slony и PgPool-II были, вероятно, первыми из них, использование Pacemaker и Corosync всегда было популярно в ранние дни, затем появились Bucardo, repmgr и EFM. Это лишь наиболее примечательные примеры, известные большинству сообщества.
Но после первоначального выпуска Patroni произошла забавная вещь: неумолимый поток инструментов высокой доступности для Postgres внезапно прекратился. Все сразу поняли, что в нём есть что-то, что кардинально отличает его от предшественников. Давайте поговорим о том, почему.
Что делает Patroni
Patroni делает то, что Postgres до сих пор не умеет: он строит кластер из узлов Postgres. Он делает это, облегчая то, что я люблю называть «сэндвичем из HAProxy и DCS», который выглядит примерно так:

Представьте это как Postgres-бутерброд (BLT), где Patroni играет роль листьев салата, которые связывают всё воедино. Это недостающий коммуникационный центр, который записывает состав кластера, статус его членов и направляет соединения туда, куда нужно.
Давайте углубимся в то, как именно он это делает и почему это важно для всех — от любителей до корпораций из списка Fortune 500.
Кворум
Первый и самый важный аспект операционной роли Patroni — поддержание кворума. Вот удобное определение кворума:
Минимальное число должностных лиц и членов комитета или организации, обычно большинство, которое должно присутствовать для действительного ведения дел.
Критический аспект здесь — голосующее большинство, иначе известное как Консенсус. Стандартная формула тут для некоторого числа узлов N: N/2 + 1. В то время как для двухузлового кластера для сохранения большинства потребуется, чтобы оба узла оставались в сети, трёхузловой кластер также потребует двух узлов для сохранения большинства. Именно этот «дополнительный» узел создаёт отказоустойчивость в сетевом кластере. Если один узел становится изолированным от других, либо из-за сбоя, либо из-за разделения сети, кворум сохраняется, и кластер остаётся работоспособным. Большее количество узлов обычно также обеспечивает лучшую защиту; в конце концов, три из пяти — лучшее соотношение. Из-за накладных расходов на коммуникацию, вызванных топологией узлов, большинство уровней консенсуса рекомендуют оставаться в пределах «горстки» узлов, что обычно означает «меньше десяти».
По иронии судьбы, Patroni обрабатывает кворум, делегируя эту ответственность другому программному обеспечению. Patroni сообщает о совместимости с четырьмя различными сервисами ключ-значение или распределёнными конфигурационными хранилищами (Distributed Configuration Store, DCS), включая etcd, Consul, ZooKeeper и даже Kubernetes. В реальности Patroni всё равно, где находится слой DCS или из чего он состоит, лишь бы он отвечал на запросы чтения и записи.
Вот почему слой «DCS» на диаграмме представляет собой плоскость, поддерживающую все узлы Postgres. DCS может находиться где угодно, использовать любое количество узлов, и Patroni не нужно им управлять.
Оркестрация
Patroni — это специализированный инструмент высокой доступности, разработанный специально для Postgres. В результате он знает, как управлять всем, что связано с кластером экземпляров Postgres, включая, но не ограничиваясь:
- запуском и остановкой службы Postgres.
- повышением реплик (promotion).
- созданием новых реплик.
- понижением статуса основных узлов.
- номерами журналов (LSN).
- слота репликации.
Patroni хранит все метаданные в слое DCS, регулярно обновляя их для каждого узла. «Кластер» всегда знает статус всех узлов, включая любую задержку репликации. Магия того, как Patroni работает так хорошо, заключается в том, как он узнает, какой узел является основным для кластера: с помощью маркера лидерства (leadership token). Вот как это работает:
- Patroni проверяет, владеет ли текущий узел маркером лидерства.
- Если да, обновляет маркер и перезапускает цикл.
- Если нет, может ли этот узел забрать маркер лидерства?
- Если да, забирает маркер, повышает этот узел и перезапускает цикл.
- Если нет, действует как обычная реплика, перенастраиваясь на текущий основной узел при необходимости.
Конечно, есть и другие шаги, но поскольку уровень консенсуса распределён, может существовать только один маркер лидерства. Как только узел получает маркер, ни один другой узел не может объявить себя основным. Основываясь на том, у какого узла есть маркер, Patroni перенастраивает все остальные узлы на использование его в качестве основного. Если реплика сталкивается с ошибками воспроизведения или была предыдущим основным узлом, Patroni использует pg_rewind, pg_basebackup с текущего основного узла или даже восстанавливается из сохранённой резервной копии для перестройки узла.
Это то, чего почти не делают другие инструменты высокой доступности. Patroni не только повысит замену основному узлу, но и перестроит вышедший из строя узел, если сможет. Если вы добавите новый узел в кластер, он создаст каталог данных от вашего имени на основе конфигурации кластера. DCS является единственным источником истины, из которого исходит Patroni, и в некотором смысле сам DCS и есть кластер.
Ситуация становится действительно интересной, когда сам слой DCS испытывает сбои.
Fencing (Изоляция)
Идея, лежащая в основе fencing, заключается в том, что некорректно работающий узел должен быть выведен из эксплуатации. Причина этого обманчиво проста: при отсутствии консенсуса нельзя доверять никаким записанным данным. Существует множество причин, по которым узел может потерять связь с DCS, или DCS может отказываться отвечать, и ни одна из них не имеет значения. Самый безопасный способ действий — остановить Postgres.
Если основной узел не может сохранить владение маркером лидерства, другой узел захватывает его. Процесс Patroni на этом узле повышает его до лидера, кластер перенастраивается вокруг этого нового основного узла, и работа продолжается. Изолированные реплики не должны беспокоиться о записи, но они также не могут участвовать в гонке за лидерство.
Изолированный основной узел знает, что другой узел был повышен в его отсутствие, что он должен отклонять записи, чтобы предотвратить риск "взрыва мозга" (split brain), что он больше не должен принимать новые соединения. Точно так же реплика, отрезанная от DCS, не может быть отслежена, вероятно, накапливает задержку репликации и в остальном вызывает подозрения. В результате Patroni останавливает службу Postgres на этом узле.
Как ни странно, большинство решений высокой доступности для Postgres упускают этот критический фактор. Почти все они обнаружат сбой основного узла и повысят реплику, но почти никто не учитывает, что произойдёт, если отказ произойдёт в сети, а не в самом узле или Postgres. В таких системах изолированный узел продолжает принимать записи от сосуществующих систем или установленных соединений, продолжает нормально работать и не знает и не заботится о том, что где-то произошло повышение.
Служба Postgres на изолированных узлах обязательно должна самоуничтожиться, и Patroni гарантирует такой результат самим своим дизайном. Потерял контакт с DCS, или DCS отказывает в запросам по любой причине — выключайся. Просто.
Примечание: Один из сценариев отказа для кластера заключается в том, что сам DCS теряет кворум. В кластере из пяти узлов сетевая ошибка может разделить два узла от остальных трёх. В такой ситуации два узла потеряли большинство и откажутся работать в этом состоянии. Служба Patroni на любом затронутом узле не знает этого, и, по сути, это не имеет значения. Конечный результат всегда один — остановить Postgres.
Маршрутизация
Последнее, что делает Patroni для создания кластера Postgres, — управляет маршрутизацией соединений. Он делает это, отслеживая владение маркером лидерства и предоставляя интерфейс состояния по HTTP REST. Любая фронтальная система маршрутизации может опросить узел Patroni на предмет его текущего состояния: работает ли Postgres, должен ли узел считаться доступным для записи, слишком ли велика задержка репликации и так далее.
Обычным выбором для этого уровня маршрутизации является HAProxy, как показано на архитектурной диаграмме, но это может быть и балансировщик нагрузки F5, и Amazon ELB и так далее. Это определяет, какие соединения достигнут какого узла — и должен ли узел вообще разрешать соединения. Пользователям, желающим подключиться к основному узлу Postgres, нужно просто подключиться к уровню маршрутизации. Важно подключиться к реплике с задержкой репликации менее 5 МБ? Уровень маршрутизации. Patroni оценивает критерии, закодированные в запросе проверки работоспособности, и отвечает соответствующим образом.
Изоляция — это одна половина уравнения, а управление маршрутизацией — другая. Если Patroni определяет, что узел не должен быть доступен для маршрутизации по какой-либо причине, он просто возвращает сбой на REST-интерфейсе. Правильно настроенный компонент маршрутизации немедленно разорвёт все установленные соединения и откажется от будущей маршрутизации на этот узел до тех пор, пока он снова не станет работоспособным.
Что более важно, пользователям и приложениям не нужно ничего знать об узле, к которому они подключаются. В некотором смысле они подключаются не к узлу, а к самому кластеру. Теперь, наконец, можно точно описать Postgres как кластер узлов. Каждый отдельный узел на самом деле работает не по-другому, но Patroni и лежащий в основе DCS создают базовую структуру, которая связывает всё воедино.
А как насчёт Kubernetes?
Kubernetes решает проблему истинного создания кластера Postgres аналогичным образом. Операторы Kubernetes, такие как CloudNativePG, либо берут на себя ту же роль, что и Patroni, либо буквально используют Patroni под капотом, как оператор от Crunchy. Но Kubernetes — это богатая экосистема взаимодействующих компонентов со своей собственной идеологией дизайна. Кластер Postgres возникает из этого дизайна как следствие его основополагающих принципов.
Kubernetes не специфичен для Postgres, и решение высокой доступности для Postgres в Kubernetes не может существовать вне этой среды. Это вполне валидный способ превратить Postgres в кластер, но программное обеспечение вне контекста Kubernetes не может использовать эти возможности. По крайней мере сейчас, подавляющее большинство инсталляций Postgres всё ещё существуют на «голом железе», виртуальных машинах и других ad-hoc ручных развёртываниях.
Заключительные мысли
Давайте на секунду посмотрим на MongoDB. Ниже приведена диаграмма, взятая непосредственно из их документации по архитектуре.

Мы здесь не для того, чтобы обсуждать достоинства Postgres против MongoDB или NoSQL в целом. Однако очень внимательно посмотрите на эту структуру. В руководстве по дизайну подробно описывается архитектура сегментов:
«Если основной узел реплики для сегмента выходит из строя, вторичные реплики вместе определяют, какая реплика должна быть выбрана новым основным узлом, используя расширенную реализацию алгоритма консенсуса Raft».
Вам это ничего не напоминает? Игнорируйте на мгновение аспект сегментирования и подумайте, что здесь происходит. У кластера есть уровень маршрутизации, а узлы координируются посредством консенсуса через Raft. MongoDB была спроектирована с самого начала как кластер, в то время как Postgres рассматривает взаимодействие узлов как запоздалую мысль. За исключением расширений, таких как Spock от pgEdge, BDR от EnterpriseDB, или форков, таких как YugabyteDB, каждый узел Postgres — это изолированный остров. Даже Citus, расширение, известное использованием узлов-координаторов и узлов данных и которое должно мыслиться как кластер, нуждается в Patroni для обработки отказоустойчивости между репликами узлов данных.
Postgres просто не является самоорганизующимся кластером без некоторого внешнего уровня оркестрации. На данный момент Patroni — лучший из них. Есть причина, по которой мы используем его в pgEdge как часть нашей архитектуры Ultra HA. Невозможно сказать, что может принести будущее, но пока что — просто используйте Patroni.
Trackbacks
The author does not allow comments to this entry
Comments
Display comments as Linear | Threaded