ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 토스 SLASH 22- 애플 한 주가 고객에게 전달 되기까지
    세미나, 영상 요약정리 2023. 7. 30. 00:01

    https://www.youtube.com/watch?v=UOWy6zdsD-c 

     

    토스증권 서버 개발자 이승천 님의 발표를 요약해 보겠습니다.

     

    해외주식 아키텍처

    게이트웨이, 채널계, 해외원장, 국내 원장로 구성되어 있습니다.

    대부분의 증권사들의 원장계는 C 기반 모놀리틱으로 구성되어 있지만 토스증권은 MSA 기반으로 구성되어 있습니다.

    토스증권은 한국법인으로 나스닥과 같은 현지 거래소와 직접 거래할 수 없습니다.

    따라서 브로커를 통해 주문을 제출하게 됩니다.

     

    매매 서버 -> 매매 요청 서버 -> 브로커에게 전달 -> 브로커는 현지 거래소에 요청

    체결 수신 서버 -> kafka를 거쳐 매매서버에서 처리가 이루어집니다.

    매매 요청서버와 체결 수신서버가 별개로 이루어진 이유는 나중에 밝힙니다.

     

     

    동시성으로부터 고객의 자산 안전하게 처리

    WTS, MTS, 자동 매매등에서 고객의 잔고를 갱신하는 트랜잭션이 발생합니다.

    이를 위한 가장 보편적인 방법은 락을 통한 동시성 제어입니다.

    고객의 잔고, 증거금, 주문 등 여러 테이블에 대한 삽입 및 갱신이 일어나는 트랜잭션이므로 대상 테이블에 모두 락을 잡아버리면 성능 저하와 데드락 지옥을 피할 수 없습니다.

     

    이런 경우 보통 락을 위한 테이블을 하나 별개로 두고 계좌락 테이블을 통해 트랜잭션을 제어하곤 합니다.

    하지만 MSA 구조로 독립적인 데이터베이스로 구성되어 있습니다.

    하나의 서버가 여러 개의 서버로 분리될 가능성도 존재합니다.

     

    Redis 기반 분산락을 사용하여 모듈의 데이터베이스를 강제하지 않고 서비스 간의 결합도도 느슨하게 유지할 수 있습니다.

    또한 높은 처리량이 보장되어야 하고 계좌락 방식에 비해 redis를 사용하면 더 높은 처리량이 가능합니다.

     

    분산락은 모든 서버가 공통적으로 사용하기 때문에 타임아웃을 잘 지정해주어야 합니다.

    다른 서버가 무한정 대기할 수 있기 때문!

     

    하지만 락 타임아웃 때문에 다음과 같은 일이 벌어질 수 있습니다.

    그림2

    [그림 2]는 트랜잭션이 끝나기 전에 락이 해제되는 경우에 무결성이 깨질 수 있는 모습을 보여줍니다.

    이를 해결하기 위해선 원자적 연산 사용, 명시적 장금, 갱신 손실 자동 감지, CAS 연산으로 해결할 수 있습니다.

     

    명시적인 잠금은 여러 테이블을 갱신하기 때문에 비용이 매우 비싸다

    원자적 연산과 갱신 손실 자동 감지는 DBMS에 의존적이어서 ORM과 궁합이 좋지 않습니다.

     

    JPA에서는 @OptimisticLocking을 통해 CAS연산을 쉽게 구현할 수 있습니다.

     

    그러면 분산락 없이 그냥 낙관적락으로만 해결할 수 없을까요?

    동시에 발생하는 트랜잭션들은 대기 없이 실패하게 되거나 별도의 재시도 구현이 필요합니다.

    이는 코드의 복잡도로 자연스럽게 이어집니다.

    대부분의 상황에서 분산락은 정상적으로 동작하기 때문에 만약의 상황에서 낙관적락으로 해결합니다.

    또한 주요 테이블은 하이버네이트 enver를 통해 히스토리를 쌓고 있습니다.

     

    해외구간 네트워크 지연으로부터 안전하게 서비스하기

    브로커와의 통신은 해외망으로 지연이 자주 발생되어 최악의 경우에는 매매서버의 쓰레드가 모두 블로킹 되어 있을 수 있습니다.

    이를 위해 고객 -> 매매 서버 -> 브로커로 나누어 모든 쓰레드가 블랑킹되는 이슈를 해결하고 있습니다.

    하지만 매매서버 -> 브로커는 여전히 지연이 빈번하기 때문에 고객은 이 주문응답을 기다리느라 아무 일도 할 수 없습니다.

    따라서 이를 비동기로 빼서 고객경험을 개선시키고 트랜잭션 시간을 최소화시킵니다.

     

    대기 상태는 브로커의 결과에따라 성공/실패로 갱신됩니다.

    하지만 브로커의 응답을 받지 못한 채 TCP 타임아웃이 발생하면 주문 상태를 임의로 판단할 수 없게 됩니다.

    또한 식별자 ID도 못 받아서 브로커 식별자로 조회도 불가능합니다.

     

    이럴 때는 재시도를 해야 하는 상황으로 판단합니다.

    추가적으로 데이터 정합성을 위해 멱등한 API를 통해 해결합니다.

    이제 성공할 때까지 요청을 계속 보내면 될까요?

     

    혼잡제어 시에 계속 요청을 보내면 네트워크지연현상을 더 악화시킬 수 있습니다.

    이를 통해 일정 횟수로 재시도를 제한하고 지수적인 증가를 통해 재시도를 시도합니다.

    모두 실패하더라도 대사를 통해 별도 오퍼레이션이 정합성을 해결합니다.

     

    브로커 의존성 격리하기

    브로커는 외부기관이고 비즈니스 정책에 의해 언제든지 변경되고 추가될 수 있습니다.

    이를 해결하기 위해 매매서버와 브로커 서버사이에 매매 서버 요청을 두었습니다.

     

    체결 수신서버는 DB에 저장 후 카프카에 발송하기 때문에 매매서버가 매우 바쁘더라도 언젠가는 내역을 수신할 수 있습니다.

    또한 카프카가 다운되어 폴링 모드로 전환되는 등 다양한 Fail Over 전략이 가능합니다.

     

    댓글

Designed by Tistory.