본문 바로가기

배포/대규모 트래픽 처리에 대한 고민

배민의 주문 아키텍쳐 설계 필기 및 메모

https://www.youtube.com/watch?v=704qQs6KoUk

배달의 민족 주문시스템: 특정 시간대(12시, 18시 30분)에 트래픽이 몰림
MSA 아키텍처로 이루어져 있음.
가게, 메뉴, 주문, 결제, 배달 등 수많은 시스템이 통신하고 있음
대용량 데이터 : 일 평균 300만 건의 데이터를 저장하고 수년간 저장
그에 따른 대규모 트랜잭션이 발생함 : 일 평균 300만 건, 점심 저녁에 트래픽이 더 몰린다.
여러 시스템과 연계로 이벤트 기반으로 통신하는 MSA로 구성되어 있다.
주문 시스템은 이벤트 기반으로 여러 시스템과 통신


기존의 아키텍처의 단점과 그 개선 방법

  1. 단일 장애 포인트 하나의 시스템 장애 -> 전체 시스템의 장애
    1. 중앙 집중 db의 장애-> 전체 시스템으로 전파
    2. 각 시스템별로 각각에 맞는 도메인을 설계하여 저장소를 따로 가져가게 됨, 각 시스템 한 게는 MQ를 통해 느슨한 통신 가져가게 됨
    3. 특정 시스템의 장애는 메시지 발행의 실패로 끝이 나고, 다른 영향을 받지 않는 시스템에는 영향이 가지 않음
    4. 영향이 가는 이벤트의 경우 이벤트 재발행시, 재소비를 하여 빠르게 서비스가 안정화됨
    5. 주요 기능 : MQ를 이용한 이벤트 기반 통신
  2. 대용량 데이터 rdbms 조인 연산으로 조회성능이 좋지 않음
    1. 4중 join을 사용하다 보니 오히려 성능이 떨어지게 되었다. 조회 성능을 올리기 위해 단일 테이블로 역 정규화를 진행
    2. mongodb(NoSQL)에 저장하여, id 기반으로 단순 조회가 가능하게 됨
    3. 주문 도메인은 생명주기에서만 도메인 변경이 발생
    4. CQRS 적용 아키텍처
    5. 커맨드 모델과 조회 모델을 분리, 조회 모델 역 정규화를 통해 조회 성능 개선
  3. 대규모 트랜잭션: 주문수 증가로 저장소의 쓰기 처리량 한계에 도달
    1. 주문 db 샤딩
      1. aws aurora는 샤딩 지원을 하지 않음
    2. 직접 샤딩을 구현하게 됨
      1. key based sharding
        1. shard key를 이용하여 데이터 소스를 결정
        2. 주문 번호를 hash function을 통해 변환하여 분배되도록 함
        3. 장점? 구현 간단함, 데이터 골고루 분배 가능
        4. 장비를 동적으로 추가, 제거할 때 데이터 재배치가 필요함
      2. Range based sharding
        1. 값의 범위 기반으로 데이터를 분산시키는 방식
        2. 일정 가격 이하는 0번 중간은 1번 고가는 2번
        3. 장점: 구현 간단함
        4. 단점: 데이터의 균등 분배가 불가능하여 성능저하가 발생할 수 있음
      3. directory based sharding
        1. 샤드가 어떤 데이터를 가질지 look up table을 유지하는 방식
        2. 중간 테이블로 어떤 샤드로 갈지 결정해 줌, 그리고 그것을 기록해 준다.
        3. 장점: 동적으로 샤드를 추가하는데 유리하다.
        4. 단점:  Lookup table이 단일 장애 포인트가 될 수 있다.
    3. 주문 시스템의 특징?
      1. 배달앱의 특성상, 주문이 정상 동작하지 않으면, 배민 서비스 자체의 신뢰도가 내려가게 됨
        1. 단일 장애 포인트는 최대한 피하자
      2. 동적 주문 데이터만 최대 30일만 저장함(배민 규정상)
        1. 샤드 추가 이후 30일이 지나면 데이터는 다시 균등하게 분배된다.
    4. 주문순번 % 샤드 수로 샤드번호를 결정하여 분배
      1. AOP와 AbstractRoutingDataSource를 이용하여 구현
      2. 이 부분은 유튭 23:12분 근처 보기
    5. 다건 조회 애그리게이트 로직: 어떻게 데이터를 조합하여 내려줄까? 몽고 db를 조회성 모델을 사용
      1. 주문 API, 주문 이터널 API를 분리해 둔 것이 큰 도움이 되었음 분산할 때
  4. 규칙 없는 이벤트 발행으로 서비스 복잡도가 높아짐
    1. 주문서비스에 집중된 관심도, 무분별한 이벤트가 발생하여 추적이 어려워짐
      1. 주문생성-주문접수-배달완료-주문취소 (도메인 로직)
      2. 알림 전송, 현금 영수증 발행, 분석 로그 전송 데이터 동기화 외부 이벤트 발행 (서비스 로직)
      3. 도메인 과점에선 아키텍처가 잘 분리되어 있음
      4. 하지만 시스템관점에선 아키텍처가 로직을 수행하는 주체를 파악하기 어려움, 
      5. 서비스 주체에 대해서 수행하는 어플리케이션이 달라짐으로써 수행하기 어려워지는 문제점이 발생하게 됨. 
      6. 기능 추가할 때도 서버 하나를 놓치게 되는 경우가 있음 -> 그에 따라 sqs event에서 유실이 발생할 경우, 재처리가 어려움
    2. 내부/외부 이벤트 정리
      1. 내부 이벤트에는 zero payload 사용
      2. 내부 이벤트에는 서비스 로직을 심기 어렵게 하기 위함. 
      3. 데이터가 없다 보니 서비스를 수행하기에는 한계가 있게끔 하고 싶었음
      4. 이벤트 처리기가 저장소에서 즉시조회함
      5. 이벤트 처리 주체를 단일화하여 해결함
    3. 이벤트 발행 실패 유형
      1. 트랜잭션 내부/외부 이벤트 발행 실패
      2. 내부의 경우 도메인 로직 전체가 실패
      3. 외부의 경우, 서비스 로직 비수행( 재발행 방법이 없어 도메인로직과 서비스로직의 일관성 보장 불가능)
    4. 트랜잭션 아웃박스 패턴 사용
      1. 이벤트 발행 실패와 서비스 실패를 격리하여 재발행 수단을 보장함
      2. 이벤트 발행 실패 시 아웃박스 엔티티에 저장된 페이로드 재발행