Ответ
Контекст: В высоконагруженном микросервисе обработки заказов возникла проблема периодических падений производительности и OutOfMemoryError в продакшене.
Анализ проблемы:
- С помощью профилировщика (VisualVM) и анализа дампов памяти выяснилось, что причиной является неограниченный рост in-memory кэша на основе
ConcurrentHashMap, хранившего историю сессий пользователей. - Объекты сессий никогда не удалялись из кэша, так как на них сохранялись ссылки из долгоживущих объектов.
Принятое решение и реализация:
Было решено заменить кэш на реализацию с TTL (Time-To-Live) и LRU (Least Recently Used) политикой вытеснения, используя Caffeine — высокопроизводительную библиотеку кэширования.
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class SessionCache {
private final Cache<String, UserSession> cache;
public SessionCache() {
this.cache = Caffeine.newBuilder()
// Максимальный размер кэша
.maximumSize(10_000)
// Время жизни записи после создания
.expireAfterWrite(30, TimeUnit.MINUTES)
// Время жизни записи после последнего доступа
.expireAfterAccess(10, TimeUnit.MINUTES)
// Слабая ссылка на значения (позволяет GC удалить объект, если нет других ссылок)
.weakValues()
// Слушатель для отладки удаления
.removalListener((key, value, cause) ->
log.debug("Removed key: {} due to {}", key, cause))
.build();
}
public UserSession getSession(String sessionId) {
return cache.getIfPresent(sessionId);
}
public void putSession(String sessionId, UserSession session) {
cache.put(sessionId, session);
}
}
Результат:
- Потребление память стабилизировалось.
- Частота сборок мусора (GC) снизилась на ~40%.
- Производительность сервиса перестала деградировать со временем.
- Решение было задокументировано и внедрено в другие сервисы команды как стандартный паттерн для кэширования временных данных.
Ответ 18+ 🔞
Да ты посмотри, что эти горе-архитекторы учудили! Сидит такой микросервис, тихо-мирно заказы обрабатывает, а потом — бац! — и накрылся медным тазом с OutOfMemoryError. Ну, думаю, ёперный театр, опять кто-то память жрёт, как не в себя.
Начинаем ковыряться. Берём VisualVM, снимаем дампы, а там — овердохуища сессий пользовательских в одной мапе сидит! В ConcurrentHashMap, блядь! И сидят они там, как на ПМЖ, до скончания времён. Никто их оттуда не выгоняет, потому что на них из других мест ссылки болтаются. Короче, классика: «закэшировали и забыли». Пиздец, а не кэш.
И что делать? А ничего умнее человечество не придумало, как выкидывать старое и ненужное. Берём библиотечку Caffeine — она, сука, умная, сама всё почистит.
Вот смотри, как я эту хитрую жопу сделал:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class SessionCache {
private final Cache<String, UserSession> cache;
public SessionCache() {
this.cache = Caffeine.newBuilder()
// Больше десяти тысяч сессий не держим, а то опять овердохуища будет
.maximumSize(10_000)
// Создал сессию — и через 30 минут, хоть тресни, она сдохнет
.expireAfterWrite(30, TimeUnit.MINUTES)
// А если тупо лежит и никто её не трогает — через 10 минут нахуй
.expireAfterAccess(10, TimeUnit.MINUTES)
// Значения держим на слабых ссылках, чтоб GC мог прибить, если совсем зассал
.weakValues()
// А это я для отладки впилил, чтоб видеть, кто и за что попал под раздачу
.removalListener((key, value, cause) ->
log.debug("Removed key: {} due to {}", key, cause))
.build();
}
public UserSession getSession(String sessionId) {
return cache.getIfPresent(sessionId);
}
public void putSession(String sessionId, UserSession session) {
cache.put(sessionId, session);
}
}
Суть в чём? Всё просто, как три копейки. Не хочешь, чтобы сервис лёг — не держи в памяти то, что уже никому не уперлось. Пусть живёт 30 минут после создания или 10 минут после последнего тыка. Места больше 10k записей — начинаем выпиливать самые старые и невостребованные. И всё это, блядь, работает асинхронно и быстро, не тормозит основные потоки.
И что вы думаете? Помогло! Память перестала расти, как на дрожжах. Сборщик мусора успокоился, стал реже бегать — нагрузка упала на 40%, представляешь? Сервис перестал постепенно превращаться в тыкву. Теперь эту схему по всей команде внедрили как стандарт. А то ведь, блядь, без TTL и LRU кэшируют — волнение ебать, терпения ноль ебать.