이번에는 지난 시간에 이어 다 읽지 못한 LMAX Disruptor Technical Paper를 3장부터 이어서 읽어보려고 한다.
초록, 개요와 기존 메모리 구조에서 오는 문제점을 해결해야 한다는게 이전 글에서의 핵심 요약이라면
이번에는 이를 해결하는 내용이 나오지 않을까 생각한다.
목표
1. LMAX Disruptor가 어느 점에서 효율적인지 이해한다.
1. 3) Design of the LMAX Disruptor
위에서 설명한 문제들을 해결하기 위해, 큐에서 혼재되어 있던 것으로 보이는 관심사들을 엄격하게 분리하는 방식을 통해 하나의 설계가 도출되었습니다.
이 접근 방식은 쓰기 접근에 대해 모든 데이터가 오직 하나의 스레드에 의해서만 소유되도록 보장함으로써 쓰기 경합을 제거하는 데 중점을 두었습니다.
이 설계는 "디스럽터(Disruptor)"라고 불리게 되었습니다. 이 이름은 Java 7에서 포크-조인(Fork-Join)을 지원하기 위해 도입된 "페이저(Phaser)" 개념[4]과 의존성 그래프를 처리하는 방식이 유사하기 때문에 붙여졌습니다.
여기서 포크-조인을 지원하기 위해 도입된 페이저 개념과 의존성 그래프를 처리하는 방식이 유사하다는데...
Disruptor를 그래서 어떻게 설계했는지 찾아보고, 그 다음에 포크조인의 페이저와 의존성 그래프 처리 방식에 대해 알아보도록 하겠다.
LMAX 디스럽터는 위에서 설명한 모든 문제들을 해결하고 메모리 할당 효율성을 극대화하며, 최신 하드웨어에서 최적의 성능을 발휘할 수 있도록 캐시 친화적인 방식으로 설계되었습니다.
디스럽터 메커니즘의 핵심에는 미리 할당된 유한한 크기의 데이터 구조인 링 버퍼가 있습니다. 데이터는 하나 이상의 프로듀서를 통해 링 버퍼에 추가되고, 하나 이상의 컨슈머에 의해 처리됩니다.
글을 읽어 보자면 쓰레드끼리의 자원 접근 경합이 일어나지 않도록 애초에 특정 데이터들이 하나의 쓰레드에 의해서만 관리가 되면, OS레벨에서 가짜 공유로 인해 캐시를 마구 지운다던지 하는 가장 심각한 문제 말이다.
또한 CAS나 QUEUE에서 데이터를 여러 곳에서 접근하면 락 혹은 CAS같은 동기화 메커니즘이 필수적인데 애초에 이것을 없애버린 것이다.
Disruptor는 미리 할당된 유한한 크기의 데이터 구조인 링 버퍼가 있다고 한다.
데이터는 하나 이상의 프로듀서를 통해 링 버퍼에 추가되고, 하나 이상의 컨슈머에 의해 처리된다고 한다.
다시 음미하고가자! 으음~ sexy food~
3.1. Memory Allocation
링 버퍼에 필요한 모든 메모리는 시작 시 미리 할당됩니다. 링 버퍼는 엔트리에 대한 포인터 배열 또는 엔트리를 나타내는 구조체 배열을 저장할 수 있습니다.
이 부분은 2장에서 CPU가 너무 빨라서 미리 데이터를 가져오는 Prefetch와도 관련이 있다.
이유는 다음과 같다.
1. 연속된 메모리 할당을 통한 '공간 지역성' 확보
2. 하드웨어 프리패처(Prefetcher)의 예측 적중률 극대화
3. 런타임 메모리 파편화 방지로 캐시 미스 차단
미리 할당을 하니 정말... 2장에서 논의된 많은 문제가 해결이 됐다.
CPU에게 이것을 맡기지 말고 소프트웨어 레벨에서 이를 fix해버린 것이다.
Java 언어의 제약으로 인해 엔트리는 객체에 대한 포인터로 링 버퍼와 연결됩니다. 이러한 엔트리는 일반적으로 전달되는 데이터 자체가 아니라 데이터를 담는 컨테이너입니다. 엔트리의 사전 할당은 가비지 컬렉션을 지원하는 언어에서 발생하는 문제를 해결합니다. 엔트리는 Disruptor 인스턴스가 실행되는 동안 재사용되고 유지되기 때문입니다.
엔트리는 전달되는 데이터 자체가 아니라 데이터를 담은 컨테이너라고 한다.
Queue로 따지만 Queue에 들어가기 위한 데이터 상자 (예: Object, String 혹은 Custom 해서 만든 Class 등등) 가 떠오른다.
이러한 엔트리에 필요한 메모리는 동시에 할당되며, 메인 메모리에 연속적으로 배치될 가능성이 높아 캐시 스트라이딩을 지원합니다. John Rose는 C와 같은 다른 언어처럼 튜플 배열을 허용하는 "값 타입"[5]을 Java 언어에 도입하여 메모리가 연속적으로 할당되고 포인터 간접 참조를 방지할 수 있도록 하자는 제안을 했습니다.
Java와 같은 관리형 런타임 환경에서 저지연 시스템을 개발할 때 가비지 컬렉션은 문제가 될 수 있습니다. 할당되는 메모리 양이 많을수록 가비지 컬렉터에 가해지는 부담이 커집니다. 가비지 컬렉터는 객체의 수명이 매우 짧거나 사실상 영구적일 때 가장 효율적으로 작동합니다.
링 버퍼에 항목을 미리 할당하면 가비지 컬렉터 입장에서 해당 객체는 영구적으로 유지되므로 가비지 컬렉터에 가해지는 부담이 적습니다.
하지만 부하가 심한 큐 기반 시스템은 백업 현상이 발생하여 처리 속도가 저하될 수 있습니다. 이로 인해 할당된 객체가 필요 이상으로 오래 남아 있게 되고, 세대별 가비지 컬렉터가 적용되는 영세대(Young Generation)를 넘어 이전 세대로 이동하게 됩니다.
이는 두 가지 문제를 야기합니다. 첫째, 세대 간 객체 복사가 필요하므로 지연 시간이 발생합니다. 둘째, 이전 세대에서 객체를 수집해야 하는데, 이는 일반적으로 훨씬 더 많은 비용이 드는 작업이며, 조각화된 메모리 공간 압축 시 발생하는 "스톱 더 월드(Stop the World)" 일시 정지 가능성을 높입니다. 대규모 메모리 힙에서는 이로 인해 GB당 수 초에 달하는 일시 정지가 발생할 수 있습니다.
두 가지 문제가 잘 이해가 안됐다.
(1) 세대 간 복사로 인한 지연, (2) Full GC와 Stop The World이다.
세대 간 복사로 인한 지연은 어디서 발생하는가?
다음을 다시 읽어보자.
"부하가 심한 큐 기반 시스템은 백업 현상(데이터 적채 현상)이 발생하여 처리 속도가 저하될 수 있습니다.
이로 인해 할당된 객체가 필요 이상으로 오래 남아 있게 되고, 세대별 가비지 컬렉터가 적용되는 영세대(Young Generation)를 넘어 이전 세대로 이동하게 됩니다"
이를 정리하면 처리되지 못한 데이터들이 큐 안에서 계속 나이(Age)를 먹거나 대피소를 터뜨리면서 굳이 안 가도 될 Old 세대까지 대거 밀려가는 현상이 발생하는 것이다.
Minor GC는 영 세대(young generation)에서만 발생하지만 부하가 심한 큐의 경우 지속적인 승급(promotion)으로 인해 Old세대로 넘어가고 Full GC가 발생하며 단편화가 발생한다. 그러면 공간을 확보하기 위해 공간 압축(Compaction)을 한다.
그러면 모든 스레드를 강제로 정지시키는 Stop The World가 발생한다. 그러면 이것이 점점 잦아지니... 성능이 점~점 안좋아지는 것이다.
그래서 "대규모 메모리 힙에서는 이로 인해 GB당 수 초에 달하는 일시 정지가 발생할 수 있습니다" 라고 언급한 것이다.
3.2 Teasing Apart the Concerns
모든 큐 구현에서 다음과 같은 문제점들이 공통적으로 나타나는 것을 확인했습니다. 이러한 문제점들은 큐가 구현하는 인터페이스를 규정하는 주요 요소로 작용합니다.
1. 교환되는 항목의 저장
2. 다음 교환 순서를 확보하려는 생산자의 조정
3. 새로운 항목이 있음을 알리는 소비자의 알림 조정
가비지 컬렉션을 사용하는 언어로 금융 거래 시스템을 설계할 때, 과도한 메모리 할당은 문제가 될 수 있습니다.
따라서 앞서 설명했듯이 연결 리스트 기반 큐는 좋은 접근 방식이 아닙니다.
계속 나온다. 일반적인 큐는 사용하면 안된다고.
처리 단계 간 데이터 교환에 필요한 전체 저장 공간을 미리 할당하면 가비지 컬렉션을 최소화할 수 있습니다. 또한, 이러한 할당을 균일한 청크 단위로 수행하면 최신 프로세서에서 사용하는 캐싱 전략에 매우 적합한 방식으로 데이터를 순회할 수 있습니다.
이러한 요구 사항을 충족하는 데이터 구조는 모든 슬롯이 미리 채워진 배열입니다. Disruptor는 링 버퍼를 생성할 때 추상 팩토리 패턴을 사용하여 항목을 미리 할당합니다. 항목이 할당되면 생산자는 해당 데이터를 미리 할당된 구조에 복사할 수 있습니다.
대부분의 프로세서에서 링의 슬롯을 결정하는 순서 번호에 대한 나머지 계산은 매우 높은 비용을 발생시킵니다. 링 크기를 2의 거듭제곱으로 만들면 이 비용을 크게 줄일 수 있습니다. 나머지 연산을 효율적으로 수행하기 위해 크기가 마이너스 1인 비트 마스크를 사용할 수 있습니다.
왜 링 크기를 2의 거듭 제곱으로 만들면 비용이 크게 줄까?
비트마스킹을 통해 최적화가 가능하기 때문에 그렇다고 한다.
앞서 설명했듯이 유한 큐는 큐의 시작과 끝 부분에서 경합이 발생합니다. 링 버퍼 데이터 구조는 이러한 경합과 동시성 문제를 해결합니다. 이는 링 버퍼에 접근해야 하는 생산자 및 소비자 장벽으로 이러한 문제를 분리했기 때문입니다. 이러한 장벽의 로직은 아래에 설명되어 있습니다.
Disruptor의 일반적인 사용 사례에서는 보통 하나의 생산자만 존재합니다. 대표적인 생산자는 파일 읽기 또는 네트워크 수신자입니다. 생산자가 하나뿐인 경우에는 순서/항목 할당에 대한 경합이 발생하지 않습니다.
여러 생산자가 있는 특수한 상황에서는 생산자들이 링 버퍼의 다음 항목을 확보하기 위해 경쟁하게 됩니다. 다음 사용 가능한 항목을 확보하기 위한 경쟁은 해당 슬롯의 시퀀스 번호에 대한 간단한 CAS 연산으로 관리할 수 있습니다.
생산자가 확보한 항목에 관련 데이터를 복사하면 시퀀스를 커밋하여 소비자에게 공개할 수 있습니다. 이는 CAS 연산 없이 다른 생산자들이 각자의 커밋에서 해당 시퀀스에 도달할 때까지 대기하는 간단한 비지 스핀(busy spin)으로 가능합니다.
비지스핀이란 뭘까? 해석해 보면 바쁘게 도는 것이다. 헥헥!
앞에서 계속 혼자 도는것이다. 아무 의미 없는 루프를 도느라 CPU 코어 하나를 100% 차지할 수 있다.
그런데 왜 이걸 쓸까? CPU가 낭비되는데?
"여러 생산자가 있는 특수한 상황에서 생산자들이 링 버퍼의 다음 항목을 확보하기 위해 경쟁하게 된다" 고 했다.
여기서 시퀀스 번호에 대한 CAS 연산으로 관리가 가능하다고 언급했는데, 이 시퀀스 번호 반환이 생각보다 엄청 빨라서 즉각 낚아챌 수 있다고 한다.
그런 다음 해당 생산자는 커서를 이동시켜 소비 가능한 다음 항목을 표시할 수 있습니다. 생산자는 링 버퍼에 쓰기 전에 간단한 읽기 연산을 통해 소비자의 순서를 추적함으로써 링 버퍼가 가득 차는 것을 방지할 수 있습니다.
소비자는 항목을 읽기 전에 링 버퍼에서 시퀀스가 사용 가능해질 때까지 기다립니다. 기다리는 동안 다양한 전략을 사용할 수 있습니다. CPU 리소스가 부족한 경우 생산자가 신호를 보내는 락 내의 조건 변수를 기다릴 수 있습니다.
하지만 이는 명백히 경쟁의 원인이 되므로 CPU 리소스가 지연 시간이나 처리량보다 더 중요한 경우에만 사용해야 합니다. 소비자는 링 버퍼에서 현재 사용 가능한 시퀀스를 나타내는 커서를 반복적으로 확인할 수도 있습니다.
이는 CPU 리소스와 지연 시간을 교환하여 스레드 양보 여부와 관계없이 수행할 수 있습니다. 락과 조건 변수를 사용하지 않으면 생산자와 소비자 간의 경합 의존성이 해소되므로 확장성이 매우 뛰어납니다. 락이 없는 다중 생산자-다중 소비자 큐도 존재하지만, 이러한 큐는 헤드, 테일, 크기 카운터에 대해 여러 번의 CAS 연산을 필요로 합니다. Disruptor는 이러한 CAS 경합 문제를 겪지 않습니다.
Disruptor가 관심사를 3개로 분리해서, 효율적으로 동작할 수 있음을 설명한다.
3.3 Sequencing
시퀀싱은 Disruptor에서 동시성을 관리하는 핵심 개념입니다. 각 프로듀서와 컨슈머는 링 버퍼와의 상호 작용 방식에 있어 엄격한 시퀀싱 개념을 따릅니다. 프로듀서는 링 버퍼의 항목을 확보할 때 순서대로 다음 슬롯을 확보합니다. 사용 가능한 다음 슬롯의 순서는 프로듀서가 하나만 있는 경우 간단한 카운터일 수 있고, 여러 프로듀서가 있는 경우 CAS 연산을 사용하여 업데이트되는 원자적 카운터일 수 있습니다.
시퀀스 값이 확보되면 해당 링 버퍼 항목은 확보한 프로듀서(Producer)가 쓸 수 있게 됩니다. 프로듀서가 항목 업데이트를 완료하면 소비자(Consumer)가 사용할 수 있는 최신 항목에 대한 링 버퍼의 커서를 나타내는 별도의 카운터를 업데이트하여 변경 사항을 커밋할 수 있습니다. 프로듀서는 아래와 같이 메모리 배리어를 사용하여 CAS 연산 없이도 바쁜 스핀(busy spin) 상태에서 링 버퍼 커서를 읽고 쓸 수 있습니다.
long expectedSequence = claimedSequence – 1;
while (cursor != expectedSequence)
{
// busy spin
}
cursor = claimedSequence;
소비자(Consumer)는 메모리 배리어를 사용하여 커서를 읽음으로써 특정 시퀀스가 사용 가능해질 때까지 기다립니다. 커서가 업데이트되면 메모리 배리어는 커서 진행을 기다린 소비자들이 링 버퍼의 항목 변경 사항을 확인할 수 있도록 합니다.
각 소비자는 링 버퍼의 항목을 처리하면서 업데이트되는 자체 시퀀스를 가지고 있습니다. 이러한 소비자 시퀀스를 통해 생산자는 소비자를 추적하여 링이 순환되는 것을 방지할 수 있습니다. 또한 소비자 시퀀스는 소비자가 동일한 항목에 대해 순서대로 작업을 조정할 수 있도록 합니다.
생산자가 하나뿐인 경우, 그리고 소비자 그래프의 복잡성과 관계없이, 락이나 CAS 연산은 필요하지 않습니다. 전체 동시성 조정은 앞서 설명한 시퀀스에 대한 메모리 배리어만으로 구현할 수 있습니다.
해당 부분은 Disruptor에서 Sequence 를 사용해서 어떻데 동시성을 관리하는지를 설명한다.
정리하자면 미리 메모리를 가져와서 링으로 만들어서 sequence값을 확보시켜서 줘야만 프로듀서 혹은 컨슈머가 접근한다는 것이다.
결국 느린 소프트웨어적 자료구조 혹은 Lock같은 무거운 제어방식을 버리고 하드웨어와 cpu 연산에 맞게 재설계한것이다.
3.4 Batching Effect
소비자가 링 버퍼에서 진행 중인 커서 시퀀스를 기다릴 때, 큐에서는 불가능한 흥미로운 기회가 발생합니다. 소비자가 링 버퍼 커서가 마지막으로 확인한 이후 일정 단계만큼 진행된 것을 발견하면, 동시성 메커니즘에 관여하지 않고 해당 시퀀스까지 처리할 수 있습니다.
흥미로운 기회라는데, interesting opportunity 라 발생한다고 표현했다.
여기서 링 버퍼 커서란, "모든 생산자가 데이터 작성을 완벽하게 끝마쳤으니, 소비자들이 안심하고 가져가서 읽어도 좋다"고 공식적으로 선언(Commit)된 최신 인덱스 번호" 이다
그런데 Consumer가 링 버퍼 커서가, 즉 소비자를 위한 최신 인덱스 번호가 마지막에 확인햇을 때 보다 뭔가 더 앞서가있다면 동시성 메커니즘같은건 고려하지 않고 그 위치까지 처리를 한다.
이는 생산자가 처리 속도를 앞지를 때 뒤처진 소비자가 빠르게 속도를 따라잡아 시스템의 균형을 유지하는 데 도움이 됩니다.
이러한 배치 처리는 처리량을 증가시키는 동시에 지연 시간을 줄인다.
관찰 결과, 이 효과는 메모리 하위 시스템이 포화될 때까지 부하와 관계없이 지연 시간을 거의 일정하게 유지하다가, 포화된 후에는 Little의 법칙[6]을 따르는 선형적인 프로파일을 나타냅니다. 이는 부하 증가에 따라 큐에서 관찰되는 지연 시간의 "J"자 곡선 효과와는 매우 다릅니다.
마지막에 "포화 후에는 Little의 법칙을 따르는 선형적인 프로파일을 나타냅니다. 이는 부하 증가에 따라 큐에서 관찰되는 지연 시간의 "J"자 곡선 효과와는 매우 다릅니다." 하는데... 이게 도대체 무슨말인가?
여기서의 포화상태는 물리적인 하드웨어의 한계에 도달함을 말한다.
또한 Little의 법칙은 뭔가?
이는 시스템 성능 테스트에 활용하는 법칙이다. 이에 대해 찾아보면 좋을 것 같다.

이게 아이겐 벨류인가... 그냥 비율을 람다로 표시한건지...
J곡선은 이와 다르게 포화에 도달하면 미친듯이 과부화가 오는 그래프로 이해한다.

3.5. Dependency Graphs
큐는 생산자와 소비자 간의 단순한 단일 단계 파이프라인 의존성을 나타냅니다. 소비자들이 사슬이나 그래프와 같은 의존성 구조를 형성하는 경우, 그래프의 각 단계 사이에 큐가 필요하게 됩니다. 이는 의존성 단계들의 그래프 내에서 큐에 대한 고정 비용을 여러 번 발생시킵니다. LMAX 금융 거래소를 설계할 때 수행한 프로파일링 결과, 큐 기반 접근 방식을 사용하면 트랜잭션 처리의 전체 실행 비용에서 큐 비용이 압도적으로 커지는 것으로 나타났습니다.
디스럽터 패턴은 생산자와 소비자의 역할을 분리하기 때문에, 핵심적으로 단 하나의 링 버퍼만 사용하면서도 복잡한 소비자 의존성 그래프를 표현할 수 있습니다. 결과적으로 실행에 필요한 고정 비용이 크게 절감되어 처리량이 증가하고 지연 시간이 단축됩니다.
단일 링 버퍼는 전체 워크플로를 나타내는 복잡한 구조의 엔트리를 하나의 응집된 공간에 저장하는 데 사용될 수 있습니다. 이러한 구조를 설계할 때는 독립적인 소비자가 기록하는 상태가 캐시 라인의 잘못된 공유를 초래하지 않도록 주의해야 합니다.
결국 기존 큐 방식과 달리 disruptor 패턴은 생산자와 소비자의 역할을 분리했을 때문에 빠르단다.
그런데 도대체 어떻게 설계를 했길래 이렇게 이게 그래서... 어떻게 코드를 작성했길래 빠르다는거야?
3.6 Disruptor Class Diagram
Disruptor 프레임워크의 핵심 관계는 아래 클래스 다이어그램에 나타나 있습니다. 이 다이어그램에서는 프로그래밍 모델을 단순화하는 데 사용할 수 있는 편의 클래스는 생략되었습니다.
의존성 그래프가 구축되면 프로그래밍 모델은 간단해집니다. 생산자는 ProducerBarrier를 통해 순차적으로 엔트리를 확보하고, 확보한 엔트리에 변경 사항을 기록한 다음, ProducerBarrier를 통해 해당 엔트리를 다시 커밋하여 소비자가 사용할 수 있도록 합니다.
소비자는 새로운 엔트리가 있을 때 콜백을 수신하는 BatchHandler 구현체만 제공하면 됩니다. 결과적으로 이러한 프로그래밍 모델은 이벤트 기반이며 액터 모델과 유사한 점이 많습니다.
큐 구현에서 일반적으로 혼합되는 관심사를 분리함으로써 더욱 유연한 설계가 가능해집니다. Disruptor 패턴의 핵심에는 경합 없이 데이터 교환을 위한 저장 공간을 제공하는 RingBuffer가 있습니다.
RingBuffer와 상호 작용하는 생산자와 소비자의 동시성 문제는 분리되어 있습니다. ProducerBarrier는 RingBuffer의 슬롯 확보와 관련된 모든 동시성 문제를 관리하고, 종속 소비자를 추적하여 링이 순환되는 것을 방지합니다. 컨슈머배리어는 새로운 항목이 있을 때 컨슈머에게 알림을 보내고, 컨슈머는 처리 파이프라인의 여러 단계를 나타내는 종속성 그래프로 구성될 수 있습니다.
이게... 참 봐도 이해가 잘 안된다. 그림을 보자.

이렇게 해서 풀었다는데 뭐하는건지 모르겟다.

3.7은 Code Example Section인데
이 부분에 대해서는 다음 글에서 DSL을 직접 사용해보고 구성해보려고 한다.
그리고 Queue와의 성능 비교를 Apache JMeter 직접 해보겠다!
4. Throughput Performance Testing
참조용으로 우리는 Doug Lea의 뛰어난 java.util.concurrent.ArrayBlockingQueue[7]를 선택했습니다.
이는 우리의 테스트에 따르면 모든 제한된 큐 중에서 가장 높은 성능을 보여줍니다. 테스트는 Disruptor와 일치하도록 블로킹 프로그래밍 스타일로 수행됩니다. 아래에 자세히 설명된 테스트 케이스는 Disruptor 오픈 소스 프로젝트에서 사용할 수 있습니다.

위 구성에서는 Disruptor를 사용한 배리어 구성과 비교하여 각 데이터 흐름 아크에 ArrayBlockingQueue를 적용했습니다. 다음 표는 Java 1.6.0_25 64비트 Sun JVM, Windows 7, Intel Core i7 860 @ 2.8 GHz(HT 미지원) 및
Intel Core i7-2720QM(Ubuntu 11.04) 환경에서 5억 개의 메시지를 처리할 때 3회 실행 중 가장 좋은 결과를 얻은 초당 연산 수(OPS) 성능 결과를 보여줍니다. 결과는 JVM 실행 환경에 따라 크게 달라질 수 있으며, 아래 수치는 관찰된 최고값이 아닙니다.

5. Latency Performance Testing
지연 시간을 측정하기 위해 3단계 파이프라인을 사용하고 포화 상태에 도달하지 않도록 이벤트를 생성합니다. 이는 이벤트를 주입한 후 다음 이벤트를 주입하기 전에 1마이크로초를 기다리고 이를 5천만 번 반복함으로써 달성됩니다.
이 수준의 정밀도로 시간을 측정하려면 CPU의 타임스탬프 카운터를 사용해야 합니다. 전력 절약 및 절전 모드로 인해 주파수가 변하는 구형 프로세서의 문제를 해결하기 위해 TSC가 고정된 CPU를 선택했습니다.
Intel Nehalem 및 이후 프로세서는 Ubuntu 11.04에서 실행되는 최신 Oracle JVM에서 액세스할 수 있는 고정 TSC를 사용합니다. 이 테스트에서는 CPU 바인딩을 사용하지 않았습니다. 비교를 위해 다시 한 번 ArrayBlockingQueue를 사용했습니다. 더 나은 결과를 제공할 가능성이 있는 ConcurrentLinkedQueue[8]를 사용할 수도 있었지만, 생산자가 소비자보다 빠르게 처리하여 역압력을 발생시키지 않도록 제한된 큐 구현을 사용하고자 했습니다.
아래 결과는 Ubuntu 11.04에서 Java 1.6.0_25 64비트를 실행하는 2.2GHz Core i7-2720QM 시스템에서 얻은 것입니다. Disruptor의 홉당 평균 지연 시간은 52나노초인 반면, ArrayBlockingQueue는 32,757나노초입니다. 프로파일링 결과, ArrayBlockingQueue에서 지연 시간이 발생하는 주요 원인은 락 사용과 조건 변수를 통한 시그널링인 것으로 나타났습니다.

6. Conclusion
Disruptor는 처리량 증가, 동시 실행 컨텍스트 간 지연 시간 단축, 그리고 많은 애플리케이션에서 중요한 요소인 예측 가능한 지연 시간 보장을 위한 획기적인 발전입니다. 테스트 결과, Disruptor는 스레드 간 데이터 교환에 있어 유사한 방식보다 뛰어난 성능을 보여줍니다. 우리는 이것이 이러한 데이터 교환을 위한 최고의 성능 메커니즘이라고 확신합니다.
스레드 간 데이터 교환과 관련된 관심사를 명확하게 분리하고, 쓰기 경합을 제거하고, 읽기 경합을 최소화하며, 최신 프로세서에서 사용하는 캐싱 기능을 최대한 활용함으로써, 모든 애플리케이션에서 스레드 간 데이터 교환을 위한 매우 효율적인 메커니즘을 개발했습니다.
소비자가 경합 없이 주어진 임계값까지의 항목을 처리할 수 있도록 하는 배치 처리 효과는 고성능 시스템에 새로운 특징을 부여합니다. 대부분의 시스템에서는 부하와 경합이 증가함에 따라 지연 시간이 기하급수적으로 증가하는 "J"자 곡선을 보입니다. 하지만 Disruptor에서는 부하가 증가하더라도 메모리 하위 시스템이 포화될 때까지 지연 시간이 거의 일정하게 유지됩니다.
저희는 Disruptor가 고성능 컴퓨팅의 새로운 기준을 제시하며, 프로세서 및 컴퓨터 설계의 최신 트렌드를 지속적으로 활용할 수 있는 매우 유리한 위치에 있다고 믿습니다. (논문은 여기 클릭)
7. Disruptor 개발자의 다른 블로그
해당 주소는 trishagee 씨가 disruptor의 구조에 대해 설명하는 글이다.
(여기 클릭!)
그리고 필자는 그래서 어떻게 코딩을 햇길래 이게... 그렇게 빠르게 작동할 수 있는지 궁금하기에 다음 글에서 이어서 볼 예정이다.
와! 드디어 다 읽었다!
개요부터 기존의 방식에서의 문제점(1탄) 과 그래서 이를 어떻게 해결했는가(2탄) 의 내용을 쭉 봤다.
해당 논문을 통해서 운영체제가 필요한가에 대한 질문도 어느정도 해결이 되었다.
다만 AI시대에 코드 그거... AI가 다 해주는게 이걸 왜 공부해? 하는 의문도 드는건 사실이다.
그래... 코드 다 짜주는데 이러한 근본적인 지식에 대한 기초나 역량이 없어서 문제를 인지하고 못하고 문제가 생기면 아 AI가 잘못했어요 ㅎ 라고 할건가?
가장 큰 문제는 AI도 실수한다는 점이다. 이러나 저러나 공부해야하는건 변하지 않으니 할 수 있을 때 열심히 하자.
출처:
https://lmax-exchange.github.io/disruptor/disruptor.html
https://itnext.io/understanding-the-lmax-disruptor-caaaa2721496
https://trishagee.com/2011/07/04/dissecting_the_disruptor_writing_to_the_ring_buffer/
https://hippogrammer.tistory.com/274