Disruptor 소개 번역본
Disruptor 소개
disruptor 를 이해하기 가장 좋은 방법은 비슷한 목적을 가진 다른 것과 비교하는것이다.
disruptor 의 경우 자바의 BlockingQueue 인데, disruptor의 목적은 Queue와
마찬가지로 프로세스내의 스레드 간의 데이터를 이동(관리)하기 위해서다.
하지만 disruptor가 제공하는 몇가지 주요 기능들은 큐와 구분 된다.
-
소비자 종속성 그래프를 사용하여 소비자에게 이벤트를 멀티 캐스트한다.
-
이벤트에 대한 메모리를 ‘미리’ 할당 한다.
-
선택적으로 Lock 해제 한다.
핵심 개념
Disruptor 의 작동 방식을 이해하기 전에 문서와 코드 전체에서 사용되는
여러 용어를 미리 정의하는것이 좋다.
DDD(도메인 주도 설계) 를 좋아하는 사람들은, 이 밑의 용어 정의는
Disruptor의 흔한 언어라고 생각하면 된다.
Ring Buffer
: 링버퍼는 종종 Disruptor 의 핵심 개념으로 간주되지만,
3.0 버전 부터는 Disruptor 의 데이터를 저장하거나 업데이트 할뿐이다.
그리고 사용자가 이것을 완전히 대체할수도 있다.
Sequence
: Disruptor는 특정 요소가 현재 어느곳에 있는지 식별하는 수단으로
Sequence를 사용한다. 각 소비자(EventProcessor)는 Disruptor 자체와
마찬가지로 Sequence 를 유지한다. concurrent code(동시성 코드) 의 대다수는 이러한
Sequence 값의 이동에 의존하므로 Sequence 는 자바의 AtomicLong 와 비슷한 많은 기능을 지원한다.
실제로 Sequence와 AtomicLong의 유일한 차이점은 Sequence 가 다른 값과 잘못된 공유를 방지하는 추가 기능이 포함되어있다는것 뿐이다.
Sequencer
: Sequencer는 진정한 Disruptor 의 핵심이다.
이 인터페이스의 두가지 구현(싱글 프로세서와 멀티프로세서)은 생산자와 소비자 간의 빠르고
정확한 데이터 전달을 위해 모든 동시성 알고리즘 사용을 구현한다.
Sequence Barrier
: Sequencer 에 의해 생성되며 Sequencer에 의해
published 된 주요 시퀀스에 대한 참조와 종속된 소비자의 시퀀스를 포함한다.
소비자가 처리 할 수 있는 이벤트가 있는지 확인을 한다.
Wait Strategy
: Wait Strategy 는 생산자가 이벤트를 Disruptor 에
배치 하기 전에 소비자가 대기하는 방법을 결정한다.
Event
: 생산자에서 소비자로 전달 되는 데이터 단위
EventProcessor
: Disruptor 의 이벤트를 처리하기 위한 기본 루프,
소비자 시퀀스의 소유권을 갖는다. 이벤트 루프를 효율적으로 사용하는
BatchEventProcessor 라는 것도 있다.
사용된 EventHandler 를 콜백한다.
EventHandler
: 사용자에 의해 구현된 Disruptor의 소비자를 나타내는 인터페이스
Producer
: 이벤트를 대기열에 추가 하기 위해 Disruptor를 호출하는 사용자 코드
Multicast Events
Queues 와 Disruptor의 동작의 가장 큰 차이점이다.
동일한 Disruptor 에서 수신 대기를 하고있는 여러 소비자가 있는 경우
단일 이벤트가 단일 소비자에게만 전달되는 Queue와 달리 모든 이벤트가
모든 소비자에게 전달된다.
Disruptor 는 동일한 데이터에 대한 여러 병렬작업을 독립적으로 수행해야하는 경우에
사용하기 때문에 이러한 전달방식이 사용된다.
LMAX가 말하는 표준 예는 Journal(영구 저널 파일에 입력 데이터 쓰기),
Replication(데이터의 원격 사본이 있는지 확인하기 위해 입력 데이터를 다른 시스템으로 전송)
및 비즈니스 로직, 이와같은 세 가지 작업이 있는 경우 이다.
동시에 여러 이벤트를 병렬로 처리하여 스케일을 찾는 Executor 스타일의 이벤트 처리도 가능하다.
물론 위의 방법은 특정 목표를 달성하는 가장 효율적인 방법이 아닐 수도있다.
Consumer Dependency Graph
병렬 처리를 실제 응용 프로그램에 지원하려면 소비자 간의 조정이 필요하다.
위의 설명의 예를 다시 참조해서 말하자면 Journal, Replication 가 작업을 완료
할때까지 비즈니스 소비자가 진행되지 않도록 해야한다.
이 개념을 gating
이라고 부르거나 이 동작의 super-set인 기능을
gating 이라고 부른다.
이러한 게이팅은 두 곳에서 발생한다.
첫째. 생산자가 소비자를 초과하지 않도록 해야한다.
이것은 RingBuffer.addGatingConsumers()를 호출하여 관련 소비자를 추가함으로써 처리
둘째. 이전에 참조한 경우는 먼지 처리를 완료해야하는 구성 요소에서 시퀀스를 포함하는 SequenceBarrier 를 구성하여 처리한다.
그림 1를 참조하면 링 버퍼에서 이벤트를 listening 하는 3개의 소비자가 있다.
이 예에는 종속성 그래프가 있는데 ApplicationConsumer는
JournalConsumer 및 ReplicationConsumer에 의존한다.
이는 JournalConsumer와 ReplicationConsumer가 서로 병렬로 자유롭게 실행될 수 있음을 의미한다.
이 종속적 관계는 ApplicationConsumer의 SequenceBarrier에서
JournalConsumer 및 ReplicationConsumer의 시퀀스로의 연결을 통해 확인 할 수 있다.
또한 시퀀서가 다운 스트림 소비자와 갖는 관계에 주목해야한다 그 역할중 하나는
publication 이 링버퍼를 wrapping 하지 않도록 하는것이다.
이를 위해 다운 스트림 소비자는 링버퍼의 크기보다 작은 링 버퍼의 시퀀스보다
낮은 시퀀스를 가질 수 없다. 그리고 여기서 종속성 그래프를 사용해 최적화가 가능하다.
ApplicationConsumers 시퀀스가 JournalConsumer 및 ReplicationConsumer (즉, 종속성 관계가 보장하는 것)보다 작거나 같도록 보장되기 때문에 Sequencer는 ApplicationConsumer의 시퀀스 만 보면된다.
그러니 일반적으로 시퀀스는 종속적 트리의 leaf node 쪽만 바라보면 된다.
Event Preallocation
Disruptor 의 목표 중 하나는 대기 시간이 짧은 환경(low latency environment)에서 사용할 수 있도록 하는것이다.
대기 시간이 짧은 시스템에서 효율적으로 동작하려면 메모리 할당을 줄이거나 제거해야한다.
보통 자바 기반 시스템에서는 가비지 컬렉터로 인한 지연 횟수를 줄이는것
C/C++ 에서는 메모리 할당의 경합으로 인해 과도한 메모리 할당도 문제가 됨.
이와같은것들을 해소하기 위해 Disruptor는 이벤트에 필요한 스토리지를 미리 할당 할수있다.
생성 중에 EventFactory는 사용자에 의해 제공되며 Disruptor의 링 버퍼의 각 항목에 대해 호출된다.
Disruptor에 새 데이터를 넣을때 API는 사용자가 생성된 개체를 얻게 해서
해당 저장소 객체의 메서드를 호출하거나 필드를 업데이트 할 수 있도록 한다.
Disruptor 에서 이런 시스템 운영이 보장되는한 concurrency-safe 가 보장된다.
Optionally Lock-free
low latency에 관련해서 필요되는 또다른 세부적인 구현은 Disruptor를 구현하기 위해
lock-free 한 알고리즘을 광범위하게 구현하는것이다.
모든 memory visibility(메모리 가시성) and correctness은
memory barriers 와 compare-and-swap operations로 보장된다.
실제 Lock이 필요한 경우는 단 하나이며 BlockingWaitStrategy 내에 있다.
새 이벤트가 도착하기전에 이벤트를 소비하는 스레드가 parked 될수 있도록
Condition 을 사용하기 위한 목적으로만 수행된다.
대기 시간이 짧은 시스템에서는 Condition을 사용하여 발생할 수있는 지터를 피하기 위해 busy-wait를 사용하지만,
많은 시스템 busy-wait 작업에서 특히 CPU 리소스가 크게 제한되는 경우 성능이 크게 저하된다.
예) 가상화 환경의 웹서버