Основная идея асинхронного поллинга

Асинхронность позволяет сильно увеличить парралельность работы. В случае сетевого взаимодействия и съема SNMP времена ожидания ответов большие, что позволяет программе заняться чем-то полезным пока не пришел ответ.

Есть «синхронные» способы реализации парралельности – форки, треды. Это просто с точки зрения написания программы, но это очень «дорого» с точки зрения ресурсов ОС

Немного математики

Математика будет инженерно-прикидочная, точность 20%

Я хотел опрашивать 100 тысяч устройств по 100 метрикам и каждое устройство опрашивать раз раз в 5 минут, с запасом – один раз в минуту.  

То есть нужно снимать 10 млн метрик в минуту или 150 тысяч в секунду.  Округлю до 100 тысяч метрик в секунду.

Опрос одной метрики занимает около 5мс:  4мс отвечает устройство, + 1мс на обработку.

Это значит, что один синхронный поток может обрабатывать 200 метрик в секунду или нужно 500 синхронных потоков. Вполне реалистично.

(Все, конец, больше писать нечего)

На самом деле это только начало. Фишка в не идеальности мира, а именно в потерях ответов на запросы.Потери происходят из-за потерь траффика или недоступности устройств.

В моем случае таймаут ожидания составлял одну секунду и делалось 2 попытки, значит на недоступном устройстве можно «потерять» 2000мс на ожидание

Посчитаем еще раз

По нашему опыту в инфраструктуре всегда недоступны 3% устройств. Причины разные – работы по модернизации, обрывы сети, отключения питания.

Итак, 3% метрик опрашиваются со скоростью 2000мс.

В таком случае один поток cможет опрашивать всего 15,5 метрик в секунду.

Но введем оптимизацию и в случае, если у устройства 5 раз подряд не удается снять метрики, остальные 95 метрик не будем снимать в текущем цикле опроса. Получится, что один поток может обрабатывать 125 метрик в секунду или нам нужно будет 800 потоков. На грани, но можно.

Учтем специфику задачи – у нас мониторинг и нам интересно чтобы он быстро работал _особенно_ когда все идет нет так. Плюс есть технические аргументы – 800 потоков генерируют высокую конкуренцию за общие ресурсы и в целом, работают плохо в системе, которая по дизайну была рассчитана на десятки потоков.

А что, если недоступно не 3%, а 20% или все 100% устройств?

Ок, посчитаем еще один раз

Чтобы не было скучно – вымышленный, но вполне возможный пример из какого ни будь постмортема  – «ошибка конфигурации X привела к недоступности по мониторингу и управлению примерно половины сети.  Исправление было сделано сразу же, проверенные вручную 10 устройств стали доступны, но реально проблема решилась только в одном сегменте, поэтому устранение для остальной части сети затянулось на 2 часа».

Хорошо бы, чтобы мониторинг мог локализовывать и такие аварии, когда и 45%, и 99% сети недоступно.

Тогда немного другая задача: 100% устройств не отвечают. Сколько нужно потоков, чтобы увидеть, что они заработали в течении 30 секунд?  - Около 7 тысяч.

Такую параллельность разумно обеспечить асинхронностью

Входные данные

Система написана на С, парралельность обеспечена форками, для синхронизации используются мьютексы, активно используется Shared Memory для хранения объектов. Реализация SNMP на основе Net-SNMP

Задача – минимальными усилиями сделать так, чтобы поллинг был асинхронным

Реализация 1 : «все что работает – пусть работает, лишнего не трогай»

Кажется, нужно просто добавить парралелизма?

Я взял код синхронной версии поллера SNMP, изменил размеры массивов, немного переписал инициацию SNMP, чуть помучался с сильно возросшими требованиями по памяти, но получил псевдо-асинхронный режим работы:

Работал он примерно так:

Первая реализация парралельного поллинга
Первая реализация парралельного поллинга

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

Схема сильно упрощена – количество одновременных запросов – 2 тысячи, и масштаб ожидания тоже другой. В реальности ожидание таймаута выглядит примерно так:

Ожидание таймаутов огромно по сравнению с нормальными ответами
Ожидание таймаутов огромно по сравнению с нормальными ответами

Несмотря на простую реализацию, прирост скорости оказался примерно 100- кратным.

С установкой на продуктив появились особенности – устройства, обладающие большим количеством элементов, стали опрашиваться нестабильно.

Но схема хорошо работала на большом количестве одинаковых «небольших» устройств.

Поэтому появилась вторая реализация:

Реализация 2 : «Синхронно-асинхронный режим работы»

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

Стало лучше. Но проблемы с хостами с большим количеством метрик остались: все еще иногда фиксировались не ответы или потери элементов.

На количестве метрик больше 2-3 тысяч такая потеря происходила примерно в каждом цикле опроса и приходилось увеличивать количество попыток или отбрасывать элементы только после 3-4 неудачных опросов, что затягивало общий цикл съема.

В итоге, вторая реализация стала работать качественней, но потеряла в скорости. Стало ясно, что малой кровью не обойтись.

Реализация 3 : «Настоящая асинхронная»

Это первая версия, которая могла считаться истинно асинхронной. Для ее реализации пришлось ввести полноценную машину состояний для каждой операции, благо в случае SNMP это протокол UDP и состояний немного.

Введение состояний позволило разсинхронизировать общую работу соединений и получать новые элементы данных по мере того, как появлялись свободные соединения:

Реализация получилась существенно сложнее, но позволила не зависеть от скорости опроса конкретных хостов и их неответов, утилизация соединений резко возросла, но опять полностью от неответов избавить не удалось, хотя они стали очень редки.

Реализация 3.5 : «еще лучше, еще быстрее»

«Вины» механизма поллинга в неответах не было. Проблема была в механизмах распределения задач. Один хост мог попасть в несколько асинхронных поллеров и в таком случае сразу несколько поллеров его начинали параллельно опрашивать.

Поэтому нужно было оптимизировать механизмы распределения задач на поллинг.

Сначала я сделал так, чтобы один хост гарантированно попадал в один и тот же поллер. Потери пропали, хотя иногда  хосты с 3-4 тысячами метрик могли ожидать 2-4 минуты пока устройство отвечало на все запросы и такие метрики фиксировались как опаздывающие.

Появилась возможность для еще одной оптимизации – я не закрывал в одном соединении сокет до тех пор, пока обрабатывал метрики одного и того-же хоста. Фаерволам стало немного легче. Но потом пришлось отказаться: некорректно работал библиотечный механизм таймаутов, который пришлось продублировать своим, но самое неприятное, случались утечки памяти в Net:SNMP.

В итоге, такими стараниями асинхронный поллинг смог достигать цифр 60-70 тысяч метрик в секунду. Дальше он выявил ряд проблем и узких мест в общей архитектуре, упираясь в блокировки больше, чем в ограничения поллинга.

Реализация 4 : «до основанья, а затем»

Раз уж удалось гарантировать, что хосты из общей очереди задач всегда будут попадать в «свои» поллеры, то необходимость держать такие хосты в общей очереди задач отпала.

В 4 версии я реализовал получение сразу всего списка во внутреннюю очередь, с сопутствующей оптимизацией – резолвинг и кеширование ДНС, макросов и конфигурации элемента данных делались на входе единоразово. Таким образом, сотни тысяч блокировок конфигурации в общей памяти трансформировались в одну – две для выполнения массовой операции обновления элементов в течении достаточно долгого периода времени.  

Версию 4.0 оказалось проще переписать заново, так как поллер стал сильно «самостоятельным» благодаря собственной очереди и опала необходимость подгонять его под логику работы классических поллеров. Еще одна диаграмма работы:

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

И осталось еще одно ограничение - количество исходящих портов. Так как серверу нужна еще прочая сетевая активность помимо SNMP, общее количество соединений ограничил в 48 тысяч.

Даже это - большая цифра. Поллер с таймаутом в 2 секунды при 100% неответов сможет проверить 15 хостов за 30 секунд в одном соединении или примерно 720 тысяч! хостов за 30 секунд в 48 тысяч соединений.

Эти цифры больше, чем устраивают, но, если нужно больше – всегда можно решить с помощью конфигурации с несколькими IP адресами или RAW socket.

Сейчас такой поллинг в тестах упирается в соединения на отметке в 120-140 тысяч метрик в секунду.

На проде снимает около 30-40 тысяч метрик в секунду, работает в один поток, использует при этом 50% одного ядра. 

Вывод:

От Реализации1 до Реализации4 прошло почти 2 года. Каждая реализация занимала по 2-3 человеко-недели.

Стоило оно того?

Как простое решение - можно же было просто «поднять пачку контейнеров для скейла».  

Думаю, что очень даже стоило.

Все, что снималось на 15 вспомогательных серверах, получилось снимать на одном и тот «курит».

А мониторинг должен быть максимально простым, а лучше всего - сосредоточенным в одном сервере. Иначе он – тоже претендент на выход из строя при инфраструктурных проблемах.

Мониторьте в радость.