Съвпадение с LMAX Disruptor - Въведение

1. Общ преглед

Тази статия представя LMAX Disruptor и разказва за това как помага да се постигне едновременност на софтуера с ниска латентност. Ще видим и основно използване на библиотеката Disruptor.

2. Какво е разрушител?

Disruptor е Java библиотека с отворен код, написана от LMAX. Това е едновременна програмна рамка за обработка на голям брой транзакции с ниска латентност (и без сложността на едновременния код). Оптимизацията на производителността се постига чрез софтуерен дизайн, който използва ефективността на базовия хардуер.

2.1. Механична симпатия

Нека започнем с основната концепция за механично съчувствие - това е всичко за разбиране на това как функционира основният хардуер и програмиране по начин, който най-добре работи с този хардуер.

Например, нека видим как процесорът и организацията на паметта могат да повлияят на производителността на софтуера. Процесорът има няколко слоя кеш между него и основната памет. Когато процесорът извършва операция, първо търси в L1 данните, след това L2, след това L3 и накрая основната памет. Колкото по-далеч трябва да стигне, толкова по-дълго ще отнеме операцията.

Ако една и съща операция се извършва на част от данните няколко пъти (например брояч на цикли), има смисъл да се заредят тези данни на място, което е много близо до процесора.

Някои примерни цифри за цената на пропуските в кеша:

Латентност от процесора до Процесорни цикли Време
Главна памет Многократни ~ 60-80 ns
L3 кеш ~ 40-45 цикъла ~ 15 ns
L2 кеш ~ 10 цикъла ~ 3 ns
L1 кеш ~ 3-4 цикъла ~ 1 ns
Регистрирам 1 цикъл Много много бързо

2.2. Защо не опашки

Реализациите на опашки са склонни да имат спорове за писане на променливите на главата, опашката и размера. Опашките обикновено винаги са близки до пълни или почти празни поради разликите в темповете между потребителите и производителите. Те много рядко работят в балансиран среден план, където темпът на производство и потребление е равномерно съпоставен.

За да се справи със спора за запис, опашката често използва заключвания, които могат да доведат до превключване на контекста към ядрото. Когато това се случи, процесният процесор вероятно ще загуби данните в кешовете си.

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

Ако две отделни нишки пишат на две различни стойности, всяко ядро ​​обезсилва кеш линията на другата (данните се прехвърлят между основната памет и кеша в блокове с фиксиран размер, наречени кеш линии). Това е спор за писане между двете нишки, въпреки че те пишат в две различни променливи. Това се нарича фалшиво споделяне, защото всеки път, когато има достъп до главата, се получава достъп и до опашката и обратно.

2.3. Как работи разрушителят

Disruptor има кръгова структура от данни, базирана на масив (буфер на пръстена). Това е масив, който има указател към следващия наличен слот. Той е изпълнен с предварително разпределени обекти за прехвърляне. Производителите и потребителите извършват записване и четене на данни на ринга без заключване или спор.

В Disruptor всички събития се публикуват на всички потребители (мултикаст), за паралелно потребление чрез отделни опашки надолу по веригата. Поради паралелната обработка от потребителите е необходимо да се координират зависимостите между потребителите (графика на зависимостите).

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

3. Използване на библиотеката Disruptor

3.1. Зависимост на Maven

Нека започнем с добавяне на зависимостта на библиотеката Disruptor в pom.xml :

 com.lmax disruptor 3.3.6 

Най-новата версия на зависимостта може да бъде проверена тук.

3.2. Определяне на събитие

Нека дефинираме събитието, което носи данните:

public static class ValueEvent { private int value; public final static EventFactory EVENT_FACTORY = () -> new ValueEvent(); // standard getters and setters } 

В EventFactory позволява на разрушаващи заделя събитията.

3.3. Консуматор

Потребителите четат данни от буфера за пръстени. Нека дефинираме потребител, който ще обработва събитията:

public class SingleEventPrintConsumer { ... public EventHandler[] getEventHandler() { EventHandler eventHandler = (event, sequence, endOfBatch) -> print(event.getValue(), sequence); return new EventHandler[] { eventHandler }; } private void print(int id, long sequenceId) { logger.info("Id is " + id + " sequence id that was used is " + sequenceId); } }

В нашия пример потребителят просто печата в дневник.

3.4. Изграждане на разрушителя

Постройте разрушителя:

ThreadFactory threadFactory = DaemonThreadFactory.INSTANCE; WaitStrategy waitStrategy = new BusySpinWaitStrategy(); Disruptor disruptor = new Disruptor( ValueEvent.EVENT_FACTORY, 16, threadFactory, ProducerType.SINGLE, waitStrategy); 

В конструктора на Disruptor са дефинирани следните:

  • Фабрика за събития - Отговаря за генерирането на обекти, които ще се съхраняват в буфер на пръстена по време на инициализация
  • Размерът на буфера на пръстена - Определихме 16 като размер на буфера на пръстена. Трябва да е степен на 2, иначе би хвърлило изключение при инициализация. Това е важно, тъй като е лесно да се изпълняват повечето операции с помощта на логически двоични оператори, напр. Мод операция
  • Thread Factory - Фабрика за създаване на нишки за процесори за събития
  • Тип на производителя - Указва дали ще имаме единични или няколко производители
  • Стратегия за изчакване - Определя как бихме искали да се справим с бавен абонат, който не е в крак с темпото на производителя

Свържете манипулатора на потребителя:

disruptor.handleEventsWith(getEventHandler()); 

Възможно е да снабдите множество потребители с Disruptor за обработка на данните, които се произвеждат от производителя. В горния пример имаме само един потребител, известен още като манипулатор на събития.

3.5. Стартиране на Disruptor

За да стартирате Disruptor:

RingBuffer ringBuffer = disruptor.start();

3.6. Продуциране и публикуване на събития

Производителите поставят данните в буфера на пръстена в последователност. Производителите трябва да са наясно със следващия наличен слот, за да не презаписват данни, които все още не са консумирани.

Използвайте RingBuffer от Disruptor за публикуване:

for (int eventCount = 0; eventCount < 32; eventCount++) { long sequenceId = ringBuffer.next(); ValueEvent valueEvent = ringBuffer.get(sequenceId); valueEvent.setValue(eventCount); ringBuffer.publish(sequenceId); } 

Тук продуцентът произвежда и публикува артикули последователно. Тук е важно да се отбележи, че Disruptor работи подобно на двуфазен протокол за фиксиране. Той чете нова последователностId и публикува. Следващият път, когато трябва да получи sequenceId + 1 като следващия sequenceId.

4. Заключение

В този урок видяхме какво е Disruptor и как той постига паралелност с ниска латентност. Видяхме концепцията за механично съчувствие и как тя може да бъде използвана за постигане на ниска латентност. След това видяхме пример, използващ библиотеката Disruptor.

Примерният код може да бъде намерен в проекта GitHub - това е проект, базиран на Maven, така че трябва да е лесно да се импортира и да се изпълнява както е.