На коротком автоматическом прогоне в 130 сообщений картина хорошая:
То есть на обычном сценарии сообщение появляется у отправителя практически мгновенно: p95 до optimistic render — меньше 100 ms.
Ручной stress-тест
Потом отдельно погонял ручной stress-тест: 2009 отправок в реальном браузере.
На этом уровне backend не развалился: все пойманные отправки получили HTTP 200.
Общие метрики ручного stress-теста
Деградация по мере роста количества сообщений
На длинной активной сессии деградация уже видна:
Самое интересное: сильнее всего растёт не только backend response time, а задержка до самого API request. Это указывает, что основной bottleneck появляется на клиенте: длинный список сообщений, React render/DOM, обработчики состояния, возможно IndexedDB/local state и отсутствие полноценной virtualization для длинной истории.
Короткий вывод
Интерфейс на обычном сценарии отзывчивый: десятки миллисекунд до optimistic render.
Backend на stress smoke тоже не развалился: 2009 отправок, 2009 успешных HTTP 200, без network failures.
Но при агрессивном long-chat stress на тысячах сообщений уже видно, где следующий инженерный шаг:
• virtualization списка сообщений; • изоляция input-компонента от rerender списка; • batching локальных updates; • оптимизация обработки статусов/echo/realtime-событий; • отдельный профайлинг decrypt/render длинной истории.
Важно: это локальный smoke/stress, а не production SLA. Ручной recorder в этом прогоне надёжно мерил send path до API 200; DOM render для длинной ручной сессии я буду мерить отдельным тестом открытия/скролла истории на 1000/5000/10000 сообщений.
Привет! Спасибо за подробный разбор. Такой комментарий действительно полезен: по обычному демо в браузере часть подобных проблем легко не заметить, а при чтении кода они становятся видны сразу.
По нескольким пунктам я с вами согласен.
Замечание про N+1 в уведомлениях при обновлении профиля справедливое. В старом варианте логика действительно могла идти через цепочку “найти чаты пользователя → для каждого чата найти участников → для каждого участника найти пользователя → отправить событие”. Для небольшого демо это незаметно, но как паттерн для роста нагрузки такой подход неудачный.
То же самое с markChatAsRead(). В исходном варианте метод делал слишком много за один вызов: получал список сообщений, проходил по ним в цикле, обновлял receipt/status и отправлял отдельные события. На длинной истории это действительно плохой путь, особенно если чат активно используется.
После вашего комментария я отдельно прошёлся по backend и переработал эти участки.
Что было изменено:
— уведомления при обновлении профиля и обновлении списка чатов переведены на batch-запросы и отправку по уникальным получателям; — mark read / mark delivered переписаны через bulk SQL-операции вместо поштучного прохода по сообщениям; — вместо множества отдельных status events добавлен bulk status event; — timeline reactions теперь грузятся пачкой, без N+1 на каждое сообщение; — список чатов получил пагинацию и сортировку ближе к БД; — часть логики вынесена из контроллеров в сервисы; — Map<String, Object> в ключевых REST-ответах заменён на typed DTO; — добавлены индексы под горячие запросы через миграцию V23__performance_indexes.sql; — после рефакторинга обновлены тесты.
Что было → стало
Отдельно прогнал тесты и нагрузочные сценарии по direct-chat path. Backend сейчас проходит 192 теста. По k6 direct-chat battery получилось:
Суммарно по direct-chat battery:
Самым важным результатом для меня было не просто отсутствие ошибок, а то, что mark read / mark delivered не начали деградировать на длинном прогоне. В 30-минутном soak:
При этом я не хочу выдавать эти результаты за полную production-готовность. Это пока проверка именно direct-chat HTTP/API-сценария. Групповые чаты, WebSocket load, сценарии с заранее набитыми 10k+ сообщений, поведение БД на больших объёмах и инфраструктурные лимиты ещё нужно проверять отдельно.
Поэтому я бы сейчас аккуратно сформулировал статус проекта так: это не production-ready мессенджер, но уже рабочий MVP/pet project, который постепенно приводится к более зрелому backend-состоянию.
Ваш комментарий помог подсветить места, которые на обычном демо не бросались в глаза. Спасибо за это. Если будет желание — буду рад продолжить предметно по query patterns, pagination strategy, receipt aggregation и group chat fanout.
Прогнал локальный stress smoke, чтобы не отвечать «на глаз».
Сценарий был довольно жёсткий: большое количество отправок в реальном браузере, не напрямую через API. То есть путь был настоящий:
UI → frontend state/update → штатный send path приложения → backend → API response
Короткий автоматический прогон
На коротком автоматическом прогоне в 130 сообщений картина хорошая:
То есть на обычном сценарии сообщение появляется у отправителя практически мгновенно: p95 до optimistic render — меньше 100 ms.
Ручной stress-тест
Потом отдельно погонял ручной stress-тест: 2009 отправок в реальном браузере.
На этом уровне backend не развалился: все пойманные отправки получили HTTP 200.
Общие метрики ручного stress-теста
Деградация по мере роста количества сообщений
На длинной активной сессии деградация уже видна:
Самое интересное: сильнее всего растёт не только backend response time, а задержка до самого API request. Это указывает, что основной bottleneck появляется на клиенте: длинный список сообщений, React render/DOM, обработчики состояния, возможно IndexedDB/local state и отсутствие полноценной virtualization для длинной истории.
Короткий вывод
Интерфейс на обычном сценарии отзывчивый: десятки миллисекунд до optimistic render.
Backend на stress smoke тоже не развалился: 2009 отправок, 2009 успешных HTTP 200, без network failures.
Но при агрессивном long-chat stress на тысячах сообщений уже видно, где следующий инженерный шаг:
• virtualization списка сообщений;
• изоляция input-компонента от rerender списка;
• batching локальных updates;
• оптимизация обработки статусов/echo/realtime-событий;
• отдельный профайлинг decrypt/render длинной истории.
Важно: это локальный smoke/stress, а не production SLA. Ручной recorder в этом прогоне надёжно мерил send path до API 200; DOM render для длинной ручной сессии я буду мерить отдельным тестом открытия/скролла истории на 1000/5000/10000 сообщений.
Доброй ночи! ответ, на ваш комментарий ниже, мучался с разметкой, не туда написал)
Возможно, местами получилось слишком гладко. Учту, спасибо.
Чем ближе закат империи, тем безумнее её законы.
Дополнительно проверили WebSocket‑часть: на локальном стенде соединения держатся стабильно, а предел дальше упирается уже в ресурсы машины.
Привет! Спасибо за подробный разбор. Такой комментарий действительно полезен: по обычному демо в браузере часть подобных проблем легко не заметить, а при чтении кода они становятся видны сразу.
По нескольким пунктам я с вами согласен.
Замечание про N+1 в уведомлениях при обновлении профиля справедливое. В старом варианте логика действительно могла идти через цепочку “найти чаты пользователя → для каждого чата найти участников → для каждого участника найти пользователя → отправить событие”. Для небольшого демо это незаметно, но как паттерн для роста нагрузки такой подход неудачный.
То же самое с
markChatAsRead(). В исходном варианте метод делал слишком много за один вызов: получал список сообщений, проходил по ним в цикле, обновлял receipt/status и отправлял отдельные события. На длинной истории это действительно плохой путь, особенно если чат активно используется.После вашего комментария я отдельно прошёлся по backend и переработал эти участки.
Что было изменено:
— уведомления при обновлении профиля и обновлении списка чатов переведены на batch-запросы и отправку по уникальным получателям;
—
mark read/mark deliveredпереписаны через bulk SQL-операции вместо поштучного прохода по сообщениям;— вместо множества отдельных status events добавлен bulk status event;
— timeline reactions теперь грузятся пачкой, без N+1 на каждое сообщение;
— список чатов получил пагинацию и сортировку ближе к БД;
— часть логики вынесена из контроллеров в сервисы;
—
Map<String, Object>в ключевых REST-ответах заменён на typed DTO;— добавлены индексы под горячие запросы через миграцию
V23__performance_indexes.sql;— после рефакторинга обновлены тесты.
Отдельно прогнал тесты и нагрузочные сценарии по direct-chat path. Backend сейчас проходит 192 теста. По k6 direct-chat battery получилось:
Самым важным результатом для меня было не просто отсутствие ошибок, а то, что
mark read/mark deliveredне начали деградировать на длинном прогоне. В 30-минутном soak:При этом я не хочу выдавать эти результаты за полную production-готовность. Это пока проверка именно direct-chat HTTP/API-сценария. Групповые чаты, WebSocket load, сценарии с заранее набитыми 10k+ сообщений, поведение БД на больших объёмах и инфраструктурные лимиты ещё нужно проверять отдельно.
Поэтому я бы сейчас аккуратно сформулировал статус проекта так: это не production-ready мессенджер, но уже рабочий MVP/pet project, который постепенно приводится к более зрелому backend-состоянию.
Ваш комментарий помог подсветить места, которые на обычном демо не бросались в глаза. Спасибо за это. Если будет желание — буду рад продолжить предметно по query patterns, pagination strategy, receipt aggregation и group chat fanout.
█████████████████████████████████▶ 75% начинаю удаление репозитория…
Спасибо!