GitHub использует MySQL в качестве основного хранилища данных для всего, что не связано с git, поэтому доступность MySQL имеет ключевое значение для нормальной работы GitHub. Сам сайт, интерфейс API на GitHub, система аутентификации и многие другие функции требуют доступа к базам данных. Мы используем несколько кластеров MySQL для обработки различных служб и задач. Они настроены по классической схеме с одним главным узлом, доступным для записи, и его репликами. Реплики (остальные узлы кластера) асинхронно воспроизводят изменения главного узла и обеспечивают доступ для чтения. Доступность главных узлов критически важна. Без главного узла кластер не поддерживает запись, а это значит, что нельзя сохранить необходимые изменения. Фиксация транзакций, регистрация проблем, создание новых пользователей, репозиториев, обзоров и многое другое будет просто невозможно. Для поддержки записи необходим соответствующий доступный узел – главный узел в кластере. Впрочем, не менее важна возможность определить или обнаружить такой узел. В случае отказа текущего главного узла важно обеспечить оперативное появление нового сервера ему на замену, а также иметь возможность быстро оповестить об этом изменении все службы. Общее время простоя складывается из времени, уходящего на обнаружение сбоя, отработку отказа и оповещение о новом главном узле.  В этой публикации описано решение для обеспечения высокой доступности MySQL в GitHub и обнаружения главной службы, которое позволяет нам надежно выполнять операции с охватом нескольких центров обработки данных, поддерживать работоспособность при недоступности отдельных таких центров и гарантировать минимальное время простоя при сбое. Решение, описанное в статье, – это новая, улучшенная версия предыдущих решений для обеспечения высокой доступности (HA), реализованных в GitHub. По мере нашего роста нам необходимо адаптировать HA-стратегию MySQL к изменениям. Мы стремимся придерживаться аналогичных подходов для MySQL и других служб в GitHub. Чтобы найти подходящее решение для обеспечения высокой доступности и обнаружения служб следует сначала ответить на несколько конкретных вопросов. Вот их примерный список: В целях демонстрации давайте сначала рассмотрим предыдущий вариант решения и обсудим, почему мы решили от него отказаться. В рамках предыдущего решения мы использовали: В том случае клиенты обнаруживали узел записи по его имени, например, mysql-writer-1.github.net. По имени определялся виртуальный IP-адрес (VIP) главного узла. Таким образом, в обычной ситуации клиентам достаточно было просто разрешить имя и подключиться по полученному IP-адресу, где их уже ожидал главный узел. Рассмотрим следующую топологию репликации, охватывающую три различных центра обработки данных:  В случае сбоя главного узла на его место должен быть назначен новый сервер (одна из реплик). orchestrator обнаруживает сбой, выбирает новый главный узел, а затем назначает имя/VIP. Клиентам на самом деле неизвестны идентификационные данные главного узла, они знают только имя, которое теперь должно указывать на новый узел. Однако обратите внимание вот на что. VIP-адреса используются совместно, серверы баз данных сами запрашивают их и владеют ими. Чтобы получить или освободить VIP, сервер должен отправить ARP-запрос. Сервер, владеющий VIP, должен сначала освободить его, прежде чем новый главный узел получит доступ к этому адресу. Такой подход приводит к некоторым нежелательным последствиям: Кое-где в рамках нашей среды VIP-адреса связаны с физическим местоположением. Они назначены коммутатору или маршрутизатору. Поэтому мы можем переназначить VIP-адрес только серверу, расположенному в том же окружении, что и первоначальный главный узел. В частности, в некоторых случаях мы не сможем назначить VIP серверу в другом центре обработки данных и должны будем внести изменения в DNS. Этих ограничений было достаточно, чтобы вынудить нас начать поиск нового решения, но учесть нужно было и следующее: Эти дополнительные шаги приводили к увеличению общего времени простоя и добавляли собственные точки отказа и возникновения проблем. Решение работало, и GitHub успешно отрабатывал отказы MySQL в фоновом режиме, но мы хотели улучшить свой подход к HA следующим образом: Наша новая стратегия, наряду с сопутствующими улучшениями, устраняет большую часть проблем, упомянутых выше, или смягчает их последствия. Наша текущая HA-система состоит из следующих элементов:  Новая схема позволила полностью отказаться от внесения изменений в VIP и DNS. Теперь при вводе новых компонентов мы можем отделить их и упростить задачу. Кроме того, мы получили возможность использовать надежные и стабильные решения. Подробный разбор нового решения приведен далее. В обычной ситуации приложения подключаются к узлам записи через GLB/HAProxy. Приложения не получают идентификационной информации главного сервера. Как и раньше, они используют только имя. Например, главным узлом для cluster1 будет mysql-writer-1.github.net. Однако в нашей текущей конфигурации это имя разрешается в IP-адрес anycast. Благодаря технологии anycast имя разрешается в один тот же IP-адрес в любом месте, но трафик направляется по-разному, учитывая местоположение клиента. В частности, в каждом из наших ЦОД развернуто несколько экземпляров GLB, нашего высокодоступного балансировщика нагрузки. Трафик на mysql-writer-1.github.net всегда направляется к кластеру GLB локального ЦОД. За счет этого все клиенты обслуживаются локальными прокси. Мы запускаем GLB поверх HAProxy. Наш сервер HAProxy предоставляет пулы записи: по одному на каждый кластер MySQL. При этом у каждого пула лишь один сервер (главный узел кластера). Все экземпляры GLB/HAProxy во всех ЦОД имеют одинаковые пулы, и все они указывают на одни и те же серверы в этих пулах. Таким образом, если приложение хочет записать данные в базу на mysql-writer-1.github.net, то не имеет значения, к какому серверу GLB оно подключается. В любом случае будет выполнено перенаправление на фактический главный узел кластера cluster1. Для приложений обнаружение заканчивается на GLB, а необходимость в повторном обнаружении отсутствует. Именно GLB перенаправляет трафик в нужное место. Откуда в GLB поступает информация о том, какие серверы включать в список? Как мы вносим изменения в GLB? Служба Consul широко известна как решение для обнаружения служб, кроме того, она также берет на себя функции DNS. Впрочем, в нашем случае мы используем ее как высокодоступное хранилище значений ключей (KV). В хранилище KV в Consul мы записываем идентификационные данные главных узлов кластера. Для каждого кластера существует набор записей KV, указывающих на данные соответствующего главного узла: его fqdn, порт, адреса ipv4 и ipv6. Каждый узел GLB/HAProxy запускает consul-template, службу, которая отслеживает изменения в данных Consul (в нашем случае это изменения в данных главных узлов). Служба consul-template создает файл конфигурации и может перезагрузить HAProxy при изменении настроек. Благодаря этому информация об изменении идентификационных данных главного узла в Consul доступна каждому экземпляру GLB/HAProxy. На основе этой информации выполняется настройка экземпляров, новые главные узлы указываются в качестве единственной сущности в пуле серверов кластера. После этого экземпляры перезагружаются, чтобы изменения вступили в силу. Мы развернули экземпляры Consul в каждом ЦОД, и каждый экземпляр обеспечивает высокую доступность. Тем не менее, эти экземпляры независимы друг от друга. Они не выполняют репликацию и не обмениваются какими-либо данными. Откуда Consul получает информацию об изменениях и как она распространяется между ЦОД? Мы используем схему orchestrator/raft: узлы orchestrator взаимодействуют друг с другом посредством консенсуса raft. В каждом ЦОД у нас один или два узла orchestrator. orchestrator отвечает за обнаружение сбоев, отработку отказов MySQL и передачу измененных данных о главном узле в Consul. Отработка отказа управляется одним ведущим узлом orchestrator/raft, но изменения, новости о том, что в кластере теперь новый главный узел, распространяются на все узлы orchestrator с помощью механизма raft. Когда узлы orchestrator получают новости об изменении данных главного узла, каждый из них связывается со своим локальным экземпляром Consul и инициирует запись KV. ЦОД с несколькими экземплярами orchestrator получат несколько (идентичных) записей в Consul. При сбое главного узла: Для каждого компонента обязанности четко распределены, и вся структура диверсифицирована и упрощена. orchestrator не взаимодействует с балансировщиками нагрузки. Службе Consul не требуются данные о происхождении информации. Прокси-серверы работают только с Consul. Клиенты работают только с прокси-серверами. Более того: Для стабилизации потока мы также применяем следующие методы: В следующих разделах мы рассматриваем проблемы и разбираем цели обеспечения высокой доступности. orchestrator использует комплексный подход к обнаружению сбоев, что обеспечивает высокую надежность инструмента. Мы не сталкиваемся с ложноположительными результатами, преждевременная отработка отказов не выполняется, а значит, исключаются необязательные простои. Схема orchestrator/raft также справляется с ситуациями полной сетевой изоляции ЦОД («ограждение» ЦОД). Сетевая изоляция ЦОД может вызвать путаницу: серверы внутри ЦОД могут взаимодействовать друг с другом. Как понять, кто на самом деле изолирован – серверы внутри данного ЦОД или все остальные ЦОД? В схеме orchestrator/raft ведущий узел raft выполняет отработку отказов. Ведущим становится узел, который получает поддержку большинства в группе (кворум). Мы развернули узел orchestrator таким образом, что ни один отдельный ЦОД не может обеспечить большинство, в то время как его обеспечивают любые n-1 ЦОД. В случае полной сетевой изоляции ЦОД узлы orchestrator в этом центре отключаются от аналогичных узлов в других ЦОД. В результате узлы orchestrator в изолированном ЦОД не могут стать ведущими в кластере raft. Если такой узел был ведущим, то он теряет этот статус. Новым ведущим будет назначен один из узлов других ЦОД. Этот ведущий будет иметь поддержку всех других ЦОД, способных взаимодействовать между собой. Таким образом, ведущий узел orchestrator всегда будет находиться за пределами изолированного от сети центра обработки данных. Если в изолированном ЦОД находился главный узел, orchestrator инициирует отработку отказа, чтобы заменить его сервером одного из доступных ЦОД. Мы смягчаем последствия изоляции ЦОД, делегируя принятие решений кворуму доступных ЦОД. Общее время простоя может быть дополнительно сокращено, если ускорить оповещение о смене главного узла. Как этого достичь? Когда orchestrator начинает отработку отказа, он рассматривает группу серверов, один из которых можно назначить главным. Учитывая правила репликации, рекомендации и ограничения, он способен принять обоснованное решение о наилучшем варианте действий. По следующим признакам он также может понять, что доступный сервер является идеальным кандидатом для назначения главным: В этом случае orchestrator сначала настраивает сервер как доступный для записи и немедленно объявляет о повышении его статуса (в нашем случае вносит запись в хранилище KV в Consul). При этом orchestrator асинхронно начинает исправлять дерево репликации, что обычно занимает несколько секунд. Вполне вероятно, что к тому времени, когда наши серверы GLB будут полностью перезагружены, дерево репликации также будет готово, хотя это и не обязательно. Вот и все: сервер готов к записи! В процессе полусинхронной репликации MySQL главный узел не подтверждает фиксацию транзакции до тех пор, пока изменения точно не будут переданы в одну или несколько реплик. Это позволяет обеспечить отработку отказов без потерь: любые изменения, примененные к главному узлу, либо уже применены, либо ожидают применения к одной из его реплик. Такая согласованность имеет свою цену, поскольку может привести к снижению доступности. Если ни одна реплика не подтвердит получение изменений, главный узел будет заблокирован, а запись остановится. К счастью, можно настроить время ожидания, по истечении которого главный узел сможет вернуться в режим асинхронной репликации, и запись будет возобновлена. Мы выбрали достаточно низкое значение времени ожидания: 500 мс. Этого более чем достаточно для отправки изменений с главного узла в реплики в локальном ЦОД и даже в удаленные ЦОД. С таким временем ожидания мы получили идеальный полусинхронный режим (без отката к асинхронной репликации), а также очень короткий период блокировки в случае отсутствия подтверждения. Мы включаем полусинхронную репликацию на локальных репликах в ЦОД и в случае выхода из строя главного узла ожидаем (хотя и не требуем) отработку отказа без потерь. Отработка отказа без потерь при полном отказе ЦОД обходится слишком дорого, поэтому мы этого и не ждем. Экспериментируя со временем ожидания для полусинхронной репликации, мы также обнаружили возможность повлиять на выбор идеального кандидата в случае сбоя главного узла. Активировав полусинхронный режим на нужных серверах и пометив их в качестве кандидатов, мы можем уменьшить общее время простоя, поскольку влияем на результат отработки отказа. Наши эксперименты показывают, что в большинстве случаев мы получаем идеальных кандидатов и, следовательно, быстрее распространяем информацию о смене главного узла. Вместо того, чтобы управлять запуском/остановкой службы pt-heartbeat на назначаемых/отключаемых главных узлах, мы решили запускать ее везде и всегда. Это потребовало определенной доработки, чтобы служба pt-heartbeat могла спокойно работать с серверами, которые либо часто изменяют значение параметра read_only, либо становятся полностью недоступны. В нашей текущей конфигурации службы pt-heartbeat работают как на главных узлах, так и на их репликах. На главных узлах они генерируют события пульса. На репликах они определяют доступность серверов только для чтения и регулярно проверяют их текущее состояние. Как только сервер становится главным, служба pt-heartbeat на этом сервере определяет его как доступный для записи и начинает генерировать события пульса. Мы также делегировали orchestrator следующие задачи: Это упрощает выполнение задач, связанных с новым главным узлом. Узел, который только что был назначен главным, явно должен быть работоспособным и доступным, иначе мы бы его не назначили. Поэтому имеет смысл предоставить orchestrator возможность применять изменения непосредственно на вновь назначенном главном узле. Использование прокси-слоя приводит к тому, что приложения не получают идентификационных данных главного узла, однако и сам этот узел не может идентифицировать приложения. Главному узлу доступны только соединения, поступающие из прокси-слоя, и мы теряем информацию о реальном источнике этих соединений. В плане развития распределенных систем, у нас все еще есть необработанные сценарии. Отметим, что при изоляции центра обработки данных, в котором находится главный узел, приложения в этом ЦОД по-прежнему могут осуществлять запись на такой узел. Это может привести к несогласованности состояний после восстановления сети. Мы стараемся смягчить последствия возникновения двух главных узлов в такой ситуации путем реализации метода STONITH изнутри самого изолированного ЦОД. Как уже говорилось ранее, пройдет некоторое время, прежде чем старый главный узел будет отключен, поэтому короткого периода «двоевластия» все-таки избежать не удастся. Эксплуатационные издержки, направленные на полное предотвращение возникновения таких ситуаций, очень высоки. Существуют и другие сценарии: отключение Consul во время отработки отказа, частичная изоляция ЦОД и т. д. Мы понимаем, что, работая с распределенными системами такого рода, невозможно закрыть все дыры, поэтому мы концентрируемся на самых важных. Наша система orchestrator/GLB/Consul обеспечила следующие преимущества: Схема «оркестровка/прокси/обнаружение служб» использует хорошо известные и надежные компоненты в несвязанной архитектуре, что упрощает развертывание, эксплуатацию и мониторинг. При этом каждый компонент можно отдельно масштабировать. Мы продолжаем искать способы улучшения, постоянно тестируя нашу систему.