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)


  1. xuexi
    24.09.2016 19:33
    +1

    Из истории futures в javascript можно сделать вывод, что futures вводят в язык только для того, чтобы следом ввести в язык async/await и опять писать последовательный псевдо-синхронный код.


    1. AndreyRubankov
      25.09.2016 15:31

      Вот только Future в Java имеются еще с версии 1.5 (2004 год). И заменять на async/await, к счастью, не спешат, есть вещи по-важнее.

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


      1. taujavarob
        26.09.2016 11:47

        AndreyRubankov >И рано или поздно язык достигнет точки в которой он перестанет развиваться или выкинет обратную совместимость и кучу фич, которые у него были.

        Пример? Было такое в истории? Или это гипотеза ваша — про выкидывания «кучи фич»?


        1. AndreyRubankov
          26.09.2016 17:31

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

          В языках есть фичи, которые негласно считаются плохими: assert, goto, множественное наследование. Их не используют (если по-хорошему), но язык обязан их поддерживать.

          Если взять С++, фич у него невероятно много, по-моему, его можно считать одним из самых сложных, если не самым. Да, он жив и он развивается, но кто сейчас захочет его изучать? Когда есть D, Rust, Go, которые предоставляют примерно те же возможности но на много безопаснее и проще, там где нужно залезть глубже можно взять чистый Си, который довольно простой.


          1. taujavarob
            26.09.2016 17:36

            Ясно, под «выкидыванием» вы очевидно подразумеваете неиспользование большинством программистом.


            1. AndreyRubankov
              26.09.2016 18:39

              Вы не на том делаете акцент. Основная идея в том, что фича в язык добавляется просто, а последствия этого добавления можно выгребать очень-очень долго, именно потому, что никто не выкидывает фичи из языков.


              1. taujavarob
                26.09.2016 19:36

                Последствия внедрения той или иной фичи в длительном времени в принципе предугадать нельзя. имхо.

                Но сейчас к примеру начали применять такой цикл (в Javascript):

                — Фича вводится не через Стандарт Языка, а через плагин (который транспалирует эту новую фичу в старый Стандартный код).

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

                Такой алгоритм позволяет постоянно наращивать фичи в Языке и хоть как-то избегать «опасных фич» (страховаться от них), от которых захочется избавиться в Стандарте по крайней мере в ближайшее время.


                1. AndreyRubankov
                  26.09.2016 20:19

                  Да, это хороший подход, но и он не спасает от проблем. Время идет, техники меняются, то что раньше было спасением, сейчас плохая практика.

                  В случае с babel уже нету особо смысла в стандарт языка что-то добавлять. Хочешь использовать какую-то фичу — ставишь как плагин к babel, а дальше транслируешь в ES5. Смысл расширять стандарт, если все и так будет транслироваться в ES5?

                  На данный момент, я считаю, что все новые фичи должны добавляться только через новые api, а не через расширение языка.


                  1. taujavarob
                    26.09.2016 21:40

                    >мысл расширять стандарт, если все и так будет транслироваться в ES5?

                    Смысл неясен. Но пока такой:

                    Цикл (Начало: Стандарт = ES5; изменение: Стандарт +1):
                    Новая фича (введённая в Стандарт + 1) траслируется в Стандарт
                    Броузеры подтягиваются к (Стандарт + 1)
                    Повторить.

                    Бонус -> Броузеры имеют возможность реализовать нативно(!) то, что уже попадёт в Стандарт.

                    >На данный момент, я считаю, что все новые фичи должны добавляться только через новые api, а не через расширение языка.

                    С этим пока неясно. По крайней мере это не практикуется. Стандарт Языка расширяется.


  1. tsabir
    25.09.2016 05:15

    Работал с Play! Framework, там тоже все асинхронно, но когда сталкиваешься с синхронным/блокирующим интерфейсом на внешний веб-сервис, то непонятно что делать. Можно конечно вызвать его в отдельном треде, но не покидает ощущение что я просто прикрыл грязь ковриком :). Я пробовал тупо писать без использования jax-ws, но на код потом смотреть страшно. Вы что посоветуете?


  1. 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);
       }
    }

    Таким образом код, вызывающий обёртку, будет соответствовать идиоме асинхронного кода.


    1. tsabir
      26.09.2016 08:52

      Ну если честно, это и есть то что я называю «спрятать мусор под коврик». Ведь вызов веб-сервиса по природе можно сделать асинхронно, хотелось бы иметь альтернативу jax-ws, которая работает асинхронно по умолчанию


  1. fogone
    25.09.2016 08:12

    Подскажите, а есть какой-то профит от передачи result в метод, вместо его возвращения (как это обычно с future делается)?


    1. Dobryj
      25.09.2016 08:23

      Передав Result следующему сервису метод выходит и освобождает микро-сервис для следующего вызова. Это соответствует второму принципу в списке.


    1. Dobryj
      25.09.2016 08:30

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


      1. Sirikid
        25.09.2016 12:12

        Какие плюсы от использования первой формы из примера http://pastebin.com/SJgcahhG а не второй?


    1. yahorfilipcyk
      26.09.2016 04:12

      Я бы утверждал, что профита никакого. Из интерфейса не совсем понятно, кто за что отвечает. Вообще, некоторые принципы функционального программирования учат хорошему стилю. Например, не использовать изменяемые данные и писать "чистые" функции. Неследование этим принципам может добавить только головной боли. Не знаком с описанным в статье фреймворком, но кажется, что вполне можно было бы написать что-то подобное:


      public interface CreditService {
          Result<PaymentStatus> pay(int amount, CreditCard card);
      }

      Создание сущности Result<PaymentStatus> вне вызова метода, по-моему, только усложняет код. Метод pay должен быть в состоянии самостоятельно позаботиться о создании сущности Result<PaymentStatus>. И, кстати, такой подход лучше описывает интерфейс. Вполне очевидно, что в результате вызова метода мы получим отложенный результат и что метод сам по себе неблокирующий. Используя же в сигнатуре void не говорит ни о чем. void, вообще, должен только указывать на функции с побочными эффектами, что, в принципе, должно использоваться как можно реже.


      1. 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)));


        1. ttim
          26.09.2016 23:46

          Из асинхронности этот Result никак не следует. Зато возможности по работе с асинхронными значениями хорошо так убивает.

          Посмотрите как можно сделать асинхронность:
          https://twitter.github.io/finagle/guide/Futures.html


        1. Sirikid
          27.09.2016 00:06

          Грубо говоря, функциональный интерфейс это интерфейс с одним абстрактным методом.


          1. Dobryj
            27.09.2016 00:23

            Да, конечно. Иначе код просто не будет компилироваться. В JDK java.util.Comparator, java.util.function.BiConsumer и другие функциональные интерфейсы наряду с единственным основным абстрактным методом, предназначенным для имплементации, предоставляют множество вспомогательных 'default' методов.


  1. sphinks
    26.09.2016 12:37
    +1

    _creditService

    _foo

    _bar


    Вот Вы зачем так переменные называете (C-style похоже)? Нас же могут читать молодые неокрепшие умы…


    1. Dobryj
      27.09.2016 00:17

      Спасибо, учту.


  1. potan
    26.09.2016 15:17

    Там поддерживается Scala Akka?


    1. Dobryj
      26.09.2016 18:08

      Прямой поддержки для Akka там нет.


  1. relgames
    27.09.2016 16:50

    Напоминает Akka (не только акторы, но и Future подход). Правда, привычней выглядит возврат Future, а не передача в сервис, что позволяет строить цепочки вызовов типа:

    service.doSomething(param)
        .thenApply(result -> transform(result))
        .thenAccept(result -> log.info(result));
    


  1. isopov
    27.09.2016 17:25

    Лицензию так и планируете оставить GPL?


    1. Dobryj
      27.09.2016 17:27

      Думаю, да.