토스 - 동시성, 네크워크 지연, 외부 서비스 의존성 관리

본 글은 토스ㅣSLASH 22 - 애플 한 주가 고객에게 전달 되기까지를 정리한 글입니다.

높은 정합성과 신뢰성을 요구하는 주식원장 시스템을 어떻게 MSA로 지원하는지 소개합니다.

목차

  1. 토스의 MSA기반 해외 원장 시스템 구조 소개
  2. 해외구간 네트워크 지연으로부터 안전하게 서비스하기
  3. 브로커 의존성 격리하기
  4. 정리

1. MSA기반 해외 원장 시스템 구조

toss01

  • Spring, Kotlin, JPA 기술스택을 사용
  • 필요에 따라 Redis, Kafka 사용
  • 각 도메인 별 모듈들이 독립적으로 구성
    • 한 모듈의 장애가 전체에 전파가 안되도록 설계

주문 체결 흐름도

  • 고객 ↔︎ 토스증권 ↔︎ 브로커(해외거래 중개인) ↔︎ 현지 거래소
    • 토스와 같은 법인은 현지 거래소와 바로 거래할수 없음

toss02

  • 토스의 주문 요청(2)과 체결 결과(4)는 다른 트랜잭션
  • 하나의 주문에 대해 다수개의 이벤트가 발생가능

동시다발적으로 발생하는 트랜잭션에서 고객의 자산을 안전하게 처리하기

MSA기반 원장에서 동시성은 어떻게 다루어지는가?

매매서버

toss03

  • 매매서버의 역할
    • 고객의 주문을 브로커에 전달
    • 원장에 기록
  • WTS, MTS, 자동매매 등 여러곳에서 고객의 잔고를 갱신하는 트랜잭션이 발생
    • 이러한 환경에서 안전하게 동시성을 제어할 수 있어야함
  • 가장 보편적인 방법은 매매 서버에서 을 통한 동시성 제어
    • 매매 요청은 고객의 잔고, 증거금, 주문 등 여러 테이블에 대한 삽입 및 갱신이 발생
    • 각 테이블에 대해 모두 락이 존재하면 성능 저하, 대드락을 피할 수 없음

toss04

  • 토스는 MSA 구성, 모듈별로 독립된 DB로 구성, 시스템의 발전으로 분리가능성이 존재
  • 위와 같은 방식(락)이 적용되면 서비스간 높은 결합도 유지, 비효율적인 자원사용이 야기됨
  • 토스는 이러한 상황을 Redis기반 분산락을 사용하여 해결중

분산락의 문제점 해결

  • 분산락 타임아웃
    • 하나의 트랜잭션이 무한정 락을 소유할 경우 다른 서버들에서 무한정 대기(데드락)
    • 이를 타임아웃으로 해결
  • 분산락 타임아웃의 문제
    • 타임아웃으로 락은 해제되었으나 트랜잭션이 끝나지 않아 다른 트랜잭션과 경합이 발생가능
    • 예, 아래그림) T1원 2000원 출금, T2는 500원을 출금하는 예제
    • 위와 같은 문제는 분산락 해제전 또는 해제후 디비 트랜잭션 커밋이 되어 발생하는 문제
    • JPA에서는 쓰기 지연등으로 이러한 문제가 발생할 가능성이 높음

toss05

  • 갱신 유실은 다음과 같은 방법으로 방지가능
    • 원자적 연산 사용 - DBMS에 의존적, ORM과 궁합이 좋지 않음
    • 갱신 손실 자동 감지 - DBMS에 의존적, ORM과 궁합이 좋지 않음
    • 명시적 잠금 - 여러 테이블을 갱신하는 트랜잭션에서 비용이 비쌈
    • Compare-and-set 연산 - 토스에서는 이 방식을 사용
      • JPA에서는 @OptimisticLocking을 사용하여 간단하게 CAS 연산 구현 가능
  • OptimisticLocking은 버전을 통해 갱신 유실을 방지
  • 예) 오른쪽의 쿼리와 4초에서 업데이트를 시도할때 버전이 안맞아서 T1 트랜잭션이 실패

toss06

  • 분산락 없이 OptimisticLocking 만으로 동시성 제거는 불가능한가? 결론은 불가능
    • 대기없이 실패, 이로인해 별도의 재시도 구현이 필요
    • 트랜잭션 재시도 구현은 여러케이스를 고려해야해서 코드의 복잡성 올라감
  • 결론
    • 토스는 분산락으로 동시성 제어, 만약의 상황을 위해 OptimisticLocking사용
    • 주요 테이블들은 하이버네이트 envers를 통해 변경 히스토리를 저장

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

toss07

  • 브로커와 통신하는 구간은 해외망으로 네트워크 지연이 빈번하게 발생
  • 브로커 요청이 지연될경우 매매서버의 스레드가 블락킹, 최악의 경우 모든 스레드가 블락킹
    • 더 이상 고객 요청 처리 불가능
  • 토스에서는 고객의 요청에 대한 스레드와 브로커에 요청하는 스레드를 분리하여 모든 스레드가 블락킹되는 이슈를 해결중

toss08

  • 브로커 통신 구간은 여전히 빈번한 지연이 발생
    • 하나의 API로 동기로 처리시 고객은 주문응답을 기다려야함
    • 토스는 이 구간을 비동기로 처리하여 고객경험을 상승시키고 트랜잭션 시간을 최소화로 유지중
  • 여전히 실패가능한 브로거 요청을 처리하기 위해 주문을 먼저 대기상태로 저장하고 성공/실패 상태로 갱신
    • TCP기반 통신처리시 타임아웃 발생가능
      • 주문 결과를 알 수 없음 → 상태 갱신 불가능
      • 브로커의 식별자를 알수 없어 주문상태를 조회할수 없음
  • 토스는 이러한 문제를 재시도 + 멱등성API 로 해결
    • 1번 주문(식별자 포함)에서 네트워크 지연이 발생해서 타임아웃 발생시 재시도 대상에 포함됨
    • 1번 주문(식별자 포함)이 다시 재시도
    • 식별자가 같으므로 하나의 주문만 생성됨
  • 타임아웃 발생시 응답이 올때까지 재시도?
    • 이 경우 네트워크 상태를 더욱 악화할 수 있음
    • 토스는 일정횟수로 재시도를 제한하고 지수적으로 간격을 둠 (1,2,4,8분)
  • 주문 및 체결 정합성이 틀어진경우를 대비해 별도의 잡이 수행중

3. 브로커 의존성 격리하기

  • 브로커(외부기관)의 비즈니스에 의해 언제든지 변경되거나 추가될 수 있음
  • 새로운 브로커가 추가되면 매매서버(네트워크 프로토콜, 인터페이스 등)를 변경 필요
  • 매매서버와 브로커의 처리량 또한 강하게 결합되어있음
  • 토스에서는 브로커 의존성을 가장 강한 격리 수준인 서버 레벨에서 격리중
    • 브로커의 지원통신방식과 관계없이 매매서버는 내부 인터페이스만 고려
    • 매매서버는 브로커와 관계없이 도메인 로직을 유지가능
    • 브로커의 처리량에 따라 매매 요청 서버를 스케일아웃하여 효율적인 리소스 사용이 가능
    • 매매 서버가 브로커 장애로 부터 격리

toss02

  • 토스증권 -> 브로커의 Outbound 트랜잭션은 매매 요청 서버에서 처리
  • 브로커 -> 토스증권의 Inbound 트랜잭션은 체결 수신서버가 담당

toss10

  • 브로커 의존성 격리시 매매 요청 뿐만 아니라 체결 수신에서도 장점이 있음
    • 매매서버가 바쁜 상황이더라도 브로커의 응답을 수신가능
    • 수신 내역을 DB에 저장하고 있기 때문에 카프카가 다운되더라도 다양한 Fail Over전략 선택가능
  • 브로커 인터페이스와 무관하게 유니크 아이디를 사용해 중복 이벤트 처리가능
    • 여러개의 브로커사용시 아이디가 경합될 수 있지만 체결수신 서버가 관리하기 때문에 전역적인 유니크 아이디 발급가능

정리

  • 동시성 제어를 위해 분산락과 낙관적락 사용
    • 분산락을 기본으로 사용시하고 데드락을 방지하기 위해 타임아웃 설정
    • 정합성을 위한 2차 안전장치로 낙관적락으로 사용
  • 외부 서비스 처리 서비스를 분리
    • 제어할 수 없는 외부 서비스의 요청을 고객 요청과 분리해서 사용성 개선
    • Inbound/Outbound 트랜잭션을 분리된 서비스로 처리해서 한 동작에서의 장애가 다른 동작에 영향을 미치지 않도록 설계
updated_at 20-08-2022