Зачем была создана Kora: взгляд назад на архитектуру до 1.0, бенчмарки и идеи, которые дожили до современной Kora¶
6 марта 2023
Эта статья — письменное дополнение к докладу, прочитанному до выхода Kora 1.0. Она охватывает архитектуру до 1.0, ранние бенчмарки и идеи, которые дожили до современной Kora.
Этот доклад запечатлевает Kora в особенно интересный момент её истории. Это не презентация о зрелой линейке Kora 1.2.x и, конечно, не презентация о гораздо более развитом поколении Kora 2.0. Фреймворк, показанный здесь, — это предрелизная Kora, примерно эпохи 0.9 до 1.0.0, когда API всё ещё свободно менялись, обратная совместимость ещё не была обещана, несколько интеграций, которые позже исчезли или изменились, всё ещё были частью стека, а команда открыто экспериментировала с тем, чем фреймворк должен стать. Этот исторический контекст важен на протяжении всей статьи, потому что многие детали реализации в докладе больше не отражают текущую Kora, в то время как архитектурные принципы удивительно узнаваемы.
Презентация начинается не с самой Kora, а со Spring. Докладчик, инженер из команды Tinkoff Core Tech, явно признаёт, почему Spring стал таким успешным: он позволяет разработчикам быстро собирать бизнес-приложения, у него огромная экосистема, он активно развивается, и почти для каждой технологии есть существующая интеграция. Критика уже и техничнее. Команда работала с растущим числом небольших сервисов и становилась всё более чувствительной к стоимости запуска, объёму памяти, косвенности времени выполнения, динамическим прокси, генерации байткода, непрозрачной обвязке приложения и объёму работы, которую фреймворк выполняет до того, как будет достигнут собственный бизнес-код приложения.
Поэтому вопрос, стоявший за Kora, был не «Как нам изобрести новый Java-фреймворк?». Он был ближе к: можем ли мы сохранить продуктивность фреймворка, сделав время выполнения гораздо более похожим на код, который инженеры писали бы напрямую против Undertow, JDBC, Kafka или другой нижележащей библиотеки? Ранний ответ, видимый в этом докладе до 1.0, заключался в том, чтобы перенести обвязку зависимостей и повторяющуюся интеграционную работу в генерацию исходного кода на этапе компиляции, держать абстракции тонкими и делать результирующий код доступным для изучения.
Это базовое направление позже стало гораздо более отточенным. К поколению 1.2.x Kora уже накопила значительно больше интеграций, исправлений, поддержки тестирования, возможностей OpenAPI,
вариантов телеметрии, функций конфигурации и гораздо более стабильную экосистему, чем показано в этой презентации. В линейке 2.0 те же принципы были продвинуты дальше: Kora представляет более
консолидированную синхронную модель, использует виртуальные потоки во всём коде приложения, отвергает реактивные и suspend-контракты в модулях фреймворка, держит DI на этапе компиляции и
сгенерированный исходник как основные архитектурные механизмы и документирует фреймворк как гораздо более зрелую производственную платформу. Поэтому доклад следует читать как историю происхождения
этих идей, а не как снимок текущей Kora.
Spring решил проблему продуктивности, но удобство времени выполнения имеет цену¶
Докладчик начинает с намеренно сбалансированной позиции. Spring продуктивен, потому что убирает огромный объём ручной работы по композиции. Разработчик может очень быстро создать сервис с контроллером, доступом к базе данных, конфигурацией и бизнес-логикой, а Spring Boot делает это ещё проще, анализируя classpath и автоматически собирая то, что, по-видимому, нужно. Для многих команд это ровно правильный компромисс, и доклад никогда не утверждает обратного.
Озабоченность появляется, когда механика времени выполнения становится видимой операционно. Простой демонстрационный сервис, использованный в докладе, требовал примерно 192 МБ памяти только для запуска при сборке обычным путём Spring Boot. При ограничениях CPU запуск растягивался на десятки секунд. Эти цифры не были призваны доказать, что Spring нельзя оптимизировать ниже; докладчик явно использовал обычные модули Boot, а не агрессивно урезанную кастомную настройку. Поэтому бенчмарк измерял стоимость удобного пути по умолчанию, который разработчики, скорее всего, используют, — а именно это и волновало команду.
Spring Boot достигает большей части своего удобства через автоконфигурацию. Такие условия, как @ConditionalOnClass и @ConditionalOnMissingBean, позволяют фреймворку изучать окружение и решать,
какая конфигурация должна существовать. Концептуально система выполняет работу примерно так:
Classpath
↓
Чтение метаданных автоконфигурации
↓
Оценка условий
↓
Определение, какие конфигурации применяются
↓
Обнаружение / создание бинов
↓
Построение контекста приложения времени выполнения
Эта механика чрезвычайно полезна, но она где-то выполняется. Смысл докладчика был в том, что платформа микросервисов, запускающая множество относительно небольших приложений, платит эту цену многократно. Даже автоконфигурации, которые в итоге не активируются, всё равно участвуют в анализе classpath и метаданных. Та же озабоченность относится к проксированию времени выполнения и инфраструктуре, генерируемой из байткода вокруг транзакций, перехватчиков, репозиториев и другого поведения фреймворка. Каждый слой может быть по отдельности дешёвым, но результирующий путь вызовов может стать гораздо больше, чем бизнес-метод, который разработчики видят в исходнике.
Поэтому возражение в докладе не «Spring использует прокси, значит, Spring плох». Оно в том, что команда хотела более сильной связи между исходным кодом, который они видят, и кодом, который JVM реально выполняет. Они хотели меньше слоёв, существующих только во фреймворке, более раннюю валидацию, более быстрый запуск и меньший операционный след.
Обычный Spring улучшил цифры, но убрал главное удобство¶
Затем команда попробовала убрать автоконфигурацию Spring Boot, сохранив обычный Spring. Это был важный эксперимент, потому что он изолировал, какая часть стоимости запуска приходится на динамическое удобство Boot, а не на сам основной контейнер. Как только бины были зарегистрированы вручную и контекст приложения стал более явным, память и запуск существенно улучшились.
Но этот результат создал другую проблему. Если команде приходилось вручную собирать почти всё, они отказывались от главной причины использовать фреймворк. Приложение становилось более предсказуемым, но конфигурация — более трудоёмкой, и большая часть современной документации Spring была ориентирована на Spring Boot, а не на этот низкоуровневый стиль. Проксирование времени выполнения и поведение, ориентированное на байткод, также остались.
Этот эксперимент заострил цель. Команда не хотела «Spring без Spring Boot». Они хотели эргономику фреймворка со временем выполнения, гораздо более близким к написанной вручную Java.
Quarkus и Micronaut решили часть проблемы, но не всю¶
Quarkus и Micronaut естественно вошли в сравнение, потому что оба были спроектированы с учётом запуска и оптимизации на этапе сборки. По мнению докладчика, они были важными улучшениями по сравнению с обычными подходами, тяжёлыми по времени выполнения, но всё же не соответствовали точным проектным ограничениям, которые команда поставила перед собой. Команда хотела избежать не только медленного запуска, но и непрозрачного сгенерированного байткода и структур времени выполнения, которые разработчики не могли легко изучить.
Желаемые ограничения были ближе к этому:
Проверка графа на этапе компиляции
+
сгенерированный человекочитаемый исходник
+
минимальные абстракции времени выполнения
+
отсутствие зависимости от байткода времени выполнения
+
производительность, близкая к прямым библиотекам
Это одна из областей, где историческая природа презентации необходима. Доклад отражает конкурентный ландшафт и детали реализации того периода Kora до 1.0. Quarkus, Micronaut, Spring, JDK и сама Kora с тех пор существенно эволюционировали. Интересна не точная критика каждого фреймворка в тот год; интересен архитектурный критерий, который Kora выбирала для себя.
Реальный базовый уровень был написан вручную Undertow + JDBC¶
Вместо того чтобы считать другой фреймворк потолком производительности, команда создала базовый уровень вообще без фреймворка. Базовый уровень использовал Undertow и JDBC напрямую. Маршруты, SQL, сопоставления и сборка приложения были написаны вручную. Это дало команде точку отсчёта для того, что может сделать низкоабстракционный JVM-сервис, если игнорировать продуктивность разработчика.
Это дало полезную цель: Kora не нужно было превосходить написанный вручную код Undertow/JDBC. Ей нужно было подойти достаточно близко, чтобы команда могла сохранить внедрение зависимостей, сгенерированную HTTP-маршрутизацию, репозитории, конфигурацию, телеметрию, жизненный цикл и другие возможности фреймворка, не платя большой налог времени выполнения.
Ранние графики бенчмарков сравнивали базовый уровень с Micronaut, Quarkus и Spring. В окружении докладчика ручное приложение было явно впереди по пропускной способности и времени отклика, а также потребляло меньше ресурсов при эквивалентной нагрузке. Точные значения в тех ранних графиках менее важны, чем закономерность: чем больше механики фреймворка существовало вокруг одного и того же базового приложения, тем дальше результат уходил от базового уровня.
Примитивное представление соотношения пропускной способности, показанного на слайде, выглядит примерно так:
RPS, раннее сравнение с базовым уровнем
(выше — лучше; визуализировано приблизительно со слайда)
baseline-undertow-jdbc ██████████████████████████████ ~3000
quarkus-vertx-jdbc ███████████ ~1100
micronaut-async-jdbc ███████ ~650
spring-java-jdbc ████ ~350
Это не новый бенчмарк, реконструированный из сырых данных; это упрощённое визуальное резюме графика, показанного в презентации. Ключевой момент — расстояние между прямым базовым уровнем и реализациями фреймворков, которое и стало мотивацией для архитектуры Kora.
Почему ещё один фреймворк имел смысл в среде микросервисов¶
Аргумент докладчика становится гораздо сильнее в контексте микросервисов. Небольшой сервис может содержать очень мало бизнес-логики: получить HTTP-запрос, провалидировать модель, выполнить запрос, вызвать другой сервис, опубликовать сообщение и вернуть результат. Когда бизнес-путь короткий, относительная стоимость инициализации фреймворка и абстракций времени выполнения становится более заметной.
То же самое применимо в масштабе флота. Несколько десятков мегабайт дополнительной памяти тривиальны для одного процесса и значительны для сотен или тысяч реплик. Запуск, занимающий несколько секунд, может быть неважен для долгоживущего синглтона и важен для автоскейлинга, катящихся развёртываний, тестовых сред и быстро перепланируемых сервисов. Накладные расходы CPU, которые выглядят небольшими на одном экземпляре, могут стать большими при умножении на всю платформу.
Это дало первоначальную цель Kora:
Удобство фреймворка
↓
но компилировать структуру приложения заранее
↓
генерировать обычный исходник
↓
держать интеграции тонкими
↓
делать время выполнения похожим на прямое использование библиотек
Опять же, это была предрелизная Kora, примерно эпохи 0.9, а не более поздний фреймворк 1.2.x или 2.0. Значение доклада в том, что основная философия была уже видна, даже пока многие конкретные модули и API были ещё экспериментальными.
Бенчмарк с ограниченными ресурсами¶
Более важный бенчмарк в докладе использовал недавно написанный сервис в ограниченной среде. Условия, показанные на слайде, были:
Лимит RAM 384 МБ
Лимит CPU 0.5 CPU
Образ контейнера Тот же базовый образ
JDK Та же версия JDK
Трафик Чередующиеся POST / GET / DELETE
Инструмент нагрузки wrk
Это важно, потому что ограничения ресурсов делают накладные расходы фреймворка более заметными. При изобилии CPU и памяти различия в запуске и времени выполнения могут раствориться в неиспользуемой ёмкости. При половине CPU и нескольких сотнях мегабайт RAM поведение фреймворка становится частью рабочего конверта приложения.
Финальное сравнение включало несколько конфигураций Spring и Kora. Результаты были поразительными, потому что постепенное удаление инфраструктуры Spring приближало Spring к Kora, усиливая первоначальную гипотезу команды о том, что значительная часть разрыва приходится на механику фреймворка, а не на бизнес-код.
Память при запуске: самое наглядное различие¶
Минимальная память, необходимая для запуска, была указана как:
| Вариант | Минимальная память для запуска |
|---|---|
| Spring Boot | 192 МБ |
| Spring без JPA | 160 МБ |
| Spring без автоконфигурации | 128 МБ |
| Kora | 64 МБ |
Те же данные можно визуализировать очень просто:
Минимальная память, необходимая для запуска
(ниже — лучше)
Spring Boot ████████████████████████ 192 МБ
Spring без JPA ████████████████████ 160 МБ
Spring без автоконфигурации ████████████████ 128 МБ
Kora ████████ 64 МБ
Для сервиса в этом эксперименте Kora требовала одну треть памяти запуска обычной версии Spring Boot и половину памяти вручную упрощённой конфигурации Spring. Это был ровно тот результат, который волновал команду для небольших сервисов, потому что память, требуемая во время запуска, может быть выше памяти в установившемся состоянии и поэтому определяет минимальный лимит контейнера.
Стоит повторить, что эти значения описывают раннюю, предрелизную Kora, примерно поколение 0.9. Современную Kora не следует судить по этим точным историческим цифрам. Семейство 1.2.x впоследствии довело до зрелости реализацию, интеграционные модули, инструментарий и производственную поддержку, а Kora 2.0 развила модель гораздо дальше. Исторический результат полезен, потому что показывает, почему была выбрана архитектура, а не потому, что 64 МБ следует считать текущим универсальным требованием Kora.
Время запуска следовало той же закономерности¶
Указанное время запуска было:
| Вариант | Время запуска |
|---|---|
| Spring Boot | 16 с |
| Spring без JPA | 10 с |
| Spring без автоконфигурации | 3,5 с |
| Kora | 3,2 с |
ASCII-сравнение делает закономерность очевидной:
Время запуска
(ниже — лучше)
Spring Boot ████████████████ 16,0 с
Spring без JPA ██████████ 10,0 с
Spring без автоконфигурации ███▌ 3,5 с
Kora ███▏ 3,2 с
Интересное наблюдение не только в том, что Kora запускалась быстрее. Как только механика Spring Boot и JPA была снята и приложение было настроено вручную, Spring подошёл очень близко к Kora. Другими словами, эксперимент показал не то, что ядро Spring по своей природе неспособно к быстрому запуску. Он показал, что слои удобства имеют измеримую стоимость, и Kora попыталась предложить удобство через компиляцию, а не через обнаружение во время выполнения.
CPU в установившемся состоянии был гораздо ближе¶
При фиксированных 500 RPS потребление CPU было указано как:
| Вариант | CPU при 500 RPS |
|---|---|
| Spring Boot | 63% |
| Spring без JPA | 48% |
| Spring | 42% |
| Kora | 40% |
Потребление CPU при 500 RPS
(ниже — лучше)
Spring Boot ███████████████████████████████ 63%
Spring без JPA ████████████████████████ 48%
Spring █████████████████████ 42%
Kora ████████████████████ 40%
Это один из самых важных нюансов в докладе. Ранняя команда Kora не утверждала, что каждый запрос магически становится в несколько раз дешевле оптимизированного запроса Spring. При фиксированной установившейся нагрузке Kora и урезанный Spring были довольно близки. Основные выигрыши были в следе запуска, поведении запуска, максимальной пропускной способности и прозрачности того, как собрано приложение.
Это делает бенчмарк более правдоподобным, потому что история не «Kora выигрывает каждый показатель с огромным отрывом». Она «удаление механики времени выполнения меняет одни затраты драматически, а другие лишь умеренно».
Максимальная пропускная способность приблизила Kora к базовому уровню¶
Указанная максимальная пропускная способность была:
| Вариант | RPS |
|---|---|
| Spring Boot | 710 |
| Spring без JPA | 1034 |
| Spring | 1367 |
| Kora | 1657 |
Максимальная пропускная способность
(выше — лучше)
Spring Boot ███████████ 710
Spring без JPA ████████████████ 1034
Spring █████████████████████ 1367
Kora █████████████████████████ 1657
Среднее время отклика показало меньшее различие:
| Вариант | Среднее время отклика |
|---|---|
| Spring Boot | 2,39 мс |
| Spring без JPA | 2,21 мс |
| Spring | 1,79 мс |
| Kora | 1,70 мс |
Вывод был в том, что Kora подошла ближе к прямому базовому уровню, не требуя от команд отказаться от возможностей фреймворка. Она всё ещё не сравнялась с написанным вручную сервисом Undertow/JDBC, что и ожидалось. Целью был не нулевой налог абстракции; целью было сделать налог абстракции небольшим и видимым.
Ключевое решение: генерировать исходник, а не байткод времени выполнения¶
Самое важное решение реализации в докладе — использование Kora сгенерированного исходного кода. Команда намеренно спросила, почему фреймворк должен сам манипулировать байткодом, когда у Java и Kotlin уже есть компиляторы, предназначенные для превращения исходника в байткод. Вместо динамического построения приложения во время выполнения процессоры Kora генерировали обычный код во время сборки.
Модель была:
Аннотации / интерфейсы / модули
↓
Процессор аннотаций / Kotlin-процессор
↓
Сгенерированный исходник Java или Kotlin
↓
Обычный компилятор
↓
Байткод приложения
Это дало команде сразу несколько свойств. Отсутствующие зависимости могли становиться ошибками компиляции. Сгенерированное поведение можно было открывать в IDE. Фреймворк был менее подвержен влиянию изменений JDK, затрагивающих неподдерживаемые трюки с байткодом. Критичные по производительности пути выглядели больше как обычные прямые вызовы методов.
Это остаётся одной из самых ясных линий, соединяющих предрелизную Kora, показанную в презентации, с более поздними версиями. К линейке 1.2.x окружающая экосистема существенно созрела, а документация 2.0 по-прежнему описывает генерацию графа приложения на этапе компиляции, отсутствие рефлексии времени выполнения, отсутствие динамических прокси во время выполнения и сгенерированный человекочитаемый исходник как фундаментальные принципы. Современная Kora гораздо больше и отполированнее фреймворка эпохи 0.9, но она не отказалась от этой основной идеи.
Внедрение зависимостей на этапе компиляции сделало граф артефактом¶
Корневой интерфейс приложения в презентации явно включал инфраструктурные модули, такие как конфигурация, Undertow, JSON, JDBC и логирование. Точные имена модулей и API эволюционировали позже, но архитектурная идея была уже ясна: возможности приложения объявлялись явно, и Kora генерировала из них финальный граф зависимостей.
Это другая мысленная модель, чем обнаружение classpath во время выполнения:
Модель обнаружения во время выполнения
Classpath
↓
сканирование
↓
оценка условий
↓
построение контейнера
↓
обнаружение отсутствующих частей
против:
Модель компиляции Kora
Исходник + модули
↓
анализ графа
↓
генерация исходника графа
↓
компиляция
↓
запуск предописанного графа
Один из слайдов показывает сгенерированный граф, и на первый взгляд он выглядит устрашающе, потому что это большой список узлов, фабрик, зависимостей и объектов, управляемых жизненным циклом. Ответ докладчика важен: у каждого DI-контейнера где-то есть эквивалентная структура. Kora просто материализует её как сгенерированный код, а не держит внутри механики времени выполнения.
Сгенерированный граф никогда не предназначался для написания вручную. Он предназначался для изучения.
Контроллеры стали обычными сгенерированными обработчиками¶
Контроллеры следовали той же схеме. Разработчик писал обычный метод контроллера с аннотациями маршрутов Kora, а процессор генерировал модуль обработчика, который понимал маршрут, декодирование запроса, сопоставление параметров, сопоставление ответа и вызов контроллера.
Эффективный путь стал:
Это гораздо ближе к структуре времени выполнения, которую разработчики ожидали бы, читая исходник. Если что-то было неясно, сгенерированный обработчик можно было открыть напрямую. Не было необходимости выводить, какой динамический прокси или сгенерированный во время выполнения байткод стоял между маршрутом и методом приложения.
Эта ранняя реализация позже сильно эволюционировала. Современная Kora 2.0 использует более зрелую синхронную модель HTTP и распределяет работу приложения по виртуальным потокам, что является крупной эволюцией по сравнению с тем, что описывает этот доклад предрелизной эпохи 0.9. Исторический момент остаётся релевантным: Kora хотела сгенерированные, читаемые адаптеры вокруг обычных методов приложения, а не всё более непрозрачную модель выполнения времени выполнения.
Репозитории автоматизировали шаблонный код, не скрывая SQL¶
Репозитории были ещё одним примером той же философии. Разработчики писали контракты репозиториев и SQL, а Kora генерировала конкретную реализацию, вызовы драйвера и связующий код сопоставления. Докладчик открыто признаёт, что разработчики, привыкшие к Hibernate или производным методам запросов, могут не любить писать SQL вручную, но команда считала явный SQL преимуществом для сервисов, чувствительных к производительности.
Принцип был в том, что репозиторий должен автоматизировать повторяющиеся механизмы, не делая фактическую операцию с базой данных загадочной:
В докладе упоминаются модули баз данных R2DBC и Vert.x, потому что они существовали во фреймворке в то время. Это ровно тот вид деталей, который нужно пометить как исторический. Это была предрелизная Kora примерно поколения 0.9. Линейка 1.2.x изменила и стабилизировала многие части фреймворка, а поколение Kora 2.0 пошло дальше, удалив реактивные контракты репозиториев и сосредоточив фреймворк на синхронном коде, выполняемом с виртуальными потоками. Философия баз данных — явные контракты, сгенерированные репозитории, тонкая интеграция — выжила; модель выполнения изменилась существенно.
Фреймворк был намеренно небольшим и неполным¶
Один из самых ценных аспектов доклада — как открыто докладчик описывает ограничения того раннего фреймворка. Kora всё ещё была ниже 1.0.0, и на вопрос об обратной совместимости ответ был по сути «её пока нет». API могли меняться. Некоторые интеграции отсутствовали. Безопасность обсуждалась, а не в спешке встраивалась во фреймворк. Удобства в духе ORM рассматривались осторожно, потому что команда не хотела, чтобы широта функций подрывала архитектуру.
Эта честность важна при сравнении презентации с современной Kora. Фреймворк, показанный здесь, — это не просто «старая Kora со старыми номерами версий». Это фреймворк, всё ещё открывающий свою стабильную форму.
К 1.2.x Kora уже стала гораздо более полной и операционно зрелой: линейка релизов накопила значительную работу вокруг OpenAPI, Redis, HTTP-клиентов, тестирования, конфигурации, телеметрии, репозиториев, KSP, поддержки GraalVM и множества исправлений ошибок и улучшений совместимости. К линейке 2.0 архитектурная поверхность снова стала более связной, а не просто большей: прямые синхронные API, виртуальные потоки, валидация на этапе компиляции, обновлённые HTTP-контракты, консолидированный набор документации и более явная производственно-ориентированная история вокруг телеметрии, жизненного цикла, проб, отказоустойчивости и сгенерированного кода.
Поэтому когда доклад говорит «мы можем добавить это позже», «совместимости пока нет» или обсуждает интеграции, которые текущая Kora больше не использует, эти утверждения принадлежат предрелизной эпохе 0.9 и не должны проецироваться на фреймворк 1.2.x или 2.0.
Java и Kotlin были целями первого класса с самого начала¶
Даже на этом раннем этапе команда не хотела, чтобы Kora была Java-only фреймворком с поверхностной поддержкой Kotlin. В докладе описываются раздельные подходы к генерации и разочарование от опоры на несовершенные сгенерированные Java-заглушки для специфичных для Kotlin языковых возможностей. Намерение состояло в том, чтобы правильно понимать конструкции Kotlin, такие как nullability и параметры по умолчанию, а не продавливать Kotlin через модель в форме Java.
Эти инвестиции стали важнее позже. В период 1.2.x поддержка Kotlin/KSP была значительно улучшена, а к линейке 2.0 сгенерированные исходники Java/Kotlin остаются частью обычного опыта разработки. Современная документация даже поощряет разработчиков изучать сгенерированные исходники как инструмент обучения и отладки, что является прямым продолжением философии, показанной в этом докладе.
Явные модули были о контроле, а не о церемонии¶
Во время вопросов и ответов один из зрителей спрашивает, почему главное приложение явно включает модули фреймворка, вместо того чтобы полностью полагаться на аннотации и автоматическое обнаружение. Ответ — архитектурный контроль. Если приложению нужна возможность, оно добавляет этот модуль. Если возможность не нужна, оно не вносит её в граф.
Это одна из самых устойчивых идей Kora:
Нужен HTTP-сервер? добавьте модуль
Нужен JDBC? добавьте модуль
Нужна телеметрия? добавьте модуль
Нужна Kafka? добавьте модуль
Не нужен? не добавляйте
Синтаксис и имена модулей до 1.0 позже изменились, но принцип выжил и стал гораздо более отполированным в новых версиях. Современная Kora 2 явно позиционирует себя как контролируемый стек, где сервисы добавляют только те модули, которые им нужны.
Тестирование отражало первоначальную цель: небольшие сервисы¶
Докладчик описывает раннее предпочтение интеграционных и black-box тестов вместо обширного специфичного для фреймворка мокирования. Рассуждение напрямую связано с целью Kora: если сервисы малы и запускаются дёшево, тестирование реального приложения становится гораздо более практичным.
На момент презентации не было богатой выделенной среды мокирования, сравнимой со зрелой инфраструктурой тестирования Spring. Опять же, это историческое ограничение предрелизной эпохи 0.9. Инструментарий тестирования Kora значительно расширился позже. Линейка 1.2.x уже содержала гораздо более развитое тестирование графа JUnit и механизмы замены, а документация Kora 2 рассматривает компонентное, интеграционное и black-box тестирование как первоклассные части фреймворка.
Философская преемственность интереснее старого ограничения. Kora предпочитает тесты, которые остаются близкими к фактическому сгенерированному графу приложения, а не строит параллельную вселенную тестирования, оторванную от производственной обвязки.
«Мы нанимаем Java-разработчиков, а не Spring-разработчиков»¶
Один из самых запоминающихся моментов в вопросах и ответах мало связан с цифрами бенчмарков. Зритель спрашивает, не будут ли инженеры беспокоиться о работе с внутренним или менее популярным фреймворком, потому что хотят иметь Spring и Hibernate в резюме.
Докладчик отвечает:
Мы нанимаем Java-разработчиков, а не Spring-разработчиков.
Идея в том, что основным навыком бэкенд-инженера должно быть решение инженерных задач: понимание JVM, конкурентности, баз данных, сетей, обмена сообщениями и поведения системы. Фреймворк — это инструмент, а не профессия.
Это утверждение также объясняет, почему Kora была спроектирована так, чтобы оставаться близкой к знакомым понятиям Java/Kotlin. Если фреймворк требует годы проприетарных знаний, чтобы стать продуктивным, значит, фреймворк стал слишком важен относительно нижележащей инженерной дисциплины.
Эволюция современной Kora делает этот момент ещё сильнее, чем ранний доклад. Поколение 2.0 намеренно использует обычные синхронные сигнатуры Java/Kotlin, доступ к данным в стиле JDBC, понятия HTTP/gRPC/Kafka, близкие к нижележащим технологиям, и сгенерированный на этапе компиляции код, который можно изучать в IDE. Фреймворк существенно созрел с 0.9, не превратившись в изолированную вселенную программирования.
Здоровье, метрики, безопасность и native image всё ещё формировались¶
Вопросы и ответы также дают полезную картину уровня зрелости фреймворка. Проверки здоровья и метрики существовали как опциональные модули, но докладчик не мог вспомнить точный набор метрик по умолчанию во время обсуждения. Безопасность ещё не рассматривалась как обязательная абстракция фреймворка и всё ещё обсуждалась. Поддержка native image существовала как эксперименты, а не как основная стратегия времени выполнения. Были явные колебания по поводу добавления функций просто потому, что они есть у других фреймворков.
Эти ответы звучали бы неполно, если бы их представили как описание текущей Kora. Они имеют смысл, когда понимаются как предрелизная Kora примерно 0.9+. К 1.2.x производственная поверхность существенно расширилась, а линейка 2.0 теперь документирует телеметрию, структурированное логирование, пробы, жизненный цикл, корректное завершение работы, отказоустойчивость, HTTP, OpenAPI, gRPC, доступ к данным, обмен сообщениями, конфигурацию и другие производственные вопросы как часть гораздо более зрелого фреймворка.
Полезный урок не в том, что ранней Kora не хватало функций. Он в том, что команда была намеренно избирательной в том, какие абстракции заслуживают стать понятиями фреймворка.
Примитивный взгляд на эволюцию¶
Историческую прогрессию можно резюмировать так:
Предрелизная Kora / эпоха ~0.9
│
├─ основная идея DI на этапе компиляции уже присутствует
├─ сгенерированные контроллеры / репозитории / граф
├─ фокус на Java + Kotlin
├─ несколько экспериментальных интеграций
├─ стабильного обещания совместимости ещё нет
└─ небольшая экосистема, сильное архитектурное направление
│
▼
Kora 1.2.x
│
├─ значительно более стабильный фреймворк
├─ более широкая поверхность интеграций и инструментария
├─ более сильная поддержка OpenAPI / тестирования / телеметрии / конфигурации
├─ улучшенный путь KSP и Kotlin
├─ множество производственных исправлений и улучшений совместимости
└─ архитектура, доказанная на гораздо большей реальной поверхности
│
▼
Линейка Kora 2.0
│
├─ гораздо более зрелая и консолидированная модель
├─ синхронные контракты приложения
├─ виртуальные потоки как стандартная модель выполнения
├─ нет реактивных / suspend-контрактов в модулях фреймворка
├─ уточнённая обработка графа на этапе компиляции
├─ более сильные документация / руководства / исполняемые примеры
└─ более широкая облачно-производственная история вокруг OTel, проб,
жизненного цикла, корректного завершения, отказоустойчивости и тестирования
Вот почему доклад стоит читать и сегодня. Многие API историчны. Архитектурное направление — нет.
Что бенчмарки на самом деле доказали¶
Бенчмарк не доказал, что Kora всегда будет превосходить Spring, Quarkus или Micronaut. Он доказал нечто более узкое и полезное: архитектурные предположения команды были измеримы. Удаление автоконфигурации снизило стоимость запуска. Удаление обнаружения во время выполнения и сокращение слоёв абстракции приблизило производительность к ручному базовому уровню. Генерация исходника и графа приложения на этапе компиляции позволила иметь модель программирования уровня фреймворка, не воссоздавая ту же механику времени выполнения, которой команда пыталась избежать.
Резюме бенчмарка можно прочитать как три отдельных вывода:
Память при запуске:
Kora имела наибольшее преимущество.
Время запуска:
Kora и вручную урезанный Spring были близки,
а обычный Spring Boot был гораздо медленнее.
Время выполнения в установившемся состоянии:
Различия сузились при фиксированной пропускной способности,
но Kora всё же достигла более высокого максимального RPS.
Это гораздо более полезная интерпретация, чем «Kora была в X раз быстрее». Она показывает, где на самом деле платилась цена.
Самым важным артефактом был не график бенчмарка, а сгенерированный код¶
Презентация тратит значительное время на показ сгенерированных контроллеров, сгенерированных репозиториев и сгенерированного графа зависимостей. Этот акцент намеренный. Команда пыталась не только снизить потребление CPU; она пыталась сделать поведение фреймворка объяснимым.
Традиционный контейнер времени выполнения можно концептуально резюмировать так:
Исходный код
↓
механика фреймворка времени выполнения
↓
динамический граф / поведение прокси
↓
фактическое выполнение
Kora попыталась превратить это в:
Этот дополнительный слой сгенерированного исходника видим, а не скрыт. Если разработчики задаются вопросом, почему контроллер ведёт себя определённым образом, они могут изучить сгенерированный обработчик. Если они задаются вопросом, какой маппер используется, они могут изучить реализацию репозитория. Если они задаются вопросом, как собран граф, они могут открыть сам граф.
Современная Kora 2 сделала эту идею ещё более явной. Сгенерированный код — не просто деталь реализации; текущая документация рассматривает его как обычную поверхность отладки и обучения. В этом смысле одна из самых сильных идей эпохи 0.9 не просто выжила — она стала более центральной по мере созревания фреймворка.
Что изменилось больше всего к Kora 2¶
Самое большое концептуальное изменение от доклада к современной Kora — модель выполнения. Ранний фреймворк экспериментировал с несколькими технологиями баз данных и асинхронными технологиями, включая
пути, ориентированные на R2DBC и Vert.x. К линейке 2.0 Kora стала гораздо более определённой: код приложения фреймворка синхронен, виртуальные потоки — обычный механизм выполнения, а реактивные
сигнатуры или Kotlin suspend больше не являются контрактами фреймворка.
Это крупный шаг зрелости, потому что он уменьшает число параллельных моделей программирования, которые фреймворку приходится поддерживать. Вместо того чтобы вырастать в более широкий фреймворк, поддерживающий любой стиль конкурентности, Kora 2 стала более узкой и связной.
Результирующая современная модель ближе к:
HTTP-запрос
↓
виртуальный поток
↓
обычный метод контроллера
↓
обычный метод сервиса
↓
JDBC / HTTP / gRPC / Kafka
↓
ответ
Это не была форма предрелизного фреймворка эпохи 0.9 из доклада. Это более поздний вывод, достигнутый после нескольких поколений разработки.
Почему исторический доклад всё ещё важен¶
Презентация полезна именно потому, что предшествует отполированному фреймворку.
Она показывает ограничения до того, как они стали лозунгами.
Она показывает базовый бенчмарк до того, как у Kora появились годы оптимизации.
Она показывает команду, обсуждающую, должны ли безопасность, удобства в стиле ORM, native image и другие функции вообще входить во фреймворк.
Она показывает проект без гарантий совместимости, объясняющий, почему сгенерированный исходник и DI на этапе компиляции стоили того, чтобы их добиваться.
Большинство документации фреймворков рассказывает вам, что существует.
Этот доклад объясняет, почему существует Kora.
Это различие делает его ценным, даже если конкретный API принадлежит периоду до 1.0, примерно 0.9+.
Заключение¶
Эту презентацию следует читать как инженерную историю происхождения Kora, а не как текущую документацию. Демонстрируемая версия — предрелизная Kora, примерно эпохи 0.9 до 1.0.0, с экспериментальными модулями, развивающимися API, без стабильного обещания совместимости и с гораздо меньшей экосистемой, чем у фреймворка сегодня. Несколько технологий, упомянутых в докладе, позже изменились или исчезли, а некоторые ограничения, обсуждавшиеся в вопросах и ответах, были устранены в последующих поколениях.
Важно то, что фундаментальные идеи уже присутствовали.
Команда хотела фреймворк, который мог бы приблизиться к производительности прямого Undertow/JDBC, не заставляя каждый сервис писать вручную. Она хотела ошибки зависимостей во время компиляции, а не развёртывания, сгенерированный исходник вместо скрытого байткода времени выполнения, явные модули вместо широкой автоконфигурации, управляемой classpath, видимый SQL вместо непрозрачного поведения ORM и путь времени выполнения с как можно меньшим числом специфичных для фреймворка слоёв.
Бенчмарк с ограниченными ресурсами поддержал это направление. В конкретной среде, показанной в докладе, Kora требовала 64 МБ минимальной памяти запуска по сравнению с 192 МБ для обычной конфигурации Spring Boot, запускалась примерно за 3,2 секунды, потребляла около 40% CPU при 500 RPS, достигала примерно 1657 RPS и давала среднее время отклика около 1,7 мс. Что ещё важнее, постепенное удаление инфраструктуры Spring Boot и Spring приближало варианты Spring к Kora, что усиливало центральный тезис: значительная часть стоимости приходилась на механику фреймворка времени выполнения, а не на саму бизнес-логику.
Оттуда Kora существенно созрела. Линейка 1.2.x была уже совсем другим фреймворком, чем этот снимок эпохи 0.9, с более широкой производственной поверхностью, более сильной поддержкой тестирования, конфигурации, телеметрии, OpenAPI, KSP, репозиториев и интеграций. Поколение 2.0 идёт гораздо дальше: фреймворк более связный, более документированный, более производственно-ориентированный и более определённый вокруг синхронного кода приложения Java/Kotlin на виртуальных потоках, сохраняя при этом DI на этапе компиляции, тонкие абстракции, сгенерированный исходник и доступность для изучения.
Поэтому лучший способ резюмировать эволюцию — не так:
А так:
Идея эпохи Kora 0.9
граф на этапе компиляции + сгенерированный исходник + тонкое время выполнения
↓
Kora 1.2.x
стабилизировать, расширить, довести до продакшена
↓
Kora 2.0
снова упростить вокруг зрелой,
связной модели с приоритетом виртуальных потоков
Самое запоминающееся утверждение докладчика по-прежнему подходит этому современному фреймворку:
Мы нанимаем Java-разработчиков, а не Spring-разработчиков.
Долгосрочное направление Kora следует той же идее. Фреймворк должен предоставлять полезные значения по умолчанию, сгенерированную инфраструктуру и производственные интеграции, но инженер должен по-прежнему работать с Java, Kotlin, SQL, HTTP, Kafka, gRPC, базами данных, конкурентностью и обычным кодом приложения, а не изучать проприетарную вселенную времени выполнения.
Это и есть настоящая история, которую запечатлевает этот ранний доклад: Kora начиналась не как попытка построить более крупный фреймворк. Она начиналась как попытка сделать фреймворк меньше во время выполнения, более явным для разработчика и ближе к коду, который JVM реально выполняет.