Предисловие
Наш основной проект — оптимизация показов рекламы в социальных сетях и мобильных приложениях. Каждый показ баннера — результат взаимодействия достаточно большого числа сервисов, расположенных на разных серверах, иногда в разных датацентрах. Естественно что существует задача мониторинга связи между серверами и сервисами. О том, в каком виде стоит эта задача, какие решения подходят, какие не подходят — речь дальше.Структура сети: фронтенды, бэкенды, сервисы
Как и многие другие веб-приложения, наше состоит из фронтендов(FE), бэкендов(BE), сервисов. Фронтенд принимает соединение от клиента и пробрасывает его на бэкенд, который обращается к сервисам за помощью и отвечает клиенту. В нашем случае FE — почти всегда — nginx, BE — tomcat. Эта архитектура достаточно стандартна, нестандартности начинаются тогда, когда нужно прнять решение о том, как маршрутизировать запросы между фронтендами, бэкендами и сервисами.
В нашем случае маршрутизацией управляет конфигурационная база данных CMDB. В этой базе отражаются как физическое устройство сети (датацентры, физические сервера, ...), так и логическое устройство приложения (какие сервисы и интерфейсы расположены в каких контейнерах на каких физических серверах, разбиение FE и BE по кластерам, какие сервера могут обслуживать траффик, а какие — нет, и т.д.)
Информация CMDB может использоваться двумя способами. Первый — условно «оффлайн», заключается в том, что по крону, раз в несколько минут, скрипт выбирает нужную информацию из CMDB и строит актуальные конфиги, например, для nginx, включая в них нужные апстримы. О том, как работает второй способ обращения к CMDB — в следующем разделе.
CMDB, короткие имена, поиск сервиса по имени
Втрой способ использования CMDB может быть условно обозначен как online, потому что он отражает изменения в CMDB не периодически, а сразу (с учетом краткосрочных кешей поверх CMDB).Этот способ используется, в основном, бэкендами для обнаружения нужных им сервисов, для связи сервисов между собой и основан на использовании DNS. Проще всего понять как это делается из картинки с пояснениями:
- Бэкенду be5, находящемуся на хосте host58, необходимо подключиться к базе данных, имя сервиса базы db1 (короткое DNS-имя)
- Из имени db1, из данных в resolv.conf строится имя db1.be5.host58.example.com. Резолвер отправляет запрос на PowerDNS
- Плагин к pDNS из имени db1.be5.host58 заключает что контейнер be5, расположенный на хосте host58 ищет сервис db1
- Данные CMDB используются для выдачи списка IP работающих контейнеров, предоставляющих сервис db1, ближайших к host58
- Сервис на be5 выбирает произвольный адрес из полученного списка и обращается к сервису db1
Подводя промежуточный итог: фронтенды, бэкенды, сервисы находят друг друга используя единый источник информации CMDB. методов получения адресов — два: через построение конфигов и через запросы к DNS.
Физическая структура сети, проблемы связи
Мы арендуем сервера у хостинг-провайдеров. Структура в различных датацентрах одинаковая — набор физических хостов, в которых запущены контейнеры с фронтендами, бэкендами или сервисами. Запрос от любого компонента может уйти на любой другой хост и даже в другой датацентр, если по каким-то причинам нужный сервис есть только в другом датацентре. Всего мы размещаемся в шести датацентрах, в каждом датацентре работает порядка сотни-двух серверов, на каждом сервере до четырех-пяти контейнеров. В общей сложности получаем около тысячи хостов и около пяти тысяч контейнеров.
Поскольку вся сетевая структура находится не в наших руках, мы не можем мониторить линки, свитчи, загруженность каналов и т.д. По той-же причине (сеть не в наших руках) мы используем ipsec для связи между серверами. Ipsec иногда преподносит сюрпризы в виде потери связи между двумя хостами (или только между контейнерами). Аналогичные сюрпризы преподносит инфраструктура провайдера. В любом случае — всё что мы можем обнаружить, это факт недоступности одного хоста (или его контейнеров) со стороны какого-то множества хостов или контейнеров. Причем сам факт недоступности не всегда проявляется на уровне приложения: сервисы дублированы, запрос может быть удовлетворён с доступного сервера. Так-же мы не можем мониторить такого рода проблемы с помощью пингов с центрального сервера — он их просто не заметит.
Во времена когда число наших серверов было ограничено десятками, мы не особенно заморачиваясь, проверяли связность путём пингов, посылаемых с каждого хоста всем остальным и отслеживая потери. С приближением числа хостов к тысяче этот простой способ перестаёт работать:
- время отправки пингов становится слишком большим, или:
- расход сети на мониторинг становится ощутимым (стандартный пинг отправленный с одного сервера на тысячу серверов в течение секунды займёт около мегабита полосы, если этим занимается вся тысяча серверов то это уже слишком много)
- расход процессора на хосте для обработки и анализа «кто кого видит или не видит» становится большим
- связность между контейнерами не мониторится — мы не видим проблем ipsec или конфигрурации рутинга, приводящих к проблемам на уровне контейнеров
Для того что-бы решить все эти проблемы одним ударом нам нужно мониторить не «всё», а только связь между компонентами, которые обращаются друг к другу. Так что первая задача, которая тут возникает — как узнать к каким компонентам обращается данный компонент. Точно это знают только девелоперы, но и они не всегда могут представить актуальную информацию, поэтому особенного смысла спрашивать их об этом, или просить вести какой-то реестр, нет — в реальности это не работает
Тут нам приходит на помощь знание того, как сервисы находят друг-друга: те компоненты, которые не используют резолвинг, а вычисляют партнёров через прямое обращение к CMDB мы попросим сохранять результат этого вычисления не только в своих конфигах, но и в каком-то общедоступном месте в оговоренном формате. А для сервисов, которые используют CMDB через резолвинг — мы будем парсить логи PowerDNS и выяснять что PowerDNS ответил на запрос. Таким образом мы сможем собрать полный список того, на какие IP могут обращаться те или иные компоненты.
Мониторинг, тесты
Мы собираем полный список актуальных связей между контейнерами в одном месте, для задачи мониторинга свзяей это одно место играет роль «сервера». Вся информация представлена на сервере в виде словаря:
контейнер | список связей |
---|---|
be5.host58 | 192.168.5.1 192.168.5.2 10.1.2.3 10.2.3.4 |
be8.host1000 | 10.0.0.1 10.0.100.5 |
Сервер делает доступной эту информацию через REST-интерфейс.
Вторая половина мониторинга расположена в каждом контейнере — эту часть назовём «агентом». Агент не знает о контейнере, в котором он расположен, ничего кроме имени, не хранит никакого текущего состояния. Раз в две минуты агент обращается к серверу за списком своих связей и сервер отвечает ему строкой из приведенной выше таблицы. Обычно длина списка IP-адресов не превышает десяти. По этим адресам агент отправляют пинги. Результат, содержащий список проблем (если такие обнаружены), через POST сообщается обратно на сервер. Поскольку все операции выполняемые сервером для агентов очень просты, он легко может обслужить большое их число — для этого ему просто нужно успеть принять запрос GET,POST и выполнить поиск в словаре или добавление данных в словарь несколько десятков раз в секунду.
Кроме того, раз уж агент всё-равно запускается раз в пару минут — есть смысл собрать еще какие-то текущие показатели работы компонента и передать их на сервер. Мы мониторим очень много всего через мунин, но мунин даёт достаточно большую задержку в представлении данных, а здесь мы можем получить кое-что прямо в режиме «real time». Поэтму агент собирает заодно и такие показатели как LA в компоненте, темп запросов к nginx, если он есть и число ошибок nginx с разбивкой по кодам.
Мониторинг, представление и анализ результатов
Как было сказано выше, агенты присылают на сервер результаты тестов связности (вместе с некторыми другими данными, список которых может быть разным для разных агентов). Вот какую информацию мы примерно имеем:
name | disconnects | DC | role | load | nginxErrRate | connectivity | nginxAccessRate |
be1.host123 | 0 | TX | be | 0.21 | 15.55 | 31.38 | |
be1.host122 | 0 | TX | be | 0.11| | 18.28 | 34.61 | |
fo1.host161 | 2 | VA | fo | 0.02 | 0.11 | 10.1.1.4,10.1.2.4 | 14.1 |
fo1.host160 | 0 | VA | fo | 0.0 | 0.00 | 0.0 | |
fo1.host162 | 2 | VA | fo | 0.01 | 0.18 | 10.2.1.4,10.2.4.3 | 17.56 |
Часть информации в этой таблице прислана агентами(disconnects, load, nginx...), часть (DC, role) — заполняется из CMDB. Поскольку данные в этой таблице очень короткоживущие и число строк в ней — порядка тысяч, есть смысл оформить её как часть базы sqlite типа ":memory:".
Основное ради чего городился весь этот огород — мониторинг проблем связи. Поэтому в первую очередь нас интересуют только колонки disconnects и connectivity — число оборванных связей и соответствующий список контейнеров, но в целом мы можем получать разные интересные факты из такой таблицы c помощью простых SQL-запросов. «SELECT name FROM data ORDER by disconnects DESC» — даёт нам список серверов с проблемами связи которые мы должны решать в первую очередь. Результаты этого запроса, если они не пусты — отправляются в Nagios в виде алерта с приложенным списком проблемных серверов. Дежурный инженер, получив информацию о проблеме предпринимает дальнейшие меры — снимает траффик с проблемных серверов, создаёт тикет в системе хостинг-провайдера и т.д.
У нас иногда возникает ситуация при которой общий уровень ошибок в системе начинает расти. Это может происходить по разным причинам, связанным как с нашими операционными проблемами, так и с внешними по отношению к нам причинами. Абстрактный пример — маршрутизация между датацентрами изменилась, в результате кросс-датацентровые запросы перестали вписываться в лимиты по времени, что приводит к росту 504 ошибок в nginx. Запрос «SELECT SUM(nginxAccessRate),SUM(nginxErrorRate),dataCenter FROM data GROUP by dataCenter ORDER BY SUM(nginxAccessRate)» даёт нам общую картину ошибок с разбивкой по датацентрам. Эта общая картина потом может детализироваться с помощью запросов работающих только с 504-ми ошибками и т.д.
Другой пример — допустим мы хотим выяснить, есть ли корреляция между загруженностию сервера и числом ошибок, которые на нем возникают. Запрос «SELECT load,nginxErrorRate FROM data WHERE role=»be1" ORDER by nginxErrorRate DESC" даёт нам возможность построить график или аналитически обнаружить такую зависимость.
Перечисление таких примеров можно продолжать. Важно тут то, что вся эта информация достаточно актуальна, в отличие от графиков мунина задержка представления данных составляет около двух минут, методов анализа данных несравнимо больше и выполнить их легче.
Это не есть решение заменяющее связки типа statsd+graphite, хотя-бы потому что тут нет ведения истории, возможность быстро получить разные срезы поведения системы здесь просто бонус к анализу коннективити между сервисами.
Заключение
Описанная система на сегодня мониторит связь для примерно тысячи контейнеров. Никаких проблмем с производительностью пока не обнаружено. С другой стороны обнаружена слишком высокая чувствительность к кратковременным пропаданиям связи, перегрузке каналов или увеличению времени отверта на ping. С этим мы боремся настройкой чувствительности алертов Nagios.