Message Только закончив проект
вы обладаете полноценным знанием,
как его надо было реализовывать.

(С) Том Демарко

Мне всегда ходелось попробовать написать приложение, модули которого между собой общаются с помощью обмена сообщениями. В принципе, это вполне даже в духе классического понимания ооп его основоположниками. Однако до Erlang я не дорос и знаком только с Golang, поэтому именно на нём и попробовал создать немного причудливую, но тем не менее любопытную архитектуру web-приложения.

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

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

Хорошо, если всё именно так, как я и говорю, то что собственно говоря дальше? Монолит — не монолит, а в чём профит? (последняя фраза оказалась в рифму, нечаянный сюрприз!). Тут я прямо как кэп, скажу, что сервис, который достаточно легко выделить из монолита, при необходимости, можно выделить из монолита. Выделенный сервис можно назвать микросервисом, чем он по большому счёту и будет. Зачем выделять сервис в микросервис? Это важный вопрос. В обычных условиях это не требуется, но если к примеру, этот сервис вдруг стал настолько тяжёлым, что ему самое время переместиться на собственный сервер? Тогда — возможно.

Впрочем, вопрос выделения сервиса в микросервис достаточно гипотетический, а вот желание поиграться с сообщениями и пощупать их — вполне реальное )) Хотя ММОА можно назвать фреймворком (скорее микрофреймворком), тем не менее, для меня это во многом скорее библиотека или тулкит с реализацией некой концепции или скажем прямо, гипотезы. Если я для избежания тавтологии, возможно, я иногда буду называть ММОА по разному, прошу понять и простить.

Википедия о фреймворках
Фре?ймворк (иногда фреймво?рк; англицизм, неологизм от framework — каркас, структура) — программная платформа, определяющая структуру программной системы; программное обеспечение, облегчающее разработку и объединение разных компонентов большого программного проекта.

Общие принципы

MMOA

Концепция ММОА следующая: разрабатываются достаточно независимые сервисы (для этого фреймворк я разделил на несколько библиотек), эти сервисы объединяются в единое приложение и в процессе работы обмениваются данными между собой посредством отправки сообщений. Адрес доставки сообщения состоит из названия сервиса и темы сообщения. (Темы — это по сути события events в сервисах, однако в данном случае для сообщений я предпочёл называть их именно так).

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

Быстрый старт


MMOA service В составе дистрибутива имеется готовый пример, наглядно демонстрирующий работу ММОА на примере простого сайта, по старой моей традиции, посвящённого латиноамериканскому танцу «румба». Перейдите в папку examples, скомпилируйте и запустите приложение. Результат работы смотреть в браузере по адресу localhost. В качестве роутера я использовал свою разработку Bxog, но можно использовать любой роутер на ваш выбор. Шаблонизация выполняется штатной библиотекой html/template.

Подробности


В демонстрационном приложении создано два сервиса:

  • article — сервис статей. Чтобы не усложнять пример работой с СУБД, статьи здесь хранятся в обычном текстовом файле, который распарсивается при загрузке приложения. По запросу Record сервис отдаёт название и текст статьи, ничего сложного. Также у него есть поддерживаемая тема List, которая отдаёт список имеющихся в базе статей.

  • menu — этот сервис должен отдавать массив id — название, т.е. список статей. Но поскольку у статей есть собственный сервис, то menu запрашивает список у article и по его получению отправляет ответ в агрегатор от своего имени. Это решение (не самое оптимальное по производительности), призвано показать взаимодействие сервисов между собой. Первоначально я хотел просто хардкорно положить в этот сервис массив ключей-значений и отдавать его по запросу, но это было бы совсем не интересно.


Состав MMOA


Для удобства и простоты создания сервисов в составе приложения некоторые части ММОА выделены в отдельные библиотеки.

tools


Эта библиотека требуется при создании как приложения, так и сервисов.

  • config.go — файл содержит в себе используемые в приложении типы, настройки таймеров и статусы
  • message.go — основная и единственная единица обмена информацией между сервисами, содержимое сообщения хранится в MsgCtx, всё остальное является конвертом.
  • themes.go — структура с перечислением всех сервисов в приложении и принимаемых ими тем сообщений. Структура создана специально для удобства (IDE не даст написать имя несуществующего сервиса или не поддерживаемой им темы сообщения).
  • exchanger.go — в этом файле хранятся все структуры данных, в формате которых сервисы могут обмениваться данными между собой

service


Эта библиотека использована при создании служебных сервисов и обязательна при написании пользовательских сервисов.

  • logger.go — простая библиотека для вывода логов в консоль
  • service.go — основа сервиса, слушает входной порт, если для сообщения есть подходящий waiting, определяет его туда, если нет, пытается вызвать метод, закреплённый за темой сообщения, если такового нет, отправляет сообщение в корзину.
  • waiting.go — хранит в себе агрегат, ожидающий прибытия сообщений, если аггрегат наполнен, он отправляется в нужный метод, если агрегат устарел, он отправляется в корзину, а waiting удаляется.
  • aggregate.go — накапливает сообщения с заданным CID (correlation identifier), после добавления сообщения отдаёт количество ещё ожидаемых сообщений.

support


Эта библиотека содержит в себе служебные сервисы агрегатора и корзины, а также шину для пересылки сообщений.

  • aggregator.go — это аналог waiting с той разницей, что здесь агрегаты накапливают сообщения для отправки в хэндлер, и в отличие от waiting агрегаты сюда приходят от хэндлеров.
  • trash.go — корзина, сюда присылаются сообщения с неправильным адресом, некорректные, а также с просроченными агрегатами.
  • bus.go — шина принимает сообщения и пересылает их в каналы адресатов. Если адресат отсутствует, сообщение отправляется в корзину.

Корень приложения


Эти файлы являются ядром ММОА, и вне его не используются.

  • cid.go — correlation identifier, идентификатор корреляции, помогающий сервисам идентифицировать сообщения.
  • handler.go — обработчик запроса, создаёт агрегат для запроса и отправляет сообщения с запросами в нужные сервисы.
  • controller.go — проводит первоначальную инициализацию приложения, создаёт шину и служебные сервисы — агрегатор и корзину.
  • view.go — отвечает за шаблонизацию. Хранит шаблоны для ответов сервисов и для страницы.

Как добавить свой сервис


За кучей вышесказанных слов может быть весьма непонятно, насколько удобно/неудобно использовать описанную ММОА концепцию, поэтому на простом примере опишу процесс добавления нового сервиса. Например, нам захотелось, чтобы на странице всегда выводилась дата.

Обновляем список сервисов и тем


В tools/services_themes.go добавляем структуру

// ThemeCalendar structure
type ThemeCalendar struct {
	Date TypeTHEME
}

в структуру Themes добавляем строку Calendar ThemeCalendar, а в структуру ListServices строку Calendar TypeSERVICE

Создаём файл сервиса


Создаём каталог example/calendar и в нём файл calendar.go со следующим содержимым:

package calendar

// Monolithic Message-Oriented Application (MMOA)
// Calendar
// Copyright  2016 Eduard Sesigin. All rights reserved. Contacts: <claygod@yandex.ru>

import (
	"time"

	"github.com/claygod/mmoa/service"
	"github.com/claygod/mmoa/tools"
)

func NewServiceCalendar(chIn chan *tools.Message, chBus chan *tools.Message) *ServiceCalendar {
	the := tools.NewThemes()
	s := &ServiceCalendar{
		service.NewService(the.Service.Calendar, chIn, chBus),
	}
	s.MethodWork = s.Work
	s.setEvents()
	s.Start()
	return s
}

type ServiceCalendar struct {
	service.Service
}

func (s *ServiceCalendar) setEvents() {
	s.Methods[s.The.Calendar.Date] = s.dateEvent
}

func (s *ServiceCalendar) dateEvent(msgIn *tools.Message) {
	t := time.Now()
	msgOut := tools.NewMessage().Cid(msgIn.MsgCid).
		From(s.Name).To(msgIn.AddsRe).
		Theme(msgIn.MsgTheme).
		Field("Day", t.Day()).
		Field("Month", int(t.Month())).
		Field("Year", t.Year())
	s.ChBus <- msgOut
}

Шаблонизация


В каталоге example/data создаём файл date.html со строкой {{.Day}}.{{.Month}}.{{.Year}}, а содержимое общего шаблона страницы меняем на:

<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" href="file/twocolumn.css">
</head>
<body>
	<div id="header"><h1>Rumba</h1></div>
	<div id="sidebar">
	{{.Sitemap}}
	
	{{.Date}}	
	</div>
	<div id="content">
	{{.Record}}	
	</div>
</body>
</html>

Правим приложение


Теперь нам осталось добавить новую библиотечку в файлы импорта, создать для неё канал, создать новую структуру и добавить строку в инициализацию хэндлера:

package main

// Monolithic Message-Oriented Application (MMOA)
// Application
// Copyright  2016 Eduard Sesigin. All rights reserved. Contacts: <claygod@yandex.ru>

import (
	"github.com/claygod/Bxog"
	"github.com/claygod/mmoa"
	"github.com/claygod/mmoa/example/article"
	"github.com/claygod/mmoa/example/calendar"
	"github.com/claygod/mmoa/example/menu"
	"github.com/claygod/mmoa/tools"
)

const chLen int = 1000

func main() {
	chBus := make(chan *tools.Message, chLen)
	chMenu := make(chan *tools.Message, chLen)
	chArticle := make(chan *tools.Message, chLen)
	chCalendar := make(chan *tools.Message, chLen)

	the := tools.NewThemes()
	app := mmoa.NewController(chBus)

	sm := menu.NewServiceMenu(chMenu, chBus)
	app.AddService(sm.Name, chMenu)
	sa := article.NewServiceArticle(chArticle, chBus)
	app.AddService(sa.Name, chArticle)
	sc := calendar.NewServiceCalendar(chCalendar, chBus)
	app.AddService(sc.Name, chCalendar)

	hPage := app.Handler("./data/template.html").
	ContentType("text/html; charset=UTF-8").
	Service(tools.NewPart(sm.Name).Theme(the.Menu.Sitemap).Template("./data/sitemap.html")).
	Service(tools.NewPart(sa.Name).Theme(the.Article.Record).Template("./data/record.html")).
	Service(tools.NewPart(sc.Name).Theme(the.Calendar.Date).Template("./data/date.html")).
	StatusCodeOf(the.Article.Record)

	m := bxog.New()
	m.Add("/:id", hPage.Do)
	m.Start(":80")
}

Производительность


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

  • ab -n 10000 -c 1 --> 3127 r/s
  • ab -n 30000 -c 100 --> 6373 r/s

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

  • BenchmarkOnlyArticle-4 50000 24722 ns/op
  • BenchmarkArticleAndMenu-4 30000 43404 ns/op
  • BenchmarkOnlyArticleParallel-4 100000 13831 ns/op
  • BenchmarkArticleAndMenuParallel-4 100000 20752 ns/op

Что было/могло бы быть


При желании можно добавить приоритеты в сообщения. Кстати говоря изначально они были, но я посчитал их функционал преждевременным. Можно «наружу» помимо сервисов вынести и хэндлер, попутно избавившись от контролёра. Такой вариант я тоже пробовал, но посчитал, что пусть лучше в ущерб гибкости всё лежит внутри и не мозолит глаза. Хэндлер вообще стоило бы упростить и дать ему в руки сплиттер. Были и другие идеи, но иногда хочется просто остановиться.

Заключение


Скорей всего в ММОА многие читатели что-то знакомое: паттерны, микросервисы, SOA, MQ и т.д. Это хорошо и смею вас уверить, ММОА не претендует на ниспровержение или присваивание себе чужих лавров. Это только инструмент, идеи которого возможно, вас заинтересуют. От себя добавлю только одно ИМХО — во многом ММОА написан под влиянием Golang, который я считаю вполне интересным и весьма подходящим для разработки самых разных приложений, и авторам языка большое спасибо за их труд.

Ссылки


Github
Поделиться с друзьями
-->

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


  1. gearbox
    09.12.2016 11:14
    +2

    >Сервисы получают и отправляют сообщения, и это единственный способ, которым они взаимодействуют друг с другом. В этом конечно есть значительный оверхед — вызвать метод с передачей сообщения будет по всякому быстрее, чем слать их через каналы и шину.

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

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

    Далее — об ограничениях языка. При программировании мы как правило упираемся в то что программа разбита на какие то части, каждая хранится в отдельном файле. В нашем случае это рудимент, который мешает программить. Почему? Потому что именно стремление рассовать все по файлам и приладить древовидную структуру приводит нас к модулям и прочему. Оно неплохо само по себе но есть другой путь.
    Давайте рассмотрим разработку программы с потоков (flow) — именно так, как ее обычно обсуждают с бизнесом. То есть у нас есть определенные диаграммы состояний и потоков, и вот эти диаграммы с точки зрения приложения, работающего на обмене сообщений — и есть исходный код программы.
    То есть мы задаем агентов, задаем связи между ними, на каждую связь указываем формат сообщения (или он может браться из описания агента) и указываем тип транспорта (вызов метода, tcp, rabbit-agent, да все что угодно) — и компиляем.
    И вот тут самый смак. То, что мы сейчас ковыряем и называем исходниками — должно быть результатом компиляции таких диаграмм. То есть это что то вроде DITA — набор кусков кода (описыващих агентов) и набор правил, которые собирают агентов в модули и прикручивают к ним транспорт.
    Знаю что запутанно, попробую пояснить на конкретном примере.
    Стандартный сервис — клиент — бекэенд — база
    на клиенте пишем на js, на бэке пусть будет php и база скажем postgresql, весь интерфейс на хранимках, две схемы — паблик и бэк.
    Рассмотрим стандартный запрос к апи
    клиент делает какой ни будь fetch, php делает какой нибудь odbc->query() а sql отрабатывает какую либо хранимку.
    В обычном проекте это будет лежать в трех локациях по десятку файлов. Как это могло бы быть?
    В одном файле

    process some_api_request tags api{
        agent client: javascript{
            fetch бла бла
        }
        agent back: php{
            odbc->query(); бла бла
        }
        agent db: plpgsql tags schema=public{
            create function бла бла
        }
    }
    

    и много таких описаний на каждый процесс. Да, и это пример без траспорта, так еще планируется что можно задавать связи между агентами с указанием формата сообщений.
    а потом собираем:
    build client.js as select all where agent=client; или что то в этом роде
    build api.php as select all where process has tag api AND agent=back;
    build db_public.sql as select all where agent=db and tag schema=public;
    

    То есть на этапе проектировании мы имеем весь код процесса в одном месте. А после сборки мы растасовываем код по контекстам. DITA в чистом виде но для программирования. Один агент естественно может участвовать в разных процессах (можно описать его в одном месте а в описании процессов просто инклудить)
    Из одного набора агентов разными правилами сборки можно собирать разные версии софта (для разных сред)
    Поменять обмен сообщениями с tcp на rabbit можно просто указав другой транспортный агент.
    подходящие под реализацию архитектуры — тот же веб, значительная часть сетевых прилад, расширения броузера (контент скрипты + бэк скрипт + попап — все обмениваются сообщениями + они могут обмениваться с сервером)
    В общем есть куда применять )

    Плюс к этому уже можно прикручивать gui и получить довольно серьезную и удобную среду разработки.
    Один не потяну, потому и забил. Искал готовое — чет не нашел. Появятся интересующиеся — велкам.


  1. claygod
    09.12.2016 11:29

    gearbox, спасибо за интересный комментарий. Есть у Вас к нему какие-то схемы?


    1. gearbox
      09.12.2016 12:47

      признаться — большая часть в голове. Есть конечно наброски, черновики, даже работающий код, но тут какое дело — это все мааааленькая часть глобального плана по захвату мира и из этой мешанины вычленить именно модель сборки прийдется потратить время. В общем я потихоньку сейчас вычленяю другую идею, собираюсь оформить в виде статьи, возможно после этого займусь и сборкой. где то через месяц, не раньше )
      А так, на поинтриговать — то что я тут сумбурно описал — это одно из проявлений семантического git-а. Есть еще одна интересная (на мой взгляд) абстракция — это оформление декораторов в виде стилей. То есть применив к коду различные наборы стилей (декораторов) получаем код для разных окружений. Тоже в канве метапрограммирования и тоже имеет свои области применения. (да, абстракцию противного веба с html+css можно использовать в няшном «код+декораторы» стиле)
      А готовлю сейчас другую тему — сквозную сборку проекта (клиент, бэк и db api) на основе деклараций (swagger и sql описания функций) то есть один раз указав спеку получаем автоматически сгенеренный client.api модуль на клиенте, каркас бэка (там остается только модулями хандлеры запросов описать) и сгенеренный для бэка клиент к базе (на основе sql дефиниций) Все на тайпскрипте, с контролем типов и все такое, но вообще можно брать любой язык — это mustache шаблоны, можно любой язык метапрограммить.
      Но, как и все порядочные программисты я жутко ленив. Хорошо зажигаюсь сам, хорошо зажигаю других, но долго и качественно гореть отказываюсь. Поэтому решил понемногу оформлять идеи в статьи и делать вбросы. Если даже не стрельнет — можно хоть пообсуждать с интересными людьми.


      1. claygod
        09.12.2016 13:22

        Я бы сказал, что увязывать клиента+бэк+бд не так просто… И про декораторно-стилевой паттерн я бы тоже почитал, так что подкиньте в топку полешек ))


        1. gearbox
          09.12.2016 14:55

          >Я бы сказал, что увязывать клиента+бэк+бд не так просто…
          ну это как раз самая готовая часть. В том смысле что оно работает, хоть и не весь функционал реализован) Там из интересного в принципе сама идея и генерация клиента для базы — на основании sql описания базы генерятся typescript интерфейсы и клиент к базе в виде sql.user.login(), sql.user.logout() (у меня бэк на ноде). Да, используется родной парсер postgresql https://github.com/lfittl/libpg_query, там есть бандл для ноды.
          Так как каркас бэка сгенерен (причем вместе с санитизацией входных параметров на основе swagger спеки) — то остается только прописать хандлеры, а для них уже есть готовый клиент для работы с базой. И для этого клиента есть описанные интерфейсы, то есть работает контроль типов, что в этом случае правильнее (на мой взгляд) назвать контрактами.
          В общем не переключайтесь, в течении месяца рассчитываю добить статью )


          1. claygod
            09.12.2016 15:47

            Ок, если что, напишите мне, почитаю. Похоже, вам дополнительное удобство добавляет JS, который и тут и там (известное удобство ноды — один язык с фронтэндом). Не забудьте про схемы, лучше 1 раз увидеть, чем 100 раз услышать.


            1. gearbox
              09.12.2016 16:01

              >Не забудьте про схемы, лучше 1 раз увидеть, чем 100 раз услышать.

              Вы про sql schema или json?


              1. claygod
                09.12.2016 16:03

                Нет, я про графические схемы, иллюстрации к Вашей работе


                1. gearbox
                  09.12.2016 16:05

                  ) Мда, то самое чувство когда хочется покраснеть.