Работаю сейчас в довольно крупной компании на позиции ведущего разработчика с ролью 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мс.
Комментарии (14)
zubrbonasus
12.10.2023 13:35Для чего вам БД, если все данные храните в кеше? Может быть отказаться от реляционной бд и хранить все данные в редис?
quorcs Автор
12.10.2023 13:35В БД хранятся данные долгосрочные, в кеше - те, которые, скажем так, в активном использовании.
zubrbonasus
12.10.2023 13:35Реляционная бд отдаёт инфу достаточно быстро, для того, чтобы использовать её непосредственно из приложения не задумываясь про дополнительный кэш. Конечно если нет ошибок проектирования. Поэтому я задал свой вопрос.
quorcs Автор
12.10.2023 13:35Безусловно РБД инфу отдает быстро, особенно если создать правильные индексы и т.д.. В среднем запрос к БД (для получения данных, которые сейчас лежат в кеше) происходит за 80-100мс. В целом результат норм, но поскольку верхний порог - 65мс, то приходится искать другие пути.
zubrbonasus
12.10.2023 13:35Рсубд может отдавать данные за 15 мкс. Nginx с php, может отдать json построенный на данных бд, за 30 мкс. Важна верная конфигурация рсубд, оптимальная структура таблицы и оптимальный запрос.
murkin-kot
12.10.2023 13:35-1Уже в который раз наблюдаю статью на тему "как мы криво напроектировали, а потом героически снизили время отклика в разы".
Чисто для развлечения - стоит посчитать, сколько ещё юных самородков напишут подобные статьи за ближайший год? Делайте ставки, господа!
quorcs Автор
12.10.2023 13:35+1Не стоит забывать, что сервис может достаться "в наследство" вместе со всеми своими болячками.
murkin-kot
12.10.2023 13:35Согласен. Правда ошибки проектирования тоже не стоит игнорировать, даже будучи белым и пушистым. И ещё красивее было бы, если причастный к косякам всё осознал и показал свою историю в назидание потомкам.
quorcs Автор
12.10.2023 13:35К сожалению, причастные к тому моменту либо уже уволились, либо ушли на другие проекты)
thevudi
12.10.2023 13:35Наверное классно, когда все разработки - это новые проекты. Большинству же приходится работать с тем, что есть.
l4rover
12.10.2023 13:35Почему бы не развернуть играйт прямо в приложении? На старте скачивать из БД данные в кэш
Andrey_Solomatin
Трейсинг туда должне втыкаться на ура (добавлением одной зависимости, поднятем одного типового сервера для хранения трейсов, ну и адрес прописать в когфигах).
Трейсинг для запросов в базу идёт из коробки. В клиенте для кэша возможно тоже уже всё есть. Но если нет, по по трейсу быстро найдёте места, где тратится время и лекго туда добавите трассирование.
quorcs Автор
Вы правы)
Скоро будет другая статья с очередными приключениями, там как раз трейсинг помог