О чём эта статья

  • Как появился фреймворк и его архитектурные решения из прикладных задач и проблем.

  • Краткое описание архитектуры, части функционала (многопоточный монолит с микросервисной дисциплиной внутри с поддержкой создания системы по сети).

  • Это не велосипедная замена промышленным Kafka или RabbitMQ.

? Содержание

  1. Эволюция через проблемы

  2. Архитектура (Основное)

  3. Отказоустойчивость

  4. Сеть и федерация

  5. Передача сообщений

  6. Применение

  7. Развитие

Эволюция через проблемы

Всё началось с задачи мониторинга сетевой инфраструктуры. Я писал лёгкую программу‑скрипт python: результаты проверок складывались в SQLite, интерфейс был на Tkinter, там же — таблицы, графики ICMP и самописная карта‑схема сети.

С ростом сложности поддерживать монолит становилось всё труднее: любое изменение в одной части тянуло за собой правки в другой. Тесные взаимосвязи не давали развивать программу. Open Closed и другие не примененные принципы проявились. Принято решение переписать скрипт используя модульность и развивая функциональность.

Проблемы и решения уровня контекста (C4 Level 1–2)

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

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

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

Так я пришёл к архитектуре многопоточного монолита с изолированными модулями‑акторами и асинхронным обменом сообщениями.

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

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

В ходе работы возникали задачи и проблемы на уровне системы (C4 Level 3–), например:

Проблема: Что если прикладному программисту не нужен секретарь и сообщения, но модуль нужно разместить в потоке с логгером, или нужны другие комбинации?
Решение: Создание типов юнитов, от которых зависят процесс сборки, поведение юнитов и работа агентов потоков.

Проблема: Есть юниты, которые не нужно крутить в цикле. Как оптимизировать и не тратить ресурсы впустую?
Решение: Использование объектов Event и ввод типов потоков с поддержкой гибридно‑адаптивной гибернации.

Так же возникали задачи и проблемы на уровне компонентов (C4 Level 3–), например:

Проблема: Как отбрасывать TCP/UDP‑пакеты от других программ?
Решение: Введение магических байтов фреймворка в начало пакета.

Проблема: Что если юниту пришла редкая команда, требующая долгой работы, и система ложно «обнаружит» зависание?
Решение: Ввести возможность динамического изменения предупреждения о предстоящей долгой работе.

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

? Вернуться к содержанию

Архитектура (Основное)

Общая схема архитектуры sanapo
Общая схема архитектуры sanapo

Ядро (Kernel) — центральный компонент платформы (Orchestrator), создаёт все системные сервисы, службы, компоненты, фабрику юнитов. Управляет жизненным циклом юнитов, потоков и слоёв.

Фасад ядра (KernelUserView) — интерфейс для прикладного программиста для вызова методов ядра.

Модуль (наследники BaseModule) — класс, написанный прикладным разработчиком. Это бизнес‑логика приложения. Модуль наследуется от базового класса платформы (BaseModule) и не должен думать о потоках или гонке состояний — за него это делает платформа.

Секретарь (Secretary) — сервис юнита, предоставляющий модулю интерфейс для асинхронного взаимодействия. Секретарь реализует паттерны Mediator, Messenger и Command/Event. Он отвечает за отправку и получение команд, событий и рапортов, отслеживает дедлайны выполнения команд, автоматически уведомляет об изменении состояния системы, поддерживает подписки на сообщения.

Адрес (Address) — адрес для отправки и получения сообщений. Состоит из двух частей system_name:unit_name, где первая часть одинакова для всех юнитов в одном запущенном приложении.

Манифест (Manifest) — паспорт юнита, описывающий его публичность, версию, роль и теги. Манифест позволяет находить адреса юнитов в других системах по смысловым признакам.

Юнит (Unit) — это изолированная вычислительная единица внутри одного процесса, аналог микросервиса по уровню изоляции и независимости. Юнит содержит модуль прикладного кода и предоставляет ему набор сервисов платформы. Он живёт в потоке и принадлежит слою, имеет логгер, интерфес платформы, и, как правило, уникальный адрес, секретаря и манифест. Существует несколько типов:

  • TIKABLE — с секретарем и регулярным вызовом итерации работы

  • ZOMBIE — с секретарем, который вызывает коллбеки модуля

  • SIGMA — без секретаря, адреса, манифеста, но с регулярным вызовом итерации работы

  • UTILITY — для модуля требующего только логгер и возможность быть в слое, потоке.

Брокер сообщений (MessageBroker) — центральный маршрутизатор всех сообщений с центральной шиной. Оперирует уникальными адресами юнитов и ведёт реестры адресов юнитов и систем sanapo, манифестов и адаптеров транспорта к юнитам и системам. Осуществляет юникаст для команд и рапортов и мультикаст для событий.

Потоки и агенты (Threads & Runners) — юниты распределяются по потокам операционной системы. Каждым потоком управляет его менеджер, который запускает агент‑раннер — цикл, вызывающий метод step() у юнитов и их секретарей в зависимости от типов и состояний юнитов. Платформа поддерживает три типа потоков для возможности оптимизации нагрузки на CRU.

Слои и загрузчик (BootMaster) — юниты объединяются в слои (Layers). Слой — группа юнитов, которые стартуют и останавливаются вместе в программе как единое целое.

Сторожевой пёс (WatchDog) — механизм отказоустойчивости (Supervisor). WatchDog следит за тем, чтобы ни один поток не зависал дольше допустимого времени.

Сетевой слой: федерация — несколько экземпляров sanapo могут объединяться в единую распределённую сеть (Federation). Для обнаружения соседей используется протокол UDP‑маяков (Beacon), поддерживается мануальное соединение.

Имеются так же некоторые компоненты в нужде которых сомневаюсь:

  • адаптер транспорта e‑mail — юниты могли бы получать сообщения поверх e‑mail от других систем — слишком медленно;

  • визуализатор загрузки и остановки программы — сомневаюсь в нужде, неиспользуется;

  • инструмент интернационализации i18n — объект передается в каждый юнит, так же логгер поддерживают эту функцию. Возможно, избыточный, функционал в рамках платформы.

Буду рад услышать комментарии или сообщения в личку по поводу лишнего и подходящего функционала.

? Вернуться к содержанию

Отказоустойчивость

Система многопоточная, а модули могут быть написаны разработчиками с разным уровнем квалификации. Если модуль написан недостаточно аккуратно, он может зависнуть. Чтобы один такой модуль не положил всю программу и конечный пользователь не обнаружил, что система не работала несколько дней, я встроил в платформу два уровня защиты: сторожевого пса и функционал в загрузчик.

Загрузчик (BootMaster). Схема запуска:

  • Запуск программы → запуск слоёв по порядку → запуск потоков → запуск юнитов

На каждом этапе объектам присваиваются состояния, которые проверяются перед переходом дальше. При неудаче запуска перезапуск, при неудаче перезапуска расширяет зону перезапуска:

  • Модуль → юнит → поток → слой → система (одна попытка, с игнорированием проблем)

Такая эскалация — по сути, аналог Graceful Degradation и Retry Policy с возрастающей областью восстановления.

Сторожевой Пёс (WatchDog). Схема работы:

Каждый модуль с псевдоциклом имеет метод step, который вызывает агент потока. В конце каждой итерации агент обновляет время последнего завершённого цикла. Поток вычисляет допустимую задержку итерации на основе допустимых задержек всех юнитов в нём.

Формула расчёта дапустимого таймаута потока:

step_timeout = max(сумма_таймаутов_юнитов × k, макс_таймаут_среди_юнитов) k = 0.3 + 0.7 × (0.8^(n - 1))
n — количество юнитов в потоке.
Коэффициент k экспоненциально затухает с ростом числа юнитов, но не даёт суммарному таймауту упасть ниже макс_таймаут_среди_юнитов.

Раз в секунду WatchDog проверяет все потоки на зависания. Чтобы не запустить ложное восстановление, сначала проверяет:

  • не находится ли поток в режиме гибернации;

  • не истекает ли таймаут на следующем шаге.

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

  • пересоздаётся модуль → юнит → поток.

Этот механизм близок к Health Check + Circuit Breaker из мира распределённых систем, только реализован внутри одного процесса. Вместо внешних оркестраторов вроде Kubernetes с его Liveness Probe — встроенный простой мониторинг.

? Вернуться к содержанию

Сеть и федерация

Когда одного процесса становится мало, встаёт вопрос выхода на несколько машин.

Изначально я планировал сделать урезанную версию платформы для агентов. Но по мере роста функциональности стало понятно: вырезать много кода без потери совместимости не получится, а полная платформа и так остаётся лёгкой. Поэтому от отдельной «агентской» версии отказался. Я внедрил функционал встроенной поддержки сети и сообщений по сети между процессами.

Состав TCP-пакета
Состав TCP‑пакета

Системы могут включать UDP‑маяки (Beacon), чтобы быть обнаруженными другими системами sanapo, и могут слушать маяки, чтобы находить соседей. После присоединения к другой системе маяки и сканеры могут автоматически выключаться — в зависимости от конфигурации.

UDP‑маяк периодически рассылает широковещательные пакеты:

  • сначала часто — для быстрого обнаружения;

  • потом реже — для поддержания присутствия и чистоты эфира.

UDP‑слушатель принимает пакеты маяков и:

  • проверяет магический заголовок и токен проекта;

  • игнорирует собственные маяки;

  • может игнорировать пакеты других проектов на фреймворке sanapo (в зависимости от конфигурации)

  • если разрешено конфигом, автоматически инициирует TCP‑подключение.

TCP‑соединения. После обнаружения соседа начинается установка соединения через двойное рукопожатие, затем поддерживается Heartbeat для отслеживания связи и реакций систем на ее разрыв. Токен защищает от атак повторного воспроизведения и других проблем.

Федерация и маршрутизация — после установления соединения брокер регистрирует на каждую подключенную систему TCP соединение в TCP сервисе платформы и TCP‑Adapter‑Transport. Манифесты новой системы сохраняются в реестре, все локальные юниты уведомляются.

Отваливание соединения — при закрытии или обрыве логической связи удаляется федеративный маршрут, информируются юниты. Согласно конфигурации могут включиться UDP‑маяк и сканер маяков.

Шифрование — тело пакета шифруется симметричным XOR, ключ — SHA-256 от пароля из конфигурации. Это не стойкое шифрование, а скорее архитектурный карман, обкатка.

? Вернуться к содержанию

Передача сообщений

Юниты — это отдельные самодостаточные вычислительные единицы, но им нужно обмениваться сообщениями.

  • на системные сообщения юнит подписывается на этапе сборки в фабрике ядра

  • на события и команды юнит подписывается выделенными методами секретаря

  • на рапорта юнит подписывается временно в момент отправки команды

Виды событий и команд — забота программы, системные и рапорта — платформы.

Существуют следующие типы рапортов: «взято в работу», «отказ: неизвестный адрес», «отказ: неизвестная команда», «отказ: модуль занят», «отказ: неверные данные», «выполнено», «нужно больше времени». Рапортами на команду могут отвечать брокер, секретарь получателя, получатель. Рапорт занятости зависит от поддержки модулем своей внутренней многопоточности.

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

При стандартном темпе итераций циклов время на локальную отправку команды и получения в ответ рапорта занимает 80–130мс, по сети в другую систему — 110–170мс. Локальные показатели считаю слишком долгими, рассматриваю как одну из главных задач для развития платформы.

В каждом сообщении есть поле payload — словарь с полезной нагрузкой с данными любого типа для локальной передачи и данными сериализуемых типов для сетевой передачи. В случае попытки отправки сообщения данными пользовательских типов в другую систему по сети модуль получит рапорт типа CANT_DO подтипа NOT_IMPLEMENTED. Такая реализация мне не нравится: здесь требуется доработка в виде ограничения или расширения функционала для унификации, либо более четкий компромисс, но пока так и работает.

Локальная передача сообщения:

Состав TCP-пакета
Локальная передача сообщения

Передача сообщения между системами:

Состав TCP-пакета
Передача сообщения между системами
Передача сообщения между системами (UML:sequenceDiagram):
Состав TCP-пакета
Передача сообщения между системами

? Вернуться к содержанию

Применение

Первое практическое применение фрейворка — система мониторинга и анализа сети. Она использует все основные механизмы платформы: модули, шину, секретарей, потоки, слои, вотчдог и сетевую федерацию.

Платформа подходит для небольших и средних производственных задач, где важны:

  • максимально простое развёртывание, в идеале одной командой в консоли;

  • отсутствие или минимальное наличие внешних зависимостей;

  • низкий порог входа для разработчиков;

  • устойчивость к зависаниям отдельных модулей;

  • работа на обычных машинах с Linux или Windows;

Что можно собирать на платформе:

  • системы диспетчеризации и контроля: датчики, станки, камеры, графики, алерты;

  • локальные интеграции: существующие сервисы, облака, БД, месседжеры;

  • внутренние сервисы: чаты, объявления, скрам‑доски, журналы смен, чек‑листы;

  • вычислительные задачи: обработка данных, расчёты, агрегация;

  • работу с нейросетями: детекции, подсчёт изделий, распознавание маркировок;

  • предоставление API наружу.

? Вернуться к содержанию

Развитие

Сейчас sanapo — это однопроцессный многопоточный монолит с поддержкой федерации. Но архитектура позволяет развивать его в сторону распределённости без переписывания ядра.

Что можно развивать дальше:

  • Интеграция с внешними шинами. Интеграция с Kafka, RabbitMQ, MQTT и др. на уровне платформы или заготовить юниты‑шлюзы.

  • Дополнительные транспорты. WebSocket, HTTP — по аналогии с уже существующим TCP‑транспортом.

  • Процессы. Разнос юнитов по процессам, а не только по потокам.

  • Перенос юнитов. Можно добавить полноценную возможность уложить юнит спать, заморозить секретарь, перенести юнит в другой поток или процесс и разбудить его там (сейчас возможно в рамках потоков уничтожать и собирать с теми же параметрами).

  • Балансировка и реплики юнитов. Добавление сущности балансировщика и распараллеливание юнитов репликами.

  • Асинхронное шифрование. Сейчас трафик защищён лёгким XOR.

  • Интерактивный интерфейс. Управления юнитами: старт, стоп, сон, пробуждение, перенос между потоками и процессами.

Вопросы сообществу

Сейчас я стою на развилке: вкладываться ли в проект как публичный проект дальше и, если да, то в каком направлении?

Пара слов о моем опыте и целях

Это не просто пет‑проект ради строчек кода. Это реализация решений главных проблем встречающихся на протяжении 15+ лет моей работы в ИТ. За эти годы я прошел путь от инженера до ИТ‑директора и теперь ищу простор в должностях архитектора ПО/решений/ситемного или CIO. Мне всегда приходилось решать задачи, где готовые вендорские решения либо не подходили по бюджету, либо просто не выдерживали суровых условий эксплуатации.

? Вернуться к содержанию

Спасибо за внимание и время!

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