Glaber и скорость перезагрузки конфигурации

Начну с обычного «Меня зовут Михаил и я поддерживаю форк заббикса, называется Glaber».

Основная суть форка — экономить время людей, пользующихся мониторингом.

Проект бесплатный, открытый, его можно скачать на гитлабе, есть пакеты под разнык linux, описание тут.

Расскажу про скорость применения изменений конфигурации.

Жизненная ситуация:

На одной из моих инсталляций на поддержке я увидел интересную настройку метрики: каждые 5 секунд проверялось наличие ежесуточного бекапа с глубиной проверки в 1 сутки.

Технически это не проблема, 16к значений в кеше — легко. Но, у меня возник вопрос — зачем?

Дело было так

Администрару нужно было создать метрику, и проверить ее работу.

Планируемый интервал проверки »1 раз в сутки» — сработает в течение суток, быстро не проверить, а задачу нужно сделать сейчас. Поэтому автор выставил минимальный интервал и забыл поменять  обратно, после того, как убедился что работает, как надо.

Проблему усугубляла неработющая кнопка «проверить сейчас» для данного типа проверки. Это было связано с тем, что механизм не был предусмотрен мной в асинхронных поллерах агентов (Agent). Косяк, баг, нужно исправить (upd: уже исправлено). Но это не главное.

Главное то, что процесс обсуждения с пользователями дал в сотый раз понять: наличие задержки между изменением конфигурации и применением ее на сервере создает существенную UX проблему.

Пользователь, изменив метрику, не хочет (да и не должен) ждать. А бывает, что нужно ждать 10 минут или час, это раздражает и съедает время.

В стоковых настройках сервер применяет новую конфигурацию каждые 30 секунд, но часто этот интервал увеличивают до 300–3600 секунд  на конфигурациях побольше. Ждем?

ждем перезагруки кофигаждем перезагруки кофига

Как бы сделать, чтобы время ожидания было меньше 10 секунд?

Решение 0, «в лоб»

Перезагружать всю конфигурацию как можно чаще, каждую секунду.

Так себе вариант — из высоких нагрузок и внтуренних блокировок скорость мониторинга деградирует, хотя для маленьких инсталлов (100 хостов, 10к метрик) — вполне можно. Но у меня есть сотни тысяч хостов с количеством метрик за 6 млн. Там загрузка конфига идет 7–8 минут. Не пойдет.

Подойдем с головой, поразбираемся с матчастью:

Смотрим код синхронизации, архитекуру.

Есть отдельный процесс «config syncer», который запускает процедуру синхронизации конфигурации из базы SQL в память сервера каждые N секунд (опция CacheUpdateFrequency в файле конфигурации).

Начало многообещающее — у него есть два режима работы — FULL sync и UPDATE sync.

Есть две группы функций для каждого типа объектов (айтемы, хосты, тригеры … таковых около десятка) — первая группа загружает из базы или полный набор строк или изменения, а вторая группа функций применяет полученный набор строк и создает новые объекты, пересоздает изменившиеся, удаляет удаленные.

Но есть одно «но» — при периодической синхронизации конфигурации в память сервера делается загрузка типа UPDATE, и при этом тратится столько же времени, как и на первоначальную INIT- синхронизацию.

Почему?

Копаем глубже — внутри процедур загрузки данных. И в INIT и в UPDATE синхронизациях загружаются целиком все данные. В том числе, при обновлении. В процессе UPDATE синхронизации делается промежуточный набор изменений, состоящий из трех видов строк — новые, изменённые, неизмененные. И по размеру и количеству операций это аналогично полной синхронизации на старте.

Это неоптимально, вот пример, синхронизация на инсталле с 6 млн метрик дает пики по загрузке CPU (похожие скачки есть и в потреблении памяти) из за такой синхронизации.

Высокие пики каждый час - это перезагрузки конфигурацииВысокие пики каждый час — это перезагрузки конфигурации

Копаю еще чуть глубже — смотрю DDL таблиц, актуальные данные. Становится понятно, что так приходится делать, потому что нет механизма, позволяющего реализовать инкрементальность обновлений: в данных нет отметок времени или признаков изменения строк и флага «удаленная строка».

Поэтому для удаления из памяти неактуальных данных нужно делать полную синхронизацию (или делать тайм-аут, но тогда какое-то время еще будет продолжаться съем удаленных метрик).

С таким решением я не согласен, нужно переделать.

Решение 1: «Чего долго думать, сейчас все по быстрому исправим»

image-loader.svg

Добавляю в коде статус строки «удалено» с отложенным удалением и временные метки. Но есть ограничение: лезть в DDL таблиц очень «дорого», так как группа функций синхронизации жестко повязана на нумерацию столбцов в ответе во всех процедурах обновления данных, примерно вот так:

Not nice, но Not nice, но «работает же»

А это примерно 7–8 тысяч строк кода.

Поэтому добавить, скажем, поле timestamp вариант плохой: нумерация cъедет и сломается при очередном апдейте. И пока хочется на уровне таблиц SQL иметь максимальную заббикс — совместимость.

Пробую влезть со своими данными в текущие таблицы:

Начинаю с самой тяжелой таблицы — items (по времени на загрузку — это около 15% времени на тестовой конфигурации). С профайлингом помогает встроенный механизм дампа длинных запросов в заббиксе:

image-loader.svg

Так как Glaber не хранит и не читает оперативные данные по item в SQL, я могу задействовать поле lastchange в таблице rtdata  для сохранения времени правки.

Ввожу новый тип статуса ITEM_STATUS_DELETED для индикации удаленных метрик и отложенную чистку таких строк в housekeeper процессе.

В процедурах удаления и изменения айтемов (метрик) в API и в коде сервера  добавляю обновление поля lastdata при изменении метрики, корректное игнорирование удаленных строк.  

В процедуре синхронизации айтемов добавляю временное условие и запоминаю время последней синхронизации

Вуаля, запрос теперь занимает несколько миллисекунд на обновлении десятка-сотни айтемов и, возвращает, то, что поменялось.

Дальше — триггеры. Повторяю весь набор действий, замечаю очень похожий код синхронизации, прямо как у метрик.  С триггерами опять «везет» — есть поле, куда записать время, хотя Glaber все еще обновляет его в SQL базе, но можно и отключить — это оперативная информация, нечего ей в базе делать.

Но начинает нарастать снежный ком изменений: с триггерами «вылезают» таблицы функций и тегов триггеров, долго грузятся теги айтемов, шаги препроцессинга. И … опять похожие процедуры обновления, но тут никаких полей под временные метки нет.

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

В текущей реализации индексация в парах зависимостей триггеров типа id1->[id2,id3,id4] есть только по id1. Если поменяется или будет удален id1 никаких проблем, а если мы id3 удалим, только полным перебором искать, не очень хорошо.

Приехали.

Делаю паузу на пару дней. Обдумываю. Новый год на дворе. Лыжи рядом.

И мне нужно масштабируемое решение, не зависящее от конкретной таблицы и ее DDL, чтобы 10–12 разных типов объектов поддерживались и легко добавлялись новые и с множественными зависимостями (индексами) что то нужно сделать, причем так, чтобы базы 10 млн метрик обновлялись легко и быстро при инкрементальных обновлениях.

Решение 2

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

Решаю, что будет три класса загрузки из базы:

1.       достаточно маленькие таблицы, которые можно целиком «старым» способом загружать.

2.       Большие таблицы, пропорциональные количеству элементов — производные от метрик (items): триггера, функции, теги. Их нужно по-новому, инкрементально.

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

Но сначала нужно «убраться».

Откатываю все из «Решения1». Все было не зря, уроки получены, обзор сделан.

Убираю всю копипасту процедур загрузки, пишу две универсальные процедуры: код модуля становится на тысячу строк короче оригинального кода, и улучшается структура и красота

Вот одна функция, (красное — дублированный код, убран). А сбоку справа — по тикеру с обзором кода можно оценить сколько все такого было:

было -> стало x15 раз» />было → стало x15 раз</p>

<p>функции стали отличаться  только запросами и параметрами, полями и именами коллбеков сравнения типов данных, то есть только тем, чем они реально отличаются. Самое главное — это легко читаемо, логично, изменяемо.</p>

<p>Для индексов универсальную обработку пока не пишу — из четырех только один создает проблемы при загрузке — зависимости триггеров, остальные «маленькие».</p>

<p><strong>Ок. Стало красиво, а как быть со скоростью загрузки? </strong></p>

<p>Возвращаюсь к задаче: нужно реализовать информирование сервера о всех изменения — то есть присылать некоторый список изменений — <strong>changeset</strong>. </p>

<p>Стоит сказать, что и сам сервер меняет базу (LLD процедуры, автообнаружение хостов). Схема там интересная, «через одно место» — через SQL —  сервер пишет апдейты в SQL базу, а потом синкер конфигурации их загружает. Механизм не оптимальный, но пока не меняю, хотя постараюсь ускорить применение изменений тоже.</p>

<p><strong>Архитектура</strong></p>

<p>Есть два логичных пути — информировать сервер об изменениях через траппер, как стандартный RPC интерфейс, либо писать изменения в базу. </p>

<p>Архитектурно первый был бы более логичен если бы первоисточником конфигурации был сервер (к чему хотелось бы стремиться), а база была бы просто интерфейсом хранения, но пока это не так.</p>

<p>Поэтому выбираю второй способ — пишем в базу, в отдельную таблицу. </p>

<p><strong>Реализация</strong></p>

<p>Таблицу изменений — «changeset» создаю и удаляю динамически сервером, поэтому можно не бояться проблем совместимости. Через переименование таблицы обеспечиваю псевдо-транзакционность. </p>

<p>Проверяю что хватает прав: сервер при смене версий делает «db — upgrade» процедуры, а значит, права на create/drop/rename table у него есть в большинстве инсталляций. Помогает то, что уже есть соответствующие функции в библиотеках самого сервера.</p>

<p>Индексирую changeset по времени, id и типу объектов, так как данные из таблицы изменений нужно будет в SQL  JOIN-ить c большими таблицами при загрузке и проверять время обновления / фильтровать по типу.</p>

<p>Cоздаю еще один тип синхронизации сервера с базой — INCREMENTAL. Он будет работать как почти как UPDATE, только из sql будет выбираться то, что поменялось, а не все записи, и удаление будет не по полному сравнению записей, а по списку из changeset таблицы.</p>

<p>Создаю новый класс в API — СChangeset, и модуль на стороне сервера changeset.h которые реализуют методы работы с таблицей изменений. Использую методы в процедурах CRUD нужных мне объектов, а на стороне сервера — в sуnc процедурах.</p>

<p>Проверям — дебажим — запускаем (так раз 20). </p>

<p>Ура! результат достигнут — сервер загружает конфигурацию _гараздо_ быстрее за счет загрузки только инкрементов по «тяжелым» таблицам.</p>

<p>Было : </p>

<p><code>server: configuration syncer [synced configuration in 16 sec; next sync in 3598 sec]</code></p>

<p>Стало: </p>

<p><code>server: configuration syncer [synced configuration in 1 sec; next sync in 3598 sec]</code></p>

<p>Но результат еще не достингут — загружаем изменения инкрементально, быстро, но нужно <strong>все равно нужно ждать</strong> 30 секунд или 1 час до перезагрузки конфигурации.</p>

<p>Делаю в RPC (trapper) поддержку вызова информирования сервера о том, что требуется инкрементальная загрузка конфигурации.</p>

<p>Добавляю метод в касс <code>CZabbixServer</code> в API и добавляю вызовы информирования в CRUD процедуры нужных объектов. </p>

<p>Дописываю траппер, чтобы сервер, получив такое сообщение, запускал INCREMENTAL обновление.</p>

<p>Итак, первое MVP готово. </p>

<p>Еще осталось решить задачу с таблицами зависимостей. </p>

<p>Тут приходится «рубить под корень» — текущая реализация не предполагает частичного обновления. Делаю двойной индекс (прямой и обратный), пакую в отдельный модуль и заодно переношу в реализацию хешей без общих блокировок: обновления такой штуки будут чуть более затратны по CPU (мизер), но зато не будет ограничения в производительность одного потока при изменении данных в кеше конфигурации. </p>

<p>По пути вычищаю пачку кода, связанной с расчетом топологии триггеров. Не нашел ни одного места, где бы уровень топологии триггеров использовался, не считая самого расчете. Выигрываю еще 0.3–0.5 секунды (3–5% времени) на скорости обновления конфигурации.</p>

<p>Убеждаюсь, что многкратное уведомление об изменении даже во время обновления обрабатывается корректно. </p>

<p>Замеряю: задержка от уведомления до применения укладывается примерно в полсекунды, это в 30 раз быстрее, чем было: </p>

<p><code>2272:20220111:111700.634 CONFIG RELOAD trapper command recieved</code></p>

<p><code>2185:20220111:111701.187 CONFIG RELOAD complete</code></p>

<p>Решаю, что пока классические UPDATE механизмы синхронизации оставляю — наверняка будут ошибки при инкрементальных загрузках и изредка полный синк будет полезен.</p>

<p><strong>Это еще не все — решение 2.5 </strong></p>

<p>Результат все равно не достигнут — не хватает главного — быстрой реакции сервера на изменение метрики. Сервер быстро подхватывает изменения, но потом ждет запланированого времени поллинга. </p>

<p>Суть же ожидаемой мной реакции после изменения или добавления метрики — отображение результата первой проверки как можно быстрее (а не где-то в течении интервала опроса).  Пользователь, конечно, может и «test now» нажать, но зачем же его заставлять? Он еще и узнать как-то должен об этом.</p>

<p>Делаю так — как только метрика поменялась, сервер сразу метрику «опрашивает» и параллельно планирует опрос согласно выставленным delay параметрам. </p>

<p>В кейсе с бекапом и ежесуточно проверкой — сразу после редактирования будет сделана первая проверка, а вторая где-то в течении суток. Уже почти совсем хорошо. </p>

<p>Side — эффект — при загрузке сервер сразу опросит все метрики. Я считаю это решение логичным. Glaber это оперативная система и поэтому делает все, чтобы иметь максимально точное отображение состояния мониторинга, это дополнительная причина на старте «узнать как оно все есть на самом деле». Тем более что старт ему сильно прощще чем заббиксу дается за счет загрузки кеша метрик с диска и ограничения запросов к истории.</p>

<p>Отвлекся, продолжаем: «за бортом» остались асинхронные поллеры, которые имеют свои собственные очереди поллинга. Они должны тоже узнать, что конфигурация частично поменялась. Пока что асинхронные поллеры относительно «редко» смотрят в конфигурацию, раз в 120 секунд, это неприемлемо медленно по скорости реакции.</p>

<p>Делаю для асинхронных поллеров отдельную очередь «новостей» об изменении конфигурации, они должны туда заглядывать почаще и забирать «свои» изменения. </p>

<p>Список изменений «недорого» часто опрашивать, в нормальном режиме он пустой или очень «небольшой» при частичных изменениях (относительно всей конфигурации). </p>

<p>В итоге решается задача баланса между частым опросом конфигурации  и времени изменения конфигурации в своих очередях. </p>

<p>Для асинхронных поллеров получаю +1 секунду к реакции на изменения конфигурации.  это мелочи, раньше были минуты. И еще жирный плюс — в эту же очередь приходит запрос на поллинг по кнопке «Test now», поддержка кнопки начинает работать и во всех асинхронных способах съема. </p>

<p>Добавляю в config sync проверку наличия изменений в changeset автоматически, каждые 3 секунды. Теперь полный цикл появления новых хостов занимает не более 6 секунд после автообнаружения, а запись первых метрик, обнаруженных через методы использующие LLD в худшем случае начинается через 9 секунд, а в общем — менее, чем за 6 секунд. Впрочем можно спокойно эти цифры раз в 5–10 уменьшить.</p>

<p><strong>Итог</strong></p>

<p>Очереди, инкрементальные загрузки, хеши, апи оптимизации и красивые ИТ истории это интересно, но вторично. А главное — вот в этом коротком видео: </p>

<p>От нажатия кнопки «сохранить» до первых пингов или SNMP запросов проходит меньше секунды: </p>

<p>Почему это важно? Потому что скорость изменений в системе пользователь-ИТ система определяет скорость получения нужного результата и нашу эффективность работы. </p>

<p>У условного Васи — администратора мониторинга будет возможность в один рабочий час сделать <strong>20–40</strong> циклов изменил — проверил — еще изменил…, а не 1–2, что будет сильно экономить «васям» время, ну и мне, как «васе» тоже.</p>

<p>С Новым годом.</p>

<p>p.s. Исправления по быстрой загрузке конфига доступны начиная с версии 2.7.1<br />две три — в релиз</p>
    
            <p class=© Habrahabr.ru