Порой нам бывает нужно добавить избыточность какому-то сервису, который оказался публичной точкой входа в нашу инфраструктуру. Например, представьте, что мы хотим добавить второй балансировщик для высокой доступности. При этом балансировщики находятся на границе нашей сети и пересылают трафик доступным бэкенд-серверам.
┌─────────────┐
│ │
┌─────►│ Backend 1 │
│ │ │
│ └─────────────┘
│
│
│ ┌─────────────┐
│ │ │
┌────────────┐ ├─────►│ Backend 2 │
│ │ │ │ │
│ │ │ └─────────────┘
Public traffic │ Load │ │
───────────────────►│ ├────┤
│ Balancer │ │ ┌─────────────┐
│ │ │ │ │
│ │ ├─────►│ ... │
└────────────┘ │ │ │
│ └─────────────┘
│
│
│ ┌─────────────┐
│ │ │
└─────►│ Backend N │
│ │
└─────────────┘
Мы не можем просто добавить ещё один балансировщик нагрузки перед парой наших балансировщиков, потому что иначе любой переключатель трафика перед нашей парой высокой доступности также сам станет единой точкой отказа. Но нам всё равно как-то нужно организовать переключение трафика между парой этих балансировщиков:
┌─────────────┐
│ │
┌─────►│ Backend 1 │
┌────────────┐ │ │ │
│ │ │ └─────────────┘
│ │ │
Public traffic │ Load │ │
───────────────────►│ ├────┤ ┌─────────────┐
▲ │ Balancer 1 │ │ │ │
│ │ │ ├─────►│ Backend 2 │
│ │ │ │ │ │
└────────────┘ │ └─────────────┘
Switching? │
│
│ ┌────────────┐ │ ┌─────────────┐
│ │ │ │ │ │
▼ │ │ ├─────►│ ... │
Public traffic │ Load │ │ │ │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─►│ ├────┤ └─────────────┘
│ Balancer 2 │ │
│ │ │
│ │ │ ┌─────────────┐
└────────────┘ │ │ │
└─────►│ Backend N │
│ │
└─────────────┘
Подходы обеспеченных людей
Давно существуют разнообразные решения для этой задачи. Все они так или иначе манипулируют сетевой коммутацией на каком-то уровне, чтобы направить входящий трафик на оба или только один балансировщик.
VRRP, CARP, Virtual IP, Floating IP, ...
По сути сопоставляет один или несколько IP-адресов одному или нескольким активным балансировщикам нагрузки. IP-адреса (пере-)присоединяются к рабочим серверам в случае отказа других серверов. Такие методы в конечном счёте используют сетевое оборудование в качестве того самого переключателя между рабочими балансировщиками нагрузки.
Стоит заметить, что упомянутое сетевое оборудование совсем необязательно имеет какую-либо избыточность (резервирование). Например, вполне возможна пара из двух балансировщиков нагрузки подключенных к одному Ethernet-свитчу, который будет в этом случае единой точкой отказа. И даже свитчи с резервированием компонентов могут быть склонны к одновременным отказам из-за схожих условий работы или общего широковещательного домена.
Это решение подходит только для балансировки нагрузки внутри одного датацентра.
Anycast, BGP и методы, основанные на динамической маршрутизации
Обычно используются не сами по себе, а в сочетании с балансировщиками внутри датацентра. Отправляют анонсы одного и того же блока IP-адресов из разных местоположений, фактически выстраивая обслуживание этих IP-адресов машинами из разных датацентров. Эти методы в конечном счёте используют своих сетевых соседей по пирингу и соседей этих соседей как переключатель трафика перед собственной инфраструктурой, анонсируя или не анонсируя блоки IP-адресов из некоторого расположения (датацентра).
Этот способ в особенности доступен только достаточно крупным сетевым операторам, вероятно даже эксплуатирующим собственные автономные системы.
Методы, использующие DNS
Производят переключение трафика на уровне DNS, приводя клиента к правильному серверу. Существуют следующие широкоизвестные варианты:
Round robin DNS - на самом деле, не-вариант, потому что обнажает все известные серверы сразу, надеясь, что клиенту либо повезёт подключиться к рабочему серверу, либо он будет достаточно упорным, чтобы перепробовать их по-очереди.
Динамический DNS, отслеживающий состояние балансируемых серверов (AWS Route53, Cloudflare DNS LB, PowerDNS dnsdist, ...). Отслеживают работоспособность серверов и отвечают на DNS-запросы адреса сервера адресом только одного работоспособного сервера.
Занимательный факт насчёт таких облачных сервисов балансировки нагрузки состоит в том, что они тарифицируются на основе принятых DNS-запросов, однако мы в общем-то не имеем возможности ни как-то влиять на объём входящих DNS-запросов, ни проверить как много DNS-запросов на самом деле было произведено клиентами.
Проблемы обеспеченных людей
Некоторые из этих методов сложно правильно реализовать. Даже для keepalived рекомендуется использовать отдельный канал связи для VRRP-протокола. Иначе дефицит полосы пропускания на рабочем соединении будет мешать протоколу выбора мастера. Некоторые из них просты в интеграции (такие как DNS GTM-ы), но могут обернуться достаточно дорогостоящими.
Решения выше подразумевают что цель пересылки трафика (локальный балансировщик нагрузки или просто сервер) либо работоспособны, либо неисправны, что является некоторым допущением. Это может быть не всегда так, в особенности для решений, переключающих трафик между разными датацентрами. Например, AWS Route53 производит периодические пробы жизнеспособности серверов из разных местоположений, чтобы убедиться, что целевой сервер доступен. Но положительный результат такой пробы не обязательно свидетельствует о глобальной доступности из всех возможных удалённых местоположений - возможность установления соединения в Интернете не всегда однозначна.
Босяцкий Кластер Высокой Доступности не делает таких допущений, не ограничен только лишь одним датацентром, не имеет "подвижных частей" и не стоит Вам практически ничего. С ним Вы можете быстро запускать глобальные отказоустойчивые сервисы, что позволяет посвятить больше времени сведению концов с концами.
Схема
Обычно процесс разрешения доменных имён выглядит следующим образом:
┌────────────┐ A? example.com ┌───────────────┐
│ │ (1) │ │
│ ├───────────────►│ DNS recursive │
│ Client │ │ │
│ │◄───────────────┤ resolver │
┌─┤ │ A example.com │ │
│ └────────────┘ (8) └┬────┬────┬────┘
│ │ ▲ │ ▲ │ ▲
│ ┌─────────────────────────────┘ │ │ │ │ │
│ │A? example.com (2) │ │ │ │ │
│ │ ┌─────────────────────────────┘ │ │ │ │
│ ▼ │NS com (3) │ │ │ │
│ ┌──┴──────────┐ ┌────────────────┘ │ │ │
│ │ ├┐ │A? example.com (4)│ │ │A? example.com (6)
│ │ ROOT ││ │NS example.com (5)│ │ │A example.com (7)
│ │ ││ ▼ ┌────────────────┘ │ │
│ │ nameservers ││ ┌──┴──────────┐ │ │
│ │ ││ │ ├┐ │ │
│ └┬────────────┘│ │ .COM ││ │ │
│ └─────────────┘ │ ││ ▼ │
│ │ nameservers ││ ┌──────┴──────┐
│ │ ││ │ ├┐
│ └┬────────────┘│ │ example.com ││
│ └─────────────┘ │ ││
│ │ nameservers ││
│ │ ││
│ └┬────────────┘│
│ └─────────────┘
│ ┌─────────────┐
│ │ ├┐
│Actual connection │ EXAMPLE.COM ││
└─────────────────►│ ││
(9) │ servers ││
│ ││
└┬────────────┘│
└─────────────┘
Клиент желает установить соединение с некоторым хостом, указанным по его доменному имени. Клиент запрашивает DNS-резолвер (обычно это DNS-серверы, предоставляемые интернет-провайдером, и находящиеся в той же сети, что и клиент). DNS-резолвер, если не имеет адреса в кэше, следует по всей иерархии авторитативных DNS-серверов. На каждом шаге он оказывается или перенаправлен на следующий более конкретный сервер имён, куда запрошенный домен делегирован, или наконец получает запрошенную ресурсную запись.
Заметьте, что на каждом шаге DNS-рекурсору обычно доступно больше одного сервера имён для осуществления запросов. DNS имеет встроенные механизмы отказоустойчивости и, если какой-то нэймсервер не доступен, будет запрошен следующий в этой группе. Например, прямо сейчас доменная зона .COM обслуживается 13-ю серверами имён:
a.gtld-servers.net.
b.gtld-servers.net.
...
m.gtld-servers.net.
Каждый из этих серверов имён может быть запрошен для получения нэймсерверов домена example.com, и DNS-рекурсор свяжется со следующим сервером имён, если не получит ответ от сервера из первой попытки. Мы можем использовать это свойство, чтобы построить переключение трафика между рабочими серверами, основанное на DNS. Идея состоит в следующем: мы можем развернуть два или больше авторитативных нэймсерверов для нашего домена на самих рабочих серверах и настроить их возвращать собственный IP-адрес. Таким образом будет причинно-следственная связь между адресом нэймсервера, который смог достигнуть DNS-рекурсор, и IP-адресом используемым для связи с целевым сервером.
В отличии от обычных решений с динамическим DNS, мы не пытаемся решить какой сервер работоспособен, мы не делаем никаких активных проб. Мы просто позволяем DNS-рекурсору определить самостоятельно, какой нэймсервер доступен, и он в свою очередь приведёт клиента к той же машине, которая предоставила успешный DNS-ответ. Фактически, это возлагает прощупывание рабочего сервера и переключение на него на DNS-рекурсор клиента, позволяя нам отделаться двумя DNS-серверами со статической конфигурацией. Диаграмма взаимодействий могла бы выглядеть следующим образом:
┌────────────┐ A? example.com ┌───────────────┐
│ │ (1) │ │
│ ├───────────────►│ DNS recursive │
┌─┤ Client │ │ │
│ │ │◄───────────────┤ resolver │
│ │ │ A example.com │ │
│ └────────────┘ (9) (=LB2) └┬────┬────┬─┬──┘
│ │ ▲ │ ▲ │ │ ▲
│ ┌─────────────────────────────┘ │ │ │ │ │ │
│ │A? example.com (2) │ │ │ │ │ │
│ │ ┌─────────────────────────────┘ │ │ │ │ │
│ ▼ │NS com (3) │ │ │ │ │
│ ┌──┴──────────┐ ┌────────────────┘ │ │ │ │
│ │ ├┐ │A? example.com (4)│ │ │ │
│ │ ROOT ││ │NS example.com (5)│ │ │ │ A example.com (8)
│ │ ││ ▼ ┌────────────────┘ │ │ │
│ │ nameservers ││ ┌──┴──────────┐ │ │ │ (=LB2)
│ │ ││ │ ├┐ │ │ │
│ └┬────────────┘│ │ .COM ││ │ │ │
│ └─────────────┘ │ ││ │ │ │
│ │ nameservers ││ │ │ │
│ │ ││ │ │ │
│ └┬────────────┘│ │ │ │
│ └─────────────┘ │ │ │
│ A? example.com (6) │ │ │
│ ┌─xxxxxxxxxxxxxxxx────────────────────┘ │ │
│ │ A? example.com (7)│ │
│ │ ┌──xxxxxxxxxxxxx ┌────────────────┘ │
│ ▼ │ ▼ │
│ ┌──┴──────────────┐ ┌─────────────────┐ │
│ │ │ │ ├─┘
│ │ example.com │ │ example.com │
│ │ │ │ │
│ │ nameserver1 & │ │ nameserver2 & │
│ │ │ │ │
│ │ loadbalancer1 │ │ loadbalancer2 │
│ │ │ │ │
│ │ (FAULTY) │ │ (HEALTHY) │
│ │ │ │ │
│ └─────────────────┘ └─────────────────┘
│ ▲
│ Actual connection (10) │
└───────────────────────────────────┘
Как показывает диаграмма, рекурсор DNS спускается по иерархии как обычно. Когда дело доходит до разрешения фактического адреса ресурса example.com, он пытается связаться с первым нэймсервером, который так же является сервером, предоставляющим сервис example.com. Первый сервер неисправен и не предоставялет ответ рекурсору DNS. Рекурсор DNS пытается связаться с другим нэймсервером и преуспевает в этом. Второй (нэйм)сервер отвечает на запрос своим собственным IP-адресом, как и всегда. Это приводит к тому, что DNS-рекурсор вернёт запросившему клиенту IP-адрес живого нэймсервера, который по совместительству является IP-адресом живого балансировщика сервиса example.com.
Реализация
Рассмотрим чуть более практический пример, где нам нужно организовать балансировку некоторого хостнейма, но нам бы не хотелось делегировать всю доменную зону на наши собственные авторитативные нэймсерверы. Мы возьмём домен example.com
и обеспечим высокую доступность для домена api.example.com
, который указывает на пару балансировщиков перед рабочими серверами.
Шаг 1. Подготовка серверов
Подготовьте два сервера для обслуживания входящего трафика. Они могут находиться даже в разных датацентрах и пересылать трафик в местную группу рабочих серверов. Далее мы будем полагать, что IP-адреса развёрнутых серверов 198.51.100.10
и 203.0.113.20
.
Проверка: убедитесь, что вы можете залогиниться на эти серверы и они доступны по их внешним адресам.
Шаг 2. Разверните полезную нагрузку на серверах
Разверните на этих серверах сервис или балансировщики нагрузки, предоставляющие настоящий сервис на этих IP-адресах.
Проверка: зависит от вашего сервиса.
Шаг 3. Установите всезахватывающие (catchall) DNS-серверы
На этом шаге нам нужно установить авторитативный DNS-сервер на каждую машину и настроить его отвечать на любые запросы IP-адресом этой машины. Почти каждый DNS-сервер может справиться с этой работой, даже простой dnsmasq. Однако разумным вариантом для этого будет сервер CoreDNS.
Установите CoreDNS на каждый сервер и примените следующую конфигурацию:
Первый сервер
/etc/coredns/db.lb
:
@ 3600 IN SOA lb1 adminemail.example.com. 2021121600 1200 180 1209600 30
3600 IN NS lb1
3600 IN NS lb2
* 30 IN A 198.51.100.10
/etc/coredns/Corefile
:
example.com {
file /etc/coredns/db.lb
}
Второй сервер
/etc/coredns/db.lb
:
@ 3600 IN SOA lb1 adminemail.example.com. 2021121600 1200 180 1209600 30
3600 IN NS lb1
3600 IN NS lb2
* 30 IN A 203.0.113.20
/etc/coredns/Corefile
:
example.com {
file /etc/coredns/db.lb
}
Проверка: команда dig +short api.example.com @198.51.100.10
должна возвращать адрес 198.51.100.10
. Команда dig +short api.example.com @203.0.113.20
должна возвращать адрес 203.0.113.20
.
Шаг 4. Добавьте A-записи для серверов в DNS
Создайте следующие DNS-записи в доменной зоне example.com:
lb1.example.com. 300 IN A 198.51.100.10
lb2.example.com. 300 IN A 203.0.113.20
Процесс редактирования зоны DNS зависит от того, где вы её хостите. Иногда это панель управления Godaddy, иногда это Cloudflare. Вам виднее.
Проверка: команда dig +short lb1.example.com
должна возвращать 198.51.100.10
. Команда dig +short lb2.example.com
должна возвращать 203.0.113.20
.
Шаг 5. Наконец делегируйте хостнейм api.example.com на балансировщики/нэймсерверы
Удалите все имеющиеся DNS-записи для имени api.example.com
. Добавьте следующие:
api.example.com. 300 IN NS lb1.example.com.
api.example.com. 300 IN NS lb2.example.com.
Готово! Через несколько минут вы сможете обратиться к домену api.example.com через два балансировщика, которые мы настроили.
Проверка: команда dig +trace api.example.com
должна давать вывод, указывающий на то, что lb1 и lb2 были затронуты при разрешении имени и результат должен давать один из их адресов.
Обслуживание
Если Вам нужно провести обслуживание одного из серверов, или сервер плохо себя ведёт, Вы можете снять с него нагрузку просто остановив CoreDNS на этом сервере и подождав TTL (30 секунд в нашем примере).