Axelix 1.3 будет поддерживать реактивный стек

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Всем привет! На связи Михаил Поливаха, технический лидер Open Source проекта Axelix (give us a star!).

Я долго думал, сделать это статьей или новостью, но для новости данный поток сознания слишком большой. Поэтому, всё же, решили выпустить новость :)

В ноябре выходит Axelix 1.3. Одно из изменений релиза заключается в том, что Spring Boot стартеры Axelix начнут нативно работать в приложениях на реактивном стеке, то есть на Spring WebFlux с Reactor Netty. Раньше подключить стартер к такому сервису было нельзя. Теперь реактивные приложения можно будет мониторить и отлаживать из Axelix Master так же, как сервлетные.

Это одна из самых частых просьб пользователей, спасибо всем, кто писал нам о ней в Telegram-канале и в issues. Я лично встертил на ряде конференций как минимум 4 команды, которые просили добавить поддержку Project Reactor-а. Мы оценили масштаб поражения

Ниже расскажем, как это устроено внутри и почему мы сделали почти так же, как сам Spring Boot Actuator.

Что изменится для пользователя

Ничего настраивать не придётся, достаточно будет добавить стартер в WebFlux-приложение:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation("com.axelixlabs:axelix-spring-boot-4-starter:1.3.0")
}

Стартер сам определит тип веб-приложения (SERVLET или REACTIVE) и поднимет нужную инфраструктуру. После старта сервис появится в Axelix Master, и в UI будут доступны бины, environment, configuration properties, логгеры и остальные разделы.

Немного контекста: как стартер работал раньше

Стартер Axelix это по большому счету набор собственных Actuator-эндпоинтов (/actuator/axelix-env, /actuator/axelix-configprops и т.д.) и набор RBAC инфраструктуры, который ее защищает.

Hidden text

P.S: Мы уже много, *много* раз пожалели о том, что стали строить Axelix путем добавления кастомных Actuator endpoints. Думаю, скоро мы поднимем ворпос о том, чтобы это поменять в общем и целом.

Master ходит в эти эндпоинты с токеном, фильтр проверяет токен и права пользователя.

Права важны не только для того, чтобы пустить или не пустить запрос. Например, axelix-env маскирует значения свойств, если у пользователя нет конкретной authority, а именно ENV_VALUES_READ. Например, очевидно, что Spring-овый enviornment может и часто содержит сенсативные вещи, такие как API ключи, УЗ к базам данных и т.д. Само собой, тут нужен RBAC, и Axelix его дате из коробки.

Но вопрос в другом. Из прмиера c env я думаю, уже понятно, что нашим эндпоинтам во время работы нужно знать, кто его вызвал.

На сервлетном стеке это решается довольно просто: один запрос обрабатывается одним потоком, поэтому фильтр кладёт контекст безопасности в ThreadLocal, а эндпоинт ниже по цепочке его оттуда читает:

// JwtAuthorizationFilter (Servlet)
User user = webIdentityAccessManager.verifyAccess(relativePath, method, token);

securityContextExecutor.runWithinSecurityContext(
        () -> filterChain.doFilter(request, response),   // весь запрос в одном потоке
        new DefaultSecurityContext(user, token));
 Servlet: thread-per-request
 ─────────────────────────────────────────────────────────────────
  http-nio-exec-7 │ JwtFilter ──► ThreadLocal.set(ctx)
                  │      │
                  │      ▼
                  │ /actuator/axelix-env ──► ThreadLocal.get() ✔
                  │      │
                  │      ▼
                  │ ThreadLocal.clear()
 ─────────────────────────────────────────────────────────────────

Эта модель совершенно не новая, сам Spring Framework использует её активно, в том числе Spring Security использует её для хранения SecurityContext.

Почему на WebFlux так не получится

В реактивном приложении всё иначе:

  1. Нет привязки запроса к потоку. Запрос может начать обрабатываться на одном потоке event loop Netty, а закончить на другом. ThreadLocal, выставленный в фильтре, эндпоинт просто не увидит. Наверняка среди читателей есть продвинутые пользователи Project Reactor и они знают, что у реактивного стека есть свой собственный Context API. В целом можно было бы использовать и его, но с ним будет много подводных камней и ограничений. Поэтому мы решили этого не делать.

  2. Event loop нельзя блокировать. Потоков reactor-http-nio-* столько же, сколько ядер. Есть некоторые операции, которые предоставляет Axelix, которые по природе блокирующие, например, снятие heap dump-а на приложении, или рендеринг Thread Dump на живом сервисе (эта проблема кстати не только для нас специфична. Чуть ниже поймете, что в таких ситуациях делает Spring Boot).

  3. Сервлетных фильтров нет. Вместо OncePerRequestFilter и FilterRegistrationBean там WebFilter, а вместо HttpServletRequest используется ServerWebExchange.

 WebFlux: event loop, без ThreadLocal
 ─────────────────────────────────────────────────────────────────
  reactor-http-nio-2 │ JwtFilter ──► ThreadLocal.set(ctx)
                     │      │
                     │      ▼  (продолжение может уйти на другой поток)
  reactor-http-nio-5 │ /actuator/axelix-env ──► ThreadLocal.get() ✘ null
                     │                         + блокирующая работа
                     │                           на event loop ✘
 ─────────────────────────────────────────────────────────────────

Как Spring Boot Actuator решает ту же проблему

Стандартные эндпоинты Actuator (/actuator/threaddump, /actuator/heapdump, и т.д.) тоже само собой блокирующие, но в WebFlux-приложениях работают без особоых проблем. Делается это двумя приёмами:

  • Блокирующие операции уходят с event loop. Если операция эндпоинта возвращает обычный объект, а не Mono/Flux, Actuator вызывает её через Mono.fromCallable(...) и subscribeOn(Schedulers.boundedElastic()), то есть в отдельном пуле, где блокироваться в целом можно.

  • Пользователь берётся из exchange, а не из потока. Аутентифицированный пользователь достаётся из ServerWebExchange.getPrincipal() и передаётся в операцию явно, как часть контекста вызова.

В Axelix мы просто сделали так же, как и Spring Boot.

Как это будет устроено в Axelix 1.3

1. Отдельный реактивный фильтр

Стартер регистрирует один из двух фильтров в зависимости от типа приложения:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
static class ServletJwtAuthConfiguration {    
     // FilterRegistrationBean<JwtAuthorizationFilter>
}

​@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
static class ReactiveJwtAuthConfiguration {    

     @Bean
     ReactiveJwtAuthorizationFilter reactiveJwtAuthorizationFilter(...) { ... }
}

Как и раньше, фильтр регистрируется в том контексте, который обслуживает Actuator: в основном или в дочернем management-контексте, если задан отдельный management.server.port.

Вообще иерархия контекстов в Spring Boot это отдельная большая тема, мы даже нечаянно из-за нее получили CVE в Axelix, но быстро пофиксили в 1.2.1.

Для тех, кто не знает, контексты в Spring Framework в целом представляют собой иерархию. В большинстве традиционных Spring Boot приложений вы работаете в рамках одного единственного контекста. Но в теории есть способы создать эту иерархию (намерянно или нет). Один из них это как раз специфицирование Spring Boot Actuator на стороннем порту, как описано выше.

Для многих чаще всего проблема в таком случае возникает в той ситуации, когда есть, например, бин Х, и он находится в дочернем контексте, в родительском его нет (частый кейс).

Когда бины из дочернего контекста пытаются найти этот бин Х, они его находят без проблем. А вот когда бин Y из родительского контекста пытается найти этот бин X, то он его не находит. Поиск работает снизу вверх, но не сверху вниз. Это может привести к различного рода интересным исходам, например, к CVE, которую мы ранее подметили. Иерархия контекстов вообще отдельная большая тема, возможно, стоит её осветить в отдельной статье. Пишите, если интересно

Источник: https://habr.com/ru/articles/1098680/


Интересные статьи

Интересные статьи

За пару лет я сделал с десяток приложений для Маркетплейса Битрикс24 — коннекторы мессенджеров, роботы, iframe-панели. На PHP, TypeScript и Python. Каждый раз одно и то же: документация разбросана, пр...
Всем привет, я Сергей — ведущий программист в e-commerce агентстве KISLOROD.Битрикс24 можно доработать «под себя», но если задачи сложные — без модулей вы далеко не уедете. Модули позволяют объед...
Приятно познакомиться, мы битриксоиды. Да-да, те самые которые:- вообще не модные,- пишут НЕ на Laravel и Symphony,- возятся с кучей мягко говоря “неидеального” кода под названием “1С-Битрикс: Управле...
Статья для начинающих аналитиков-стажеровКогда вводишь в поисковой запрос "Курсы для аналитика данных", то сразу в глаза бросается SQL, Phytnon, R и другие инструменты. Но в первую очередь аналитику д...
Я в компании отвечаю за работу команды разработчиков. Команда небольшая -  всего 6 разрабов, но за последний год с небольшим мы с нуля разработали и внедрили пять проектов. Причем это были не дет...