Baratine сервер для микро-сервисов — одна из самых необычных платформ над которой мне довелось работать. В основе дизайна этого сервера лежат несколько дополняющих друг друга принципов.
- Асинхронные сервис интерфейсы
- Выполнение вызовов сервиса в едином потоке
- Неразделённое владение данными
- Асинхронный Web
- Асинхронная платформа исполнения сервисов
Асинхронные сервис интерфейсы
Микро-сервисы в Baratine описываются интерфейсами. Интерфейс определяет операции предоставляемые сервисом. Особенностью асинхронного интерфейса является то, что методы интерфейса возвращают результат асинхронно, подобно объекту Future.
Например привычный интерфейс для операции оплаты кредитной картой может выглядеть следующим образом:
public interface CreditService {
PaymentStatus pay(int amount, CreditCard card);
}
Такой метод возвращает результат по факту проведения оплаты, а код использующий его, выглядит следующим образом:
CreditService _creditService;
PaymentStatus executePayment(int amount, Client client) {
return _creditService.pay(amount, client.getCreditCard());
}
Асинхронный интерфейс не возвращает результат, а заполняет Future–объект асинхронно:
public interface CreditService {
void pay(int amount, CreditCard card, Result<PaymentStatus> result);
}
Пользовательский код для такого интерфейса может выглядеть следующим образом:
CreditService _creditService;
void executePayment(int amount, Client client, Result<PaymentStatus> result) {
return _creditService.pay(amount, client.getCreditCard(), result.then());
}
Особенностью этот клиентского кода является то, что код передаёт свой Future-объект в конечный сервис Payment с помощью result.then().
В тех случаях когда клиенту нужно дополнительно обработать результат можно использовать лямбду, которая будет вызвана по заполнению результата:
void executePayment(int amount, Client client, Result<PaymentStatus> result)
{
_creditService.pay(amount,
client.getCreditCard(),
result.then(
status -> {
log(status);
return status;
}
));
}
На первый взгляд асинхронные интерфейсы могут показаться не совсем удобными, но такая организация кода позволяет быстро высвобождать потоки для выполнения следующих задач, а клиенты получают результаты по их готовности.
Выполнение вызовов сервиса в едином потоке
Микро-сервисы в Baratine выполняются в одном, выделенном этому сервису, потоке. Поток выделяется сервису сразу по появлению вызовов. В общем случае вызовы к сервису идут от множества клиентов. Вызовы помещаются в очередь и выполняются последовательно одним выделенным потоком.
В этом контексте следует отметить что сервисы должны быть написаны таким образом, чтобы не занимать поток ожиданием выполнения операций. Для этого используются Future–объекты типа io.baratine.service.Result (см. выше). Именно они позволяют перенести обработку результата вызова дорогих блокирующих операций в будущее.
Например, оплата с использованием чекового счёта может занять несколько часов, а пользовательский код инициации оплаты будет выполнен в реальном времени за доли миллисекунды.
CheckingService _checkingService = ...;
void executePayment(int amount, Client client, Result<PaymentStatus> result)
{
_checkingPayment.pay(amount,
client.getCheckingAccInfo(),
result.then(
status-> {
log(status);
if (status.isAppoved()) {
shipGoods();
} else {
handleFailedPayment(status);
}
}
));
);
}
В приведённом выше коде исполнение лямбды вызова then() будет отложено до возвращения _checkingService'ом результата оплаты, a метод executePayment() моментально становиться доступным для следующего вызова.
Выполнение в едином потоке положительно сказывается на производительности через сокращение числа смены контекстов и хорошее согласование с процессорным кэшем.
Неразделённое владение данными
Владение правами записи на мастер-копию — одна из отличительных особенностей архитектуры микро-сервисов на Baratine. Так как микро-сервис обрабатывает вызовы последовательно, а не параллельно, данные могут храниться в памяти единичного экземпляра (singleton) сервиса и всегда является последней, наиболее актуальной копией данных.
(В данном случае использование слова "копия" не совсем уместно и используется идиоматично).
Микро-сервис с данными имеет расширенный жизненный цикл, в котором, прежде чем сервис поступит в использование, Baratine выполнит метод сервиса с аннотацией @OnLoad или загрузит поля экземпляра из асинхронной объектной базы данных (Kraken) являющейся частью Baratine.
Микро-сервис подкреплённый данными может представлять пользователя системы следующим образом:
@Asset
public class User
{
@Id
private IdAsset _id;
private UserData _data;
}
В приведённом выше коде экземляр UserData с данными о пользователе будет загружен из Kraken.
Асинхронный Web
Для достижения быстродействия и лучшего сопряжения с асинхронными сервисами принцип асинхронности подчинил себе и выполнение Web запросов. Это достигается при помощи Future–объекта для ответа.
io.baratine.web.RequestWeb, подобно io.baratine.service.Result предоставляет возможность отложить заполнение ответа до тех пор, пока не будут готовы данные для ответа.
К примеру, код для запроса по протоколу REST может выглядеть следующим образом:
@Service
public class QuoteRestService
{
QuoteService _quotes;
@Get
public void quote(@Query("symbol") String symbol, RequestWeb requestWeb)
{
_quotes.quote(symbol, requestWeb.then(quote -> quote));
}
}
В приведенном выше коде метод quote() помечен аннотацией Get и это делает метод доступным для Web запросов. В Baratine платформе единственный экземпляр сервиса отвечает на все приходящие запросы в одном, отведённом для этого сервиса, потоке. В такой архитектуре производительность достигается быстрым возвратом из метода quote() с помощью делегирования операции по запросу конкретной quote сервису отвечающему за Quotes — QuoteService.
Асинхронная платформа исполнения сервисов
В процессе работы над платформой сама собою стала выкристаллизовываться тенденция распространения асинхронности на компоненты платформы. Таким образом все встроенные сервисы предоставляемые платформой являются асинхронными.
Так в результате разработки системы появились сервисы базы данных (Kraken), Scheduling, Events, Pipe, Web; и все они починились правилу тяготения к асинхронности.
Как одному из разработчиков этой системы мне было бы очень интересно узнать мнение хабра-сообщества о Baratine.
Комментарии (28)
tsabir
25.09.2016 05:15Работал с Play! Framework, там тоже все асинхронно, но когда сталкиваешься с синхронным/блокирующим интерфейсом на внешний веб-сервис, то непонятно что делать. Можно конечно вызвать его в отдельном треде, но не покидает ощущение что я просто прикрыл грязь ковриком :). Я пробовал тупо писать без использования jax-ws, но на код потом смотреть страшно. Вы что посоветуете?
Dobryj
25.09.2016 05:34Блокирующие интерфейсы действительно плохо сопрягаются с асинхронными. В Baratine блокирующий внешний web-service обертывают в асинхронный микро-сервис и аннотируют его аннотацией @Workers(). @Workers регулирует количество потоков для такого сервиса-обёртки.
@Workers(32) @Service public class MyJAXWSServiceWrapperImpl implements MyJAXWSServiceWrapper { public void execute(Result<Reply> result) { Reply reply = ...// obtain reply in a blocking call to external service result.ok(reply); } }
Таким образом код, вызывающий обёртку, будет соответствовать идиоме асинхронного кода.
tsabir
26.09.2016 08:52Ну если честно, это и есть то что я называю «спрятать мусор под коврик». Ведь вызов веб-сервиса по природе можно сделать асинхронно, хотелось бы иметь альтернативу jax-ws, которая работает асинхронно по умолчанию
fogone
25.09.2016 08:12Подскажите, а есть какой-то профит от передачи result в метод, вместо его возвращения (как это обычно с future делается)?
Dobryj
25.09.2016 08:23Передав Result следующему сервису метод выходит и освобождает микро-сервис для следующего вызова. Это соответствует второму принципу в списке.
Dobryj
25.09.2016 08:30Передача Result в следующий сервис необходима потому, что Result ещё не готов, а следующий сервис и должен его заполнить. Передав Result следующему сервису метод выходит и освобождает микро-сервис для следующего вызова.
Sirikid
25.09.2016 12:12Какие плюсы от использования первой формы из примера http://pastebin.com/SJgcahhG а не второй?
yahorfilipcyk
26.09.2016 04:12Я бы утверждал, что профита никакого. Из интерфейса не совсем понятно, кто за что отвечает. Вообще, некоторые принципы функционального программирования учат хорошему стилю. Например, не использовать изменяемые данные и писать "чистые" функции. Неследование этим принципам может добавить только головной боли. Не знаком с описанным в статье фреймворком, но кажется, что вполне можно было бы написать что-то подобное:
public interface CreditService { Result<PaymentStatus> pay(int amount, CreditCard card); }
Создание сущности
Result<PaymentStatus>
вне вызова метода, по-моему, только усложняет код. Методpay
должен быть в состоянии самостоятельно позаботиться о создании сущностиResult<PaymentStatus>
. И, кстати, такой подход лучше описывает интерфейс. Вполне очевидно, что в результате вызова метода мы получим отложенный результат и что метод сам по себе неблокирующий. Используя же в сигнатуреvoid
не говорит ни о чем.void
, вообще, должен только указывать на функции с побочными эффектами, что, в принципе, должно использоваться как можно реже.Dobryj
26.09.2016 09:15-1Асинхронный стиль программирования не вписывается полностью в привычные идиомы и правила, но с его развитием появятся дополняющие его идиомы и шаблоны.
В данном фреймворке создание Result<> клиентом является идиомой.
Так, например, в цепочке из двух сервисов где клиент вызывает сервис Foo, а сервис Foo вызывает сервис Bar создание сущности Result<> производится в коде клиента (RestHello) и выглядит верным подходом.
@Service public class Foo { @Service @Inject Bar _bar; public void foo(Result<String> result) { _bar.bar(result.then()); } } @Service public class Bar { public void bar(Result<String> result) { result.ok("Hello World!"); } } public class RestHello { @Service @Inject Foo _foo; @Get public void hello(RequestWeb request) { _foo.foo(request.then()); //request.then() создаёт сущность Result для передачи в foo и //ставит себя во главу цепочки принимающей результат. По готовности результата //RequestWeb отсылает результат удалённому клиенту } }
Кроме того, Result является функциональным интерфейсом и может быть использован в стиле лямбда выражения:
_foo.foo(request.then((value, r)->r.ok(value)));
ttim
26.09.2016 23:46Из асинхронности этот Result никак не следует. Зато возможности по работе с асинхронными значениями хорошо так убивает.
Посмотрите как можно сделать асинхронность:
https://twitter.github.io/finagle/guide/Futures.html
Sirikid
27.09.2016 00:06Грубо говоря, функциональный интерфейс это интерфейс с одним абстрактным методом.
Dobryj
27.09.2016 00:23Да, конечно. Иначе код просто не будет компилироваться. В JDK java.util.Comparator, java.util.function.BiConsumer и другие функциональные интерфейсы наряду с единственным основным абстрактным методом, предназначенным для имплементации, предоставляют множество вспомогательных 'default' методов.
relgames
27.09.2016 16:50Напоминает Akka (не только акторы, но и Future подход). Правда, привычней выглядит возврат Future, а не передача в сервис, что позволяет строить цепочки вызовов типа:
service.doSomething(param) .thenApply(result -> transform(result)) .thenAccept(result -> log.info(result));
xuexi
Из истории futures в javascript можно сделать вывод, что futures вводят в язык только для того, чтобы следом ввести в язык async/await и опять писать последовательный псевдо-синхронный код.
AndreyRubankov
Вот только Future в Java имеются еще с версии 1.5 (2004 год). И заменять на async/await, к счастью, не спешат, есть вещи по-важнее.
К сожалению, синтаксический сахар в языках программирования — это плохая идея. Как модная фича — это выглядит круто, но если смотреть на возможности дальнейшего развития языка, каждая языковая фича является якорем, который тянет на дно. Языки, в которых миллион клевых фич очень тяжело выучить, еще тяжелее развивать и поддерживать. И рано или поздно язык достигнет точки в которой он перестанет развиваться или выкинет обратную совместимость и кучу фич, которые у него были.
taujavarob
AndreyRubankov >И рано или поздно язык достигнет точки в которой он перестанет развиваться или выкинет обратную совместимость и кучу фич, которые у него были.
Пример? Было такое в истории? Или это гипотеза ваша — про выкидывания «кучи фич»?
AndreyRubankov
По правде говоря, в историю языков программирования я не сильно углублялся, может и есть. По большей части мой комментарий можно считать гипотезой. Но в подтверждение этой гипотезы есть несколько фактов.
В языках есть фичи, которые негласно считаются плохими: assert, goto, множественное наследование. Их не используют (если по-хорошему), но язык обязан их поддерживать.
Если взять С++, фич у него невероятно много, по-моему, его можно считать одним из самых сложных, если не самым. Да, он жив и он развивается, но кто сейчас захочет его изучать? Когда есть D, Rust, Go, которые предоставляют примерно те же возможности но на много безопаснее и проще, там где нужно залезть глубже можно взять чистый Си, который довольно простой.
taujavarob
Ясно, под «выкидыванием» вы очевидно подразумеваете неиспользование большинством программистом.
AndreyRubankov
Вы не на том делаете акцент. Основная идея в том, что фича в язык добавляется просто, а последствия этого добавления можно выгребать очень-очень долго, именно потому, что никто не выкидывает фичи из языков.
taujavarob
Последствия внедрения той или иной фичи в длительном времени в принципе предугадать нельзя. имхо.
Но сейчас к примеру начали применять такой цикл (в Javascript):
— Фича вводится не через Стандарт Языка, а через плагин (который транспалирует эту новую фичу в старый Стандартный код).
— Если фича в течении НЕ короткого времени (уж по крайней мере больше года) зарекомендовала себя положительно и ей многие с охотой пользуются, то тогда рассматривается вопрос о включении её в Стандарт Языка.
Такой алгоритм позволяет постоянно наращивать фичи в Языке и хоть как-то избегать «опасных фич» (страховаться от них), от которых захочется избавиться в Стандарте по крайней мере в ближайшее время.
AndreyRubankov
Да, это хороший подход, но и он не спасает от проблем. Время идет, техники меняются, то что раньше было спасением, сейчас плохая практика.
В случае с babel уже нету особо смысла в стандарт языка что-то добавлять. Хочешь использовать какую-то фичу — ставишь как плагин к babel, а дальше транслируешь в ES5. Смысл расширять стандарт, если все и так будет транслироваться в ES5?
На данный момент, я считаю, что все новые фичи должны добавляться только через новые api, а не через расширение языка.
taujavarob
>мысл расширять стандарт, если все и так будет транслироваться в ES5?
Смысл неясен. Но пока такой:
Цикл (Начало: Стандарт = ES5; изменение: Стандарт +1):
Новая фича (введённая в Стандарт + 1) траслируется в Стандарт
Броузеры подтягиваются к (Стандарт + 1)
Повторить.
Бонус -> Броузеры имеют возможность реализовать нативно(!) то, что уже попадёт в Стандарт.
>На данный момент, я считаю, что все новые фичи должны добавляться только через новые api, а не через расширение языка.
С этим пока неясно. По крайней мере это не практикуется. Стандарт Языка расширяется.