본문 바로가기

Develop

👨🏻‍💻 2020 WOOWACON(우아한테크콘서트)

📌 SESSION 01: 배달의 민족 마이크로서비스 여행기(김영한)

매년 주문 수가 평균 3배 이상 증가할 정도로 빠르게 성장하고 있다.

2015

  • 하루 주문수 5만 이하
  • MS SQL + PHP,ASP
  • 대부분 루비DB(MS SQL) 스터어드 프로시저 방식 사용
  • 하나의 루비DB를 사용했기 때문에 장애 시 전체 서비스에 장애가 발생했다.

2016

  • 하루 주문수 10만 돌파
  • PHP에서 Java로 변경
  • 마이크로서비스 도전 시작 (결제 서비스)
  • 결제, 주문중계 독립
  • IDC에서 AWS 클라우드 인프라로 이전 시작

치킨 디도스

  • 선착순 결제 할인 이벤트 (7천 원 할인)
  • 프론트 서버 -> 주문 -> 결제
  • 많은 트래픽으로 인해 프론트 서버가 죽어버렸다.
  • IDC에서 하루 만에 AWS로 이전 (장비 100대 증설)
  • 하지만 주문 서버가 죽어버려서 주문 시스템도 AWS로 100대 증설
  • 결제까지 넘어갔지만 외부 PG, 카드사에서 죽어버림 (2배 장비 투입)

2017

  • 하루 주문수 20만 돌파 (많은 장애의 시대)
  • 메뉴, 정산, 가게 목록 시스템 독립
  • 장애 발생 시 타격이 심함.

2018 상반기

  • 전사 1순위 과제는 시스템 안전성
  • N 공고 폭파시키고 장애 대응 TF 창설
  • 쿠폰, 포인트 탈루비
  • 오프라인 모드 적용
  • 신규 가게상세 -> 1분~5분 주기로 큰 쿼리를 날려 AWS 다이나모 DB에 올렸다.

2018 하반기

  • 주문, 리뷰 탈루비 
  • 모든 부분에서 주문 시스템이 엮이기 때문에 커머스 도메인에서 가장 복잡하다.
  • 레거시 3대장 - 주문, 가게/업주, 광고
  • 기존 API 기반이라 주문 시스템이 다른 시스템에 영향을 미쳤다. 이벤트 기반으로 데이터를 전달하면서 장애가 급격하게 줄어드는 효과를 봤다.

먼데이 아키텍처

  • 대용량 트래픽에 어떻게 대응할 것인가?
  • 메인, 가게 리스트, 가게 상세 API는 초당 15,000회 호출
  • 모든 시스템이 대용량 트래픽을 감당하기는 어렵다. 트래픽보다 정확성이 더 중요한 시스템이 있기 때문이다.
  • 가게나 광고 같은 내부 서비스나 DB에 장애가 발생해도 고객 서비스를 유지하고 주문이 가능해야 한다.
  • 마이크로서비스는 데이터가 분산되기 때문에 어떻게 동기화를 시켜야할지 고민이 있었다.
  • 해결 방안 - CQRS 시스템으로 결정, 핵심 비즈니스 명령 시스템, 조회 중심 서비스를 분리
  • 장애 격리  - 각 시스템 내부에 필요한 데이터 보관하고, 내부 서비스의 모든 변경 내역이 이벤트로 전달하여 장애시 데이터 싱크가 늦어도 고객 서비스는 가능하다.

정리

  • 배달의 민족 시스템은 거대한 CQRS
  • 성능이 중요한 외부 시스템과 비즈니스 명령이 많은 내부 시스템으로 분리
  • 이벤트 발행을 통한 Eventually Consistency(최종적 일관성)
  • 각 시스템은 API or 이벤트 방식으로 연동
  • 마이크로서비스는 규모가 있어야 하고, 데이터를 싱크하고 여러가지 문제들에서 발생하는 비용이 크기 때문에 그 만큼 가치가 있어야 도입하는 것이 맞다고 생각한다.

📌 SESSION 02: 배민 프론트서버의 사실과 오해(권용근)

프론트 서버란?

사용자 전면 서버 (전시 시스템 등 다양한 용어들이 있지만 여기선 프론트 서버라고 명칭한다)

프론트 서버의 기술은 단순하다?

  • Spring, MVC, Redis를 사용할 것으로 예상했다.
  • 수 많은 외부 서비스의 실시간 상태가 필요하다.
    • ex) 1개 가게 노출하는데 필요한 외부 요청 수는 13개가 이다. 요청마다 50ms가 소요된다고 하면 650ms가 걸리므로 사용자 입장에선 느리게 느껴질 수 있다.
    • 따라서 비동기로 작업해야 한다. 하지만 스레드 지옥을 마주할 수 있다.
    • 대기 상황에서 스레드가 차단되지 않고 다른 작업을 수행할 수 있는 Non-Blocking I/O가 필요하다.
    • Spring 5의 WebFlux를 사용해야 했는데 기술과 패러다임이 달라 기술적인 큰 도전이 필요한 선택이었다.
    • 많은 동시 요청, 트래픽, 좋은 사용자 경험을 제공해야 했기에 WebFlux를 사용했다.
  • MSA 최전방
    • CQRS - 명령과 조회를 분리하는 시스템
    • 정합성이 중요한 시스템과 사용자에게 빠른 응답과 좋은 사용성을 제공하는 시스템을 분리
    • Eventually Consistency(최종적 일관성) - 직접 요청하는 것이 아닌 이벤트를 발행하여 스스로 할 일만 잘하면 되어서 각 시스템이 정합성이 높아질 수 있다. 이벤트는 유실되지 않는 큐에 보내어 수신하는 시스템들은 이벤트를 언젠가는 수신할 수 있다.

프론트 서버는 객체, 도메인 중심적인 개발을 할 수 없다?

  • 구체적인 데이터
    • 저장소에 요구사항에 맞게 가공된 데이터가 저장되어 있으면 잘못된 데이터를 복구하는데 걸리는 시간은 수시간이 소요된다.
    • 저장소에 원본의 형태와 유사한 데이터가 저장되어 있다면 복구하는데 걸리는 시간은 짧아진다.
    • 안정된 의존 관계 원칙(SDP) - 불안정성 = 나가는 의존성 / (들어오는 의존성 + 나가는 의존성) -> 1은 불안정한 상태
    • 안정된 추상화 원칙(SAP) - 안정된 정도만큼만 추상화되어야 한다. (구체적인 것은 추상적인 것과 반대되는 말)
    • 의존 역전 원칙(DIP) - 안정한 쪽에서 불안정을 의존하는 것을 인터페이스로 추상화하여 의존을 역전할 수 있다.
  • 도메인에 관하여
    • MSA는 큰 문제를 분할하여 작은 문제를 만들어 문제를 풀어간다.
    • 프론트 서버는 작은 문제를 다시 모아 큰 문제를 풀어나간다.
    • Serivce는 API 정보와 데이터들의 정보를 조합해야 하는데 이러면 서비스는 두꺼워질 수 밖에 없다.
    • Service는 얇아야 한다.
    • 비대해진 서비스를 도메인 쪽으로 흡수하여 얇은 서비스로 만들고, 메인이 더 뚱뚱해지는 마틴 파울러의 이상적인 아키텍처를 구성할 수 있다.

📌 SESSION 03: 배달의 민족 DB IDC 탈출기(박주희)

데이터베이스 클라우드 마이그레이션 과정을 기승전결로 나눠서 살펴보도록 한다.

기: 우리에게 주어진 과제

  • 메인 데이터베이스 복잡도는 증가하고 서비스는 성장하고 있었기에 시스템의 구조적인 문제를 해결하고자 했다.
  • 초기에 통짜 모델로 빠르게 개발, 배포 할 수 있었지만, 변화에 대해 몇가지 문제가 발생
  • 장애 확산 - 하나의 DB를 공유하고 있기 때문에 모든 서비스에 전파할 수 있다. (원하는 시간에 원하는 것을 먹지 못함)
  • 트래픽 증가 대응 불가 - 점심, 저녁 시간 트래픽이 수십배 차이가 난다.
  • 신규 서비스 개발 속도 저하 - 관련된 모든 테이블을 분석하고 수정해야 했기에 복잡도가 매우 증가하였다.

승: 우아한형제들의 클라우드 마이그레이션, 그 시작

  • 달리는 수레에 바퀴를 다는 것이 쉽지 않다.
  • AWS 도입, MSA 구조 도입
  • 의존도를 낮추는 작업을 했다.
  • 데이터 싱크를 유지하는 것을 완벽하게 극복하지 못했다.

전: 실패와 성장

  • 모놀리틱 구조를 클라우드 환경으로 이관하는 작업을 했다.
  • 당시 메인 데이터베이스 크기는 클라우드 환경에서 감당할 수 없었다. (코끼리를 냉장고에 넣을 수 없었다) 이와 같은 이유로 실패하였다.
  • 실패 이유 - 기술적 복잡도 증가, 비용 절감 효과 미비, 부족한 확장성
  • 기존 메인 데이터베이스의 구조상 스케일 아웃으로 트래픽 대응이 힘들어서 스케일 업을 하였지만 최고 스펙의 장비를 가져 더 이상의 확장은 힘들었다.

두가지 전략

  • 무중단 마이그레이션
    • 마이그레이션 범위가 넓고, 서비스 중단이 불가능한 경우 적합하다.
    • 마이그레이션 중 지속적인 데이터 변경이 발생하게 된다.
    • Validation을 위한 충분한 시간이 필요하다.
    • Master-Standby 구조로 마이그레이션 이후 이슈 발생 시 빠른 롤백이 가능하다.
  • 빠른 마이그레이션
    • 서비스 운영 시간이 제한 되어 있거나, 내부 운영을 위해 사용하는 경우 적합하다.
    • 서비스 중단으로 데이터 변경을 방지할 수 있다.
    • 추가 데이터 검증 과정을 생략 할 수 있다.

결: 불가능할 줄 알았던 일

  • 600개 이상의 마이크로서비스로 구성
  • 아키텍처 최적화, 론칭 시간 단축, 장애 확산 방지
  • 서비스 단위 성능 최적화(스케일 업, 스케일 아웃을 적절하게 활용), 강화된 모니터링

📌 SESSION 04: 겉바속촉 치킨의 골든타임을 지키기 위해 배민이 하는 일(이재일)

문제점

  • 원래 도착 예정 시간보다 늦게 배달될 확률이 높을 수 있다.
  • 무리한 배차로 사고가 일어날 수 있다.
  • 이와 같은 문제를 해결하고자 한다.

AI 추천배차(beta)

  • 1단계: 비용을 계산하자
  • 2단계: 경로 선택 (내 치킨은 어디에 끼어들지?) - 모든 경로 계산
    • 하나의 주문에 대해 경우의 수가 많은데 문제는 많은 주문이 일어난다.
    • Greedy Algorithm(탐욕 알고리즘) 활용
      • 모든 경우의 수를 구하기 보다 최대한 근사값을 구한다.
      • n개 배달 수행하는 최적 경로 = n-1개 배달을 수행하는 최적의 경로 중 배달을 사이 사이에 처리할 수 있는 모든 경로중 최적의 경로
      • 최적의 비용보다, 가능한 낮은 비용을 계산할 수 있다.
  • 3단계: 최적의 라이더 선정
    • 신규 비용 - 이전 비용이 가장 적은 라이더 선정
  • 4단계: 거리가 직선거리는 좀 그렇잖아요?
    • 직선으로 거리를 측정한다면 산을 넘어갈 수도 있다.
    • OSRM - 최적 경로를 제공하는 오픈소스 툴
  • 고민거리
    • 꼭 정확한 실거리는 아니여도 된다.
    • 가능한 데이터는 Cached여야 한다.
    • 미리 모두 계산해서 저장할 수는 없을까? - REDIS 선택

마지막 난관

  • 덩치가 큰 데이터
  • 바이너리 압축 프로토콜을 사용하면 되는데 빠른 구현을 위해 바이트로 전송

📌 SESSION 07: 수십억건에서 QUERYDSL 사용하기(이동욱)

적재된 데이터가 1000만건에서 10억건까지 되는 과정에서 얻은 Querydsl-JPA 개선 TIP에 대해 소개하고자 한다.

워밍업

  • extends / implements 사용하지 않기
  • 매번 인터페이스/Impl 구조가 필요하다.
  • 꼭 무언가를 상속/구현 받지 않거나, 특정 엔티티를 지정하지 않더라도 Queydsl을 사용할 수 있는 방법에 대해 알아보도록 한다.
  • JPAQueryFactory만 있으면 Querydsl은 사용할 수 있다. (extends/implements 제거 가능)
  • if문으로 분기처리하면 어떤 쿼리인지 파악하기 힘들기 때문에 동적쿼리는 BooleanExpression를 사용한다.

성능개선 - Select

  • exist 메서드를 금지한다.
    • exists가 빠른 이유는 조건에 해당하는 데이터 1개만 찾으면 다음 데이터를 순회하지 않기 때문이다. 그럼에도 금지하는 이유는 Querydsl의 exsists는 실제로 count() > 0 으로 실행되기 때문에 우리가 원하는 대로 이루어지지 않는다.
    • JPQL은 from 없이는 쿼리를 생성할 수 없다. 그래서 이를 직접 구현해야 한다.
    • limit 1로 조회를 제한하는 방법을 사용하면 되는데 조회 결과가 없으면 null이라서 체크해줘야 한다. (이 방법을 이용하면 성능상 큰 차이가 없다)
  • Cross Join 회피해야 한다. 묵시적 Join으로 Cross Join이 발생하는데 Hibernate 이슈라서 Spring Data JPA도 동일하게 발생한다. 이를 해결하는 방법은 명시적 Join으로 Inner join을 발생시키는 것이다.
  • Entity 보다 Dto를 우선으로 한다. Entity 조회시 Hibernate 캐시 불필요한 컬럼을 조회하며 OneToOne N+1 쿼리 등 단순 조회 기능에서는 성능 이슈 요소가 많다.
    • 조회 컬럼을 최소화 해야 하는데 우리가 미리 알고 있는 컬럼은 제외하면 좋다.
  • Select 컬럼에 Entity 자제 - id 컬럼을 조회하는 것이 성능상 이점이 크다.
  • Group By 최적화
    • MySQL에서 Group By를 실행하면 Filesort 가 필수로 발생한다.
    • MySQL에서 order by null을 사용하면 제거 가능하다.
    • Querydsl에서 order by null 문법을 지원하지 않기 때문에 조건 클래스를 생성해서 적용한다.
    • 정렬이 필요해도 조회 결과가 100건 이하라면, 애플리케이션에서 정렬하는 것이 좋다. 단, 페이징일 경우 order by null을 사용하지 못한다.
  • 커버링 인덱스
    • 쿼리를 충적시키는데 필요한 모든 컬럼을 갖고 있는 인덱스
    • select/where/order by/group by 등에서 사용되는 모든 컬럼에 인덱스에 포함된 상태
    • JPQL은 from절의 서브쿼리를 지원하지 않는다.
    • Cluster Key(PK)를 커버링 인덱스로 빠르게 조회 후, 조회된 Key로 SELECT 컬럼들을 후속 조회한다.
    • 두 번의 네트워크 차이를 제외하고 비슷한 성능을 가질 수 있다.

성능개선 - Update/Insert

  • 일괄 Update 최적화
    • 무분별한 DirtyChecking을 꼭 확인해봐야 한다.
    • 1만건 단일 컬럼 기준 약 2000배나 차이난다.
    • 일괄 Update가 항상 좋은 것은 아니다. 하이버네이트 캐시는 일괄 업데이트시 캐시 갱신이 안되고 업데이트 대상들에 대한 Cache Eviction이 필요하다.
    • DirtyChecking은 실시간 비즈니스 처리 및 실시간 단건 처리시 적합하다.
    • Querydsl.update는 대량의 데이터를 일괄로 Update 처리시 적합하다.
  • 진짜 Entity가 필요한게 아니라면 Querydsl과 Dto를 통해 꼭 필요한 항목들만 조회하고 업데이트해야 한다.
  • JPA로 Bulk Insert는 자제한다.
    • JdbcTemplate로 Bulk Insert는 처리되는데 컴파일체크, 코드-테이블 불일치 체크 등 Type Safe 개발이 어렵다.
    • Type Safa한 Bulk Insert를 할 수 없을까?
      • Querydsl != Querydsl-jpa
      • JdbcTemplate를 사용하지 않고, Querydsl-SQL을 사용할 수 있지 않나?
      • 로컬 PC에 DB를 설치하고 실행 후, 로컬 DB 정보를 등록해서 flyway로 테이블 생성 후 Querydsl-SQL 플러그인으로 테이블 Scan한다면 사용 가능하다. 번거롭다.. -> EntityQL로 가능? -> 단점이 명확해서 사용하지 않고 있다.

마무리

  • 상황에 따라 ORM / 전통적 Query 방식을 골라 사용할 것
  • JPA / Querydsl로 발생하는 쿼리 한번 더 확인하기
  • 신입 개발자에게 JPA 책이나, RealMySQL 책을 권하고 있다.

'Develop' 카테고리의 다른 글

👀 IntelliJ: 메서드 사용 정보 표시하기  (0) 2020.09.19
🗣 "시리야 배포해줘" (Travis CI)  (0) 2020.04.11
🚪 SSH 쉽게 접속하기  (0) 2020.03.27