Pull to refresh

Monolithic Message-Oriented Application (MMOA)

Reading time 9 min
Views 5.6K
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
Tags:
Hubs:
+10
Comments 9
Comments Comments 9

Articles