Súbežnosť s LMAX Disruptor - úvod

1. Prehľad

Tento článok predstavuje LMAX Disruptor a hovorí o tom, ako pomáha dosiahnuť súbežnosť softvéru s nízkou latenciou. Uvidíme tiež základné využitie knižnice Disruptor.

2. Čo je to disruptor?

Disruptor je open source knižnica Java napísaná programom LMAX. Jedná sa o rámec súbežného programovania pre spracovanie veľkého množstva transakcií s nízkou latenciou (a bez zložitosti súbežného kódu). Optimalizácia výkonu sa dosahuje softvérovým dizajnom, ktorý využíva efektívnosť základného hardvéru.

2.1. Mechanická sympatia

Začnime základným konceptom mechanickej sympatie - to je všetko o porozumení fungovania základného hardvéru a programovaní spôsobom, ktorý s týmto hardvérom najlepšie funguje.

Pozrime sa napríklad, ako môže mať organizácia procesora a pamäte vplyv na výkon softvéru. CPU má medzi sebou a hlavnou pamäťou niekoľko vrstiev medzipamäte. Keď CPU vykonáva operáciu, najskôr hľadá v L1 dáta, potom L2, potom L3 a nakoniec hlavnú pamäť. Čím ďalej to bude trvať, tým dlhšie bude operácia trvať.

Ak sa rovnaká operácia vykonáva s dátami viackrát (napríklad počítadlom slučiek), má zmysel načítať tieto dáta na miesto veľmi blízko CPU.

Niektoré orientačné údaje o nákladoch na vynechanie vyrovnávacej pamäte:

Latencia od CPU poCykly CPUČas
Hlavná pamäťViacnásobné~ 60-80 ns
Vyrovnávacia pamäť L3~ 40-45 cyklov~ 15 ns
Vyrovnávacia pamäť L2~ 10 cyklov~ 3 ns
Vyrovnávacia pamäť L1~ 3-4 cykly~ 1 ns
Registrovať1 cyklusVeľmi veľmi rýchle

2.2. Prečo nie fronty

Implementácie front majú tendenciu mať spory o zápis do premenných hlavy, chvosta a veľkosti. Poradie je zvyčajne vždy takmer úplné alebo takmer prázdne kvôli rozdielom v tempe medzi spotrebiteľmi a výrobcami. Veľmi zriedka fungujú vo vyváženom prostredí, kde je miera výroby a spotreby vyrovnaná.

Na riešenie sporu o zápis fronta často používa zámky, ktoré môžu spôsobiť prepnutie kontextu do jadra. Ak k tomu dôjde, je pravdepodobné, že zapojený procesor stratí údaje vo svojich cache.

Aby sa dosiahlo čo najlepšie správanie pri ukladaní do medzipamäte, návrh by mal mať iba jedno jadro zapisujúce na ľubovoľné miesto v pamäti (viacnásobné čítačky sú v poriadku, pretože procesory často používajú medzi svoje medzipamäťami špeciálne rýchle spojenia). Poradie zlyháva v princípe jedného autora.

Ak dva samostatné vlákna zapisujú na dve rôzne hodnoty, každé jadro zneplatňuje riadok vyrovnávacej pamäte druhého (dáta sa prenášajú medzi hlavnou pamäťou a vyrovnávacou pamäťou v blokoch pevnej veľkosti, ktoré sa nazývajú riadky vyrovnávacej pamäte). To je spor o zápis medzi týmito dvoma vláknami, aj keď píšu do dvoch rôznych premenných. Toto sa nazýva nepravé zdieľanie, pretože zakaždým, keď sa pristupuje k hlave, dostane sa aj k chvostu, a naopak.

2.3. Ako funguje disruptor

Disruptor má kruhovú dátovú štruktúru založenú na poli (ring buffer). Je to pole, ktoré má ukazovateľ na ďalší dostupný slot. Je vyplnený vopred pridelenými objektmi prenosu. Výrobcovia a spotrebitelia uskutočňujú zápis a čítanie údajov do kruhu bez blokovania alebo sporu.

V disruptore sa všetky udalosti zverejňujú všetkým spotrebiteľom (multicast) na paralelnú spotrebu prostredníctvom samostatných následných frontov. Z dôvodu paralelného spracovania spotrebiteľmi je potrebné koordinovať závislosti medzi spotrebiteľmi (graf závislostí).

Výrobcovia a spotrebitelia majú počítadlo sekvencií, ktoré označuje, na ktorý slot vo vyrovnávacej pamäti momentálne pracuje. Každý producent / spotrebiteľ môže napísať svoje vlastné počítadlo sekvencií, ale môže čítať počítadlá sekvencií iných. Výrobcovia a spotrebitelia čítajú počítadlá, aby sa ubezpečili, že slot, do ktorého chce zapisovať, je k dispozícii bez akýchkoľvek zámkov.

3. Používanie knižnice Disruptor

3.1. Maven závislosť

Začnime pridaním závislosti knižnice Disruptor do pom.xml:

 com.lmax disruptor 3.3.6 

Najnovšiu verziu závislosti si môžete skontrolovať tu.

3.2. Definovanie udalosti

Definujme udalosť, ktorá prenáša údaje:

verejná statická trieda ValueEvent {hodnota súkromného int; verejná konečná statická EventFactory EVENT_FACTORY = () -> nový ValueEvent (); // štandardné getre a setre} 

The EventFactory nechá Disruptor predurčiť udalosti.

3.3. Spotrebiteľ

Spotrebitelia načítajú údaje z medzipamäte zvonenia. Definujme spotrebiteľa, ktorý bude udalosti spracovávať:

verejná trieda SingleEventPrintConsumer {... public EventHandler [] getEventHandler () {EventHandler eventHandler = (event, sequence, endOfBatch) -> print (event.getValue (), sequence); vrátiť nový EventHandler [] {eventHandler}; } private void print (int id, long sequenceId) {logger.info ("Id je" + id + "použité ID sekvencie je" + sequenceId); }}

V našom príklade spotrebiteľ iba tlačí do guľatiny.

3.4. Konštrukcia disruptora

Zostavte disruptor:

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

V konštruktore Disruptor sú definované nasledovné:

  • Event Factory - zodpovedný za generovanie objektov, ktoré budú počas inicializácie uložené v ring buffri
  • Veľkosť Ring Buffer - 16 sme definovali ako veľkosť ring bufferu. Musí to byť sila 2, inak by to spôsobilo inicializáciu. To je dôležité, pretože je ľahké vykonať väčšinu operácií pomocou logických binárnych operátorov, napr. prevádzka mod
  • Thread Factory - Továreň na vytváranie vlákien pre procesory udalostí
  • Typ producenta - určuje, či budeme mať jedného alebo viacerých producentov
  • Stratégia čakania - definuje, ako by sme chceli zaobchádzať s pomalým predplatiteľom, ktorý nedrží krok s tempom producenta

Pripojte obslužný program spotrebiteľa:

disruptor.handleEventsWith (getEventHandler ()); 

Je možné dodať Disruptoru viacerým spotrebiteľom na spracovanie údajov produkovaných výrobcom. Vo vyššie uvedenom príklade máme iba jedného spotrebiteľa, ktorý obsluhuje udalosti tiež.

3.5. Spustenie disruptora

Spustenie disruptora:

RingBuffer ringBuffer = disruptor.start ();

3.6. Produkčné a publikačné udalosti

Producenti umiestňujú údaje do medzipamäte kruhu postupne. Výrobcovia musia byť informovaní o ďalšom dostupnom slote, aby neprepísali údaje, ktoré ešte nie sú spotrebované.

Použi RingBuffer od Disruptora za zverejnenie:

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

V tomto prípade producent vyrába a publikuje položky v poradí. Tu je dôležité poznamenať, že Disruptor funguje podobne ako protokol 2 fázového odovzdania. Číta sa to nové sequenceId a publikuje. Nabudúce by to malo byť sequenceId + 1 ako ďalší sequenceId.

4. Záver

V tomto tutoriáli sme videli, čo je Disruptor a ako dosahuje súbežnosť s nízkou latenciou. Videli sme koncept mechanických sympatií a toho, ako sa dá využiť na dosiahnutie nízkej latencie. Potom sme videli príklad využívajúci knižnicu Disruptor.

Vzorový kód nájdete v projekte GitHub - jedná sa o projekt založený na Maven, takže by malo byť ľahké ho importovať a bežať tak, ako je.