TIL

23.11.15

오잉머신 2023. 11. 15. 18:04

2023 우아콘에 참석해서 다양한 세션을 들었다.

1. 대규모 트랜잭션을 처리하는 배민 주문시스템 규모에 따른 진화

## 1. 배민 주문 시스템
독특한 특징 : 12, 18:30에 주문수 급상승
커머스 + 음식 도메인

- MSA
시스템 간 장애 전파되지 않도록. 시스템 간 격리

- 대용량 데이터
데이터 정합성, 조회 성능

- 대규모 트랜잭션
순간적으로 몰리는 트래픽에 대한 대규모 트랜잭션 처리

- 이벤트 기반으로 통신

## 2. 성장하는 배민 주문 시스템
매년 주문수 가파르게 증가(일 평균 300만 이상)

## 3. 성장통
1) 단일 장애 포인트
하나의 시스템의 장애가 전체 시스템의 장애로 이어짐

루비라 불리는 중앙 집중 저장소에 모든 시스템 의존
-> 특정 시스템 장애는 루비(중앙 저장소)의 장애
-> 서비스 전체 장애
=> 탈루비 프로젝트

중앙 저장소에서 각 시스템을 분리하는 프로젝트
=> 시스템 간 통신은 mq로

특정 시스템 장애는 메시지 발행의 실패로 끝

‘’’MSA’’’

2) 대용량 데이터
Dbms 조인 연산으로 조회 성능 별로

주문 생성, 주문 조회 모두 하나의 rdbms 바라봄
정규화된 데이터 -> 조인해야함
역정규화를 통해 단일 테이블로 (mongoDB)

동기화 어떻게?
주문 이벤트 발행하면 주문 이벤트 처리기가 mongoDb에 주문 데이터 동기화

저장은 rdbms (정구화)
조회는 mongoDB (역정규화)

‘’’CQRS’’’

3) 대규모 트랜잭션
쓰기 처리량 한계

주문 조회는 스케일 아웃
근데 주문 저장은 스펙업
근데 더이상 최고 사양의 스펙으로 쓰기 처리를 감당할 수 없었음
=> 샤딩을 통해 쓰기 부하 분산
근데 AWS오로라는 샤딩을 지원하지 않음

=> 어플리케이션 샤딩을 구현하자
고민 2개
- 어느 샤드에 접근할지 결정하는 샤딩 전략
Key-based : shard key
+) 구현 간단, 데이터 골고루 분배
-) 샤드 추가,제거할 때 데이터 재배치 필요

Range-based : 값의 범위
가격기반
+) 구현 간단
-) 데이터 균등배분x, hotspot

Directory-based : look up table
key랑 유사. 근데 중간에 look up table 
+) look up table 분리되어 있어서 동적으로 샤드 추가 유리
-) look up table 이 SPOF될 수 있음

주문 시스템의 특징
주문 정상 동작하지 않으면 배민 전체 좋지 않은 경험 => SPOF는 피한다
주문 데이터는 최대 30일만 저장 => 샤드 추가 이후 30일 지나면 데이터 다시 균등하게 분배
=> key-based로

주문 번호를 샤드 키로 사용 (주문 순번%샤드수=샤드번호)
=> 주문 순번 순으로 샤드에 고르게 분배

AOP와 AbstractRoutingDataSource를 이용해 구현

- 여러 샤드에 있는 데이터를 애그리게이트 어떻게(다건 조회 애그리거트 로직)
이미 저장과 조회 로직 분리해놔서 편했다

‘’’어플리케이션 샤딩’’’

4) 복잡한 이벤트 아키텍처
규칙 없는 이벤트 발행 -> 서비스 복잡도 높아짐

내부 / 외부 이벤트 정리
- 주문 도메인 이벤트 : 내부 이벤트
- 서비스 로직 : 외부 이벤트
=> 이벤트 처리 주체 단일화

이벤트 발행 실패 유형
- 트랜잭션 안에서 이벤트 발행 실패 : 도메인 로직 전체가 실패
- 트랜잭션 밖에서 이벤트 발행 실패 : 도메인 로직 성공, 이벤트 발행 실패 -> 서비스 로직 일관성
=> 트랜잭션 아웃박스 패턴으로 해결

‘’’이벤트 구조 개선’’’
 

2. 모놀리식에서 점진적 서비스 분리: 사업과제와 병행하여 시스템 개선하기

배민상회? 택배 커머스

모놀리식 서비스의 단점
- 복잡도가 너무 커서 시스템 다루기 어렵다
- 특정 도메인의 장애가 다른 시스템의 장애로 전파
- 빌드시간이 오래 걸린다
- 부트 스트랩 시간 오래 걸린다
- 등등…
=> 개발자 생산성 하락

## 빌드/개발 단위 작게
큰 단위의 개발 : 도메인간 결합이 크다
작은 단위의 개발 : 도메인간 결합이 작다
큰 단위의 빌드 : 빌드 시간이 길다
작은 단위의 빌드 : 빌드 시간이 짧다

## 1) 컴포넌트 분리
작은 단위의 컴포넌트는 하나의 서비스가 될 수도, 하나의 모듈이 될 수도

컴포넌트를 어떤 기준으로 나눠야 할까? -> 도메인 단위 (팀 단위)

일단 모듈로 분리

도메인 모듈 : 여러 도메인 로직이 존재하는 모듈

### 양방향 의존성 제거
의존성을 한 방향으로 흐르게 만들어병행 불가
리팩토링으로 분리? 근데 사실상 불가능. 사업 과제랑 병행해야함.
=> 의존성 역전 법칙 시키기 (인터페이스로 분리)

인터페이스의 위치는 어디에?
멤버 모듈에? X
도메인 모듈에? X
인터페이스를 위한 모듈 도입!

### 충돌 최소화하기
기존 구현체의 클래스명을 변경한다
기존 구현체와 동일한 이름으로 인터페이스 생성
-> 기존 코드에 변화x

## 2) 서비스 분리
서비스 분리는 꼭 필요한가요?
아니요.
개발 빌드 단위와 배포 단위는 일치할 필요 없다
컴포넌트와 서비스가 일치할 필요 없다

모놀리식으로 하다가 나중에 필요할 때 msa 도입해도 된다

서비스 분리는 트레이드 오프를 고려해서 결정 (비용, 네트워크 토신 등)

배민 상회는 장기적인 관점에서 주요 조직 단위로 개발, 배포. 실행하고자 서비스 분리

소스 코드 호출에서 네트워크 통신 호출 변경했을 때 생기는 사이드 이펙트
- 네트워크 통신 비용과 제약
  - 내트워크는 실패, 지연 발생 가능 -> 재시도 매커니즘 추가
  - 너무 오래 걸림 -> 성능 개선, 작게 나눠서 병렬 요청
  - 메소드 인자를 path variable, query parameter로 넘길 때 제약
- 동일 스레드, 인메모리 처리되던 로직이 다른 프로세스/머신에서 처리
  - 즉, in memory, thread-bound 데이터가 전파되지 않음 (@transactional, 로컬 캐시, 스레드 로컬 등)

사이클 제거하는 법
- 조회성(query : 조회 순서 조정
- 명령성(command) : mq를 통해 상호작용하도록

## 3) 안전하게 배포
기존 모놀리식 서비스의 느린 빌드, 배포 -> 장애 발생 시 빠르게 롤백하기 위해 feature flag로 빠르게 롤백
코드 변경없이 특정 기능 on/off 할 수 있다 (if 분기)

배포 없이 런타임에 장애가 나는 특정 서비스만 롤백하려면?
Spring cloudconfig, bus로 구현

## 점진적으로 작업
결합이 크고 복잡한 소프트웨어일수록 공개 인터페이스와 사이클 많다 (응집도 낮고 결합도 높다)
최대한 공개 인터페이스와 사이클을 줄여야함

작게, 자주 작업하기

3. 대용량 트래픽을 받는 모놀리식 서비스에서 Woowa하게 RPC 적용하기

배경설명
왜 rpc가 필요?
30000tps
코드 20만줄..
병렬적인 업무 처리
-> 작은 단위로 하자

## 검토 과정
하나의 큰 서비스를 작은 여러 서비스로 나눌 때, 공통 코드를 어떻게 해야할까?
1) 라이브러리로 제공
2) REST API방식으로 제공
3) RPC 방식으로 제공 (+ 개발자들이 쉽게 쓸 수 있도록 잘 추상화해서 제공하자)

RPC (Remote Procedure Call)
- 다른 주소 공간(다른 컴퓨터)
- 프로시저(함수나 메소드)
- 호출

Client(호출 하는) - server(호출 받는)
어떤 함수? (IDL)

RPC 개념. gRPC등 다양한 구현제 존재

## 요구 사항
IDL을 만드려다 보니 800개가 넘음..
유지보수 어려움 -> 기존 클래스와 인터페이스를 잘 활용하는 방안 고민
thrift든 gRPC든 구현체 상관없이 들어와~

요구사항
- 자바 클래스를 최대한 활용하여 유지보수 편하도록
- RPC 구현체 선택해서 사용할 수 있도록
- 기존 spring의 사용성과 크게 차이가 없도록
=> 얘를 기반으로 woowaboot를 만들어보자~~

## 최종 산출물

일반 http controller는 내가 아는그거 (@restcontroller)
@woowabootcontroller 붙이면 rpc controller로 사용 가능!
자바 인터페이스가 IDL 역할

서비스 메쉬

4. Kafka를 활용한 이벤트 기반 아키텍처 구축

점점 더 복잡해지는 문제를 해결하고자 이벤트 기반 아키텍처 적용하는 과정

## 1. 이벤트 기반 아키텍처를 왜?
배달 시스템의 복잡도 증가
- 알림 (배달 상황이 변경되었을 때)
- 배달시간
- 통계
- 쿠폰

대부분 기능은 배달과 강한 일관성을 가지지 않는다
(강한 일관성 : 배달이 변경되었을 때 관련 기능도 동시에 반영되어야 한다)

결과적 일관성
- 배달이 변경되었을 때 관련 기능이 ‘언젠가’ 반영되면 된다
- 이벤트는 시스템에서 일어난 행위이다

배달은 배달만 잘 수행하게.
배달이 아닌 행위들은 이벤트를 통해.

이벤트는 어떤 정보를 가지고 있어야 할까?
도메인 이벤트 : 도메인에 영향을 주는 관심 대상
이벤트의 구성요소 “배달에서 발생한 행위를 알려주고 싶어”
- 대상
  - 어떤 대상이 변경되었는지 (어떤 배달이 변경되었는지)
- 행동
  - 이미 벌어진 사건이므로 과거형으로 표현
- 정보
  - 행위와 관련된 값
  - 필요하다면 행위 외의 값도 추가 가능
- 시간
  - 행위가 발생한 시간
  
배달이 a라이더에게 11시에 배차되었다

## 2. 이벤트 기반 아키텍처 적용 후
좋은 점
- 요구사항이 추가되더라도 배달 시스템에는 복잡도 영향 없음
  - 배달은 배달만 잘 수행하면 됨
  - 배달 이벤트를 수신해서 할 동작만 추가하면 됨
- 소비처 결합도 감소
  - 배달변경 사항을 이벤트로 파악 가능
  - 소비처가 배달 상세정보를 조회하지 않아도 됨(api 등을 통한 조회 필요 없음)
- 데이터 분석
  - 배달에서 일어난 사건을 상세히 저장함으로서 도메인 히스토리 파악 용이
  - 분석 정보로 활용 가능

단점 (풍부한 정보로 이벤트 구성했을 때)
- 이벤트 데이터 무분별한 추가 주의
  - 행위자 기반의 데이터 정의 필요
  - 소비처 요구사항에 대한 무분별한 데이터 추가 주의
- 이벤트의 순서가 중요하다
  - 배달 생성 -> 배차 완료 -> 픽업완료 -> 배달완료

## 3. 이벤트 파이프라인
어떤 메시지 브로커를 사용할까?
Sns->sqs / Kafka 둘 중 뭐?

Kafka 선택 이유
1) 순서 보장
토픽의 파티션을 통해 key별로 순서 보장
2) 고성능,고가용성
실시간 이벤트를 처리할 고성능 고가용성 제공
파티션 증대 등, 브로커 클러스터 관리 등을 통해 고가용성 제공
3) 통합 도구
Kafka streams, kafka connect 등 다양한 통합 도구 제공
4) 전담팀 지원
우형 사내에 kafka팀이 있음. (브로커 관리, 모니터링 툴 제공)
-> 서비스팀에서 안정적 사용 가능

kafka를 도입해도 문제가?
이벤트 발행 실패하거나 재시도 처리를 하면서 이벤트 순서 바뀜
즉, 도메인 상태 !=이벤트 발행 결과
=> 시스템 장애로 확산

Transactional outbox pattern의 도입으로 해결!
발행할 이벤트를 db에 저장 -> message relay가 db에서 이벤트 읽어 발행
=> 이벤트 유실&이벤트 순서 바뀜 해결

Message relay를 구현할 때 고려한 점
저비용, 안전성, 처리량
debezium이라는 오픈소스 사용

## 4. 이벤트 활용 사례
순서가 보장된 이벤트는 이벤트 스트림을 구성

- 이벤트 스트림으로 cqrs 적용
- 이벤트 스트림으로 데이터 분석 환경 구축
- 이벤트 스트림으로 스트림즈 애플리케이션 구현