Николай Ельцов Performance & Reliability Engineer

Кейсы

Восемь работ по одному порядку: проблема, анализ, решения, проверка. Заказчиков, внутренние компоненты продуктов и абсолютные цифры не называю — только роль, метод, инструмент и результат.

01

Платформа API-безопасности · 2025–2026

Утечка памяти в Go-компоненте

Проблема

Компонент распределённой инсталляции в Kubernetes неограниченно рос по памяти под нагрузкой и получал OOM kill по достижении лимита. Воспроизвести можно было только в кластере: развернуть многокомпонентный стенд, подать нагрузку, ждать. Цикл проверки — часы, поэтому гипотезы разработки проверялись медленно и по одной.

Анализ

Свернул кластерную инсталляцию в локальный docker-compose, сохранив то, что влияет на поведение памяти: лимиты контейнеров и переменные окружения аллокатора. Критерий воспроизведения задал измеримо и заранее — превышение лимита за считанные минуты под нагрузкой, а не «вроде растёт». Нагрузку подал профилем с крупными телами запросов, на них утечка и проявлялась. Дальше профили кучи через pprof, снятые в динамике и сопоставленные между собой: так отделяется «компонент много аллоцирует» от «компонент не отдаёт».

Решения

Свёл профиль к конкретному месту в коде и отдал владельцу компонента отчёт с точечными рекомендациями по этому месту, а не «поищите утечку». Правку вносил владелец кода — это правильное разделение: воспроизведение, локализация и подтверждение мои.

Проверка

Ретест на том же стенде тем же профилем, сравнение до и после по тому же критерию. Цикл проверки гипотез сократился с часов на кластере до минут на ноутбуке, а репро-стенд остался активом: регрессию по этому месту можно проверить в любой момент.

02

Платформа API-безопасности · 2025–2026

Генерация больших тел с атаками внутри

Проблема

Нужен был профиль нагрузки, которого не даёт ни один готовый инструмент: крупные тела запросов в разных форматах — JSON, XML, multipart, form-urlencoded — с атакующими payload внутри тела, генерируемые на лету. Статика не подходит принципиально: она кешируется и на стороне генератора, и на стороне тестируемой системы. В лоб k6 такой профиль не тянул: память генератора росла неограниченно, стенд уходил в своп, до целевого RPS тест не доживал.

Анализ

Причина не в объёме данных, а в том, где собираются тела: сборка шла внутри итерации, а в k6 каждый VU — отдельный JS-рантайм со своей кучей, поэтому расход множился на число VU.

Решения

Перенёс сборку в пул предсобранных тел в разделяемой между VU структуре, итерация берёт следующий элемент по кругу; отдельные пулы под каждую комбинацию формата, числа атак и размера тела. Из горячего пути убрал аллокации: пулы готовых строк, преаллоцированные массивы, сборка конкатенацией в один проход, свой быстрый генератор идентификаторов. Тело добивается до заданного размера с сохранением валидности формата — иначе система отбросит запрос на парсинге и тест измерит не то.

Проверка

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

03

Платформа API-безопасности · 2025–2026

Подбор инстансов под класс нагрузки

Проблема

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

Анализ

Перебрать типы инстансов руками нельзя: ручная сборка одного стенда съедает день. Автоматизировал сборку целиком — окружение, тестируемая конфигурация, backend-заглушки и генератор нагрузки одной командой. Зафиксировал один профиль нагрузки на все конфигурации, иначе сравнение бессмысленно, и прогнал матрицу «тип инстанса × профиль», снимая CPU, память, сеть и метрики отказа в каждой точке.

Решения

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

Проверка

На некоторых типах трафика ARM-инстансы дали двукратный выигрыш по CPU при той же нагрузке — разница, которую по традиции не замечали. Сама процедура осталась воспроизводимой: повторный прогон матрицы — запуск пайплайна, поэтому пересчитать можно на новой версии продукта или на новом семействе инстансов.

04

Платформа API-безопасности · 2025–2026

Capacity planning вместо экспертной оценки

Проблема

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

Анализ

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

Решения

Ridge-регрессия с кросс-валидацией: данных и признаков мало, зависимость близка к линейной, сложная модель переобучилась бы на шуме. Для параметров, которых заказчик обычно не знает, заложил значения по умолчанию из медианных соотношений выборки — он задаёт то, что знает. Предсказание не только среднего, но и перцентилей утилизации: планировать надо по хвосту, иначе система ляжет на пике. Сверху веб-инструмент расчёта CPU и RAM, чтобы пресейл и внедрение считали сами, без обращения к перф-команде.

Проверка

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

05

Платформа API-безопасности · 2025–2026

Нагрузочное тестирование как сервис

Проблема

Тесты запускались через CI: чтобы прогнать сценарий, инженер правил переменные пайплайна и помнил наизусть их комбинации. Для всех, кроме двух-трёх человек, это был чёрный ящик, а результаты жили в артефактах джоб.

Анализ

Сложное здесь не веб-интерфейс. Первое — честный пересчёт между полосой и RPS через фактические размеры тел и заголовков: заказчик думает в гигабитах, инструмент в запросах в секунду, и перевод должен быть точным, иначе тест мерит не тот профиль. Второе — доказательство, что сгенерированный сценарий действительно даёт заданную нагрузку, а не только синтаксически корректен.

Решения

Генератор сценариев: по заданным параметрам — распределение методов по долям, заголовки, форматы и размеры тел, формы подачи нагрузки (плато, ступени, линейный рост, спайки), каталог атакующих payload с внедрением в тело нужного формата — собирается самодостаточный k6-скрипт, готовый к запуску и ручной правке; VU и итерации считаются из RPS. Валидация в три уровня: статические проверки, реальный старт скрипта на бэкенде и полная проверка подачей на эмулятор со сверкой фактического RPS с заданным в допуске ±1%, плюс фоновая перепроверка в простое, чтобы протухшие сценарии не всплывали в нужный момент. Рядом сервис запуска на Go и React: очередь задач, разворачивание конфигурации в Kubernetes через Helm, стрим логов по WebSocket, история прогонов в PostgreSQL.

Проверка

Сервис работает в проде как внутренний инструмент команды. Сценарии версионируются, переиспользуются и не протухают незаметно, а результаты прогонов стали структурированными данными — именно они легли в основу модели из кейса 04.

06

Промышленная платформа в Kubernetes · 2022–2025

Delete pod: readiness probe, которая ничего не проверяла

Проблема

Платформа ставится заказчику в Kubernetes, поды перезапускаются при каждом обновлении — нужно было знать, что видит клиент в это окно.

Анализ

Удалял поды сервисов и воркеров во время прогона и смотрел ошибки и latency в окне перезапуска. Один из сервисов продолжал отдавать 5xx уже после того, как кластер считал под готовым: readiness probe проверяла только открытый порт, а не готовность зависимостей и прогрев кеша, и трафик приходил раньше времени. Рядом нашлась вторая дыра: drain без PodDisruptionBudget снимал реплики одновременно, поэтому в момент обновления сервис мог остаться вообще без живых экземпляров.

Решения

Probe переключили на реальный health endpoint. В Helm-чарт добавили PodDisruptionBudget с minAvailable, равным 1.

Проверка

Повторный прогон с теми же воздействиями: окно с 5xx после перезапуска закрылось, drain больше не оставлял сервис без живых реплик. Оба сценария остались в релизном прогоне: перезапуск и обновление кластера проверяются под нагрузкой, а не при обновлении у заказчика.

07

Промышленная платформа в Kubernetes · 2022–2025

Failover PostgreSQL под нагрузкой

Проблема

Заказчику нужен был RTO по базе — не из документации, а замеренный под нагрузкой.

Анализ

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

Решения

Рекомендации ушли на сторону приложения: работа с пулом соединений и переподключение после смены мастера, чтобы приложение не держало мёртвые соединения дольше, чем длится сам фейловер. В протокол для заказчика пошли фактическое окно и число потерянных операций, а не кластерное время переключения: заявлять RTO надо по тому, что видит клиент.

Проверка

Повторный failover под той же нагрузкой — с замером по тому же критерию, чтобы видеть эффект правок. Сценарий остался в методике испытаний: RTO базы теперь не декларация, а замер, повторяемый на любой инсталляции.

08

Промышленная платформа в Kubernetes · 2022–2025

Приёмка не закрывалась, пока тормозила сцена

Проблема

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

Анализ

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

Решения

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

Проверка

Прогон на профиле заказчика после переделки хранения — по тем же критериям, что были согласованы до теста. Протокол испытаний закрыл требование к акту. Регулярный прогон здесь был не формальностью: этот функционал переезжал с ORM на ручные запросы постепенно, и каждый шаг переезда надо было сверять с предыдущим замером — иначе одна переписанная выборка могла тихо вернуть тормоза на следующем релизе.