etcd — одно из нескольких решений задачи, с которой сталкиваются многие программы, работающие в распределённом режиме на наборе хостов, любой из которых может выйти из строя или потребовать перезагрузки в произвольный момент.
Одна из таких программ — Patroni; ранее я уже публиковал введение в Patroni, а также руководство по построению высокодоступного кластера PostgreSQL на его основе.
В том руководстве я лишь вкратце затронул причину, по которой Patroni нужен инструмент вроде etcd. Вот краткое напоминание:
-
Каждый экземпляр Patroni отслеживает данные о состоянии соответствующего экземпляра PostgreSQL.
-
Эти данные о состоянии нужно где-то хранить — там, где к ним имеют доступ все остальные экземпляры Patroni.
-
На основе этих данных каждый экземпляр Patroni решает, какие действия нужно предпринять, чтобы поддерживать работоспособность кластера в целом.
-
Экземпляр Patroni может решить, что ему необходимо повысить свой экземпляр PostgreSQL до primary, обнаружив, что в данный момент primary отсутствует.
-
Такой экземпляр Patroni должен быть уверен, что, пока он пытается повысить базу данных, ни один другой экземпляр Patroni не попытается сделать то же самое. Этот процесс называется «гонкой за лидерство» (leader-race), где условной финишной чертой является захват блокировки «лидера» (leader lock). Как только экземпляр Patroni захватил эту блокировку, остальные не могут её получить, пока новый лидер сам её не отпустит или не сумеет продлить её время жизни (time to live).
Задача теперь состоит в том, чтобы предоставить механизм, гарантирующий, что лишь один экземпляр Patroni сможет успешно захватить эту блокировку.
В обычных, нераспределённых вычислительных системах это условие охранялось бы средством взаимного исключения — так называемым мьютексом (mutex). Мьютекс — это программное решение, которое помогает гарантировать, что в каждый момент времени данную переменную может изменять только одна программа.
Для распределённых систем реализовать такой мьютекс сложнее:
Программы, конкурирующие за переменную, должны отправить свой запрос на изменение, после чего где-то должно быть принято решение о том, можно ли этот запрос принять. В зависимости от результата на запрос приходит ответ — «успех» или «отказ». Однако, поскольку любой из хостов вашего кластера может стать недоступным, делать этот механизм принятия решений централизованным было бы неразумно.
Вместо этого нужен инструмент, обеспечивающий распределённый механизм принятия решений. В идеале он же мог бы хранить и сами переменные, которые пытаются изменять ваши распределённые программы. Такой инструмент в общем случае называется распределённым хранилищем консенсуса (Distributed Consensus Store, DCS): он обеспечивает необходимую изоляцию и атомарность для изменения охраняемых им переменных в режиме взаимного исключения.
Пример такого инструмента — etcd, но есть и другие: consul, cockroach, и, вероятно, появятся новые. Некоторые из них (etcd, consul) строят свои распределённые решения на протоколе Raft, включающем концепцию выбора лидера (leader election). Пока члены Raft-кластера способны путём голосования выбрать лидера, DCS может нормально работать и принимать изменения данных, поскольку именно лидер текущей временной линии решает, можно ли принять запрос на изменение переменной. Хорошее объяснение и наглядный пример протокола Raft можно найти в тематических визуализациях.
В etcd и consul запросы на изменение ключей и значений можно формировать с условиями, например: «измени эту переменную, только если ранее ей ничего не было присвоено» или «измени эту переменную, только если раньше она была равна 42».
Patroni использует такие запросы, чтобы гарантировать, что устанавливает leader-lock только в том случае, если он ещё не установлен; он же применяет их для продления leader-lock, но лишь если блокировка соответствует его собственному имени члена кластера. Patroni ждёт ответа от DCS и затем продолжает работу. Например, если Patroni не удалось захватить leader-lock, он не станет повышать или инициализировать экземпляр PostgreSQL.
С другой стороны, если Patroni не сможет продлить время жизни, связанное с leader-lock, он остановит базу данных PostgreSQL, чтобы понизить её (demote), поскольку иначе он не может гарантировать, что после истечения ключа лидера ни один другой член кластера не попытается стать лидером.
Один из шагов, описанных в предыдущей статье, — шаг развёртывания кластера Patroni — поднимает кластер etcd:
etcd > etcd_logfile 2>&1 &
Однако этот кластер etcd состоит всего из одного члена.
Проблемы кластеров etcd с единственным членом
В этом сценарии, пока этот единственный член жив, он может избрать сам себя лидером и, следовательно, принимать запросы на изменение. Именно так наш первый кластер Patroni смог функционировать поверх единственного члена etcd.
Но если бы этот экземпляр etcd упал, это означало бы полную потерю DCS. Вскоре после сбоя etcd экземпляр Patroni, управляющий primary-узлом PostgreSQL, вынужден был бы остановить этот primary, так как продлить время жизни ключа лидера стало бы невозможно.
Чтобы защититься от таких сценариев, — когда единичный сбой etcd или плановое обслуживание могут нарушить работу нашего столь желанного высокодоступного кластера PostgreSQL, — нужно добавить в кластер etcd больше членов.
Кластеры etcd из трёх членов
Число членов кластера, которое вы решите развернуть, — важнейшее соображение при развёртывании любого DCS. Здесь есть два основных аспекта:
Насколько вероятно, что член etcd выйдет из строя или будет недоступен из-за обслуживания?
Обычно члены etcd не отказывают просто так. Поэтому, если вы позаботитесь о том, чтобы избегать пиков нагрузки, проблем с хранилищем и памятью, вам стоит рассмотреть как минимум два члена кластера, чтобы один можно было выключить для обслуживания.
Как в процессе голосования находится большинство?
По своей конструкции Raft-кластеры требуют абсолютного большинства для избрания лидера. Это абсолютное большинство зависит от общего числа членов кластера etcd, а не только от тех, что доступны для голосования. Это значит, что число голосов, необходимое для большинства, не меняется, когда член становится недоступен. Названное только что число — два — было лишь нижней границей с точки зрения стабильности системы и удобства обслуживания.
На деле DCS из двух членов работать не будет: как только вы выключите один член, второй не сможет получить большинство голосов за свою кандидатуру в лидеры, ведь 1 из 2 — это не более 50%. Если мы хотим иметь возможность выборочно выключать члены для обслуживания, придётся добавить третий узел, который затем сможет участвовать в выборе лидера. Тогда всегда будет два голосующих — из трёх членов в сумме — чтобы выбрать лидера; две трети — это больше 50%, так что требование абсолютного большинства выполняется.
Размещение членов кластера etcd в разных дата-центрах
Второе по важности соображение при развёртывании кластеров etcd — размещение узлов etcd. Из-за упомянутого истечения ключа лидера и необходимости для Patroni обновлять его в начале каждого цикла, а также из-за того, что неудача при этом приводит к принудительной остановке лидера, размещение членов кластера etcd косвенно определяет, как Patroni будет реагировать на сетевые разделения (network partitions) и на отказ меньшинства узлов.
Для начала: простейшая схема размещается в одном дата-центре («полностью смещённое размещение членов кластера»), так что на все члены Patroni и etcd не влияют проблемы, обрывающие сетевой доступ во внешний мир. В то же время такая схема просто справляется с потерей меньшинства кластера etcd: ей всё равно — до тех пор, пока остаётся большинство членов etcd, способных общаться друг с другом и с которыми по-прежнему может общаться Patroni.
Но если вы предпочитаете схему с несколькими дата-центрами, где реплицирующиеся базы данных расположены в разных дата-центрах, размещение членов etcd становится критически важным.
Если вы следили за рассуждениями, то согласитесь, что размещать все члены etcd в основном дата-центре («полностью смещённое размещение членов кластера») неразумно. Если ваш основной дата-центр окажется отрезан от внешнего мира, для членов Patroni во вторичном дата-центре не останется ни одного etcd, с которым можно общаться, и даже будучи в исправном состоянии, они не смогут выполнить повышение. Альтернатива — разместить большинство членов в первом дата-центре, а меньшинство — во вторичном («смещённое размещение членов кластера»).
Кроме того, если один член etcd в вашей основной базе данных остановлен — из-за чего первый дата-центр остаётся без большинства членов etcd, — и вторичный дата-центр также становится недоступен, ваш лидер базы данных в первом дата-центре будет остановлен.
Вы, безусловно, могли бы вручную сгладить этот граничный случай, разместив один член etcd в третичной позиции («размещение третичного арбитра»), вне и первого, и второго дата-центров, с подключением к каждому из них. Тогда оба дата-центра должны содержать равное число членов etcd.
Таким образом, даже если один из дата-центров станет недоступен, оставшийся дата-центр вместе с третичным членом etcd всё ещё сможет прийти к консенсусу. Размещение одного члена кластера etcd в третичной локации может дать и дополнительное преимущество: его восприятие сетевых разделений может быть ближе к тому, как сетевые разделения воспринимают ваши клиенты и приложения.
Размещение третичного члена кластера в роли арбитра
При упомянутых выше смещённых стратегиях размещения консенсус может быть достигнут внутри дата-центра, который полностью отрезан от всего остального. Размещение одного члена в третичной позиции означает, что консенсус может быть достигнут только с членами того дата-центра, у которого сохранился исправный аплинк.
Чтобы ещё больше повысить устойчивость такого кластера, мы можем разместить в третичной позиции даже более одного узла — чтобы застраховаться от отказов единственного третичного члена и от сетевых проблем, способных отключить единственный третичный член.
Как видите, при желании создать по-настоящему устойчивый кластер etcd нужно учитывать довольно много вещей. Приведённые примеры не являются исчерпывающим списком, и на потребности в размещении членов могут влиять и другие факторы. Тем не менее наш опыт показывает, что члены etcd отказывают редко (если только не возникают всплески задержек диска или перегрузка процессора) и что большинство клиентов всё равно хотят смещённое решение, поскольку основной дата-центр является предпочтительным. Обычно в смещённых схемах можно добавить новый член во вторичный дата-центр и исключить один из членов в основном, чтобы сместить перевес во второй дата-центр. Так вы сможете продолжать работу базы данных, даже если первый дата-центр станет полностью недоступен.
Настройка кластера etcd
Разобравшись с соображениями по размещению, посмотрим, как создать простой сервер etcd. В демонстрационных целях мы ограничим эту установку тремя хостами: 10.88.0.41, 10.88.0.42, 10.88.0.43.
Есть несколько разных методов настроить и запустить («забутстрапить», bootstrap) кластеры etcd; я опишу два из них:
-
Метод статического бутстрапа требует знания сетевых адресов всех членов кластера. Все адреса затем явно перечисляются в файле конфигурации каждого члена кластера. Этот подход хорош для обучения и прост в отладке, так как всегда можно заглянуть в файл конфигурации и увидеть, какие хосты входят в кластер.
-
Метод бутстрапа через discovery несколько более неявный и лучше подходит для схем, где адреса могут регулярно меняться или где реальных адресов может вовсе не быть, например: в кластерах контейнеров, где все контейнеры доступны по одному и тому же адресу, но через разные порты.
Некоторые настройки нужно прописать в конфигурацию etcd независимо от метода установки.
Поскольку etcd использует два разных канала связи — один для одноранговой (peer) связи с другими членами кластера etcd, другой для клиентской связи с пользователями и приложениями, — некоторые параметры конфигурации могут выглядеть почти одинаково, но тем не менее необходимы.
Каждому члену etcd нужно следующее:
-
name: уникальное в пределах кластера etcd имя. -
listen-peer-urls: URL, указывающий, где etcd должен слушать запросы от своих пиров. Обычно это IP или имя хоста, доступное с хостов других членов etcd. -
listen-client-urls: URL, указывающий, где etcd должен слушать запросы от клиентов. Обычно это IP или имя хоста, доступное с хостов других членов etcd. Если вы хотите разрешить также локальные подключения пользователей и приложений, можно добавить URL на основе локального IP-адреса. -
initial-advertise-peer-urls: URL, указывающий, какой адрес должны использовать другие члены кластера для подключения к этому члену кластера etcd. -
advertise-client-urls: URL, указывающий, какой адрес должны использовать клиенты, если они хотят подключиться к этому члену кластера etcd. Это должен быть адрес, доступный из сети, а не локальный, даже если этот член слушает локальные подключения.
Перечисленные выше параметры обязательны для каждого члена etcd и должны различаться у всех членов etcd, поскольку у них у всех должны быть разные адреса — или, по крайней мере, разные порты.
Настройка кластера etcd методом статического бутстрапа
Для метода статического бутстрапа всем членам etcd дополнительно нужны следующие параметры:
-
initial-cluster-token: токен, уникальный для вашего кластера. Просто сгенерируйте несколько случайных символов. -
initial-cluster: безусловно, важнейший компонент, необходимый для успешного бутстрапа etcd. Это строка с перечислением через запятую всех членов кластера и их анонсируемых peer-url. Пример:centos_test_1=http://10.88.0.41:2380,centos_test_2=http://10.88.0.42:2380,centos_test_3=http://10.88.0.43:2380
Теперь, если вы запустите экземпляр etcd на каждом из своих узлов с подходящими файлами конфигурации, они попытаются связаться друг с другом и — если конфигурация верна и никакие правила брандмауэра не блокируют трафик — забутстрапят кластер etcd.
Поскольку эта статья и так получилась довольно длинной, я подготовил архив для скачивания, содержащий логи с файлами конфигурации, а также вывод процессов etcd.
Настройка кластера etcd методом бутстрапа через discovery
Другой подход к бутстрапу, как я упоминал ранее, опирается на существующий кластер etcd для discovery. При этом не обязательно иметь настоящий кластер etcd. Помните, что один процесс etcd уже действует как кластер, пусть и без высокой доступности. В качестве альтернативы можно использовать публичный сервис discovery по адресу discovery.etcd.io.
Для целей этого руководства я создам временный локальный кластер etcd, доступный всем будущим членам:
Здесь нам не нужны параметры peer, так как у этого бутстрапера не предполагается наличия пиров.
Нужно создать специальный каталог и ключ, указывающий ожидаемое число членов нового кластера. Мы сгенерируем уникальный discovery-токен для этого нового кластера:
UUID=$(uuidgen)
echo $UUID
860a192e-59ae-4a1a-a73c-8fee7fe403f9
Следующий вызов curl создаёт специальный ключ size, что неявно создаёт каталог для бутстрапа кластера в хранилище ключ-значение бутстрапера:
Трём будущим членам etcd теперь нужно лишь (помимо пяти базовых параметров, перечисленных ранее) знать, где найти этот сервис discovery, поэтому во все будущие члены кластера etcd добавляется следующая строка:
Теперь, когда экземпляры etcd будут запущены, они зарегистрируются в каталоге, который мы создали ранее. Как только там соберётся столько членов, сколько указано в _config/size, они самостоятельно забутстрапят новый кластер. В этот момент вы можете безопасно завершить работу экземпляра etcd-бутстрапера.
Вывод метода бутстрапа через discovery вместе с файлами конфигурации также можно найти в архиве.
Проверка работоспособности etcd
Чтобы проверить, был ли бутстрап успешным, можно вызвать команду etcdctl cluster-health.
Если вы хотите выполнить эту команду не на хостах, содержащих члены etcd, вам придётся указать конечные точки (endpoints), с которыми etcdctl должен обращаться напрямую:
Обычно etcd хочется запускать в виде некоторого демона, стартующего при каждом запуске сервера, чтобы защититься от периодических сбоев и от невнимательности после планового обслуживания.
Если вы установили etcd через пакетный менеджер вашей операционной системы, service-файл уже будет установлен. Этот service-файл устроен так, что все необходимые параметры конфигурации загружаются через переменные окружения. Их можно задать в файле /etc/etcd/etcd.conf; у них немного другая нотация по сравнению с параметрами, которые мы размещали в YAML-файлах в примерах выше. Чтобы преобразовать YAML-конфигурацию, нужно просто перевести все имена параметров в верхний регистр и заменить дефисы (-) на подчёркивания (_).
Подводные камни
Если вы попробуете следовать этому руководству на Ubuntu или Debian и установить etcd через apt, вы столкнётесь с проблемами. Это следствие того, что всё, что напоминает сервер, автоматически запускает экземпляр через systemd после завершения установки. Этот экземпляр нужно убить, иначе вы не сможете запустить свои члены кластера etcd на портах 2379 и 2380.
С недавним обновлением до etcd 3.4 API v2 в etcd по умолчанию отключён. Поскольку API v3 в etcd в настоящее время непригоден для работы с Patroni (из-за отсутствия в библиотеке поддержки нескольких конечных точек etcd, см. соответствующий pull request), вам придётся вручную снова включить поддержку API v2, добавив в файл конфигурации enable-v2=true.
Заключение
Забутстрапить сервер etcd может быть довольно сложно, если браться за это вслепую. Однако, как только ключевые концепции кластеров etcd поняты и вы разобрались, что именно должно попасть в файлы конфигурации, вы сможете забутстрапить кластер etcd быстро и легко.
Хотя при размещении членов в более сложных схемах с несколькими дата-центрами нужно учитывать множество вещей, простой кластер из трёх узлов, вероятно, вполне подойдёт для любого тестового окружения и для схем, охватывающих лишь один дата-центр.
Имейте в виду, что продемонстрированные мной схемы кластеров были лишены каких-либо соображений безопасности ради удобства экспериментов. Настоятельно рекомендую изучить различные варианты защиты etcd. Как минимум стоит включить аутентификацию по имени роли и паролю, а серверные сертификаты — вероятно, хорошая идея для шифрования трафика, если ваша сеть может быть подвержена атакам прослушивания. Для ещё большей безопасности можно добавить и клиентские сертификаты. Разумеется, Patroni хорошо работает со всеми тремя механизмами безопасности в любой комбинации.
Серия также охватит: — конфигурацию и устранение неполадок; — переключение (failover), обслуживание и мониторинг; — обработку и маршрутизацию клиентских подключений; — архивирование WAL и резервное копирование баз данных с помощью pgBackRest; — восстановление на момент времени (PITR) кластера Patroni с помощью pgBackRest.