Статьи

Моделирование микросервисных шаблонов в коде

Сервисное взаимодействие

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

Для одноранговой передачи сообщений одним из подходов является Lambda Architecture, однако, благодаря поддержке графических интерфейсов, клиент-серверные модели могут быть проще в работе.

Мне кажется, важно разрабатывать компоненты, которые можно было бы тестировать и отлаживать вместе, как в монолите, но которые можно развернуть в виде нескольких сервисов правильного размера в разных потоках, JVM или машинах.

Обмен сообщениями между клиентом и сервером

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

Использование удобочитаемого протокола

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

  • он предназначен для чтения человеком вместо подмножества другого языка / формата.
  • поддерживает сообщения, типы и комментарии.
  • он может быть отправлен в двоичном виде для производительности и преобразован в текст по мере необходимости

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

Ответ на запрос

Клиент отправляет сообщение на сервер. Это сообщение содержит тип сообщения для выбора действия, выполняемого на сервере, и обычно включает в себя полезную нагрузку данных. Ответ, как правило, просто данные.

На каждое сообщение, которое клиент отправляет на сервер, он отвечает одним сообщением. Часто клиент ожидает ответа, однако клиент может обрабатывать ответ асинхронно для повышения пропускной способности.

Ответ на запрос

При асинхронной обработке ответов клиент может отправлять несколько запросов по одному и тому же каналу, а не ждать завершения каждого из них.

Синхронный сервер с синхронным клиентом

1
2
3
interface OneRequestResponse {
    Response requestType(RequestData data);
}

Синхронный сервер с асинхронным клиентом

1
2
3
interface OneRequestResponse2 {
    void requestType(RequestData data, Consumer<Response> responseConsumer);
}

Использование этого API может быть переведено на YAML, например:

Синхронный сервер с синхронным или асинхронным клиентом

01
02
03
04
05
06
07
08
09
10
11
12
13
14
# client sends to server
---
requestType: {
  data: 1,
  text: my text
}
 
# server sends to client
---
reply: !MyResponse {
  moreData: 128,
  message: Success
}
...

Запрос / Proxy

Это похоже на запрос / ответ, за исключением того, что возвращаемый объект является прокси для дальнейших событий. Это полезно, когда возвращаемое значение представляет сложный или очень большой объект.

Request-Proxy

Это используется в Map, возвращая набор ключей или значений, которые являются прокси для базовой карты.

Метод на java.util.Map

1
2
3
4
5
6
7
8
public interface Map<K, V> {
 
    Set<K> keySet();
 
    Set<Map.Entry<K, V>> entrySet();
 
    Collection<V> values();
}

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

Метод на java.util.Map

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# client sends to server
--- !!meta-data # binary
csp: /map/my-map?view=map
---
keySet: []
---
entrySet: []
---
values: []
 
# server sends to client
---
reply: !set-proxy {
   csp: /map/my-map?view=keySet
}
---
reply: !set-proxy {
   csp: /map/my-map?view=entrySet
}
---
reply: !set-proxy {
   csp: /map/my-map?view=values
}
 
# client sends to server
--- !!meta-data # binary
csp: /map/my-map?view=keySet
--- !data # binary
size: []
---
 
# server sends to client
---
reply: 128000 (1)
 
# client sends to server
--- !!meta-data # binary
csp: /map/my-map?view=keySet
--- !data # binary
remove: "key-111"
---
 
# server sends to client
---
reply: true (2)
...
1 Не нужно отправлять 128 000 ключей только для того, чтобы определить, сколько их было.
2 ключ был удален на сервере, а не копия отправлена ​​клиенту.

Запрос / Обратный звонок

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

Это похоже на подписку, за исключением того, что ожидается только одно событие.

Запрос обратного вызова

Использование обратного вызова обеспечивает более богатое взаимодействие между вызывающим абонентом и вызываемым абонентом.

Синхронный сервер с обратным вызовом

01
02
03
04
05
06
07
08
09
10
11
interface OneCallback {
    void resultOne(ResultOne result);
 
    void resultTwo(List<ResultOne> results);
 
    void errorResult(String message);
}
 
interface OneRequestCallback {
    void requestType(RequestData data, OneCallback callback);
}

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

Синхронный сервер с обратным вызовом

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
# client sends to server
---
requestType: {
  data: 1,
  text: my text
}
 
# server sends to client
---
resultTwo: [
  {
      moreData: 128,
      message: Success
  },
  {
      moreData: 1111,
      message: Failure
  }
}
...

Запрос / посетителей

Клиент отправляет одного или двух посетителей на сервер для применения к локальным объектам или актерам. Этот посетитель может быть обновлением, которое атомарно применяется к действующему субъекту, и / или может быть применен vistor для получения конкретной информации.

Request-посетителей

Передайте функцию для применения на сервере для данного ключа

1
2
3
4
5
6
7
interface KeyedResources<V> {
 
    void asyncUpdate(String key, Visitor<V> vistor);
 
    <R> R syncUpdate(String key, Visitor<V> updater, Function<V, R> returnFunction);
 
}

Такой подход позволяет вызывающей стороне применять операцию к действующему лицу, не зная, где находится этот действующий субъект.

Передайте функцию для применения на сервере для данного ключа

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
# client sends to server
---
asyncUpdate: [
    "key-5",
    !MyVisitor { add: 10 }
]
# no return value
--- # subtract 3 and return x * x
syncUpdate: [
    "key-6",
    !MyVisiitor { add: -3 },
    !Square { }
];
 
# server sends to client
---
reply: 1024
...

Запрос / Подписка

Запрашивая подписку, клиент может получить несколько асинхронных событий. Это может начаться с начальной загрузки существующей информации, сопровождаемой живыми обновлениями.

После того, как подписка сделана, ее следует изменить или отменить

Запроса Подписка

Передайте функцию для применения на сервере для данного ключа

01
02
03
04
05
06
07
08
09
10
11
12
interface Queryable<E> {
 
    <R> Subscription<E, R> subscribe(Filter<E> filter, Function<E, R> returnMapping, Subscriber<R> subscriber);
 
}
 
interface Subscription<R> {
    // change the current filter.
    void setFilter(Filter<E> newFilter);
 
    void cancel();
}

До этого момента все сообщения являются действиями с одним ответом. В Chronicle-Engine мы связываем путь обслуживания csp или Chronicle для каждого субъекта и идентификатор tid или Transaction ID для каждой операции. Это позволяет несколько одновременных действий для разных субъектов. Эта информация о маршруте передается в метаданных, а действия для этого пункта назначения следующие

Передайте функцию для применения на сервере для данного ключа

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# client sends server
--- !!meta-data # binary
csp: /maps/my-map
tid: 12345
--- !!data # binary
subscribe: [
   !MyFilter { field: age, op: gt, value: 18 },
   !Getter { field: name }
]
request: 2 # only send me two events for now.
 
# server sends client
--- !!meta-data # binary
tid: 12345
--- !data-not-complete # binary
reply: Steve Jobs
--- !data-not-complete # binary
reply: Alan Turing
 
# client sends server
--- !!meta-data # binary
tid: 12345
--- !data # binary
cancel: []
 
# server sends client
--- !!meta-data # binary
tid: 12345
--- !data # binary
cancelled: "By request"
...

Клиент, введенный обработчик

Такой подход позволяет клиенту создавать версии и настраивать, какие обработчики используются на сервере от имени клиента. В частности, это полезно при одновременной поддержке нескольких версий клиентов.

Клиент-Введенный-Handler

Клиент передает обработчик для интеграции с сервером и действует от его имени

1
2
3
4
5
6
7
8
interface AcceptsHandler {
 
    /**
     * The accept method takes a handler to pass to the server.
     * and it returns a proxy it can call to invoke that hdnler on the server.
     */
    <H extends ContextAcceptor> H accept(H handler);
}

Простой пример обработчика, который мы используем, для сердцебиения

1
2
3
4
5
6
7
8
9
# client sends server
--- !!meta-data # binary
csp: /
cid: 1
handler: !HeartbeatHandler {
    heartbeatTimeoutMs: 10000
    heartbeatIntervalMs: 2000
}
...
Это позволяет различным клиентам одновременно работать с разными версиями обработчиков пульса, поддерживая старых и новых клиентов с помощью одного сервера.

Вывод

В дополнение к моделям Lambda Architecture для серверных и одноранговых сервисов мы можем поддерживать широкий набор взаимодействий между клиентами и серверами.

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

Ссылка: Моделирование микросервисных шаблонов в коде от нашего партнера JCG Питера Лоури из блога Vanilla Java .