Хабрахабр

Балансировка HTTP(S) трафика

Добрый день, %username%. Меня зовут Антон Резников, я работаю над проектом Облако Mail.Ru Сегодня я хочу рассказать о технологиях балансировки трафика, проиллюстрировав историей о развитии социальной сети. Все персонажи выдуманы, а совпадения почти случайны. Статья обзорная, составлена по следам доклада на Highload Junior 2017. Некоторые вещи могут показаться элементарными, но опыт проведения собеседований показывает, что это не совсем так. Кое-что будет спорным, не без этого.

Как ни странно, это не серверы и не сетевое оборудование, а, в первую очередь, источник трафика, эффективный, легальный. Если интересно, добро пожаловать под кат.
Что нужно для балансировки трафика? Нам идеально подойдут котики, их одобряет даже РКН. И без возрастных ограничений.

Команда

Далее нам нужна команда профессионалов.

Слева менеджер Майк. Познакомьтесь. В центре программист Павел, он немного перфекционист и очень любит писать красивый код, а ещё больше его переписывать. Амбиции — его второе имя. Справа администратор Тони, он в меру ленив, но ответственен и профессионален.

План

И последнее, нам нужен план! Мы будем говорить о запуске социальной сети для любителей постить и смотреть фотографии котиков net.cat. План амбициозен и достаточно прост.

Мы начнём с запуска бета-версии по инвайтам. Далее захватим Москву, через нее пойдем на Россию, а там и до мирового господства рукой подать.

Beta

В качестве платформы был выбран LAMP (Linux, Apache, MySQL и что-нибудь на букву P). В данном случае нас более всего интересует Apache, потому что он использует модель prefork.

Почему pre и почему fork? Каждый процесс обрабатывает единовременно только одно пользовательское соединение. Но нам невыгодно запускать процесс во время установки соединения, поэтому процесс запускается заранее и обрабатывает несколько соединений.

Но под Django или PHP FPM лежит та же самая модель. Многие из вас скажут: «Да, но Apache — это же не модно и не эффективно».

Получить оценку сверху достаточно просто, нужно взять размер свободной оперативной памяти, поделить его на максимальный размер воркера, и мы получим некое N, повышать которое крайне нежелательно. Как много воркеров (worker) мы можем запустить?

Мы берем количество воркеров и делим на среднее время обработки запроса. Как много RPS мы можем получить с одного сервера? Нетрудно заметить, что при увеличении времени обработки запроса, например, если ваш MySQL немного притормаживает, RPS падает.

Первый вызов

Итак, Павел запустил прототип сети на своем компьютере.

Павел любил раскладывать всё по полочкам. Страница получилась достаточно легкой, но содержащей большое количество мелких файлов. К сожалению, сайт работал на тестовом сервере сильно медленнее, чем на локальном компьютере. Затем сайт был выгружен на тестовый сервер, чтобы показать его менеджеру, отдыхающему в Таиланде. Давайте разберемся, почему.

Этот лимит сильно разнится, так как до недавнего времени RFC имел явное ограничение (RFC 2616 8. Каждый браузер имеет лимит на количество соединений к одному HTTP-хосту. 4), и с недавнего времени формулировка заменена на очень обтекаемую — «разумное количество соединений». 1. Но подождите, у нас всё ещё есть канал 100 Мбит, который стоит не очень дешево. Этим, например, воспользовались разработчики Internet Explorer. Неужели провайдер нас обманывает?

Нет, не в этот раз.

Давайте рассмотрим процесс передачи файла на примере файла lapka.png.

В реальности это происходит прямо при записи HTTP сервером данных в сокет, но ради красивой картинки мы разбили его заранее. Чтобы передать данные по сети, файл нужно разбить его на пакеты. Это происходит в виде так называемого трехступенчатого TCP-рукопожатия, и тратится на это около одного round-trip, или пинга (ping). Первое, что должен сделать клиент — установить соединение. Также на этой стадии передаётся сам запрос (в нашем примере запрос достаточно мал).

Но сколько пакетов отправить? Далее сервер должен отправить данные. Мы пока этого не знаем, поэтому сервер начинает с небольшого количества пакетов, так называемого TCP-окна, и отправляет их клиенту. Насколько широкий канал у клиента?

В реальности их побольше. В данном случае — два пакета. Получив подтверждение от клиента, сервер понимает, что канал не перегружен, и отправляет больше данных, расширяя тем самым окно. Если не ошибаюсь, в современных Linux стоит значение 10. После этого соединение нужно ещё закрыть.
На последнем этапе он отправляет последний пакет.

В этом примере мы предполагаем, что потери обусловлены только перегрузкой канала. Примерно так выглядит изменение ширины TCP-окна с течением времени в рамках одного соединения. Форма колебания зависит от алгоритма выбора окна. Поэтому со временем размер TCP-окна достигает максимума и колеблется около этого значения. В реальности очень часто размер окна будет просаживаться из-за того, что где-то на магистральных каналах происходят пиковые нагрузки и пакеты теряются. Это идеальный случай.

Из всего этого вытекает простое соотношение: вы не можете передать через одно соединение данных больше, чем длина TCP-окна, делённая на пинг.

Именно поэтому популярны разного рода менеджеры скачивания, которые скачивают большие файлы в несколько потоков. Именно поэтому с локальной машины сайт работал быстро, а из Таиланда — очень медленно.

Но что поменялось? Тони быстро нашел решение — был включён Keepalive. Мы так же устанавливаем соединение, так же передаем данные, так же расширяем TCP-окно, но не закрываем соединение. Для первого запроса всё осталось на прежнему.

А через некоторое время, если не истек таймаут на Keepalive, браузер может снова воспользоваться этим соединением, уже установленным, уже разогретым, с более высокой скоростью передачи данных.

9% случаев. Внимательный читатель может заметить, что Keepalive в Apache включен по умолчанию в 99. Но всё не так однозначно, когда у тебя зовут Тони и у тебя лапки. Всё так.

Keepalive не панацея

Итак, Майк был очень доволен результатом и решил приступить к бета-тестированию.

Что-то пошло не так, и очень скоро пользователи, получившие инвайты, начали жаловаться на проблемы с доступом к сайту.
Зря.

При включённом Keepalive пользователи не разрывали соединение, в результате даже небольшое количество клиентов полностью исчерпывали количество воркеров, и новые клиенты не могли установить соединение.

Тони был очень недоволен тем, что Майк не предупредил его о запуске, но он быстро нашел решение. Мы всё ещё используем Apache и prefork-модель. Nginx использует другую модель обработки запросов. На самом деле, он давно хотел установить Nginx, но проклятая прокрастинация не давала это сделать.

Что же изменилось? И даже медленные клиенты, которые раньше нагружали наш сервер, не будут создавать нам проблемы, так как ответ от Apache будет считан в буфер Nginx и отдан с требуемой скоростью. Один его воркер может вытянуть тысячи, и даже десятки тысяч соединений, и воркеров можно запустить много. В результате Nginx превратил множество соединений от пользователей в минимально необходимое количество запросов к Apache.

Москва

Окей, пора идти на Москву. Для начала нужно увеличить количество серверов. Тони выбрал схему балансировки через DNS.

К нашему DNS-серверу обращается пользователь и спрашивает: «А что за адрес у этой замечательней социальной сети net.cat?». Как это происходит? За счет того, что клиентов много и особой закономерности в выборе сервера нет, мы получаем равномерно размазанную нагрузку.

Как много серверов можно балансировать при такой схеме? Сервер ему отдает два IP-адреса, после чего клиент случайным образом выбирает себе сервер и отправляет запросы.

TCP желательно поддерживать, но необязательно, чем пользуются многие системные администраторы. Согласно действующему ныне RFC 1123, DNS-сервер должен поддерживать UDP и передавать через него данные размером не более 512 байтов. На самом деле, обязательно с 2016 года, но вспомните про IPv6, и попробуйте спрогнозировать, когда это требование стандарта воплотится в жизнь.

Итогом получаем максимум 25 серверов при использовании UDP. Согласно тому же стандарту, мы должны потратить 16 байтов на одну А-запись, и около 100 байтов на заголовок.

Но есть нюанс.

Обычно между нашим сервером и клиентом есть DNS-сервер провайдера, роутер клиента (router в оригинали произносится как [ˈruːtər]), возможно, ещё локальный кеширующий DNS у него на машине. Клиенты почти никогда не обращаются к нашему серверу напрямую. Поэтому крайне редко кто-либо масштабирует таким образом более четырех-шести адресов.

К тому же при передаче картинок соединения с сервером «забивались», и маленькие запросы к API вынуждены были ждать своей очереди достаточно долго — сайт становился менее отзывчивым. Кроме того, при балансировке серверов таким образом встал вопрос: что делать с картинками?

Во-первых, не хотелось бы хранить их в четырёх копиях, а именно четыре сервера планировал поставить Тони. Это не только позволило сэкономить на дисках, но и увеличило скорость работы сайта. Поэтому статика и клиентский контент были вынесены на отдельный сервер.

Загадочный сбой

Итак, сервис работал на четырёх серверах, поддерживающих API, но однажды ранним утром понедельника сервер «дельта» ушёл и не вернулся.

25% пользователей начали испытывать проблемы с доступом к сайту. Хуже того, TTL (а это время, в течение которого клиенты могут не перезапрашивать информацию об IP-адресах сайта) имел продолжительность в один день.

Но как только сервер был поднят, проблема тут же исчезла сама собой. Тони вызвонил дежурного специалиста из дата-центра и попросил поднять любой сервер с этим IP-адресом, чтобы проксировать запросы с поднятого сервера на рабочие серверы с Apache.

Что же произошло?

Если ваш сервер лежит и не отвечает, в течение таймаута на соединение клиент будет ждать: а вдруг сервер ему ответит. Любой современный клиент способен перебрать все адреса из ответа от DNS-сервера. После чего клиент прозрачно для пользователя идет на другой сервис. Если, как в нашем случае, сервер поднят, но на нём не запущен никакой веб-сервис, и никто не принимает соединение на 80-м порту, клиент получает отказ практически за время, равное одному пингу.

Котики v.s. Собаки

Собаководы очень не любили эту социальную сеть. Как это: котики, котики, котики, и ни одной собаки? И однажды они обнаружили, что поисковые запросы работают очень медленно. Они подумали: «А что, если мы натравим Apache Benchmark (это утилита для тестирования производительности вашего сервиса) на эту социальную сеть?». Ведь утилита не спрашивает, ваш вы сайт тестируете или нет. Так они и сделали. Сгенерировали большое количество запросов к методу API Search, и серверам стало плохо. MySQL замедлялся, и всё шло к тому, что сайт в скором времени должен был полностью «лечь». Хуже того, так как на тех же серверах хранилась куча статики — JS, CSS-файлы, — нельзя было использовать лимиты на коннекты. Клиенты не смогли бы быстро загрузить статику.

Nginx также не умеет работать с лимитами на путь.
На самом деле с netfilter можно было решить проблему, но какой администратор ранним утром в понедельник сможет его настроить? А реализовывать софтверные лимиты, когда ваш сервер пытаются положить, уже поздно.

А по результатам инцидента Тони и Павел решили перевести всю статистику на отдельный сервер. Поэтому Тони принял волевое решение: лучше потерять одну функцию, чем все, и поиск был отключён. Это не только упростило жизнь Тони, так как теперь он мог легко лимитировать количество запросов, но и увеличило скорость доступа к сайту.

Покоряем Россию

Начнем с переезда в новый дата-центр с хорошим SLA. Тони предусмотрительно выставил TTL на зоне в пять минут, и рассчитывал, что в течение 10, максимум 30 минут все пользователи перейдут с IP-адресов старого дата-центра на IP-адреса из нового дата-центра.


Как бы не так.

Менеджер Майк вынужден был заплатить за две дополнительных недели аренды. Значительная часть пользователей так и продолжала пользоваться старыми DNS-серверами, что, между прочим, влетело в копеечку. И он требовал объяснений.

В городе было три популярных провайдера. История оказалась проста и занятна.

Evil Telecom экономил на трафике, поэтому все запросы к DNS были принудительно закэшированы на один день. Один из них, Cat Telecom, не вносил никаких изменений в трафик, и с ним было всё хорошо. School Telecom экономил на системных администраторах, поэтому бо́льшая часть их конфигурационных файлов представляла собой компиляцию со Stack Overflow, и кэширование загадочным образом было настроено в одну неделю.

Раздача контента

Также встал вопрос с серверами контента пользователей. Фотографий было много, и их нельзя было хранить на одном сервере, поэтому было решено купить ещё два.

Тони и Павел долго спорили. Как же балансировать нагрузку между ними? Тони предлагал распределять запросы редиректами, ведь стороне сервера известно, где лежит файл, мы можем направить клиента на нужное хранилище.

Он предложил отдавать клиенту полный адрес картинки, а алгоритм выбора хранилища на клиенте.
Павел не хотел создавать новый сервис, так же он резонно заметил, что такой способ не подходит для аплоада. Задача алгоритма забрать список доступных для загрузки серверов и выбрать один для загрузки картинки, случайным образом, либо хешируя ID клиента.

Тем не мене, он был категорически против решения на JS. Тони вежливо напомнил про 307 редирект, но согласился что с POST-запросами он работает на всех клиентах. Не смотря на то, что Павел обещал всё это предусмотреть, Тони на отрез отказался принимать в эксплуатацию код, работающий «где-то там». Тони нужна была возможность перемещать данные, выводить сервера из нагрузки и добавлять новые. Мы можем управлять только тем, до чего дотягивается лапка — сказал Тони.

Сервера с контентом скрыли за слоем прокси, которые «знали» куда идти за определённой картинкой и какой сервер лучше выбрать для сохранения новой.
Не мало времени было потрачено, но друзья пришли к компромиссу. Схема позволяла, при необходимости настроить кэш для особо горячего контента, а так же перейти к хранению нескольких копий файлов. На JS оставили выбор прокси-сервера, их список периодически запрашивался с сервера, что позволяло достаточно быстро убрать новые соединения с прокси-сервера и вскоре вывести его из нагрузки незаметно для пользователей. Тони был доволен до кончика хвоста.

Балансировка API

Также встал вопрос о масштабировании серверов под API. Никто не хотел 20 серверов забивать DNS, поэтому Тони выделил отдельные серверы, назвав их балансерами, и поставил там Nginx.

Более того, если по каким-то причинам один из серверов ломался, Nginx прозрачно для пользователя перенаправлял запрос на другой работающий сервер.
Nginx равномерно распределял запросы между серверами с Apache, всё шло замечательно. Тони нравилось.

Query of DEATH

Часто в высоконагруженных проектах тяжелая логика реализуется на языках более низкого уровня. Так было и в нашем случае. Специальный код для добавления стикеров на фотографиях был написан на C++. К сожалению, одно из исключений не было обработано, и, получив «битый» jpeg, такой код падал вместе с воркером.

Что же делал Nginx? Решив, что сервер сбойный, Nginx помечал его на 10 секунд как недоступный, и переходил к новому серверу. Баг замечательно воспроизводился на всех серверах кластера, пока хоть что-то работало. После чего сайт становился недоступен на 10 секунд.

Тони кусал себе локти. Он был очень недоволен, но понимал, что дело не в Nginx, а в том, что он не достаточно внимательно прочитал документацию. Нужно было всего лишь ограничить количество попыток обращения к другим серверам. Достаточно было двух, максимум трёх. Зачем обходить весь кластер? Так Тони и сделал. А также решил, что после одной ошибки сервер ещё рано помечать как сбойный.

Резервируем nginx

У нас осталась ещё одна точка отказа — это сам сервер с Nginx.

Тони решил, что VRRP спасёт ситуацию. Если что-то идет не так — уборщица в дата-центре запинается о кабель, или перегорает блок питания, — сайт становится недоступен примерно для половины пользователей.

Master обслуживает запросы всегда, backend — если что-то пошло не так. Это решение, в котором есть так называемый VIP (Virtual IP), который обслуживает два сервера: master и backend.

Поэтому он решил переделать схему для работы с IBGP. К сожалению, эта схема требовала наличия L2-сети между серверами, да и Тони недавно купил новое оборудование фирмы «Киса». В этой схеме уже не требовалось наличие L2-сети между серверами, но так же был Virtual IP, который анонсировался двумя серверами с разной метрикой. На самом деле он мог использовать и другой протокол, например, OSPF. Если что-то шло не так, то backup принимал на себя нагрузку. Если master работал, то трафик шел на него, так как он анонсировал IP с меньшей метрикой.

А вы бывали в Рио?

Майк грезил Рио-де-Жанейро. Полтора миллиона человек, и все поголовно в белых штанах! Поэтому экспансию решили начать с Бразилии. Для увеличения отзывчивости сайта были подняты серверы в Рио-де-Жанейро, а трафик от пользователей распределялся с помощью geoDNS. Всё шло хорошо, но некоторые пользователи из России попадали в Бразилию. Точнее, попадали не пользователи, они не возражали бы против такого расклада. Попадали их запросы. И сайт у них работал сильно медленнее, чем у тех, чьи запросы попадали в Россию. В основном это было вызвано использованием клиентами публичных DNS серверов, но иногда случалось из-за недостаточно быстрого обновления базы geoDNS.

Поэтому Тони, в силу своей лености, решил применить менее хлопотное решение.
Схема, в принципе, рабочая, но требует активной поддержки.

Это набор IP-адресов, которые управляются одним администратором. Была куплена автономная система. Под администратором подразумевается не сисадмин, а некая организацию, которая покупает данный набор адресов.

В данном случае это три точки. Автономная система поднимается в нескольких местах и анонсируется соседям, с которыми мы должны договориться о покупке каналов. Но маршрут от клиента выбирает не он, маршрут выбирает в первую очередь провайдер. После чего клиент может обратиться к любой из точек, где поднят нужный IP-адрес. Если адрес доступен из нескольких мест, то выбирается маршрут с наименьшей метрикой. Провайдер пользуется всё тем же BGP (теперь без I), и его оборудование выстраивает маршруты для всех адресов в интернете.

Если не сильно вмешивается политическая конъюнктура, то трафик пройдет кратчайшим маршрутом. Как правило, это количество пройденных автономных систем между источником и получателем. Если клиент находится где-нибудь в Подмосковье, то у него практически нет шансов попасть на другие серверы вне Москвы.

Так и было сделано. Если договориться с соседями (соседними автономными системами), то трафик через них может пойти со всего мира. С VIP достаточно было отдать клиенту одну HTML страницу, в которой были прописаны сервера с API, статикой и контентом в Рио, в Москве или Нью-Йорке. Автономная система была поднята в трёх точках.

А если упадет дата-центр в Москве, то BGP-сессии с соседями будут разорваны, и более они не будут видеть маршрута к нужным IP в этом месте. Пользователи из Москвы начали попадать на серверы в Москве, а пользователи из Бразилии — на серверы в Рио-де-Жанейро. Естественно, всё будет работать медленнее. Пожалуй, Нью-Йорк в данном случае намного ближе, чем Бразилия, и трафик пойдет туда.

Эпилог

Майкл привел свою команду к намеченной цели. А к чему пришли мы? Позволю себе озвучить несколько простых истин, немного нудно, но максимально коротко.

Не ищите универсального решения, лучшее враг хорошего.

Ломается всё, если не сломается у вас, то сломается у соседей, но узнают они об этом от вас… Имейте план B, а лучше B, C и т.д.

И не пренебрегайте документацией, как бы банально это не звучало.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть