Работаю сейчас в довольно крупной компании на позиции ведущего разработчика с ролью TL. Занимаюсь разработкой сервиса, который в обозримом будущем станет принимать приличную нагрузку. И по договоренностям с клиентами время ответа (HTTP) нашего сервиса должно быть не более 65мс.

Когда я пришел в компанию в июне 2022 года, время ответа уже составляло примерно 50мс при нагрузке в пике около 80 RPS. Стек на тот момент: Java 11 (Spring MVC) + PostgreSQL + Apache Ignite в качестве кэша.

Проблема is coming

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

Принятие неизбежного

Стадия 1: отрицание

Не верилось, что прекрасно работающий сервис вот так просто стал тормозить. Поэтому совместно с командой DevOps решили пройтись по верхам и проверить состояние виртуалки, на которой крутился сервис. В итоге мы лишь убедились, что с ней все в порядке: памяти хватает, запас по ЦПУ еще есть, да и вообще работает как часы.

Стадия 2: гнев

Не получив удовлетворительного результата проверки виртуалки, пришла пора узнать, как чувствует себя БД. Но этот вариант был маловероятным: она крутилась на той же виртуалке, что и сервис, поэтому проблем с коннектом быть не должно. Да и БД использовалась нечасто, потому что основной кусок информации лежал в кэше. И само собой, с БД все оказалось в порядке: пул коннектов относительно пустой, ничего не висит, транзакции выполняются нормально.

Стадия 3: торг

Поняв, что хороводы вокруг "околосервисного пространства" результата не дали, было решено дать нагрузку на сервис на тестовом окружении, подозревая, что проблема там всплывет, и получится ее оперативно решить. Но, увы, нагрузка показала вполне приличный результат: ответ около 60мс при 3000 RPS. Выходило, что проблема воспроизводится только на продакшене, поэтому что? Правильно! Дебаг прода! Что может быть лучше?

Стадия 4: депрессия

Потратив довольно большое количество времени на танцы с бубном, я был готов уже бросить все эти ваши АйТи и уйти в проститутки монастырь. В этот момент появилась идея, что, возможно, не хватает выделенной памяти самому приложению (все вот эти настройки Xmx и Xms и т.д.). Накинули памяти, но результат остался прежним. В довесок включил логи GC (сборщик мусора), чтобы посмотреть, как он работает (могла быть ситуация, что он не успевает вычищать память). И на всякий случай воткнул G1 GC. Логи показали, что GC работает штатно, просадок в сборке мусора нет, запускается нечасто - в общем никаких нареканий.

Стадия 5: принятие

"Дорогой дневник! Прошло уже несколько недель в попытках реанимировать сервис. К этому моменту время ответа уже составляет 140мс и продолжает расти. Все ресурсы исчерпаны. PO устал отвечать клиентам, что вот-вот починим. Второй разработчик пишет завещание. Тестировщики бьются в истерике. Команда DevOps'ов перестала отвечать на сообщения. Кажется, это с нами надолго. Пора смириться и принять неизбежное...".

Неочевидное решение

В одну прекрасную воскресную ночь мою голову посетила чудеснейшая идея: почему бы не сохранить данные в памяти приложения, отказавшись от внешнего кэша. Это оставалась единственная часть, которая не подвергалась проверке, поскольку никто и не подозревал, что в Ignite может скрываться древнее зло.

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

Отключаю Apache Ignite. В рамках проверки пихаю данные в HashMap. Начинаю заливать сборку на прод... Эти несколько минут деплоя, казалось, растянулись в года. Сервис запустился, запросы пошли, время ответа... 2мс!

Это был успех! Нет! Это был ошеломительный успех в 5 утра. В тот момент думал, что теперь-то для меня нет ничего невозможного. Я - повелитель Java. Я - виртуоз отладки. Я - отписался в чат и пошел спать.

Тайна кэша

В чем же была причина такого поведения? Если честно, конкретного ответа у меня нет до сих пор. Только какие-то отрывки информации. Например, никто из команды DevOps не смог мне сказать, где вообще физически находится инстанс Apache Ignite. В конфигах указаны только "рядовые" урлы по типу "/ignite" - никаких IP адресов, никаких полноценных URL'ов - ничего.

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

Эпилог

Естественно, использование HashMap в качестве кэша было чисто экспериментальным решением, и на следующий день был подключен In-memory кэш EHCache. Данная конфигурация отлично работает по сей день (скоро переедем на Redis) и держит нагрузку в 5000 RPS со временем ответа 30мс.