애플 한주가 고객에게 전달되기 까지

유재희·2023년 3월 17일
0

Conference

목록 보기
8/9

컨퍼런스 영상 링크
https://www.youtube.com/watch?v=UOWy6zdsD-c


용어 정리

  • 원장 : 계획과목별로 계정계좌를 설정한 장부, 거래를 계정별로 기록, 계산하는 장부를 말한다.

  • WTS : Web Trading System

  • MTS : Mobile Trading System

  • Resdis 분산락

  • 갱신 유실에 대해

  • JPA @Optimistic Lock

  • 정합성 : 데이터가 서로 모순 없이 일관되게 일치해야 함

컨퍼런스 목차


1. 해외주식 아키텍쳐

대부분 증권사들의 원장계는 C기반의 모놀리틱
토스는 MSA
스펙은 SpringBoot, kotlin, jpa, kafka, redis

토스 증권과 브로커의 주문 정합성이 중요
주문 요청과 체결 수신의 트랜잭션은 별도의 트랜잭션으로 분리되어있다.
하나의 주문에 대해 다수개의 이벤트가 발생 가능하다 (?)
이렇게 하나의 주문에 대해 발생한 이벤트들은 체결 수신 서버 -> Kafka를 거쳐 매매서버에서 처리된다.

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

  • 증거금, 고객잔고, 주문 등 많은 트랜잭션이 동시에 발생하게 되는데 이때 테이블 하나하나에 모두 락을 걸어버리면 데드락 지옥 발생

  • 대부분 증권사의 경우 락을 위한 테이블을 별도로 두어 계좌에 대한 락을 획득하고 트랜잭션 처리

  • 토스는 MSA 환경, 서비스가 각각 독립적인 DB를 갖고있어서 이런 방식은 맞지 않는다. (서비스간 높은 결합도, 비효율적인 자원 사용)

  • 해결책 Redis 기반 분산락을 사용한다.

  • 분산락 환경에서도 데드락이 발생하기 때문에 분산락 타임아웃을 걸어야한다.

  • 그러나 분산락 타임아웃도 불안정한 상황이 발생할 가능성이 있음

분산락 타임아웃과 트랜잭션의 지연으로 인한 정합성이 깨지는 예시

  • 명시적 잠금 (위의 테이블 예제) -> 여러 서비스에서 독립적인 DB를 갖고 있을때 적합하지 않음

  • 원자적 연산, 갱신 손실 자동감지는 DBMS에 의존적, ORM과 맞지않는다.

  • CAS : 현재 쓰레드에 저장된 값과 메인 메모리에 저장된 값을 비교하여 일치하는 경우 새로운 값으로 교체하고 일치하지 않는다면 실패하고 재시도를 한다

@Optimistic Lock

분산락 + Optimistic Lock을 사용한다고 하는데
왜 두개 다 이용하지? Optimistic Lock만 사용하면 재시도 로직 구현과 재시도 자체 실패시 대응으로 코드의 복잡성이 증가한다지만 분산락만 사용하면 동시성에 문제 없는거 아닌가?

hibernate envers활용 데이터 흐름 파악
데이터 변경 이력을 로깅하기 위한 라이브러리

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

  • 브로커와 통신하는 구간은 해외망으로 네트워크 지연이 빈번하다.
  • 브로커 요청이 지연될경우 매매 서버의 스레드까지 함께 블락킹이 되어 최악의 경우 모든 스레드가 행에 걸려 고객의 요청을 더이상 받을 수 없음

  • 고객이 매매 서버에 요청하는 스레드와 매매 서버가 브로커에 요청하는 스레드를 분리해서 모든 스레드의 블락킹을 막음

  • 동기로 처리할 경우 사용자는 많은 대기시간을 가질 수 있다.
  • 비동기 처리로 진행하는데 브로커 요청 이전에 주문을 대기상태로 저장
  • 브로커 응답 결과에 따라 접수 성공/실패로 주문 상태를 갱신
  • TCP 통신이라 타임아웃 발생 가능성
  • 브로커의 응답을 받지 못한채 타임아웃 발생시 요청 재시도 대상이 된다.

요청 재시도시 문제 상황 예시


  • 멱등한 API 요청으로 처리

  • 토스 주문 ID를 멱등키로 보내 하나의 토스 주문 ID는 하나의 브로커 주문만 생성

  • 계속 성공할때까지 요청만 보내면 될까?

  • 타임 아웃의 특성상 짧은 주기로 재요청을 보내게 될 경우 네트워크 지연 상황을 악화 시킬 수 있다. (왜?)

  • 토스 증권은 재시도 요청 횟수를 제한하고 지수 간격을 통해 재요청을 한다. (1 -> 2 -> 4 -> 8분 주기)

  • 혹시라도 정합성이 틀어질 경우 배치로 별도 처리

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

  • 위와 같이 매매 서버와 브로커가 직접적으로 연결되어 있다면 새로운 브로커 추가시 네트워크 프로토콜, 인터페이스까지 매매 서버가 새로운 브로커에 맞추게 되며 변경이 같이 일어난다 (강한 결합도)

  • 서버 레벨에서 격리 (매매 서버 요청 서버를 추가) 도메인 로직과 별개로 브로커 확장 가능, 브로커와 토스 증권 서비스를 격리 가능

  • 토스 증권에서 브로커로 향하는 Outbound 트랜잭션들은 매매 요청 서버를 거치게 되고 브로커에서 토스 증권으로 들어오는 Inbound 트랜잭션들은 체결 수신 서버를 통해 들어온다.
  • 체결 수신서버는 브로커의 이벤트를 DB에 적재하고 Kafka로 발송하는 책임을 갖게 된다. -> 매매서버가 바쁘더라도 브로커 이벤트를 유실하지 않는다.
  • 여러개의 브로커 파트너에서 이벤트를 수신할 경우 브로커들끼리 ID 경합이 일어날 수 있다. 이 책임을 체결수신 서버가 가져감으로써 여러개의 브로커 이벤트 ID를 중복없이 글로벌하게 유니크 ID를 발급할 수 있게된다.

이해하기


갱신 유실 방지

1. 원자적 연산 사용

  • 여러 데이터 베이스에서 원자적 갱신 연산을 제공한다. 이런 연산은 애플리케이션 코드에서 read-modify-write 주기를 구현할 필요를 없애준다. 하지만 객체 관계형 매핑 프레임워크를 사용하면 뜻하지 않게 데이터베이스가 제공하는 원자적 연산을 사용하는 대신 불안전한 read - modify - write 주기를 실행하는 코드를 작성해야 할때가 많다.
쓰기 연산에 원사성 (Atomicity) 성질을 부여함으로서 동시성 안전 획득
exclusive lock 을 획득하여 구현 → 갱신이 적용될 때까지 다른 트랜잭션에서 그 객체를 읽지 못함
or 모든 원자적 연산을 단일 스레드에서 실행되도록 강제하는 방법

2. 명시적 잠금 (위의 계좌 락 테이블과 같음)

  • 애플리케이션은 복잡한 비즈니스 로직을 구현해야 하는 경우가 많기 때문에 주로 명시적인 잠금을 사용한다. 즉 애플리케이션에서 갱신할 객체를 명시적으로 잠그는 것이다. 그러면 애플리케이션에서 read-modify-write 주기를 수행할 수 있고 다른 트랜잭션이 동시에 같은 객체를 읽으려고 하면 첫 번째 주기가 완료될 때까지 기다리도록 강제된다.

3. 갱신 손실 자동 감지

  • 여러 트랜잭션의 병렬 실행을 허용하고 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 abort 시키고, 재시도하도록 강제하는 방법

4. Compare-and-set 연산

  • 이 연산의 목적은 값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 허용함으로써 갱신 손실을 '회피' 하는 것이다. 현재 값이 이전에 읽은 값과 일치하지 않으면 갱신은 반영되지 않는다. 따라서 갱신이 적용됐는지 확인하고 필요하다면 재시도해야 한다. 그러나 데이터베이스가 where 절이 오래된 스냅숏으로부터 읽는 것을 허용한다면 이 구문은 갱신 손실을 막지 못할 수도 있다.
  • @Optimistic Lock

Redis 분산락

  • 재고시스템으로 알아보는 동시성이슈 해결방법 Redis를 활용한 Lock
  1. Lettuce 클라이언트를 이용한 스핀락
  2. Redisson 클라이언트를 이용한 pub/sub 방식의 락

1. Lettuce 스핀락

  1. 락을 획득한다는 것은 “락이 존재하는지 확인한다”, “존재하지 않는다면 락을 획득한다” 두 연산이 atomic하게 이루어져야한다. 레디스는 “값이 존재하지 않으면 세팅한다” 라는 setnx 명령어를 지원한다. 이 setnx를 이용하여 레디스에 값이 존재하지 않으면 세팅하게 하고, 값이 세팅되었는지 여부를 리턴 값으로 받아 락을 획득하는데에 성공했는지 확인한다.
  2. try 구문 안에서 락을 획득할때까지 계속 락 획득을 시도합니다.
  3. 락을 획득한 후에 연산을 수행한다.
  4. 락을 사용 후에는 꼭 해제하도록 finally에서 락을 해제해준다.

문제점

  1. 타임아웃이 설정되어 있지 않다.
  2. 레디스에 많은 부하가 간다.

2. Redisson pub/sub 락

  1. Redisson은 tryLock 메소드에 타임아웃을 명시. 첫 번째 파라미터는 락 획득을 대기할 타임아웃이고, 두 번째 파라미터는 락이 만료되는 시간.

    첫 번째 파라미터만큼의 시간이 지나면 false가 반환되며 락 획득에 실패. 그리고 두 번째 파라미터만큼의 시간이 지나면 락이 만료되어 사라지기 때문에 어플리케이션에서 락을 해제해주지 않더라도 다른 스레드 혹은 어플리케이션에서 락을 획득할 수 있음.

    이로 인해 락이 해제되지 않는 문제로 무한 루프에 빠질 위험이 사라졌기 때문에 위의 1번 문제를 해결.

// RedissonLock의 tryLock 메소드 시그니쳐
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException

  1. 스핀락이 아닌 pub/sub 방식의 락 획득 요청. 락을 획득할 클라이언트들은 락 획득에 실패시 대기 상태 -> 락이 해제된 시점에 서버는 락 획득 가능 메시지를 publising -> 해당 채널을 subscribe하고 있는 클라이언트들은 다시 락 획득 시도 -> 실패시 대기 -> 타임아웃까지 실패시 false 반환

Optimistic Lock

낙관적 락

실제 데이터에 락을 걸지 않고 버저닝을 통한 트랜잭션 재시도 처리

문제점

  • 수동적인 재시도 로직 -> 코드 복잡성 증가
  • 충돌이 잦을 경우 오히려 성능이 저하된다.

Pessimistic Lock

비관적 락

실제 데이터에 대한 작업이 끝날때까지 락을 걸어버림

문제점

  • 충돌이 별로 없을경우 좋지 못한 성능
  • 데드락 가능성이 높다.

왜 토스는 레디스 분산락 쓰면서 낙관적 락까지 쓰는걸까?

  • 영상에 의하면 분산락 타임아웃과 비즈니스 로직 트랜잭션 커밋 시점이 일치하지 않을때 갱신 유실 때문이라고 한다.

왜 분산락만 안써? -> 타임아웃 설정 안하면 데드락 걸려
왜 Optimistic 락만 안써? -> 잦은 충돌로 트랜잭션 재시도가 많아지면 코드 구현하기 힘들어

위 두가지를 동시에 쓰면서 동시성 처리에 대한 안정성을 높이는듯

왜 TCP 연결 타임아웃되는 요청을 빠른 주기로 재요청 할때 네트워크 혼잡이 더 악화될까?

TCP 통신의 혼잡제어와 같은 맥락인가?

  • 송신측의 데이터 전달과 네트워크 데이터 처리 속도를 해결하기 위한 기법이다.
  • 한 라우터에게 데이터가 몰려 모든 데이터를 처리할 수 없는 경우, 호스트들은 재전송을 하게 되고 결국 혼잡을 가중시켜 오버플로우나 데이터 손실이 발생한다.
  • 이러한 네트워크의 혼잡을 피하기 위해 송신측에서 보내는 데이터의 전송 속도를 제어하는 것이 혼잡 제어의 개념이다.

Reference


https://derekpark.tistory.com/108
https://www.devkuma.com/docs/data-intensive-application/7/
https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
https://velog.io/@jsj3282/TCP-%ED%9D%90%EB%A6%84%EC%A0%9C%EC%96%B4%ED%98%BC%EC%9E%A1%EC%A0%9C%EC%96%B4-%EC%98%A4%EB%A5%98%EC%A0%9C%EC%96%B4

profile
몰라요

0개의 댓글