Обнаружение и использование тенденций данных в программных системах
Визуализация данных и моделирование процессов являются ключевыми понятиями для понимания системы, с которой вы работаете. У вас могут быть метрики для понимания текущего состояния системы (RPS, использование ресурсов) с возможностью просмотра исторических данных, и они хороши для обнаружения аномалий, но может быть сложно определить тенденции на ближайшие месяцы или даже годы.
Понимание тенденций позволит вам лучше понять, как у вас дела и, что более важно, как вы будете будете работать. Такие аналитические данные помогут вам заранее прогнозировать нагрузку, соглашение об уровне обслуживания, требуемые ресурсы и подготовиться к быстрому росту. Это позволит строить вашу систему осознанно. Более глубокое понимание обеспечивает больший контроль над ним.
В этой статье я хотел бы показать вам пример того, как можно получить полезные знания из системы, с которой я работал.
вступление
В качестве основной функции в PandaDoc у нас есть редактор документов, который позволяет пользователям редактировать документы в режиме реального времени. У многих конкурентов есть возможность загрузить PDF-документ и поместить в него настраиваемые поля, но великая сила шаблонов заключается в том, что вы можете изменять все, что добавляете в свой шаблон — текст, мультимедиа, ссылки, поля и т. д. Это то, что предлагает наш редактор. хорош в том, что вы можете ввести любой текст, оставить местозаполнитель, чтобы автоматически заполнить его данными вашего клиента, настроить стиль документа, рассчитать свои услуги и многие другие интересные функции.
Каждое действие пользователя — это отдельное событие, которое произошло с документом, будь то обновление текста, удаление поля, создание страницы или что-то еще. Представление документа в виде совокупности таких событий открывает много полезных вещей:
- вы можете путешествовать во времени — просматривать время жизни документа и возвращаться в любое состояние в прошлом,
- гораздо более простая реализация отмены недавних изменений,
- воспроизводимость (например, для аналитики или отладки),
- и многое другое.
Наш редактор реализован по принципу источника событий — каждое действие пользователя мы храним как отдельное событие в единой таблице базы данных журнала событий. Капитан очевиден, но чем больше пользователей редактируют свои документы — тем быстрее растет журнал событий. Давайте посмотрим, как мы можем извлечь что-либо из таких данных.
Учитывая ввод
Формализуем имеющиеся данные. У нас есть последовательность записей (я буду называть их «событиями»), представляющая каждое действие пользователя. Такая последовательность является последовательностью «только для добавления» — нет возможности удалить или изменить существующие элементы. События хранятся в таблице базы данных (фактическое хранилище с указанными выше характеристиками большой роли не играет, но в нашем случае это была таблица Postgres) под названием «журнал событий» с автоинкрементируемым первичным ключом. Каждое событие имеет следующую информацию:
timestamp
- когда произошло событие (предположим, что это значение не по убыванию),metainfo
- Меня это не особо заботило, но это может быть любое имя, параметры, id пользователя и так далее.
Я собираюсь ссылаться на таблицу журнала событий следующим образом:
Сбор данных
Для начала нам нужно собрать количество событий за некоторый интервал времени. Он может быть разным в зависимости от целевой цели вашего исследования. Я выбрал день в качестве временной группы между часом и неделей.
В зависимости от объема данных вы можете выбрать два возможных варианта сбора ваших данных:
- Последовательно просмотрите всю таблицу и сгруппируйте каждую запись в соответствии с ее
timestamp
. Очевидно, что сложность задачи O(N), где N — количество записей в базе данных. Результаты будут очень точными. - Для каждого дня исторических данных найдите первую и последнюю запись (используя
timestamp
) за этот день. Это можно сделать за время O(D*log(N)), где:
D
— количество дней, за которые вы хотите агрегировать данные,N
— это количество записей в базе данных.
Последний случай основан на двух важных допущениях:
- временные метки событий уменьшаются достаточно, т.е. могут быть локальные нарушения инварианта, например, если временная метка отражает время прикладного уровня, но они мало влияют на результат,
- и количество событий может быть определено как
last_event.id - first_event.id + 1
. Это означает, что в значенияхid
нет «дыр» (каждый идентификатор события равен количеству записей в этой точке) или они существуют, но мы можем их игнорировать. Например, в реальном случае относительная ошибка составила около 0,1% (о том, что объемid
значений отсутствовал в записях, аevent.id
отличалось на 0,1% от реального количества записей), которым я пренебрегал. Отсутствующие значенияid
могут появиться, например, когда транзакция INSERT прерывается — базовая автоматически увеличивающаяся последовательность обновляется, но запись окончательно не записывается.
Как только данные собраны, мы можем перейти к следующему шагу — визуализации данных. Визуализация обычно играет большую роль в понимании данных.
Визуализация данных
Давайте просто воспользуемся фреймворком панды и создадим простой сюжет:
Процедура проводилась пару лет назад, и, как вы видите, база данных содержала около 250 млн событий.
Что мы можем здесь увидеть?
- Последние десятилетия выглядят «ребристыми» — если сравнить даты, то можно заметить, что это соответствует еженедельному распределению событий — активное использование в рабочие дни и гораздо меньшая нагрузка в выходные.
- Пара «в гору», вероятно, может означать успешные маркетинговые кампании или большую нагрузку из-за потребностей бизнеса.
Круто, попробуем продолжить линию.
Построение прогнозной модели
Мы можем продолжить линию, чтобы прогнозировать рост событий в будущем.
Функция выглядит как простая геометрическая формула:
f(x) = A * x^B
Для подбора нужных коэффициентов можно решить задачу простой линейной регрессии (проще говоря — алгоритм позволяет найти коэффициенты следующей функции: f(x1, x2, x3, ..., xn) = C + W1 * x1 + W2 * x2 + ... + Wn * xn
или f(x) = C + W*x
в векторном виде).
Нам нужно немного преобразовать нашу исходную функцию:
f(x) = A * x^B; ln(f(x)) = ln(A * x^B); ln(f(x)) = ln(A) + ln(x^B); ln(f(x)) = ln(A) + B * ln(x). ---->> g(x') = A' + B' * x' <<------
И в итоге получаем вот такую картинку (оценка показывает нам, насколько хороша аппроксимация: чем ближе значение к 1,0 — тем оно совершеннее):
Score 0.928 f(x) = 178.728 * x^2.04, x - id(day), f(x) - #events in total
Что на самом деле… не то, что мы ожидали.
Но из рисунка можно сделать вывод, что алгоритм сильно следовал за линией в начале, не сумев повторить ее кривую в конце.
Однако мы пытаемся предсказать значения, поэтому нам хотелось бы получить более точное приближение ближе к концу. Другими словами, давайте просто аппроксимируем только последние точки!
... X_, y_ = np.log(X)[-100:], np.log(y)[-100:] ...
Что производит следующий вывод:
Score 0.999 f(x) = 0.001 * x^3.997, x - id(day), f(x) - #events in total
Намного лучше.
С помощью такой функции мы теперь можем прогнозировать количество записей в журнале событий — например. удвоение числа произойдет к концу года (2020 г.), а первый миллиард записей — летом 2021 г. Или вы можете узнать, когда вы достигнете максимального целочисленного значения в виде записи таблицы id
и вам нужно мигрировать ее некоторым bigint
. Или, зная текущий размер базы данных, мы можем спрогнозировать необходимое дисковое пространство через год, и, возможно, к этому времени нам нужно будет разбить таблицу журнала событий (например, путем разбиения).
Может быть, что-то еще полезное мы можем извлечь из этих данных? Я уверен, что мы можем.
Рост событий
Производные функции показывают скорость изменения значения. Чтобы применить это к вашему случаю, мы можем сказать, что можем оценить количество событий, хранящихся ежедневно (интервал времени определяется в начальной агрегации данных).
f(x) = 0.0011575004 * x^3.9969877517; f'(x) = 0.0011575004 * 3.9969877517 * x^(3.9969877517 - 1); f'(x) = 0.0046 * x^2.997;
На момент наличия около 250 млн событий база данных ежедневно хранила около 1,4 млн событий по этой формуле. Что составляет около 1k событий в минуту. С помощью таких расчетов вы можете планировать свои ресурсы с точки зрения сервисных машин или виртуальных машин, шлюза RPS, статистики одновременных подключений к базе данных и т. д.
Рост пользователей
Отлично, до сих пор мы пытались оценить требуемые вычислительные ресурсы.
Можем ли мы получить какую-либо информацию о росте числа пользователей? Звучит нереально, потому что мы не группировали исходные события по идентификатору пользователя. Но давайте все же попробуем.
Если бы нас попросили описать формулу количества событий в журнале событий в конкретный день, на какие параметры мы бы опирались? Первая идея, которая приходит на ум, — опираться на результаты предыдущего дня и вывести рекуррентную формулу:
f(x) = f(x - 1) + e(x)
где e(x)
количество событий, добавленных в x
день.
Представьте, что мы являемся активным пользователем, редактирующим документы онлайн. Каждый день мы приходим на работу, включаем офисный компьютер и начинаем печатать. Мы производим примерно одинаковое количество пользовательских правок каждый день, поэтому мы можем определить среднее количество правок в день для одного активного пользователя как E
.
С другой стороны, наш бизнес растет. А наша маркетинговая кампания позволяет нам регистрировать новых пользователей. Давайте представим, что наша пользовательская база растет на постоянное число каждый день (потом мы решим, достаточно ли этого) — на U
.
Теперь формула активных пользователей в данный день будет выглядеть так:
u(x) = u(x - 1) + U = U * x
И объем событий в журнале событий будет следующим:
e(x) = u(x) * E f(x) = f(x - 1) + e(x); f(x) = f(x - 1) + u(x) * E; f(x) = f(x - 1) + U * E * x; f(x) = U * E * [x + (x - 1) + (x - 2) + (x - 3) + ... + 1]; f(x) = U * E * [x * (x + 1) / 2]; f(x) ~ x^2
В итоге мы получили важный результат — количество событий в журнале событий растет квадратично (с точки зрения асимптотической эквивалентности)!
Если мы посмотрим на график зависимостей, наша функция f(x)
зависит от e(x)
(среднее количество пользовательских правок в день), а сама e(x)
зависит от u(x)
(количества активных пользователей). Таким образом, единственным «фактором влияния» целевой формулы является количество активных пользователей или наша пользовательская база. В приведенном выше примере мы предположили, что пользовательская база растет линейно (u(x) ~ x
). Но что, если мы поменяем его на что-то более агрессивное?
u(x) ~ x^2; e(x) = u(x) * E; e(x) ~ x^2; f(x) = f(x - 1) + e(x); f(x) ~ f(x - 1) + x^2; f(x) ~ x^2 + (x - 1)^2 + (x - 2)^2 + ... + 1^2; f(x) ~ x^3
Разматывая результат и используя известные формулы суммирования целых степеней, приходим к выводу, что функция роста пользователей асимптотически эквивалентна x³!
u(x) ~ x^3; e(x) = u(x) * E; e(x) ~ x^3; f(x) = f(x - 1) + e(x); f(x) ~ f(x - 1) + x^3; f(x) ~ x^3 + (x - 1)^3 + (x - 2)^3 + ... + 1^3; f(x) ~ x^4
Эпилог
Важно знать, что чем дальше вы уходите от исходных данных (и чем больше вы полагаетесь на моделирование), тем менее точным становится ваш результат. В сценарии роста пользователей мы не столкнулись, например, с оттоком пользователей и автообновлениями (или «Smart Content», когда документ может меняться автоматически в зависимости от каких-то триггеров). Вы должны найти компромисс между «чрезмерным моделированием» и «недооценкой». Но даже несмотря на то, что такие наблюдения не дают точных цифр, что более важно, они позволяют увидеть динамику использования системы и то, насколько хорошо идет бизнес.
О платформе API PandaDoc:
PandaDoc — это простое и масштабируемое все-в-одном API-решение для создания документов и захвата электронной подписи путем их встраивания в ваше приложение, веб-сайт или с помощью серверного API. У нас есть подробная документация, поэтому вы можете легко начать работу. Если вам нужна помощь или нужна экспертиза решения вашей бизнес-задачи, пишите нам сюда.