Статьи

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

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

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

Наконечник
Передача данных между потоками не является бесплатной. Эта информация должна быть передана по шине когерентности кэша ЦП L2. Если вы делаете это неконтролируемым образом и просто позволяете приложению обнаруживать объекты, которые ему нужно извлечь из кэша одного потока в свой, это может быть медленнее, чем только передача необходимых вам данных, передаваемых последовательно. Chronicle Queue обеспечивает большую прозрачность, записывая каждое событие, позволяя вам точно определить, что один поток передает другому.
Совет 2
В одной торговой системе с низкой задержкой до добавления Chronicle Queue средняя задержка от чтения до записи составляла 35 микросекунд, после использования Chronicle Queue для оптимизации данных, передаваемых между потоками, задержка снизилась до 23 микросекунд. Использование Chronicle Queue также выявило проблемы, которые раньше не были очевидны, и с воспроизведением событий вселило уверенность, что эти проблемы были исправлены.

Бенчмаркинг с JMH

JMH (Java Micro-benchmark Harness) — отличный инструмент для измерения пропускной способности и сквозных задержек выборки. Мы можем посмотреть на пример того, для чего он хорош в отношении нашего микросервиса для образцов.

В Chronicle-Core мы создаем собственный микропроцессорный жгут для измерения асинхронных задач, выполняемых в нескольких потоках, где вы хотите распределить время между отдельными порциями (что будет описано в части 4). А пока посмотрим на задержки сквозные.

Тест латентности JMH нашего сервиса

С помощью JMH мы можем измерить время от начала до конца. Мы должны включить производителя для проведения теста, а не время автономного обслуживания. Мы смотрим на однопоточное время.

Наш тест JMH

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
@Setup
public void setup() {
    String target = OS.TMP;
    upQueuePath = new File(target, "ComponentsBenchmark-up-" + System.nanoTime());
    upQueue = SingleChronicleQueueBuilder.binary(upQueuePath).build(); (1)
    smdWriter = upQueue.createAppender().methodWriter(SidedMarketDataListener.class);  (2)
 
    downQueuePath = new File(target, "ComponentsBenchmark-down-" + System.nanoTime());
    downQueue = SingleChronicleQueueBuilder.binary(downQueuePath).build();  (3)
    MarketDataListener mdWriter = downQueue.createAppender().methodWriter(MarketDataListener.class); (4)
 
    SidedMarketDataCombiner combiner = new SidedMarketDataCombiner(mdWriter); (5)
 
    reader = upQueue.createTailer().methodReader(combiner); (6)
    System.out.println("up-q " + upQueuePath);
}
 
@TearDown
public void tearDown() {
    upQueue.close();
    downQueue.close();
    IOTools.shallowDeleteDirWithFiles(upQueuePath);
    IOTools.shallowDeleteDirWithFiles(downQueuePath);
}
 
@Benchmark
public void benchmarkComponents() {
    switch (counter++ & 3) {
        case 0:
            smdWriter.onSidedPrice(sidedPrice.init("EURUSD", 123456789000L, Side.Sell, 1.1172, 1e6));
            break;
        case 1:
            smdWriter.onSidedPrice(sidedPrice.init("EURUSD", 123456789100L, Side.Buy, 1.1160, 1e6));
            break;
        case 2:
            smdWriter.onSidedPrice(sidedPrice.init("EURUSD", 123456789000L, Side.Sell, 1.1172, 2e6));
            break;
        case 3:
            smdWriter.onSidedPrice(sidedPrice.init("EURUSD", 123456789100L, Side.Buy, 1.1160, 2e6));
            break;
    }
    assertTrue(reader.readOne()); (7)
}
1
2
3
4
5
6
7
Create an upstream queue.
Create a proxy which writes all methods calls to the upstream queue.
Create a downstream queue.
Create a proxy for the downstream queue.
Create the component which will write all outputs to the downstream queue.
Create a reader for the upstream queue which will call the combiner.
After writing a message to the queue, read it and call the appropriate method in the component.

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

Запуск тестов в JMH

01
02
03
04
05
06
07
08
09
10
11
Percentiles, us/op:
      p(0.0000) =      2.552 us/op
     p(50.0000) =      2.796 us/op
     p(90.0000) =      5.600 us/op
     p(95.0000) =      5.720 us/op
     p(99.0000) =      8.496 us/op (1)
     p(99.9000) =     15.232 us/op (1)
     p(99.9900) =     19.977 us/op (2)
     p(99.9990) =    422.475 us/op
     p(99.9999) =    438.784 us/op
    p(100.0000) =    438.784 us/op
1
2
Critical latency threashold for many systems.
Can still be important in some systems.

Это работает на моей машине разработки (Ubuntu 15.04, два E5-2650 v2, 128 ГБ памяти). Для лучших результатов я предлагаю последние Haswell или Skylake и Centos. Точное время этого теста не имеет значения, так как количество и тип полей в сообщении также является существенным фактором. Что особенно интересно для меня, так это задержка тайла 99,9% (наихудшая 1 из 1000), которая в этом примере постоянно составляет менее 20 микросекунд. Это демонстрирует как высокую производительность, так и постоянные быстрые задержки.

Глядя на то, как называется JMH.

Для управления работой JMH были использованы следующие параметры:

01
02
03
04
05
06
07
08
09
10
11
12
int time = Boolean.getBoolean("longTest") ? 30 : 3;
System.out.println("measurementTime: " + time + " secs");
Options opt = new OptionsBuilder()
        .include(ComponentsBenchmark.class.getSimpleName())
        .warmupIterations(8)
        .forks(1)
        .mode(Mode.SampleTime) (1)
        .measurementTime(TimeValue.seconds(time))
        .timeUnit(TimeUnit.MICROSECONDS)
        .build();
 
new Runner(opt).run();
1
<code>SampleTime</code> mode to test latencies rather than throughput.

Однако у меня возникли проблемы с профилированием и отладкой тестов JMH, поэтому я меняю способ запуска теста в зависимости от его запуска:

Работает в Flight Recorder и Debug

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
if (Jvm.isFlightRecorder()) {
    // -verbose:gc -XX:+UnlockCommercialFeatures -XX:+FlightRecorder
    // -XX:StartFlightRecording=dumponexit=true,filename=myrecording.jfr,settings=profile
    // -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints (2)
    System.out.println("Detected Flight Recorder");
    main.setup();
    long start = System.currentTimeMillis();
    while (start + 60e3 > System.currentTimeMillis()) { (1)
        for (int i = 0; i < 1000; i++)
            main.benchmarkComponents();
    }
    main.tearDown();
 
} else if (Jvm.isDebug()) {
    for (int i = 0; i < 10; i++) {
        runAll(main, Setup.class);
        runAll(main, Benchmark.class);
        runAll(main, TearDown.class);
    }
1
2
Run for 1 minute before shutting down.
Enable profiling between safepoints.

В нашей следующей части

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

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