본문 바로가기

MY개발생각

[개발생각] LMAX Disruptor에 대해서

한때 코인이 엄청나게 인기가 있을때, 거래소를 만들고 싶어서 많은 공부를 했던 기억이 있다.

거래소를 만들기 위해서는 여러 기술등이 필요했고, 고민들이 필요했다.

 

거래소에서 거래 기능은 ms를 다투는 초저지연으로 기능이 구현되어야 한다.

거래라고 하면, 비트코인 매도가 올라오면, 해당 매도 수량에 대해서 동시에 많은 구매자가 매수를 시도하게 되고,

해당 기록은 디비에 남겨지고, 그 매수량에 따라 각각의 구매자의 수량을 더해줘야하고,

매도자의 수량을 빼줘야하는 동시성이 아주 중요한 기능을 ms단위로 수행을 해야한다.

 

이기능을 단순히 db로 구현하게 되면,

몇명만 동시에 거래를 수행해도, db가 맛이 가게되거나, 정합성이 맞지 않게 된다.

여기서 해결책은 db에 로우락을 거는 방법인데,

이러면, 동시처리 성능이 현저하게 떨어지게 되어 사용을 할수없을 정도가 된다.

 

이것들을 해결하기 위해,

REDIS을 이용하여, 거래를 수행하고, 거래 결과는 writeback방식으로 이후에 db에 정리해서 넣는 방법을 하면,

좀더 퍼포먼스를 낼수있다.

 

여기서 중요한것은 redis를 사용할때,

원자성을 보장해 주도록 기능을 구현해야한다는 것이다.

일반 명령으로는 redis는 동시성을 제어는 해주지만, 원자성을 보장해 주지는 않기 때문이다.

 

원자성을 보장해 주도록 하기위한 방법으로는 redis의 명령을 lua script를 통해 수행하도록 구현하면 해결할수있다.

lua script를 통해, 거래가 이루어 졌을때,

매수자의 수량을 빼고, 금액을 더하고, 매도자의 수량을 더하고, 금액을 빼주는 레디스 명령을 묶을을 구현하면,

레디스는 해당 lua script내의 명령을 원자적으로 실행해주게 된다.

다만 주의할것은 원자성을 보장해 주는것이지, 트랜잭션을 보장해 주지는 않는다는 것이다.

redis에는 트랜잭션 롤백 기능이 없기때문이다.

(성능을 높이기 위해 포기한것이겠지..그래서 레디스가 빠른것이고..)

 

또하나 원자성 보장의 방법은 Multi명령을 통해, 여러 명령을 하나로 묶는 방법이다.

이렇게 사용하면, multi안에서의 각각의 명령이 실행될때, 다른 인스턴스에서의 명령어들의 수행이 막히기에 원자성을 보장할수있다.

 

트랜잭션 롤백은 알아서 잘 처리를 한다고 가정하고... 

(레디스에서 변경 명령어를 사용할때는 최대한 미리 확인(get, exist)를 하고 실제 변경가능한지를 먼저 체크하도록 구현해야한다. Watch명령어도 적절히 잘사용해야하고.. 고민할것이 많은 부분이다..)

 

여기서 대부분은 끝을 낸다..

대용량/저지연 처리를 하는 다른곳에서도 대부분은 레디스를 사용하는 방법정도로 기능을 구현한다.

그리고 레디스 클러스터 노드를 늘리고, 웹서버를 늘리는 식으로 성능을 스케일아웃하는 방법을 사용한다.

 

여기서 한발짝 더 나가면, 전혀 다른 세상을 경험할수있다.

바로 LMAX Disruptor의 사용이다.

 

LMAX Disruptor를 사용하는 법을 말하려는것이 중요함이 아니라,

어떻게 초저지연으로 레디스,큐보다 빠르게 처리를 할수있는지 그 원리를 이해하는게 중요하다는 이야기이다.

 

잘 생각해보자.

동시성에서 지연을 일으키는 원인이 무엇이고, 성능을 떨어트리는 원인이 무엇인가를 생각해봐야한다.

 

1) 거짓 공유 - 메모리 파편화에 따른 캐시 비효율적 EVICT 현상.

2) 락으로 인한 성능 저하 - 같은 자원을 여러곳에서 사용하려고 하는것에 대한 해결로 락을 사용함에 따른 성능저하.

 

위 2가지가 동시성에서 지연을 일으키는 가장 중요한 사항이다.

1번은 하드웨어적인 요인이라고 볼수있고, 2번은 소프트웨어적 요인이라고 봐도 무방할듯하다.

 

이 2가지를 LMAX Disruptor를 아무 똑똑하게 해결을 했다.

하나씩 설명하겠다.

 

1) 거짓 공유 - 메모리 파편화에 따른 캐시 비효율적 EVICT 현상.

결국 메모리가 파편화됨은 소프트웨어에서 할당하여 사용하는 변수들이 파편화됨에 따라 거짓공유가 일어나게 된다. (L1,L2,L3..)

따라서 LMAX Disruptor에서는 링버퍼를 이용하여,

미리 연속된 메모리를 확보하고, 해당 메모리 내부에서만 데이터를 저장하고 사용하는 방법을 사용한다.

어떤 새로운 데이터를 저장하기 위해 새로 메모리를 할당받지않고,

이미 큼지막하게 할당받은 메모리에서 사용하지않는 부분을 재사용하는 방법을 통해, 메모리 파편화를 줄인다.

링버퍼를 이용하여, 현재사용하는 위치와 사용하지않는 위치를 판단하고 사용하지않는 위치에 새로운 데이터를 오버라이트하여 사용함을 사용한다. GC대상도 줄어들기에 일석이조이다.

(음...링버퍼은 내가 대학생때도 있던것 구조긴한데.. 응용을 했다는것이 대단해..ㅋ)

 

2) 락으로 인한 성능 저하 - 같은 자원을 여러곳에서 사용하려고 하는것에 대한 해결로 락을 사용함에 따른 성능저하.

락을 걸어야하는 이유가 여러곳에서 특정자원을 접근하려고 할때인데,

LMAX Disruptor에서는 싱글 프로세스,싱글 쓰레드로 동작한다.

원천부터 멀티 접근을 통한 데이터 저장을 허용하지 않는 방법을 취했다.

이렇게 함으로서 CAS, SPIN등의 락프리,완화된 락의 방법조차 필요없게 되고, 완벽한 락프리로 동작하도록 할수있다.

(물론, 코딩을 거지같이 하면... 문제가 발생하겠지만..)

 

 

LMAX Disruptor는 그것말고도 배리어 개념과 sequence개념이 존재하는데,

이 개념으로 인해, 성능을 극도로 올릴수있게 된다.

큐등에서는 내가 컨슘을 해야하는 갯수, 주기가 정해져있어서, 내가 더 처리를 할수있는 시점에도 정해진 처리만 한다거나,

내가 더 처리를 할수없는 상황에서도 처리를 하려고 시도한다거나 할수있는 구조이다.

 

하지만, LMAX Disruptor에서는  배리어 개념과 sequence개념으로 인해,

내가 어디까지 최대한 처리할수있는지 그 인덱스를 미리 알수있기에, 내 성능에 맞게 최대한으로 처리량을 늘릴수있고, 줄일수있다. 

 

LMAX Disruptor는 초지연처리에서는 정말 좋은 솔류션이다.

 

하지만, 단점도 있는데, 

모든것이 싱글 인스턴스.싱글 스레드에서 동작하고, 로컬 변수로 동작하기에, 갑작스런 시스템다운등에는 취약하다.

기존에 메모리에 담고있던 모든 데이터가 한순간에 날아가기 때문이다.

 

이부분은 복구 전략을 잘 만들어야하는 부분이다.

LMAX Disruptor를 사용할때는 저널링이라고 하여 일명 WAL기능을 넣는것이 안정적이다.

 

저널링이란, LMAX Disruptor에 데이터를 추가하기전에 미리 파일에 해당 데이터를 기록하고 수행하는 방식이다.

장애가 났을때는 해당 저널링 파일을 읽어서 리플레이(복구)를 하도록 한다

(레디스등에 aof, rdb와 비슷하다고 보면 되겠다)

 

내가 LMAX Disruptor를 깊게 알수있게 도움을 준 오픈 github소스는 아래 링크이다. (전체 코드를 하나하나보면서 모두 파악했다)

https://github.com/exchange-core/exchange-core

 

GitHub - exchange-core/exchange-core: Ultra-fast matching engine written in Java based on LMAX Disruptor, Eclipse Collections, R

Ultra-fast matching engine written in Java based on LMAX Disruptor, Eclipse Collections, Real Logic Agrona, OpenHFT, LZ4 Java, and Adaptive Radix Trees. - exchange-core/exchange-core

github.com

외국 개발자가 LMAX Disruptor를 이용하여, 거래소를 만들 오픈소스엔진인데, 이 코드를 보면, 대단하다는 것을 느낄수있다.

여기에 저널링기능도 구현되어있다.

(그런데...저널링 기능에 버그가 있다... 나는 버그를 수정해서 사용했었다)

 

나만의 생각인지는 모르겠지만,

싱글 스레드에 퍼시스트한 원격저장소 기반으로 동작하지않는 구조가 위험하다고 생각하는 개발자가 많을것같다.

하지만, 그것은 고정관념일수있다는점을 알아야 할수도...