«Каждый охотник желает знать, где сидит фазан»

«Сколько килограммов железа мне нужно через год? Как снизить нагрузку на главного инженера и при этом подстелить соломки?» — спрашивал я себя в процессе работы по Capacity Planning. Прогнозирование временных рядов — одна из типовых аналитических задач, решив которую, мы можем спрогнозировать, что нас ждет в будущем. В ИТ это всегда актуально, поскольку ресурсы не резиновые, а застраховать себя от ошибки хочется всегда.

Однажды такая задача появилась и у меня — в виде автоматизированных отчетов по Capacity Planing одной из систем нашего заказчика. В данной статье я расскажу, как простой инженер, не погруженный в тонкости аналитики и сложного матаппарата, может автоматизировать данную задачу с приемлемым результатом. Почему приемлемым? Мне нужно точно знать значение вот этой вундервафли через три поколения! Об этом мы поговорим ближе к концу статьи.

Немного о самом процессе и алгоритмах

Никто не обещал, что будет легко (а так хотелось!). Перейдем к теме. Итак, существует достаточно большое количество математических методов прогнозирования временных рядов. Среди них:

· Naïve

· ARMA, SARMA

· ARIMA, SARIMA

· ARCH

· ETS и т.д.

Эти инструменты выстраивают прогноз с учетом различных характеристик исходных данных (сезонность, тренд); используют метод экспоненциального сглаживания исходных данных или прибегают к данным, опираясь только на последние известные значения за конкретный период. В целом базовые методы, как правило, сводятся к построению прогноза на разных скользящих статистиках и сглаживаниях (например, модель Хольта-Винтерса). Не ищите полный список — вот он.

Но какой инструмент нужен нам, можете спросить вы. Выбор модели делается на основе знаний о самих данных. Если все же непонятно, какую модель использовать в вашем случае, попробуйте сравнить точность построения прогнозов на основе разных методов. А теперь пример для наглядности. Берем выборку данных за какой-то отрезок времени (неделя, месяц, год) и разделяем ее в пропорциях 80/20%, где 80% отрезка — это наши исторические данные, на которых будет обучаться модель, а оставшиеся 20% нужны для валидации наших прогнозов. Схематично это выглядит так:

На графике показана ситуация, когда прогноз близок к реальному значению, но все же ошибается. Ошибка в прогнозировании в данном случае может быть результатом неверно подобранных параметров модели или метода. Чтобы решить проблему, оцениваем величину ошибки, используя различные статистические методы. Например, MAPE и MAE — как простые и наиболее распространенные из них.

MAE (Mean Absolute Error) — абсолютная ошибка прогноза. Показатель отображает, насколько ошиблась модель при прогнозировании следующей точки в будущем, и измеряется в единицах (в штуках, миллисекундах, процентах). Пусть

y_i —

  — историческое значение, а

y ̂_i

 — соответствующий этой величине прогноз нашей модели. Тогда

e_i=y_i- y ̂_i

 — ошибка прогноза.

MAPE (Mean Absolute Percentage Error) — средняя абсолютная ошибка прогноза. Показывает, на сколько в среднем ошибается модель при построении. Для этого считает MAE для каждой точки, а далее считаем среднее значение разности ошибки прогноза на историческое значение.

MAPE=mean(|e_i/y_i |)

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

На самом деле затронутая тема требует более пристального внимания и глубокого погружения. Но мы сознательно максимально обобщаем, чтобы вы начали знакомиться с инструментом и могли сделать Capacity Planning сами.

А есть что попроще

Первое — нужно выбрать алгоритм и подобрать коэффициенты на основании сведений о ваших данных. Но в целом если у вас не стоит цели построить очень точный прогноз (отклонение до 2–5%), то можно воспользоваться коробочными решениями. Среди них есть популярная штука Prophet.

Prophet («Пророк») — это open-source-библиотека, разработанная компанией Facebook (запрещенная в России организация) для прогнозирования различных бизнес-метрик. Она хорошо справляется с этой задачей с помощью дефолтных параметров модели. Ее главная особенность — возможность влиять на точность прогноза, используя понятные человеку параметры.

По сути, Prophet относится к классу Additive Regression Model. Это значит, что мы влияем на прогноз, задавая следующие параметры.

· Сезонность — определяет периодические изменения данных на интервале недели и года.

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

· Аномалии — указывают аномальные часы/дни/недели, которые не будут влиять на весь остальной прогноз.

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

Кстати, преимуществом Prophet является возможность использовать библиотеку с такими языками, как R и Python, что здорово расширяет круг потенциальных пользователей и количество статей.

К сожалению, сама библиотека сейчас не развивается и имеет некоторые проблемы с зависимостями. Но существует достойная замена – NeuralProphet. Его авторы вдохновлялись Prophet и AR-net, взяв лучшее из «двух миров».

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

От теории к практике по планированию ресурсов

Приступаю к практической части. У меня было:

  • десятки prod-серверов;

  • система мониторинга;

  • сотня метрик (ресурсы и бизнес-метрики) на каждый узел;

  • карты, деньги, два ствола — ладно, шутки в сторону.

А теперь конкретнее. У заказчика внедрена система мониторинга Zabbix, она собирает данные по утилизации ресурсов всех систем. Немного бизнес-метрик, которые отображают качество работы этих систем и количество пользователей. Историческим данным 30 дней, а трендам — более года. В общем, есть с чем работать.

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

Для начала определяем, что будем прогнозировать. Логично начать с набора CPU/MEM/NET/IO/DISK, но не стоит забывать о количестве пользователей.

Чтобы использовать таланты «Пророка», сначала устанавливаем библиотеку. Пишем на Python, поэтому выполняем команду Pip Install neuralprophet.

Еще нам понадобится модуль для работы с API Zabbix, чтобы найти и извлечь необходимые данные. Рекомендую использовать официальную библиотеку от вендора — Zabbix-utils. Кроме модуля для API используем Pandas и Plotlib для визуализации.

Для начал подключимся к Zabbix:

from zabbix_utils import ZabbixAPI
import sys
from neuralprophet import NeuralProphet, set_log_level
import pandas as pd
import warnings

warnings.filterwarnings("ignore")
set_log_level("ERROR")

if __name__ == '__main__':
    # Zabbix API object initialization
    try:
        z_api = ZabbixAPI(url="zabbix.fqdn.com")
        z_api.login(user="User", password="zabbix")
    except Exception as error:
        print(error)
        sys.exit(1)
    else:
        print('Zabbix API connection established.')

    z_api.logout()

Теперь выгружаем из системы мониторинга данные. Поскольку это пример, то будем забирать тренды, сгенерированные системой мониторинга, а не исторические данные. Для истории нужна информация о типе собираемых данных, а это совсем тонкости работы с API Zabbix. Поэтому делаем отдельную функцию Collect_Data, принимающую объект для работы с Zabbix, Itemid интересующего нас элемента и дату, с которой мы начинаем читать данные. Сам Itemid можно получить из БД системы мониторинга или URL строки веб-интерфейса.

def collect_data(api: ZabbixAPI, itemid: str, collect_from: str):
    # Создаем карту для хранения данных. ds — массив со временем, y — массив со значениями.
    # Заранее форматируем данные для дальнейшей работы с NeuralProphet, ведь он ожидает их именно в таком формате.
    data = {
        "item_name": "",
        "trends": {"ds": [], "y": []},
    }

    try:
        # Запрашиваем данные.
        trend_data = api.trend.get(
            output=["clock", "value_avg"], itemids=itemid, time_from=collect_from
        )
        
        # Получаем название метрики для дальнейшего использования.
        data["item_name"] = api.item.get(output=["name"], itemids=itemid)[0]["name"]

        for item in trend_data:
            data["trends"]["ds"].append(int(item["clock"]))
            data["trends"]["y"].append(float(item["value_avg"]))

    except Exception as error:
        print(f"Произошла ошибка при получении данных: {error}")
        sys.exit(2)

    return data

Теперь перейдем к основному — прогнозированию наших данных. Для этого создаем отдельную функцию — Forecast_Data. Она принимает объект с нашими данными и интервал времени, на который нужно построить прогноз.

def forecast_data(zabbix_data, forecast_interval_hours):
    try:
        # Преобразовываем данные в Pandas Dataset.
        data = pd.DataFrame(zabbix_data["trends"])
        # Изменяем формат даты.
        data["ds"] = pd.to_datetime(data["ds"], unit="s")

        # Создаем объект класса Prophet и меняем стандартное значение параметра Changepoint_Prior_Scale.
        # Данный параметр влияет на частоту выбора контрольных точек для оценки.
        m = NeuralProphet(n_changepoints=10,
                seasonality_reg=0.1,
                future_regressors_model="neural_nets",
                ar_layers=3,
                n_forecasts=10,
                loss_func="mse",
                global_normalization=True,
                yearly_seasonality=False,
                weekly_seasonality=False,
                daily_seasonality=False
            )

        # Запускаем обучение модели и указываем, насколько далеко необходимо построить прогноз.
        m.fit(data, early_stopping=True, progress="plot")  # df is a pandas.DataFrame with 'y' and 'ds' columns
        future = m.make_future_dataframe(df=data, periods=forecast_interval_hours, freq="H")
        # Рассчитываем прогноз    
        forecast = m.predict(future)
        forecast.index = forecast["ds"]

        # Переименовываем колонки и удаляем лишние, чтобы пока не засорять вывод.
        forecast.rename(
            columns={
                "ds": "Время",
                "yhat1": "Прогноз",
                "trend": "Тренд",
            },
            inplace=True,
        )
        
        print(forecast)
    except Exception as error:
        print(error)

Теперь дополним нашу функцию Main и запускаем скрипт.

if __name__ == '__main__':
    # Zabbix API object initialization
    try:
        z_api = ZabbixAPI(url="zabbix.fqdn.com")
        z_api.login(user="User", password="zabbix")
    except Exception as error:
        print(error)
        sys.exit(1)
    else:
        print('Zabbix API connection established.')

    item_data = collect_data(z_api, "18321", "1710671487")
    forecast_data(item_data, 144)

    z_api.logout()

На выходе получаем датасет с прогнозом (yhat), трендами и интервалом неопределенности, который визуально выглядит вот так:

Верить или нет — вот в чем вопрос

А теперь главный вопрос. Насколько этим данным можно доверять? Оценим отклонение MAE и loss test. Видим значение в 3%, но стоит сразу отметить, что рассматриваемые данные практически статичные.

    df_train, df_test = m.split_df(df=data, freq="H", valid_p=0.2)

    metrics_train = m.fit(df=df_train, freq="MS")
    metrics_test = m.test(df=df_test)

    print(metrics_test)
    

######## Вывод

   MAE_val  RMSE_val  Loss_test  RegLoss_test
0  1.35034  2.269206   0.037568    

В целом это неплохо с учетом того, что в качестве входных данных была статистика по утилизации каталога. Но это, если честно, не показатель, потому что в ходе тестов мы получали и большие значения.

Например, в этом кейсе рассчитывался линейный тренд утилизации CPU и памяти на 2 года вперёд на основе статистики за предыдущие 2 года.

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

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

Благодаря такому подходу удалось:

  • автоматизировать рутинную задачу и уменьшить время, которое затрачивает главный инженер на аудит систем, на 50%.

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

Послесловие…

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

Комментарии (0)