Я хотел бы поделиться опытом реализации микроядерной архитектуры (microkernel) на Java с помощью OSGI (Open Service Gateway Initiative). Этот подход является промежуточным вариантом между микро-сервисной и монолитной архитектурой. С одной стороны присутствует разделение между компонентами на уровне VM с другой - межкомпонентное взаимодействие происходит без участия сети, что ускоряет запросы.

Введение

Источник: Изображение o'reilly
Источник: Изображение o'reilly

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

При этом ядру системы требуется информация о запущенных модулях и способах взаимодействия с ними. Наиболее распространенный подход для решения этой задачи лежит через организацию реестра плагинов, включающий в себя информацию о названии плагина и доступных интерфейсах. 

Данный паттерн может быть реализован с использованием совершенно разных технологий. Например, мы можем выделить ядро и подключать плагины через динамическую загрузку jar файлов без дополнительной изоляции. 

OSGI предлагает подход к изоляции плагинов с помощью разделения кода для каждого плагина на уровне загрузчика классов. Каждый плагин может загружаться отдельным загрузчиком, тем самым обеспечивая дополнительную изоляцию. Недостаток такого решения в потенциальных конфликтах классов: одинаковые классы, загруженные с помощью разных загрузчиков, не могут взаимодействовать. 

В качестве высокоуровневого решения можно рассмотреть Apache Karaf, который позиционирует себя как Modulith Runtime и предоставляет интеграцию с основными фреймворками: JAX-RS и Spring Boot. Данный инструмент упрощает взаимодействие с OSGI технологией, предоставляя высокоуровневые абстракции. 

Источник: Apache Keraf
Источник: Apache Keraf

В качестве альтернативных вариантов можно рассмотреть непосредственные реализации OSGI: Apache Felix, Eclipse Equinox и Knopflerfish. Использование низкоуровневых решений даст нам большую свободу в процессе проектирования. 

Плагинизированная архитектура на базе Apache Felix

Контекст

Для интеграции с различными источниками данных заказчика нами использовалось решение на базе Apache Camel, которое на основе пользовательской конфигурации подключалось к произвольному источнику данных (от FTP до OPC UA) и применяло определенные пользователем трансформации над получаемыми данными. Такое решение зарекомендовало себя своей надежностью, а также легкостью в расширении для случая протоколов, которые уже есть в Apache Camel. Недостаток данного решения заключался в сложности подключения новых протоколов, которых нет в Apache Camel. Проблема была в появлении dependency hell, который состоял в появлении несовместимых транзитивных зависимостях. 

Именно это послужило основным драйвером для исследования иных подходов в построении сервиса интеграции. Помимо этого у меня было ощущение, что возможно  реализовать более эффективную инициализацию приложения за счет исключения Spring из проекта и ручной конфигурации сервисов. Это было возможно из-за небольшого количества зависимостей между компонентами. 

В качестве решения было предложено использовать Apache Felix, самостоятельно определить интерфейс для компонента обработки данных и динамически подключать плагины на этапе старта приложения. Стоит подчеркнуть, что нам требовалось реализовать конвейер обработки данных: получение данных из удаленного источника, несколько этапов трансформации и запись в нашу систему хранения данных или чтение из нашей системы, несколько этапов трансформации и запись результата в удаленный источник данных. 

  • READ FLOW: Чтение из системы заказчика, Преобразование, Запись в нашу систему

  • WRITE FLOW: Чтение из нашей системы, Преобразование, Запись в систему заказчика

Важно было учитывать контекст задачи, который заключается в наличии несложных взаимодействий между этапами обработки данных. Формат value object был унифицирован. При этом конвейер обработки данных не содержал логических блоков или связей один ко многим в процессе передачи данных. Это значительно упрощало обработку данных.

Структура проекта

Launcher. Был выделен отдельный проект - launcher, который выполнял функцию ядра системы. Его зона ответственности была ограничена запуском osgi Framework, чтением конфигурации и динамическим подключением необходимых плагинов, которые были явно указаны в конфигурации; а также связыванием всех плагинов в единых конвейер на основе пользовательской конфигурации.

В процессе реализации ядра и подключения базового плагина оказалось, что документации недостаточно для корректной конфигурации приложения. Оказалось очень полезным использовать поиск по Github для сравнения своего и чужого, вероятно работающего решения.

Shared Code. Общий код был выделен в два проекта: api - набор интерфейсов для реализации конвейерной обработки данных и parent - общий parent для всех проектов содержащий api в качестве зависимости, а также конфигурацию maven plugin, который позволял получить jar файл с кодом плагина. 

Plugins. Каждый плагин размещался в отдельном maven проекте и упаковывался в jar файл со специальной структурой (bundle в терминах osgi). За генерацию правильной структуры отвечает maven плагин org.apache.felix:maven-bundle-plugin, который принимает в качестве настроек название проекта, активатор (entrypoint) и перечень private/export/import/embed зависимостей. 

Структура плагина (bundle)

Каждый плагин содержит в себе activator - класс, который будет запущен в момент подключения плагина. Ожидается, что в этот момент плагин будет регистрировать свои сервисы в контексте. Каждый сервис может содержать мета-информацию, которую он запишет в Dictionary.

public class Activator implements BundleActivator {
  @Override
  public void start(final BundleContext bundleContext) {    
      Dictionary<String, Object> dictionary = new Hashtable<>();
      dictionary.put("CustomField", "API_IMPL_V1");
      bundleContext.registerService(ApiService.class, new ApiServiceImpl(), dictionary);
    }
}

Ядро приложения (Host в терминах OSGI) может обратиться к контексту с запросом на получение зарегистрированных сервисов с указанием полей метаданных:

var references =
        context.getServiceReferences(ApiService.class, "(CustomField=*)");
Map<String, ConnectorService> index = new HashMap<>();
for (ServiceReference<ConnectorService> reference : references) {
    var  service = context.getService(reference);
    index.put(reference.getProperty("CustomField").toString(), service);
}

При этом плагин будет содержать зависимости, недоступные остальным плагинам, если они будут помечены как Private. 

Неочевидности, о которых хотелось бы знать

№1. Спецификация не позволяет иметь классы в default package. Данное требование распространяется не только на ваш проект, но и на все ваши зависимости. Ошибка, которая будет выведена в случае нарушения требования, не будет информативной:

[ERROR] Bundle {groupId}:{artifactId}:bundle:{version} : The default package ‘.’ is not permitted by the Import-Package syntax.
This can be caused by compile errors in Eclipse because Eclipse creates
valid class files regardless of compile errors.
The following package(s) import from the default package null
[ERROR] Error(s) found in bundle configuration

Для решения этой проблемы нужно разместить условный breakpoint в коде плагина “org.apache.felix:maven-bundle-plugin” и самостоятельно найти зависимость, содержащую неправильную структуру классов. 

Подробное решение этой проблемы я разместил в отдельной статье : https://medium.com/@mark.andreev/how-to-fix-the-default-package-is-not-permitted-by-the-import-package-syntax-in-osgi-3b59a6c18e71 

№2. Неочевидные обязательные настройки “org.osgi.framework.launch.Framework”. У вас не получится запустить apache felix без указания временной директории “Constants.FRAMEWORK_STORAGE”. В случае возникновения проблем ошибка не будет информативной. 

№3. Отсутствие ошибки в случае проблем во время загрузки bundle. Единственный способ понять, что bundle не загрузился - это сравнить SymbolicName у bundle с null. 

Bundle addition = bundleContext.installBundle(location);
if (addition.getSymbolicName() != null) {
   // TODO: add error
}

№4. Сложности в передаче библиотечных классов в плагин. Решением оказалось унификация интерфейсов в библиотеке api и использование только этих классов для общения между плагинами. 

Заключение

Решение на базе Apache Felix продемонстрировало не только сложность в адаптации недостаточно популярной технологии, которое выражалось в нехватке знаний на Stackoverflow и необходимости использовать отладчик для исследования большинства проблем, что усложняет разбор инцидентов.  С другой стороны, благодаря данной технологии мы получили низкую связность между компонентами системы, изоляцию плагинов на уровне загрузчика классов и более простую структуру проекта за счет выделения каждого компонента конвейера в отдельный проект;  и значимое ускорение запуска.  

Важно учитывать, что позитивный опыт напрямую связан со слабой связанностью между плагинами и отсутствием общих совместно используемых зависимостей помимо api library. 

Если вам требуется более тесное взаимодействие, то стоит обратить внимание все же на Apache Karaf. Скорее всего вам будет удобнее не реализовывать низкоуровневое взаимодействие с OSGI, аналогичное описанному в проекте. 

Послесловие

А был ли у вас опыт реализации микроядерной архитектуры? Как вы решали данную проблему?

Марк Андреев

Senior Software Engineer

Комментарии (17)


  1. ultrinfaern
    20.03.2024 16:04
    +1

    У меня тоже был опыт работы с OSGI. Например Spring поддерживается версии 3 - это 2010 год. То есть на технологию забили уже 14 лет. Ни одна современная бибилотека не поддерживает OSGI. Попытка что-то использовать выливается в перманентное сражение, и не факт что успешное. Да и OSGI, по моему мнению, стоит рассматривать как динамические сервисы И написание бандлов и вообще использование этой технологии стоит рассматривать именно так. Если у вас нет динамики - оно вам не нужно. А с учетом того, что сейчас бал правят микросервисы и даже application server - уже тоже не в почете, то что говорить о OSGI.

    Поэтому перед использованием, следует не 10 а 1000 раз подумать, и выбрать что-то другое.


    1. sshikov
      20.03.2024 16:04
      +7

      Karaf OSGi Runtime 4.4.5

      January 10, 2024

      Сказать что технология активно развивается - конечно соврать. Но "забили" в ситуации, когда последний релиз был вот только что - тоже соврать.


      1. ultrinfaern
        20.03.2024 16:04

        Судя по тому, что вы говорите про Karaf, вы не знаете что такое OSGI, как оно работает, и что я имел ввиду, говоря про его поддержку.

        Разъясняю: Karaf - это ядро/движок. К нему нужны бандлы, без них он никому не нужен. А бандлы перестали делать как раз, наверное, в году 2010.

        Евли у вас все самописное, как например Eclipse, то все прекрасно. А если вам нужно что-то из современных библиотек и технологий - то тут все печально.


        1. sshikov
          20.03.2024 16:04
          +2

          Очень смешно. У меня уже третий проект на нем. Начиная с 2010 года я с маленькими перерывами на нем работаю. А судя по тому что ни одной статьи про OSGI у вас нет (в отличие от меня), то я вами и спорить не стану. Оставайтесь при своем мнении.


  1. sshikov
    20.03.2024 16:04

    Автор, а зачем вы берете Felix в чистом виде? Есть же Karaf и Fuse. И ServiceMix. Там все сильно удобнее. Или я вас не так понял?


  1. AlexunKo
    20.03.2024 16:04

    Я хотел бы поделиться опытом реализации микроядерной архитектуры (microkernel) на Java с помощью OSGI (Open Service Gateway Initiative). Этот подход является промежуточным вариантом между микро-сервисной и монолитной архитектурой. С одной стороны присутствует разделение между компонентами на уровне VM с другой - межкомпонентное взаимодействие происходит без участия сети, что ускоряет запросы.

    А разве микроядра - это не про операционки? Что значит промежуточный вариант, что за котопес? Без участия сети можно организовать компоненты и без OSGi. Вы точно понимаете о чем пишете?.. без обид


    1. ris58h
      20.03.2024 16:04
      +2

      Я хоть OSGI и не трогал, но всегда думал, что это технология для изоляции нескольких программных модулей в рамках одной JVM. Т.е. можно, например, использовать разные несовместимые версии одной и той же библиотеки в разных модулях и это не сломается. Как бы вы организовали такое без OSGI? Shading?


    1. mrk-andreev Автор
      20.03.2024 16:04
      +3

      А разве микроядра - это не про операционки?

      Нет, это architecture patterns.

      Вы точно понимаете о чем пишете?

      Я думаю, что да. Могу предложить исследовать статью oreilly, посвещенную этому вопросу, https://www.oreilly.com/content/software-architecture-patterns/. В статье представлен общий экскурс в architecture patterns. Надеюсь, что вы считаете это издание достаточно авторитетным.


  1. rustamlr
    20.03.2024 16:04

    Для тех кто живёт в экосистеме Spring выглядит попроще. Встроенный loader позволяет подгрузить любые джарки в класс пасс, главное собрать fat jar. Конечно не в рантайме но если это не нужно то это кажется самый оптимальный способ.


    1. sshikov
      20.03.2024 16:04

      Как раз OSGI упрощает этот процесс сильно. Другое дело, что не всегда это реально нужно. И не всегда можно. Иногда собрать fat.jar таки приходится, но сильно реже, чем если вы собираете простое spring приложение.


  1. rvit34
    20.03.2024 16:04

    Сегодня такое можно сделать не прибегая к сторонним фреймворкам. Java SDK позволяет построить модульный проект с изоляцией модулей на уровне загрузчиков классов и с динамической загрузкой/выгрузкой.


    1. mrk-andreev Автор
      20.03.2024 16:04

      Я не уверен, что это возможно. Проект Jigsaw, к которому вы вероятнее всего отсылаете, про изоляцию модулей, которая не позволяет решить проблему одновременного использования зависимостей разных версий. Например, использовать Guava разных версий в разных частях проекта.

      Возможно, что я ошибаюсь и у вас получится собрать контрпример.


      1. rvit34
        20.03.2024 16:04

        Что вы понимаете под разными частями проекта? Если эти части возможно выделить в отдельные модули и загрузить паралельно, то вполне себе возможно использовать разные версии guava в них.

        Если интересно то советую посмотреть доклад Липского https://youtu.be/aw6YJLJG5hw?si=F0qDmPZivPw63-c1

        В нем он кстати приводит недостатки OSGI.

        Я уже делал подобную систему на Jigsaw и это работает.


        1. mrk-andreev Автор
          20.03.2024 16:04

          В процессе исследований я обратил внимание на доклад Липского, но у меня не получилось на основе этой технологии реализовать одновременное использование разных версий guava в разных сервисах.

          Root Service 
              ||
              ||====> Service A ===> Guava 10.0	
              ||
              ||====> Service B ===> Guava 33.1.0-jre	

          Если вдруг у вас получилось с помощью Jigsaw это реализовать, пожалуйста, подскажите пример демо проекта / статьи с описанием реализации.


          1. rvit34
            20.03.2024 16:04

            Да, получилось. Как руки дойдут я выложиу его на github.


        1. sshikov
          20.03.2024 16:04

          Заставлять нас искать кусок про OSGI в довольно длинном видео - это садизм :)

          Можете примерно сказать, на какой минуте про OSGI? Все прочие доклады что я видел на эту тему, страдали тем, что авторы не понимали OSGI с практической стороны, т.е. никогда не использовали в жизни. Чистые теоретики.


    1. sshikov
      20.03.2024 16:04

      Вопрос не в том, прибегая или нет. Вопрос скорее в том, как проще. И ответ на него не всегда одинаковый. Скажем, я не встречал тех проблем, что автор тут описывает, зато например split package, когда один пакет определен в двух разных jar, это как правило геморрой с гарантией.

      Но в большинстве классических случае использования зависимостей (и конфликтов в процессе) в OSGI контейнере выглядит как-то примерно так (у меня опыт работы с чистым karaf. Fuse и ServiceMix, они все основаны на карафе же):

      • загружаем в контейнер очередной наш модуль

      • выясняем, что он не стартует, потому что зависимости не разрешились - нет каких-то пакетов

      • выясняем, откуда эти пакеты

      • устанавливаем зависимости прямо из репозитория maven

      • обновляем наш компонент

      И при этом нам как правило до лампочки, что одному нашему модулю нужна guava другой версии - мы их все можем загрузить.

      То есть, по сути это выглядит как добавление зависимостей при сборке проекта в pom.xml или чем мы там собираем - только в рантайме.