📌 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 |