Статьи

Микросервисы в мире хроники — часть 1

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

Микросервисы в мире Chronicle созданы вокруг:

  • Простота — просто, быстро, гибко и проще в обслуживании.
  • Прозрачность — вы не можете контролировать то, что не понимаете.
  • Воспроизводимость — это должно быть в вашем дизайне, чтобы обеспечить качественное решение.

Что мы подразумеваем под простым?

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

Асинхронный вызов метода это тот, который;

  • ничего не возвращает,
  • не меняет своих аргументов;
  • не генерирует никаких исключений (хотя основной транспорт может).

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

Давайте посмотрим на пример.

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

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

Наша входящая структура данных

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class SidedPrice extends AbstractMarshallable {
    final String symbol;
    final long timestamp;
    final Side side;
    final double price, quantity;
 
    public SidedPrice(String symbol, long timestamp, Side side, double price, double quantity) {
        this.symbol = symbol;
        this.timestamp = timestamp;
        this.side = side;
        this.price = price;
        this.quantity = quantity;
    }
}

Наша исходящая структура данных

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class TopOfBookPrice extends AbstractMarshallable {
    final String symbol;
    final long timestamp;
    final double buyPrice, buyQuantity;
    final double sellPrice, sellQuantity;
 
    public TopOfBookPrice(String symbol, long timestamp, double buyPrice, double buyQuantity, double sellPrice, double sellQuantity) {
        this.symbol = symbol;
        this.timestamp = timestamp;
        this.buyPrice = buyPrice;
        this.buyQuantity = buyQuantity;
        this.sellPrice = sellPrice;
        this.sellQuantity = sellQuantity;
    }
 
    // more methods (1)
}
Наконечник
Для полного кода TopOfBookPrice.java

Компонент, который принимает односторонние цены, может иметь интерфейс;

Входящий интерфейс для первого компонента

1
2
3
public interface SidedMarketDataListener {
    void onSidedPrice(SidedPrice sidedPrice);
}

и его вывод также имеет один метод;

Исходящий интерфейс для первого компонента

1
2
3
public interface MarketDataListener {
    void onTopOfBookPrice(TopOfBookPrice price);
}

Как выглядит наш микросервис?

На высоком уровне объединитель очень прост;

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class SidedMarketDataCombiner implements SidedMarketDataListener {
    final MarketDataListener mdListener;
    final Map<String, TopOfBookPrice> priceMap = new TreeMap<>();
 
    public SidedMarketDataCombiner(MarketDataListener mdListener) {
        this.mdListener = mdListener;
    }
 
    public void onSidedPrice(SidedPrice sidedPrice) {
        TopOfBookPrice price = priceMap.computeIfAbsent(sidedPrice.symbol, TopOfBookPrice::new);
        if (price.combine(sidedPrice))
            mdListener.onTopOfBookPrice(price);
    }
}

Он реализует наш интерфейс ввода и принимает интерфейс вывода в качестве слушателя.

Что обеспечивает AbstractMarshallable?

Класс AbstractMarshallable является вспомогательным классом, который реализует toString() , equals(Object) и hashCode() . Он также поддерживает записи MarshallableMarshallable writeMarshallable(WireOut) и readMarshallable(WireIn) .

Реализации по умолчанию используют все нестатические непереходные поля для печати, сравнения или построения hashCode.

Наконечник
результирующий toString () всегда можно Marshallable.fromString(CharSequence) с помощью Marshallable.fromString(CharSequence) .

Давайте посмотрим на пару примеров.

01
02
03
04
05
06
07
08
09
10
11
12
13
SidedPrice sp = new SidedPrice("Symbol", 123456789000L, Side.Buy, 1.2345, 1_000_000);
assertEquals("!SidedPrice {\n" +
        "  symbol: Symbol,\n" +
        "  timestamp: 123456789000,\n" +
        "  side: Buy,\n" +
        "  price: 1.2345,\n" +
        "  quantity: 1000000.0\n" +
        "}\n", sp.toString());
 
// from string
SidedPrice sp2 = Marshallable.fromString(sp.toString());
assertEquals(sp2, sp);
assertEquals(sp2.hashCode(), sp.hashCode());

Как вы можете видеть, toString() написан на YAML , лаконичен, читаем для человека и в коде.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
TopOfBookPrice tobp = new TopOfBookPrice("Symbol", 123456789000L, 1.2345, 1_000_000, 1.235, 2_000_000);
assertEquals("!TopOfBookPrice {\n" +
        "  symbol: Symbol,\n" +
        "  timestamp: 123456789000,\n" +
        "  buyPrice: 1.2345,\n" +
        "  buyQuantity: 1000000.0,\n" +
        "  sellPrice: 1.235,\n" +
        "  sellQuantity: 2000000.0\n" +
        "}\n", tobp.toString());
 
// from string
TopOfBookPrice topb2 = Marshallable.fromString(tobp.toString());
assertEquals(topb2, tobp);
assertEquals(topb2.hashCode(), tobp.hashCode());
}

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

Даже в тривиальном тесте не очевидно, в чем проблема

1
2
3
4
TopOfBookPrice tobp = new TopOfBookPrice("Symbol", 123456789000L, 1.2345, 1_000_000, 1.235, 2_000_000);
TopOfBookPrice tobp2 = new TopOfBookPrice("Symbol", 123456789000L, 1.2345, 1_000_000, 1.236, 2_000_000);
 
assertEquals(tobp, tobp2);

Однако, когда вы запускаете этот тест в вашей IDE , вы получаете окно сравнения.

Рисунок 1. Сравнение Windows в вашей IDE

Если у вас есть большой вложенный / сложный объект, где assertEquals терпит неудачу, это действительно может сэкономить вам много времени, чтобы найти, какова дискретность.

Издеваться над нашим компонентом

Мы можем смоделировать интерфейс, используя такой инструмент, как EasyMock . Я считаю, что EasyMock проще при работе с интерфейсами, управляемыми событиями. Он не такой мощный, как PowerMock или Mockito, однако, если вы делаете все просто, вам могут не понадобиться эти функции.

01
02
03
04
05
06
07
08
09
10
11
// what we expect to happen
SidedPrice sp = new SidedPrice("Symbol", 123456789000L, Side.Buy, 1.2345, 1_000_000);
SidedMarketDataListener listener = createMock(SidedMarketDataListener.class);
listener.onSidedPrice(sp);
replay(listener);
 
// what happens
listener.onSidedPrice(sp);
 
// verify we got everything we expected.
verify(listener);

Мы также можем высмеивать ожидаемый результат компонента тем же способом.

Тестирование нашего компонента

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

01
02
03
04
05
06
07
08
09
10
MarketDataListener listener = createMock(MarketDataListener.class);
listener.onTopOfBookPrice(new TopOfBookPrice("EURUSD", 123456789000L, 1.1167, 1_000_000, Double.NaN, 0)); (1)
listener.onTopOfBookPrice(new TopOfBookPrice("EURUSD", 123456789100L, 1.1167, 1_000_000, 1.1172, 2_000_000)); (2)
replay(listener);
 
SidedMarketDataListener combiner = new SidedMarketDataCombiner(listener);
combiner.onSidedPrice(new SidedPrice("EURUSD", 123456789000L, Side.Buy, 1.1167, 1e6)); (1)
combiner.onSidedPrice(new SidedPrice("EURUSD", 123456789100L, Side.Sell, 1.1172, 2e6)); (2)
 
verify(listener);
1
2
Setting the buy price
Setting the sell price

Тестирование серии компонентов

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// what we expect to happen
OrderListener listener = createMock(OrderListener.class);
listener.onOrder(new Order("EURUSD", Side.Buy, 1.1167, 1_000_000));
replay(listener);
 
// build our scenario
OrderManager orderManager = new OrderManager(listener); (2)
SidedMarketDataCombiner combiner = new SidedMarketDataCombiner(orderManager); (1)
 
// events in
orderManager.onOrderIdea(new OrderIdea("EURUSD", Side.Buy, 1.1180, 2e6)); // not expected to trigger
 
combiner.onSidedPrice(new SidedPrice("EURUSD", 123456789000L, Side.Sell, 1.1172, 2e6));
combiner.onSidedPrice(new SidedPrice("EURUSD", 123456789100L, Side.Buy, 1.1160, 2e6));
 
combiner.onSidedPrice(new SidedPrice("EURUSD", 123456789100L, Side.Buy, 1.1167, 2e6));
 
orderManager.onOrderIdea(new OrderIdea("EURUSD", Side.Buy, 1.1165, 1e6)); // expected to trigger
 
verify(listener);
1
2
The first component combines sided prices
The second component listens to order ideas and top of book market data

Отладка наших компонентов

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

Когда какое-либо отдельное событие вызывает ошибку, вы можете увидеть в трассировке стека, какое событие вызвало проблему. Однако, если вы ожидаете событие, которое не произойдет, это будет сложно, если ваши тесты не просты (или вы делаете серию простых тестов с помощью verify() , reset() и replay() .

Наконечник
запуск теста и его отладка в вашей среде IDE практически не занимает времени. Вы можете запустить сотни таких тестов менее чем за секунду.

Источник для примеров

https://github.com/Vanilla-Java/Microservices/tree/master/src/main/java/net/openhft/samples/microservices

Как мы создаем их как сервисы?

Мы показали, как легко тестировать и отлаживать наши компоненты. Как мы превращаем их в услуги в части 2 .

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