Ответ
Разработка и оптимизация высоконагруженного микросервиса для обработки финансовых транзакций.
Результаты:
- Обработка пиковой нагрузки 10 000 RPS с P99 latency < 50 мс.
- Сокращение GC pauses на 70% за счёт оптимизации аллокаций и настройки JVM.
- Сервис вошёл в топ-3 по производительности в компании.
Пример технической оптимизации (снижение нагрузки на GC):
Была проблема с частым созданием строк в «горячем» пути (hot path):
// Было: новая строка на каждый вызов
String formatTransaction(Transaction t) {
return t.getId() + "|" + t.getAmount(); // Создаёт StringBuilder и String
}
Решение — повторное использование StringBuilder через ThreadLocal:
// Стало: переиспользуемый буфер
private static final ThreadLocal<StringBuilder> TL_BUILDER =
ThreadLocal.withInitial(() -> new StringBuilder(64));
String formatTransactionOptimized(Transaction t) {
StringBuilder sb = TL_BUILDER.get();
sb.setLength(0); // «Очистка» буфера
sb.append(t.getId()).append("|").append(t.getAmount());
return sb.toString();
}
Итог: Снижение количества аллокаций в критическом участке кода на 40%, что напрямую уменьшило частоту и продолжительность сборок мусора.
Ответ 18+ 🔞
А, ну вот, смотри, приходит ко мне однажды задачка — типа, сделай нам микросервис, чтобы он транзакции финансовые гонял. Ну, думаю, очередная хрень какая-то, но ладно.
А потом открываю требования по нагрузке — десять тысяч запросов в секунду, и чтобы 99 процентов из них летели быстрее, чем я успеваю сказать «ёпта». Задержка меньше 50 миллисекунд. Я посмотрел на это и подумал: «Ну, пиздец, сейчас опять начнётся».
И началось, блядь. Первый прототип запустили — он как начал тормозить! Сборщик мусора (GC, короче) там такой плясал, что казалось, будто сервис не транзакции обрабатывает, а в танцевальном марафоне участвует. Паузы по полсекунды, всё встаёт колом. Клиенты, ясное дело, сразу в истерику: «Где наши деньги, сука?».
Стал копаться. Смотрю в профилировщик, а там, в самом горячем участке кода, где каждая наносекунда на счету, какая-то дичь творится. На каждый чих создаётся новая строка. Представляешь? Каждую транзакцию надо в лог запихнуть, типа id|amount. И код был вот такой, простой как три копейки:
String formatTransaction(Transaction t) {
return t.getId() + "|" + t.getAmount();
}
А под капотом-то что? Java же для каждого плюсика новый StringBuilder создаёт, потом в строку конвертирует. И так десять тысяч раз в секунду! Это ж овердохуища мусора, ёпта! Сборщик просто с ума сходит, не успевает убирать этот бардак.
И тут меня осенило, как гром среди ясного неба. Надо эту ерунду остановить. А что если... взять один StringBuilder на поток и переиспользовать его, как хитрая жопа? Чтоб не создавать новый каждый раз, а просто старый почистить и заново заполнить.
Сделал я вот такую штуку:
private static final ThreadLocal<StringBuilder> TL_BUFFER =
ThreadLocal.withInitial(() -> new StringBuilder(64));
String formatTransactionOptimized(Transaction t) {
StringBuilder sb = TL_BUFFER.get();
sb.setLength(0); // Просто обнуляем, типа "стираем" старое
sb.append(t.getId()).append("|").append(t.getAmount());
return sb.toString();
}
Смотри, в чём фокус: для каждого потока создаётся свой личный StringBuilder один раз и хранится в ThreadLocal. Когда нужно строку сформировать — достаём его, обнуляем длину (это быстро, он внутри массив символов не пересоздаёт), пихаем туда новые данные и возвращаем строку. И никаких лишних аллокаций в цикле!
Запустил, замерил — мать его, так и есть. Количество аллокаций в том самом адском месте упало на 40 процентов, блядь! Сборщик мусора успокоился, как удав. Паузы сократились на 70%, представляешь? Сервис перестал икать и пошёл как по маслу.
В итоге этот наш сервис не только нагрузку вытянул, но ещё и в топ-3 по скорости по компании залетел. А я сижу, смотрю на графики и думаю: «Вот же мудя, иногда решение лежит на поверхности, нужно просто перестать тупо складывать строки через плюсик и включить голову». Чих-пых тебя в сраку, оптимизация!