Всех приветствую! Я Артём Седых, работаю ведущим разработчиком команды банковского сопровождения. Наш основной сервис существует 8 лет, команда насчитывает 39 человек, бэкенд представляет собой монолит на PHP и Ext JS на фронте, также есть ряд микросервисов на разных фреймворках PHP. По функционалу есть большая интерфейсная база и много разнообразных интеграций с внешними системами. Постепенно модернизируем сервисы, переходим на обновленный технологический стек, однако от легаси на живом проекте никуда не деться и необходимо поддерживать всю старую инфраструктуру, одновременно внося изменения под новые бизнес-требования.
В данном наборе статей расскажу про наш опыт разработки и внедрения системы мониторинга бэкенда с возможностью просмотреть, как именно проседают экшены, и даже попытаться спрогнозировать их поведение. Довольно быстро получилось разработать и внедрить в проект инструмент мониторинга на базе микробиблиотеки SDK, ClickHouse и Grafana. Теперь можем не только ответить, что тормозит, но и заглянуть внутрь экшена и сказать, почему тормозит, когда отвалится, и даже давать первичные рекомендации по оптимизации экшена. Разработанный инструмент имеет большую гибкость и применим к любому проекту на PHP, а также при реализации микробиблиотеки – и на других языках бэкенда (и при необходимости не только бэкенда). Будет много примеров дашбордов и кейсов, которые удалось обнаружить и оптимизировать с помощью мониторинга.
Материал получился объемный, для комфортного прочтения и чтобы не забирать много вашего времени поделен на три части. Здесь, в первой части, расскажу о предпосылках внедрения инструмента анализа производительности на проекте, о типичных проблемах производительности, встречавшихся на наших проектах, о выборе подхода к сбору метрик для мониторинга, а также о самих метриках и принципу их отбора. В последующих немного поговорим о ClickHouse и как туда метрики отправляем, всё остальное – реальные примеры разработанных дашбордов Grafana на реальных данных с продакшена. В заключении оценим получившийся набор инструментов и попробуем сопоставить плюсы и минусы выбранного подхода. Приятного прочтения!
Вступление
В крупных веб-проектах с постоянной активностью пользователей необходимо мониторить систему в самых разных аспектах и уровнях. При выпуске продукта в продакшен в первую очередь, конечно, стоит мониторить железо: процессор, RAM, диски, сеть, а также базу данных и смежные подсистемы. Далее, когда с инфраструктурой всё нормально, бутылочным горлышком зачастую становится именно бэкенд и его экшены (API-эндпоинты): как хорошо написан код, какие алгоритмы используются, насколько эффективно экшен использует ресурсы. Особо важно отслеживать производительность системы, использовать методики SRE вроде контроля метрик SLI, а также показателя Apdex.
Если проект большой, может быть тяжело постоянно контролировать скорость работы каждого экшена на непродуктовых стендах. Сложно уследить за быстротой API на этапах доработки, ревью и теста, особенно при коллективной работе множества человек. Экшены часто дорабатываются, всё больше усложняя логику, увеличивая время отклика и вероятность ошибок. Здесь и далее под экшеном или эндпоинтом буду подразумевать не только конкретные точки API (хотя в основном именно их), но и любую часто повторяющуюся задачу в коде, которая имеет имя, свой алгоритм, начало и конец, которую можно и нужно отслеживать, например итерация воркера, работа скриптов по крону и обработка сообщения брокеров типа rabbitmq/kafka.
Одной из основных целей такого мониторинга в разрезе производительности – узнать об ухудшении времени отклика на уровне отдельных эндпоинтов. Получить информацию об отказе работы экшенов не в грозных письмах и звонках от пользователей, а автоматически и прозрачно. В идеале отловить момент оптимизации экшена до того, как на продакшене проблема всплывет остро. Заранее узнать, где, почему и что именно оптимизировать.
Почему было принято решение разработать новый собственный инструмент и использовать именно ClickHouse? Безусловно, в открытом доступе уже есть инструменты, помогающие собирать нужные метрики и отслеживать производительность:
- локальные, такие как Sentry и Pinba (PHP);
- облачные с поддержкой OpenTelemetry вроде Uptrace и highlight.io;
- самописные решения, часто реализуются в связке Prometheus + Grafana по готовым примерам.
На самом деле, больше в качестве эксперимента. Изначально хотели подключить именно pinba: с прошлых горящих моментов на продакшене накопилось много положительных впечатлений от возможности «заглянуть» внутрь рабочего процесса и выяснить узкие места, почти как в профайлерах. Но в процессе подключения возникли вопросы:
– Получится ли с минимальным влиянием на производительность держать включенной pinba на проде для всей кодовой базы на постоянной основе?
– Получится ли в случае чего безболезненно расширять метрики?
– Сможем ли копить логи месяцами и производить по ним аналитику?
Нужна была максимальная гибкость, возможность добавлять новые метрики и новые графики по мере необходимости. Контролировать объем, частоту и принципы сбора метрик. Добавлять кастомные сценарии для оповещений, в перспективе углубленный анализ нейронками. Поэтому решили попробовать разработать самописное решение с большим вдохновением пинбой.
После запуска первой версии мониторинга (назвали VESNA.Insight) моментально стало видно весь бэкенд, как на ладони, каждый аспект и каждый критерий мониторинга раскинут по дашбордам графаны. Наконец-то появилась возможность прозрачно увидеть узкие места и осознать, где именно проседает экшен. Начали получать запросы адаптировать инструмент для других команд: к мониторингу приглядываются devops, захотели подключиться и смежные проекты компании. Собрали много пожеланий по развитию инструмента, добавлению метрик, модификации аналитических панелей и алертов. В данный момент планируем заниматься универсальным решением с увеличенными гибкостью и скоростью подключения, добавлением большего количества языков в SDK по сбору и отправке метрик, автоматизацией настройки и сбором для популярных фреймворков.
Но что привело нас к необходимости мониторить экшены на восьмом году жизни проекта?
Предпосылки, проблема тяжелых экшенов и критических оптимизаций

За 8 лет жизни проекта имеем следующие показатели:
● Команда из 16 разработчиков, 8 аналитиков, 7 тестировщиков, 7 менеджеров поддержки + руководитель лаборатории.
● 30 тыс. ежемесячных пользователей, в среднем 5 тыс. из них ежедневно совершают множество действий.
● Суммарно 4,6 млн позиций контрактов, 7 млн параметров банковского сопровождения, 5 млн документов объемом 4 ТБ.
● Ежедневно по интеграции отправляется и принимается в среднем 20 тыс. тяжелых запросов (время обработки более 10 секунд).
● Более 1 тыс. различных экшенов, обслуживающих разные бизнес-цели с разной степенью сложности логики и объемов обрабатываемых данных.
Пользователи периодически сталкивались с проблемами разной степени критичности:
● Непредсказуемые зависания операций (подписание заявок, импорт данных, интеграционное взаимодействие), блокирующие работу критически важных пользователей и бизнес-процессов. Проблемы могли возникать у отдельных пользователей на специфичных наборах данных, что усложняет поиск узких мест во время ввода новых доработок в эксплуатацию.
● Ошибки таймаутов и проблемы с нехваткой оперативной памяти при работе с большим объемом данных. Низкая производительность определенных действий приводила к выраженному неудобству работы с системой по опросам пользователей.
● Компания и клиенты носили повышенные репутационные риски из-за срочных инцидентов, требующих время на экстренную оптимизацию кода в ущерб новым задачам.
● Новые задачи по бизнес-требованиям добавляли логику к существующим методам, тем самым замедляя уже и так медленно работающие эндпоинты.
● Рост объема данных и общего числа пользователей при отсутствии инструментов мониторинга производительности и прогнозирования нагрузки по конкретным действиям добавлял неопределенность и неуверенность в стабильной и качественной работе сервиса.
Как видно, у нас ни разу не highload-проект, однако сервис часто сталкивался с необходимостью критической оптимизации: то по одному экшену, то по другому. Зачастую источником проблем оказывался неоптимальный алгоритм в цепочке бизнес-логики, который сильно увеличивал время обработки запроса в случае, если пользователь работал с большим объемом данных по одной из вложенных сущностей. Раз в месяц случалась подобная история:
«Всё сломалось, ничего не могу сделать с заявкой, еще вчера надо было отправить, второй день пытаюсь, срочно помогите!» – пользователь.
Сайт открывается, сервис работает, проблема у конкретного пользователя:
– Ребята, подключайтесь, получится быстро поправить?
– Пока не понятно, надо смотреть, где именно тормозит и оптимизировать.
Приходилось выдергивать лучших разработчиков с их основных задач на такие оперативные оптимизации, причем не сразу видно, что именно пошло не так, где и при каких условиях нужна оптимизация. Каждый раз нужно анализировать контекст проблемы на продакшене, воспроизводить замедление работы, проходиться профайлингом и править код.
Попробуем разобрать причины появления таких тяжелых экшенов:

● Большой легаси-проект со множеством разнообразных экшенов (CRUD + много сущностей, логики, вычислений, интеграций). Некоторый процент экшенов можно отнести к медленным, серьезно провисающим на больших объемах данных.
● Много чувствительных пользователей, которые прилично могут нагрузить систему. Так как рабочий функционал побеждает время ожидания, пользователи терпят и не говорят о проблеме (а если говорят, жалобы теряются на фоне других оперативных задач).
● Вырисовывается ряд экшенов, которые могут у нас проседать по времени или памяти или даже существенно влиять на общую производительность системы (тяжелые экшены).
● Негативные впечатления о быстродействии и отказоустойчивости системы накапливаются. И в один момент происходит либо накладывание негативных факторов, либо нагрузка бОльшая, чем обычно, – и экшен в определенной ситуации становится невозможно использовать.
● Когда экшен перестает работать, пользователи жалуются и ниже оценивают работу сервиса, что приводит к ущербу репутации компании. В критичных ситуациях, когда «нужно еще вчера было отправить и подписать заявку», счет идет на часы.
● Система активно разрабатывается. Текущие экшены дополняются новой логикой, появляются новые экшены. Часто бывает, что разработчик, увлекшись задачей, добавляет новый функционал в существующий метод и не обращает внимания на то, что тем самым он довел быстродействие метода со среднего до тяжелого – в особенности этого не видно без нагрузочного теста.
Мониторинг по проекту показывал только логи и состояние железа в виде CPU/RAM/сеть. Если проблема не на всем сервере, а у конкретных пользователей на конкретном наборе данных – не особо поможет разобраться в этом. Отдельно живет мониторинг запросов в БД с помощью pgbadger, совсем медленные или часто повторяющиеся запросы покажет. Это нормальный источник оптимизационных задач, но не очень прозрачный, при большой кодовой базе запросы могли вызываться из многих мест и при множестве условий. Не хватало инструмента, который бы показал с точки зрения пользователя производительность по отдельным действиям на бэкенде и смог соотнести на конкретные причины в коде для внедрения оптимизации.
Выбор решения
Логично появляется желание заранее узнать о надвигающейся проблеме и исправить ее до того, как ситуация станет критической.
Решение в идеальном случае – автоматические нагрузочное тестирование на каждый экшен. Преимущество – можно заметить и исправить проблему до вывода задачи в продуктив. Недостаток – каждый тест необходимо разрабатывать отдельно, что существенно увеличит время вывода задач. Каждый релиз должен прогоняться через пайплайн нагрузочного тестирования, что может задерживать сроки вывода при возникновении проблем. Такие тесты лучше писать на ряд самых критичных и часто вызываемых действий пользователей.
Если предварительно узнать о проблеме не можем, остается только отслеживать состояние экшенов на продуктиве (мониторинг и методы анализа).
Внедрение мониторинга
Попробуем разобрать по этапам внедрение мониторинга в проект. Задача – собирать метрики по экшенам в статистику, по которой отслеживать слабые места для закладывания оптимизации.
При решении задачи внедрения мониторинга в проект нужно ответить на несколько вопросов:
- Как собираем статистику?
- Что именно собираем, какие метрики?
- Где складываем статистику для хранения?
- Как мониторить и проводить анализ собранных данных?
Метод сбора метрик
При выборе метода сбора статистики представим себе эмпирическую (и субъективную для проекта) градацию некоторых инструментов на картинке:

Слева – мы можем вообще не вмешиваться работу системы и сканировать логи веб-сервера типа nginx. Это идеальный вариант, если не хочется / нельзя дополнительно нагружать сервис. Но и получаем ограниченный набор метрик: время события, название эндпоинта, статус ответа и длительность. Этого может быть достаточно для базового мониторинга производительности. Но если можем позволить себе собрать больше, стоит попробовать.
Справа – подключение полноценного профайлера со снятием статуса в каждой строчке кода. Это незаменимая вещь при оптимизации конкретной проблемы. Видно полностью весь процесс и каждый показатель, как на ладони. Но за исключением особых случаев, профайлинг на проде не включают – проводят исследование на локальной машине, предварительно воспроизведя проблему. Хранящиеся при этом объемы кажутся лишними: для нашей задачи мониторинга и анализа производительности тысячи экшенов у тысяч ежедневных пользователей в течение месяцев – столько отчетов хранить будет негде и особо незачем.
Как нечто среднее изначально и хотелось выбрать pinba, а после анализа специфики и требований по гибкости принял решение разработать собственный микроинструмент для конкретной задачи и максимальной кастомизации. Такой инструмент может быть реализован с помощью кастомного счетчика в PHP, например статического класса, вызывая методы которого будет собираться вся нужная информация. В конце, после передачи ответа клиенту, класс будет отправлять данные в хранилище. На первый взгляд, ничего сложного, беремся за дело!
О функционале Sentry Insights разузнал после, этот мегакомбайн действительно дает много информации, и кое-что похоже на наши получившиеся дашборды в графане. В планах провести сравнительный анализ, возможно, подошло бы под наши требования. Круто, если Sentry тоже поддерживает кастомный доступ в БД, добавление своих метрик и при этом не хранит много избыточной для беглого анализа производительности информации.
Выбор метрик

Классическим наглядным разделением метрик можно считать USE и RED.
В USE замеряем использование ресурса в %, насыщение и количество ошибок. Здесь к серверу относимся как к белому ящику и замеряем показатели внутри железа, поэтому его однозначно стоит применить к мониторингу по железу. Такой набор о производительности отдельных действий может сказать мало.
В RED как раз мониторим запросы, представляет собой идею мониторинга черного ящика. Смотрим на узлы системы и наблюдаем, сколько пришло запросов, какой процент из них вернул ошибку, сколько время запрос отрабатывал.
Оба набора полезны, зачастую при мониторинге производительности веб-сервиса объединяют оба подхода для более полного отображения, что происходит с веб-сервисом.
В нашем случае получилась вариация метода RED с насыщением метриками, полезными для мониторинга и анализа именно экшенов. Это уже больше чем черный ящик: с выбранным нами подходом к сбору метрик можем проявить фантазию и извлечь дополнительную информацию, полезную для мониторинга производительности.
Попробуем классифицировать метрики, которые можно получить изнутри экшена бэкенда:

IDENT – метрика, позволяющая определить отдельный экшен и отличить его от других. В первую очередь время и название экшена. Далее может быть любая идентифицирующая информация: теги (тип операции), различные идентификаторы пользователя и его роли – всё что поможет определить и найти нужные кейсы применения экшенов. Самые основные метрики, по которым будет выполняться поиск и строиться мониторинг (название экшена и время события), положим в составной индекс БД, а по остальным обычно сможем построить доп. фильтрацию или группировку (в зависимости от типа анализа).
INFO – внутренняя информация по событию, которая может сказать что-либо о производительности и принести пользу при анализе экшена:
- Single – одиночная информация, например сколько длилось событие, код/статус ответа, потребление памяти, список параметров и т. п.
- Count – т. н. счетчик – повторяющееся действие, количество которого можно отследить, например количество успешных/неуспешных обращений к кэшу или отправленных сообщений в брокеры а-ля rabbitMQ. При этом понимаем, что нам неважно, сколько это действие заняло времени.
- Duration – один из самых полезных типов метрик. Это любая операция, влияющая на время выполнения экшена. Для метрик данного типа отслеживаем общее количество операций и общую длительность их выполнения. Желательно собрать информацию о самых затратных операциях. Это в первую очередь обращения к внешним сервисам по HTTP, выполнение запросов БД, обращения к кэшу, операции с файлами, время на обслуживание ORM. Получив по два числа для каждой метрики типа Duration, сможем понять «внутрянку» экшена и попытаться разобраться в причинах его замедления.
По Duration может возникнуть вопрос: почему бы, помимо общего числа и длительности, не сделать, как в pinba, и не хранить отдельно факт каждой операции, каждую длительность и, возможно, контекст (например, в какой строчке кода вызвана операция – какой именно запрос SQL был выполнен?). На постоянной основе эта информация не нужна. Анализировать сложно, мониторить еще сложнее. И база данных спасибо не скажет за миллионы событий экшенов, у которых могут быть десятки тысяч внутренних операций. Однако вопрос валиден, и даже стоит оставить возможность хранить эту ценную информацию, например с возможностью включения по условию для избранных экшенов.
Проанализировав контекст проекта и определив наиболее влияющие на производительность метрики, решили оставить данный набор:
IDENT:
● environment – среда выполнения (dev, test, preprod, production);
● start_datetime – дата и время начала вызова действия;
● request_method – HTTP-метод действия;
● action_group_tag – тэг (тип операции – get_info/get_list/save/delete/sign/import/export/files/integration);
● controller_name – контроллер в представлении MVC;
● action_name – экшен в представлении MVC;
● user_id – ид пользователя;
● bs_owner – активная роль в системе.
INFO Single:
● duration_s – длительность действия;
● success – успешность действия;
● parameters – параметры запроса (нужна фильтрация чувствительных данных либо на таких экшенах отключаем сбор параметров);
● max_memory_mb – результат вызова функции memory_get_peak_usage, память виртуальная у fpm, но общее представление о потреблении ресурса выдаст;
● data – дополнительные данные, которые могут пригодиться для анализа.
INFO Duration:
● total_sql_queries, duration_sql_s – общее число и длительность запросов к БД;
● total_ext_requests, duration_external_s – общее число и длительность запросов к внешним системам по HTTP.
С метриками и методом сбора разобрались, попробуем представить, как будут выглядеть наш статичный класс и процесс его подключения в существующий проект на PHP.

Так выглядит процесс подключения системы сбора и отправки метрик (в виде SDK) в любой проект – несколько статичных функций, вызов которых нужно встроить в пайплайн экшена на разных этапах. Для каждой метрики типа Duration нужно подключиться к соответствующей операции и в стиле pinba залогировать ее время.
По результатам замеров профайлером на локальной машине, добавление сбора статистики влияет на скорость ответов по экшенам совсем немного: 3 мс – инициализация, чтение конфига из файла и автоматический сбор плюс по 5 нс на каждый вызов типа Duration (БД, внешние запросы). На продуктивной машине будет еще быстрее. Отправку метрик в подсчет не берем, т. к. выполняется после ответа пользователю (pinba-style), при этом методы отправки в хранилище могут отличаться по времени и надежности.
Конец первой части
На этом вынужден остановиться, чтобы не задерживать вас слишком надолго. Это первая статья из трех планируемых. Во второй части расскажу, как выбирали метод отправки метрик, почему именно ClickHouse в качестве БД, а также самое интересное – покажу несколько важных дашбордов Grafana которые получилось разработать первыми. По каждому дашборду будут реальные примеры с прода, какие проблемы удалось обнаружить и какие рекомендации по оптимизации можно получить с каждого элемента.
Пока же в комментариях предлагаю поделиться впечатлениями по теме, очень приветствуются свои истории медленных и тяжелых запросов, расскажите, как решали проблемы производительности на проектах? Какие инструменты используете, какие мысли по поводу выбранного здесь подхода? Есть ли метрики, которые особенно хотелось бы отслеживать у себя? Большое спасибо за внимание! Жду вас в следующий четверг на второй части =)