На последней конференции РИТ++ мне посчастливилось стать впервые докладчиком конференции такого масштаба и такой значимости. В этой статье я не просто хочу пересказать всё, о чём я докладывал. Выступать впервые перед такой большой аудиторией для меня было непривычно и я половину забыл рассказать, нервничал немного. Речь пойдет о создании с нуля собственной отказоустойчивой структуры для веб-проектов. Мало кому из системных администраторов дается возможность с нуля запустить в production крупный проект. Мне повезло.
Как я уже написал, я не смог рассказать всё, что планировал со сцены, в этой статье я восполню эти пробелы, да и для того, кто не смог там присутствовать — это будет приятно, видео с конференции так и не дали бесплатно всем. Да и стать пользователем Хабра я хотел давно, вот только не было времени. Майские праздники дали время и силы. Статья будет не столько технической с кучей конфигов и графиков — статья будет принципиальная, все пробелы мелких технических вопросов можно будет восполнить в комментариях.
Ставим задачу: нам нужно организовать на 99,9% отказоустойчивый веб-проект, который не будет зависеть от конкретного дата-центра (ДЦ), иметь двойное резервирование на коммутации в ДЦ, иметь отказоустойчивость на балансировке и очень быстро вводить и выводить Real сервера. При падении одного из ДЦ, второй должен принять весь трафик без дропов и ретрансмитов. Говорить мы будем только о фронт, об отказах, балансировках и немного о деплое такого количества серверов. У всех своя специфика backend.
Резервирование связанности
Проект, которым я занимаюсь очень критичен к даже минутным сбоям. Если просуммировать RPS 400+ топ проектов Рунета: Яндекс, Мейл, Рамблер, Афишу, Авто, и пр., получите наш суммарный RPS. В какой-то момент стало понятно, что если далее держать все яйца в одной корзине (ДЦ), можно очень хорошо и красиво упасть. Если вспомнить пыхи Чубайса по Москве — меня понять можно. Мы начали изучать вопрос разноса фермы серверов на несколько ДЦ. Главный вопрос — не раскидать сервера на несколько ДЦ, не связанных одним и тем-же магистралом, а быстро и незаметно для клиента переключать трафик с одного ДЦ на другой.
Сразу оговорюсь: выбранная нами реализация возможна только при наличии собственной AS минимум с маской /23, официально порезанных в RIPE на 2 сети по /24. Честно говоря я не в курсе, как в настоящий момент, но раньше были проблемы с анонсами на транзите у того-же ТТК сетей менее /24. Мы выбрали в качестве маршрутизаторов в ДЦ Cisco ASR 1001 с соответствующей начинкой и возможностью работать с EXIST-MAP, NON-EXIST-MAP. Рассмотрим пример 2х ДЦ. 2 ASR-а держат туннель просто для того, чтобы знать, что связанность обоих ДЦ жива. Из одного ДЦ мы анонсируем одну сеть класса С, из второго ДЦ — вторую сеть. Как только мы видим из второго ДЦ, что связанность нарушена, мы начинаем с помощью NON-EXIST-MAP анонсировать вторую сеть /24 из первого ДЦ. Желательно ДЦ разнести географически — но это на бюджет и цвет.
Тут важный момент, к которому мы вернемся немного позже: на серверах балансировки и на Real серверах должны быть постоянно одновременно подняты IP адреса из обоих сетей в обоих ДЦ, это важно. При корректной связанности всех ДЦ маршруты руководят, кто отвечает на адреса той или иной сети, поэтому нужен туннель между маршрутизаторами, чтоб голову не унесло. Прошу OpenSource сообщество не пинать ногами — я знаю как это реализовать на quagga с весами префиксов, но проект крупный и есть соответствующие требования. Я просто рассказываю, как это реализовано у нас.
При тестировании смен маршрутов, мы наткнулись на неприятную ситуацию: у многих на доступе стоят Juniper. Сейчас объясню почему неприятную. По-умолчанию смена маршрута анонсируется 1 раз в 3 минуты. Для нас — это потеря сотен гигабайт данных. Мы начали экспериментировать с уменьшением времени анонсов — до 20 секунд было все отлично: анонсы поднимались из разных концов нашей огромной родины отлично и все быстрее, но после уменьшения данного параметра мы потеряли один из ДЦ. Как оказалось у Juniper, который обслуживал один из аплинков менее 20 секунд считается флудом и не возможно уменьшить, это особенности прошивки. Итог: 21 секунда — самый оптимальный параметр.
Так. Всё. Failover для 2х ДЦ мы сделали, протестировали — это работает. Реальное время свичинга маршрутов по России — от 25 до 60 секунд — в зависимости от удалённости и транзита. На этом этапе у нас есть 2 голых ДЦ с маршрутизаторами. Начнём ставить начинку в оба ДЦ.
Резервировние коммутации внутри ДЦ
Тут я буду краток — коммутация внутри ДЦ сделана 2 на 2: 2 физических интерфейса в Bond смотрят на разные коммутаторы в vlan, обслуживающий реальные IP адреса, 2 физических интерфейса в Bond — на разные коммутаторы в vlan, обслуживающий backend. Режим бондинга мы выбрали самый простой mode=1 — на отказ, просто нулевые части бондинга мы разбросали на разные коммутаторы. Вообще, чем проще, тем надёжнее. На каждом сервере, участвующем в фронт и бэк — 4 физических интерфейса. На рисунке показан только фронт. Коммутаторы связаны не только по 10G, но и кабелем резервного питания и подключены к разным UPS. Mode=1 не требует специальной настройки коммутаторов, что упрощает управление всей инфраструктурой.
Балансировка
Собственно особых альтернатив не было, был выбран IPVS в режиме DR (Direct Routing). Как я говорил выше, этот метод балансировки менее затратен. Он лишает прелестей дампа соединений в обе стороны, как в NAT, но требует меньше ресурсов. Любителям notrack скажу — лучше так не делать, если Вам важно сохранить 100% соединений. IPVS — штатный в Centos и не требует особых шаманств при установке. Единственный нюанс с которым мы столкнулись — по-умолчанию IPVS готов принимать 4096 одновременных соединений — это 12 бит.
Чтобы балансировщик был готов принимать миллион соединений этот параметр нужно увеличить до 20 битов. Через options в управлении модулем (modprobe.d) это сделать не удалось — ipvsadm так же упорно твердил, что готов обслуживать 4096 соединений. Пришлось запустить свои ручки в src и пересобрать модуль. Для небольшого проекта это не столь важно, но если Вы обслуживаете сотни тысяч и выше соединений — это критично.
У балансировки есть много методов. Самый простой — RR (Round Robin) — отдавать на Real сервера входящие соединения «по-кругу». Но мы выбрали тип WLC — взвешенная балансировка с контролем соединений. Если мы параметр persistence на балансировщике синхронизируем с keepalive параметром в nginx на Real серверах, то получим гармоничность в соединениях. Просто если, например, persistence на балансировщике будет 90 секунд, а keepalive в nginx на Real — 120, то мы получим следующую ситуацию: балансировщик через 90 секунд отдаст все соединения клиента на другой Real, а старый Real будет ещё держать соединения клиента. Если учитывать, что при 30 секундном keepalive на 10 тысяч ESTABLISHED соединений мы имеем порядка 120 тысяч TIME_WAIT (это совершенно нормально, мы с Игорем Сысоевым считали в кулуарах технофорума мейла), то это достаточно критично.
Теперь по самой технологии балансировки, как это работает: IP адрес, который у нас прописан в DNS для домена мы вещаем на внешний физический интерфейс балансировщика и на алиасы loopback-ов Real серверов. Ниже будет пример конфигурации sysctl Real серверов — там нужно обязательно прикрыть arp аннонсы и, по желанию, source routing, для того, чтобы коммутация не сошла с ума от дублирования IP. Я не буду описывать весь sysctl — он достаточно специфичен для каждого проекта — покажу только параметры, обязательные для именно DR специфики.
Для балансирощиков:
# Fast port recycling (TIME_WAIT)
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
# Local port range maximized
net.ipv4.ip_local_port_range = 1024 65535
# Dont allow LB send icmp redirects on local route
net.ipv4.conf.eth1.send_redirects = 0
net.ipv4.conf.eth0.send_redirects = 0
net.ipv4.conf.lo.send_redirects = 0
# Dont allow LB to accept icmp redirects on local route
net.ipv4.conf.eth1.accept_redirects = 0
net.ipv4.conf.eth0.accept_redirects = 0
net.ipv4.conf.lo.accept_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
# Do not accept source routing
net.ipv4.conf.default.accept_source_route = 0
# Ignore ARP
net.ipv4.conf.eth0.arp_ignore = 1
net.ipv4.conf.eth1.arp_ignore = 1
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.default.arp_ignore = 1
Для Real серверов:
# sysctl.conf
net.ipv4.conf.bond0.arp_ignore = 1
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.default.arp_ignore = 1
Маршрутизация для алиаса loopback-а с реальным IP на Real серверах с маской 255.255.255.255 должна быть прописана на IP маршрутизатора, обслуживающего вашу сеть и аннонсирующий её во вне, иначе запросы-то на балансировку придут, а вот Real не ответит. Некоторые мудрят с iproute2 и VIP таблицами, но это усложняет конфигурацию.
Теперь об отказоустойчивости балансировки и Real. Keepalived был выбран совершенно логично — сами разработчики IPVS рекомендуют использовать его в связке со своим продуктом. Штатная схема — MASTER + SLAVE подойдёт 99% проектам, но нам это не подошло, ибо в настоящий момент в одном ДЦ 5 балансеров, в другом 3. Средняя стоимость лезвия 120-140 тысяч, поэтому решили немного сэкономить. Если внимательно посмотреть в секцию конфигурации keepalived, где описываются группы, то станет понятно, что, если на 2х мастерах задать разные группы, а на 1м slave описать в конфиге обе, то 1 slave будет работать на 2 мастера по схеме: выпал 1 мастер, slave подхватил его IP, если же выпал второй, то и второй IP подхватывается на этот же slave. Минус конечно есть, при вылете обоих мастеров, на slave упадёт двойная нагрузка, но для балансировщика это не сильно критично, в отличии от Real.
И ещё одна маленькая хитрость: в конфиге keepalived предусмотрен CHECK Real серверов. Он может быть простым TCP, может дёргать по HTTP страничку для контроля 200того ответа, но есть MISC_CHECK. Штука удобная и функциональная. Если на Real сервер положить небольшой скрипт, не важно на чём написанный, который будет по Вашей логике вычислять текущий LA и генерить динамический вес Real сервера для балансировщиков, то Вы с помощью параметра MISC_DYNAMIC на балансировщике получите динамическое изменение веса Real-а.
Deploy
Немного о деплое. Деплой — это автоматизация развёртывания серверов. Если у Вас 2-3-4 сервера, конечно Вы не будете разворачивать автоматизацию, но если у Вас их десятки или сотни, то однозначно стоит, тем более, если сервера сгруппированы одинаковыми задачами.
У меня на фронте всего 2 группы — это балансировщики и Real сервера с nginx, которые собственно и отдают контент пользователю.
# file top.sls
base:
'*':
- ssh
- nginx
- etc
'ccs*':
- ccs
'lb*':
- lbs
Есть много систем деплоя — puppet, chief ..., но мы выбрали salt (saltstack.org). Как это работает: на сервере salt в конфигурации Вы создаёте группы серверов и для каждой группы расписываете план деплоя — он включает в себя системных пользователей и групп, которые требуются для функционирования сервисов, конфигурационные файлы, которые должны быть на всех серверах, установленный софт…
# file /srv/salt/etc/init.sls
/etc/hosts:
file:
- managed
- source: salt://files/hosts
/etc/selinux/config:
file:
- managed
- source: salt://files/selinux.config
/etc/snmp/snmpd.conf:
file:
- managed
- source: salt://files/snmpd.conf
Если в конфигурационных файлах требуются уникальные для каждого сервера данные, в salt присутствует шаблонизатор pillar, который умеет генерить на лету уникальные конфиги. Я перед тем, как развернуть всю ферму, начал именно с сервера деплоя, чтобы после окончания формирования фермы серверов быть уверенным, что деплой написан корректно. И Вам так советую.
Это лишь часть доклада. Обязательно выберу время и напишу отдельную статью по методикам тестирования web фермы, «выжимания» максимума из nginx без дропов и ретрансмитов, разницам синтетических и продакшн тестов.
Удачи в highload!