Monolithic Message-Oriented Application (MMOA)

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").
Сервіс(tools.NewPart(sm.Name).Theme(the.Menu.Sitemap).Template("./data/sitemap.html")).
Сервіс(tools.NewPart(sa.Name).Theme(the.Article.Record).Template("./data/record.html")).
Сервіс(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
Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.