Реализации Microkernel архитектуры с помощью Java OSGI
Я хотел бы поделиться опытом реализации микроядерной архитектуры (microkernel) на Java с помощью OSGI (Open Service Gateway Initiative). Этот подход является промежуточным вариантом между микро-сервисной и монолитной архитектурой. С одной стороны присутствует разделение между компонентами на уровне VM с другой - межкомпонентное взаимодействие происходит без участия сети, что ускоряет запросы.
Введение
Микроядерная архитектура подразумевает разделение функционала приложения на множество плагинов, каждый из которых гарантирует расширяемость, обеспечивает изоляцию и разделение функционала. Предполагается разделение компонентов на два типа: ядро и плагины. Ядро содержит минимальную функциональность, необходимую для работы системы, а логика приложения разделена между плагинами. При этом ожидается, что взаимодействие между плагинами будет сведено к минимуму, это позволит улучшить изоляцию каждого компонента, что улучшит тестируемость и упростит сопровождение.
При этом ядру системы требуется информация о запущенных модулях и способах взаимодействия с ними. Наиболее распространенный подход для решения этой задачи лежит через организацию реестра плагинов, включающий в себя информацию о названии плагина и доступных интерфейсах.
Данный паттерн может быть реализован с использованием совершенно разных технологий. Например, мы можем выделить ядро и подключать плагины через динамическую загрузку jar файлов без дополнительной изоляции.
OSGI предлагает подход к изоляции плагинов с помощью разделения кода для каждого плагина на уровне загрузчика классов. Каждый плагин может загружаться отдельным загрузчиком, тем самым обеспечивая дополнительную изоляцию. Недостаток такого решения в потенциальных конфликтах классов: одинаковые классы, загруженные с помощью разных загрузчиков, не могут взаимодействовать.
В качестве высокоуровневого решения можно рассмотреть Apache Karaf, который позиционирует себя как Modulith Runtime и предоставляет интеграцию с основными фреймворками: JAX-RS и Spring Boot. Данный инструмент упрощает взаимодействие с OSGI технологией, предоставляя высокоуровневые абстракции.
В качестве альтернативных вариантов можно рассмотреть непосредственные реализации 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 зависимостей.
Каждый плагин содержит в себе 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, аналогичное описанному в проекте.
А был ли у вас опыт реализации микроядерной архитектуры? Как вы решали данную проблему?