Kora на практике: что показал реальный бенчмарк JVM-фреймворков¶
26 сентября 2024
Эта статья — письменное сопровождение доклада, в котором разработчик без предварительного опыта работы с Kora сравнил Kora с несколькими знакомыми JVM-фреймворками. В ней разбираются дизайн бенчмарка, результаты и архитектурные уроки, которые раскрыл эксперимент.
Kora, вероятно, была одним из наименее знакомых фреймворков, обсуждавшихся в докладе. Она зародилась внутри Т-Банка и позже была выпущена публично под организацией Kora Projects. На первый взгляд она может выглядеть как ещё один JVM-бэкенд-фреймворк, конкурирующий в уже переполненном пространстве. Интересна не сама по себе факт существования ещё одного фреймворка, а дизайнерские решения за ним и то, что произошло, когда эти решения проверили на нескольких более знакомых альтернативах.
Докладчик подошёл к Kora почти без предварительного практического опыта. Это делает результат особенно интересным: это был не бенчмарк, созданный давним специалистом по Kora, потратившим месяцы на настройку фреймворка. Это был практический эксперимент, в котором один и тот же сервис был реализован на нескольких JVM-технологиях, а затем измерен под одной и той же нагрузкой.
В итоге Kora показала самый сильный результат в этом конкретном бенчмарке.
Но самая полезная часть доклада была не просто в том, что Kora заняла первое место. Эксперимент также вскрыл, почему фреймворк ведёт себя иначе, какие компромиссы он делает, где экосистема всё ещё меньше, как потребление ресурсов соотносилось со Spring Boot WebFlux и почему бенчмаркинг существующей системы перед переписыванием по меньшей мере так же важен, как бенчмаркинг замены.
Что делает Kora другой¶
Центральная архитектурная идея в том, что Kora старается перенести работу фреймворка из времени выполнения в компиляцию.
Вместо сильной опоры на рефлексию времени выполнения, динамическое обнаружение или большие объёмы логики контейнера при запуске, Kora использует обработку аннотаций и генерацию кода. Разработчики описывают контроллеры, репозитории, маппинги, отношения зависимостей и другие заботы фреймворка в исходном коде, а требуемая реализация генерируется во время сборки приложения.
Концептуально модель проста:
Исходный код приложения
↓
аннотации
↓
обработка на этапе компиляции
↓
сгенерированный код
↓
скомпилированное приложение
Получившееся время выполнения не нуждается в повторном обнаружении структуры приложения при каждом запуске процесса.
Докладчик также подчеркнул второй дизайнерский выбор: Kora старается держать абстракции фреймворка относительно тонкими. Вместо оборачивания каждой нижележащей технологии в несколько специфичных для фреймворка слоёв, она остаётся сравнительно близко к фактическим клиентам и библиотекам, используемым приложением.
Практическое следствие в том, что сервис на Kora может вести себя гораздо ближе к вручную собранному JVM-сервису, чем фреймворки с более тяжёлой инфраструктурой времени выполнения.
Эти два свойства — генерация на этапе компиляции и относительно тонкие абстракции — образуют фундамент истории производительности, показанной позже в бенчмарке.
Явное, а не магическое¶
Модель программирования Kora более явная, чем та, к которой привыкли многие разработчики Spring.
Докладчик отмечает, что аннотации приходится размещать в большем числе мест, потому что процессорам Kora нужно достаточно информации на этапе компиляции, чтобы корректно сгенерировать итоговое приложение. Во фреймворках, которые сильнее полагаются на обнаружение во время выполнения или конвенции, часть этой информации может оставаться неявной.
Это различие поначалу может ощущаться слегка многословным.
Польза в том, что структура приложения становится более предсказуемой. Меньше скрытого поведения фреймворка ждёт времени выполнения, чтобы решить, что означает компонент или как он должен быть соединён.
Оценка докладчика по сути заключалась в том, что подход разумен, даже если, приходя из Spring, дополнительные явные аннотации иногда могут слегка раздражать.
Kora поддерживает и Java, и Kotlin, так что одна и та же общая модель фреймворка может использоваться на любом из языков.
Доступ к данным: сгенерированные репозитории, но SQL остаётся видимым¶
Слой доступа к данным следует той же философии.
Kora использует JDBC, а не заменяет доступ к реляционной базе данных тяжёлой ORM-моделью. Она может генерировать реализации репозиториев и маппинги сущностей, но SQL остаётся явным.
Это делает понятие репозитория знакомым, не скрывая фактическое взаимодействие с базой данных.
Поток примерно таков:
Фреймворк предоставляет макросы для сокращения повторяющихся фрагментов SQL, например когда иначе пришлось бы повторять длинные списки колонок или имена таблиц. Но эти удобства не превращают слой репозитория в ORM.
Это значит, что часть SQL всё равно приходится писать.
Докладчик рассматривает это как разумный компромисс: Kora убирает шаблонный код, но сохраняет фактический контракт базы данных видимым.
Точный результат бенчмарка Kora¶
Реализация на Kora показала самые сильные цифры в бенчмарке.
Измеренный результат был:
| Метрика | Kora |
|---|---|
| Средняя пропускная способность | 3729 RPS |
| Среднее время ответа | 19.62 ms |
| Медианное время ответа | 19.82 ms |
| P95 время ответа | 37.76 ms |
Эти цифры поставили Kora на первое место в итоговой отсортированной таблице.
Ключевой момент — не только пропускная способность. Kora также сохранила лучшие или почти лучшие характеристики задержки в том же тесте.
Полная таблица результатов, показанная в докладе, была:
| Вариант | Среднее, ms | Медиана, ms | P95, ms | RPS |
|---|---|---|---|---|
| Kora | 19.62 | 19.82 | 37.76 | 3729 |
| Spring Boot WebFlux | 22.13 | 22.76 | 40.80 | 3305 |
| Spring Boot Undertow | 30.60 | 31.50 | 55.73 | 2393 |
| Spring Boot Tomcat + VT | 31.44 | 30.13 | 71.68 | 2330 |
| Исходный сервис | 35.82 | 34.64 | 70.27 | 2044 |
| Helidon | 49.01 | 50.03 | 89.05 | 1496 |
| Ktor | 279.36 | 280.45 | 537.38 | 263 |
Эта таблица информативнее простого утверждения «Kora была самой быстрой».
Spring Boot WebFlux на самом деле был очень близок. Его средняя задержка отставала от Kora лишь на несколько миллисекунд, его P95 был близок, а пропускная способность достигла 3305 RPS.
Более значимые различия проявились, когда в расчёт приняли потребление ресурсов.
Kora и WebFlux были двумя явными лидерами¶
Докладчик выделил Kora и Spring Boot WebFlux как двух самых сильных исполнителей и сравнил их поведение во времени.
На протяжении прогонов бенчмарка оба сохраняли относительно стабильную пропускную способность после прогрева системы. Kora в целом оставалась выше WebFlux, а их поведение задержки следовало похожей общей форме.
Это важно, потому что бенчмарк был не просто одним мгновенным измерением. Графики показывали поведение на протяжении всего прогона, включая запуск, прогрев, активную нагрузку, паузу и ещё один интервал нагрузки.
Преимущество Kora было видно рано и в целом оставалось устойчивым.
Точные значения, естественно, колебались от прогона к прогону, но относительный порядок оставался достаточно похожим, чтобы докладчик счёл результат значимым, а не разовым всплеском.
Потребление ресурсов сделало разницу более интересной¶
Самое большое различие между Kora и WebFlux проявилось в потреблении CPU.
Слайд со сравнением использования ресурсов утверждает, что в этом тесте Kora была примерно на 50% более эффективна по CPU.
Spring Boot WebFlux провёл большую часть активного периода бенчмарка вблизи очень высокого использования CPU, тогда как Kora оставалась существенно ниже. Kora поначалу показала короткий всплеск использования, но затем устоялась примерно на среднем уровне, а не оставалась вблизи полного насыщения CPU.
Важен не точный процент в каждую секунду прогона. Важно то, что Kora обеспечила более высокую пропускную способность, используя при этом заметно меньше CPU.
Это меняет интерпретацию бенчмарка.
Результат вроде:
уже интересен.
Результат вроде:
операционно значительно значимее.
Графики кучи также показали разное поведение. Обе реализации демонстрировали ожидаемые циклы аллокации и сборки мусора, но WebFlux в целом работал с более высоким объёмом памяти, тогда как Kora оставалась ниже.
Докладчик также заметил, что Spring Boot использовал примерно несколько сотен мегабайт даже в основном в простое, тогда как у Kora базовый уровень был меньше.
Опять же, бенчмарк не следует интерпретировать как универсальный закон памяти для любого из фреймворков. Поведение кучи зависит от кода приложения, конфигурации GC, опций JVM, формы трафика и зависимостей. Но в рамках этого эксперимента ресурсный профиль Kora был явно лучше.
Исходный сервис оказался лучше ожидаемого¶
Один из самых важных моментов доклада наступил после сравнения фреймворков.
Докладчик решил измерить исходный сервис.
Это, вероятно, следовало сделать до переписывания.
Исходный сервис показал:
| Метрика | Исходный сервис |
|---|---|
| Средняя пропускная способность | 2044 RPS |
| Среднее время ответа | 35.82 ms |
| Медианное время ответа | 34.64 ms |
| P95 время ответа | 70.27 ms |
Эти результаты не были исключительными, но были далеки от катастрофических.
Исходная реализация расположилась примерно в середине итоговой таблицы.
Это было критическое наблюдение, потому что оно изменило контекст переписывания. Новая реализация на Kora была значительно быстрее, но старое приложение, возможно, уже было достаточным для нагрузки, которую на самом деле требовал бизнес.
Урок не в том, что переписывание было обязательно ошибкой.
Урок в том, что исходную систему следовало измерить в первую очередь.
Прежде чем заменять работающий сервис, потому что ожидается, что другая технология быстрее, команда должна знать, действительно ли производительность является ограничивающим фактором и насколько велико улучшение на самом деле требуется.
Реальный результат был ещё более впечатляющим¶
Один из финальных слайдов добавляет важную деталь о продакшне.
Сервис, переписанный на Kora, в итоге заменил исходный сервис, и, по словам докладчика, в реальных условиях эксплуатации он показал примерно 5-кратное улучшение.
В устном комментарии он несколько уточняет эту цифру: после накопления большей статистики эффективное улучшение, возможно, было ближе к примерно трём разам, а не ровно к пяти.
Это уточнение важно.
Слайд фиксирует главный итог проекта, тогда как комментарий докладчика отражает более консервативный взгляд после дополнительных наблюдений.
Более широкий вывод остаётся неизменным: переписывание на Kora дало существенное улучшение в реальном мире, а не только победу в синтетическом бенчмарке.
Но точный множитель следует рассматривать как зависящий от конкретной нагрузки, а не как общее утверждение о производительности Kora.
Меньшая экосистема была реальным ограничением¶
Kora не была безупречна в эксперименте.
Докладчик столкнулся с отсутствующей интеграцией авторизации.
Вместо готового модуля для точного случая ему пришлось использовать внешнюю библиотеку и написать часть интеграционного кода вручную. Это включало создание необходимого перехватчика и поведения валидации.
Ключевой момент в том, что проблема была неудобной, а не блокирующей.
По словам докладчика, дополнительная работа заняла примерно полчаса.
Этот опыт демонстрирует обе стороны меньшей экосистемы фреймворка.
Минус очевиден:
Плюс в том, что модель фреймворка Kora достаточно явная, чтобы добавление недостающей интеграции не обязательно требовало глубокого погружения во внутренности фреймворка.
Это важное различие.
Фреймворку не нужен готовый адаптер для всего, если добавление недостающего остаётся дешёвым.
Документации было достаточно для новичка¶
Ещё одна интересная деталь в том, что докладчик не был опытным пользователем Kora.
Он фактически впервые столкнулся с Kora, работая над этой реализацией бенчмарка.
Несмотря на это, документации и примеров из репозитория было достаточно, чтобы собрать сервис и решить проблему недостающей интеграции.
Это полезный практический сигнал.
Высокопроизводительный фреймворк гораздо менее привлекателен, если достижение этой производительности требует лет специфичной для фреймворка экспертизы.
В этом случае докладчик смог понять из документации достаточно, чтобы создать самую сильную реализацию в своём сравнении.
Это усиливает более широкое преимущество Kora: большая часть поведения фреймворка явна и генерируется на этапе компиляции, а не скрыта за конвенциями времени выполнения.
Реактивное программирование всё ещё было чрезвычайно конкурентоспособным¶
Ещё один интересный вывод из бенчмарка в том, что реактивное программирование было далеко не устаревшим.
Spring Boot WebFlux занял второе место:
- 3305 RPS;
- 22.13 ms средняя задержка;
- 22.76 ms медиана;
- 40.80 ms P95.
Этот результат был значительно лучше протестированных вариантов Spring Boot Undertow и Tomcat + Virtual Threads.
Так что хотя виртуальные потоки чрезвычайно упрощают синхронный код приложения, этот эксперимент не показал, что они немедленно превосходят устоявшийся реактивный стек.
Докладчик шутит, что виртуальные потоки, возможно, со временем догонят через несколько лет по мере дальнейшего улучшения JVM.
Серьёзный вывод проще:
Не предполагайте, что модель конкурентности победила только потому, что так говорит отраслевой нарратив.
Измеряйте нагрузку.
Почему рейтинг не следует обобщать¶
Докладчик осторожно оговаривает результат.
Kora выиграла этот бенчмарк.
Это не значит, что Kora выиграет каждый JVM-бенчмарк.
Рейтинг могли изменить разные параметры:
- другая нагрузка на базу данных;
- другая сериализация;
- другая конкурентность;
- другой HTTP-стек;
- другие опции JVM;
- другая конфигурация пула соединений;
- больше времени на настройку;
- другая машина;
- нагрузка, ограниченная CPU, а не I/O.
Рейтинг — это свидетельство, а не универсальный закон.
Эта оговорка особенно важна, потому что публичные бенчмарки фреймворков часто неправильно используются как архитектурные решения.
Фреймворк может доминировать в TechEmpower или другом синтетическом бенчмарке и всё равно быть неправильным выбором для конкретного приложения.
И наоборот, фреймворк, который не доминирует в публичном рейтинге, может отлично работать под фактической бизнес-нагрузкой.
Самый ценный вывод был методологическим¶
Финальные слайды делают более широкий вывод докладчика явным.
Если позволяют время и ресурсы, командам следует попробовать несколько технологий, прежде чем остановиться на одной.
Что ещё важнее, им следует измерить существующее решение до его переписывания.
Процесс принятия решения в идеале должен быть таким:
Существующий сервис
↓
измерить текущее поведение
↓
выявить фактическое узкое место
↓
определить репрезентативную нагрузку
↓
создать прототипы реалистичных альтернатив
↓
измерить пропускную способность
↓
измерить задержку
↓
измерить CPU и память
↓
сравнить стоимость реализации
↓
выбрать
Плохая последовательность такая:
прочитать бенчмарк
↓
предположить, что технология X быстрее
↓
переписать сервис
↓
измерить старый сервис задним числом
Докладчик прямо признаёт, что анализ существующего решения был проведён слишком поздно.
Это, вероятно, самый переносимый урок во всём докладе.
Что этот бенчмарк говорит о Kora¶
В рамках ограничений этого эксперимента Kora продемонстрировала несколько заметных свойств.
Во-первых, генерация кода на этапе компиляции может создать полнофункциональный фреймворк без несения больших затрат времени выполнения.
Во-вторых, тонкие абстракции могут сохранять знакомые технологии, такие как JDBC, всё ещё убирая повторяющийся шаблонный код фреймворка.
В-третьих, меньшая экосистема не обязательно делает фреймворк трудным для расширения.
В-четвёртых, Kora можно выучить достаточно быстро, чтобы разработчик с небольшим предварительным опытом смог создать серьёзную реализацию для сравнения.
В-пятых, самый сильный результат был не только в пропускной способности. Kora объединила:
самый высокий RPS
+
самую низкую среднюю задержку
+
самую низкую медианную задержку
+
самый низкий P95
+
гораздо меньшее потребление CPU
среди сравниваемых вариантов.
Именно эта комбинация делает результат интересным.
Заключение¶
Kora вошла в доклад как один из менее известных фреймворков и покинула бенчмарк на вершине таблицы.
Её измеренный результат был:
Spring Boot WebFlux был близок по задержке и пропускной способности, но Kora потребляла в тесте существенно меньше CPU. Сравнение ресурсов представило Kora как примерно на 50% более эффективную по CPU под измеренной нагрузкой.
Исходный сервис, тем временем, оказался гораздо здоровее, чем ожидалось, при 2044 RPS, со средней задержкой 35.82 ms и P95 70.27 ms. Этот вывод стал полезным напоминанием, что существующую систему следует измерять до начала переписывания.
Продакшн-результат после внедрения Kora также был существенным: докладчик сообщает о нескольких-кратном ускорении в реальных условиях, предупреждая при этом, что точный множитель зависит от того, какой период и накопленная статистика используются.
Так что самый сильный вывод — не просто «Kora — самый быстрый JVM-фреймворк».
Он точнее этого.
Дизайн Kora на этапе компиляции, тонкие абстракции, ориентированный на JDBC доступ к данным, низкие накладные расходы времени выполнения и эффективность ресурсов сделали её самым сильным исполнителем в этом конкретном реалистичном сравнении. Она также доказала, что достаточно доступна, чтобы разработчик с очень небольшим предварительным опытом Kora смог успешно построить и расширить её.
Поэтому финальный урок доклада касается и Kora, и инженерной дисциплины:
Не выбирайте фреймворк, потому что бенчмарк говорит, что он должен быть быстрым. Постройте репрезентативную реализацию, измерьте систему, которая у вас уже есть, сравните несколько реалистичных альтернатив и позвольте вашей собственной нагрузке решить.
В этом эксперименте такой процесс привёл к Kora.