Блог

Как один тяжёлый чат мог уронить наш API

28 сентября, примерно через 17 минут после релиза, оба наших продакшен API-сервера одновременно закончили память, перезапустились, и примерно пять минут балансировщик нагрузки не имел ни одного исправного сервера, куда мог бы направить трафик. Наше первое исправление выделило API вдвое больше памяти. Настоящая причина нашлась позже в тот же день: открытие одного конкретного чата заставляло API загрузить 53 МБ сообщений одним запросом. Теперь история чата загружается страницами, ограниченными по размеру, а также по количеству, и этот чат открывается с первой страницей в 5 МБ вместо падения.

Что упало

Наш API работает как контейнер на каждом из двух серверов за балансировщиком нагрузки. Каждому контейнеру был задан лимит памяти 384 МиБ. В покое он использует 130–140 МиБ, так что лимит выглядел как большой запас.

Оба контейнера одновременно достигли лимита и упали с сообщением «JavaScript heap out of memory». Куча — это память, которую использует процесс Node.js для объектов, с которыми работает. Поскольку оба сервера упали одновременно, некуда было направлять запросы. Для схемы с двумя серверами это худший вид отказа: резервирование помогает только тогда, когда сбои не происходят одновременно.

Первое исправление: больше места

Сроки указывали на деплой. После релиза ходы агента, которые деплой прервал, возобновляются и заново сохраняют свои стенограммы, и этот всплеск работы выглядел очевидным источником скачка памяти.

Поэтому первое изменение подняло лимит API с 384 МиБ до 768 МиБ. Мы применили его вживую на обоих серверах в тот же день и закоммитили в конфигурацию деплоя, чтобы следующий деплой незаметно не вернул 384. Исправление, сделанное вручную на сервере, который следующий деплой откатывает, — частый способ, которым та же авария возвращается.

Больше памяти было разумным запасом. Но это не было объяснением.

Настоящая причина: один запрос на 71 МБ

Позже в тот же день мы воспроизвели падение на копии одного чата с прежним лимитом памяти продакшена. Копия сама по себе уронила API без всякого деплоя.

Когда вы открываете проект, веб-приложение запрашивает у API историю чата. Эндпоинт возвращал последние 30 сообщений целиком. Для большинства чатов 30 сообщений — это немного. Для этого чата последние 30 сообщений занимали 53 МБ в базе данных и давали ответ в 71 МБ. Несколько ответов агента были по 4–10 МБ каждый, в основном стенограммы субагентов: вспомогательных агентов, которых главный агент запускает для параллельной работы над частью задачи, и полная работа которых сохраняется вместе с ответом.

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

«30 сообщений» никогда не было настоящим ограничением по размеру. Это было ограничение по количеству, которое для обычных чатов случайно оказывалось небольшим.

Исправление: страницы, ограниченные по размеру и по количеству

Страница истории теперь состоит из 30 сообщений или примерно 4 МБ, в зависимости от того, что наступит раньше.

Это работает в два шага:

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

Два правила делают это безопасным:

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

Мы нашли ещё одно место с той же формой. Когда вы останавливаете ход, API проверяет последние несколько ответов, чтобы понять, не пустые ли какие-то из них и не нужно ли их очистить. Для этого он загружал восемь сообщений целиком. В чате с ответами по несколько мегабайт это та же проблема в меньшем виде. Теперь он задаёт базе данных вопрос «да или нет» — есть ли в этом сообщении хоть какое-то содержимое? — не загружая само содержимое.

Как мы проверили

На копии чата, при старом лимите 384 МиБ:

  • Первая страница — 5 МБ. Раньше тот же запрос ронял API.
  • Пять вкладок браузера, открывающих чат одновременно, пикируют на 154 МБ памяти.
  • Прокрутка назад через весь чат возвращает все 190 сообщений, каждое ровно один раз и по порядку.

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

Что мы из этого вынесли

  • Количество — не ограничение размера. Любой эндпоинт, который возвращает «последние N» чего-то, что может расти без предела, нуждается и в бюджете байтов. Данные рано или поздно найдут этот пробел.
  • Очевидный подозреваемый не всегда причина. Деплой был настоящим триггером для дополнительной работы, и больше памяти стоило иметь. Но падение воспроизводилось без деплоя, и без воспроизведения мы остановились бы на увеличении памяти.
  • Два сервера падают вместе, когда получают один и тот же запрос. Резервирование защищает от гибели одного сервера. Оно не защищает от запроса, который убивает любой сервер, до которого доходит, особенно если пользователь естественным образом повторит его, обновив страницу.
  • Исправляйте в конфигурации, а не только на машине. Увеличение памяти попало в конфигурацию деплоя в тот же день, поэтому следующий деплой его сохранил.

Это вторая наша недавняя история о памяти, связанная с выводом агента. Первой была метка статуса, которая поставила в очередь 690 МБ строк. Если вы используете субагентов в Mythex, о том, как они работают, можно почитать в документации.

Mythex · Pricing · Создавайте что угодно с ИИ · Шаблоны сайтов и приложений · Сравнения Mythex · Глоссарий по созданию приложений с ИИ · Бесплатные инструменты для создания приложений с ИИ · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)