출처 : https://www.youtube.com/watch?v=BnS6343GTkY
배달의 민족은 매년 주문수가 평균 2.3배 증가할 정도로 굉장히 급성장하는 서비스 (이런 수준의 성장은 과거의 비트코인 거래소 정도가 있음)
여기서 말하는 마이크로 서비스는 완전히 DB까지 분리된 것을 말한다. 이당시에 DB는 Maria DB 사용
위처럼 결제를 마이크로 서비스로 분리하면 얻는 장점
➡ 결제가 장애가 나도, 최소한 전화주문이라도 할 수 있다.
주문 중계 서비스라 함은, 고객들이 치킨 주문을 했을 때, 사장님들이 이 주문을 App으로 받을 수도 있고, PC로 받을 수도 있고, 단말기로도 받을 수도 있다.
즉 중간에서 이 주문을 fowarding 해주는 게이트웨이 서비스였다. (게이트 웨이는 라우터처럼 목적지로 패킷(혹은 데이터) 보내주는 역할)
게이트웨이 서비스는 Node JS처럼 가벼운 기술을 사용하는 것이 좋다고 생각해서, 메인 기술은 Java지만 다른 기술들도 비즈니스 상황이 맞으면 사용할 수 있다고 생각해서 사용. (물론 지금은 서비스가 굉장히 복잡해지면서 다 Java로 바꾼 상황)
특정시간대에 트래픽이 확 몰리니까 준비할게 많음
해당 이벤트가 성공하려면 앞단에 메인화면, 상세 리스트를 통과해서 주문 결제까지 성공해야함.
나름 준비는 했지만, 생각했던 것보다 훨씬 많은 사람들이 들어옴.
그당시에 너무 많은 트래픽을 받고, 프론트 서버 자체가 죽어버림. 메인 리스트 상세쪽이 죽어버리니까 주문까지 트래픽이 들어오지도 않음.
첫날 실패하니까, 다음날에도 해야하니까 골치임
사실 원래 IDC에서 AWS로 이전해야하는 한달 플랜을 잡아놨었는데, 한달에 할거를 하루만에 하는 기적이 일어남 🤣
개발자분들의 고생으로 프론트 서버 시스템을 AWS로 이전, 서버 100대 증설! (AWS를 사용하니까 abusing이 됨, 장비 100대 증설)
장비가 늘어나있으니까 트래픽을 받을 수 있음!!
그런데 바로 주문서버가 죽어버림 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ
주문서버도 하루만에 기적이 일어남 ㅋㅋㅋㅋㅋㅋ 서버대수 확 늘려서 100대 증설
기도 메타로 모니터링 화면앞에서 상태를 같이 보고있었음.
성공했나? 했는데 결제까지 완료가 되었는데, 외부 PG사에서 장애가 나버림 😱
그날 새벽 PG사랑 카드사에서 별도로 장비를 늘려서 이를 해결함
당시에 광고와 검색이 하나의 프로젝트로 되어있었음
해당 기능을 루비 DB에서 떼어내서 엘라스틱 서치로 이전함. 이렇게하면 루비 DB에 가는 부하가 좀 줄어듦
당시에 PHP 매우 잘아는 분도, 이정도 트래픽이면 무조건 자바로 가야한다고 조언
루비 DB에 있는 것을 1분 ~ 5분 주기 배치로 AWS 다이나모 DB (일종의 NoSQL) 에 퍼부음 ㅋㅋㅋ (일단 살고보자)
이렇게 해놓으면 어쨌든 루비 DB에 가는 트래픽이 많이 줄어듬, AWS 다이나모 DB가 해결하니까.
루비 DB가 일반 승용차라고 하면, AWS 다이나모 DB는 고급 스포츠카라고 할 수 있음. 트래픽 굉장히 잘받고 잘 안죽음. 대신 쓸 수 있는 기능이 작음.
단점은 데이터 Sync가 조금 늦는다는 것 (1~5분 주기로 배치처리하니까)
또 요구사항 변경되면 수천라인의 SQL쿼리를 수정해야함. 뒤(벡엔드)에서 뭔가 가게나 광고쪽이 바뀌면 데이터 퍼올리는 쿼리 자체도 굉장히 많이 수정해야함
가게상세랑 가게목록+검색 빠져나오고, 쿠폰과 포인트도 빠져나옴
주문이 가운데에 있다.
커버스 도메인을 해본 사람은 알겠지만, 주문이 제일 복잡함. 주문을 모으는 시스템은 다 엮인다. 결제, 포인트, 쿠폰, 주문중계, 정산, 메뉴 등등 다 엮임. 여기다가 돈까지 왔다갔다 하기 때문에.
리뷰까지 뽑아냄
주문은 지금까지 온 데이터 트랜잭션이 다 쌓임. 데이터 지분률 1위고, 비즈니스가 연관도가 되게 높음. 당시 하루 100만인데, 지금은 하루 수백만건의 데이터가 쌓이고 있음.
가게 업주데이터는 모든 시스템에 다 필요하기 때문에, 시스템 연관도 1위
광고같은 경우 당시에 스토어드 프로시저 사용률이 1위였어서 걷어내기 쉽지 않음. 또 돈을 다루다보니 삐끗하면 문제생김. 어쨌든 회사의 메인 비즈니스 모델이니.
콘웨이의 법칙 : 시스템의 구조가 조직의 구조를 따라간다.
배달의 민족이라는 서비스가 있고, 배민 라이더스 서비스가 있다.
배민 라이더스 서비스는 배달의 민족이 전용 라이더들을 수급을해서 배달을 해주는 서비스다.
배달의 민족은 배민에서 직접 배달해주는 것이 아니고, 일반적인 가게들, 배달하던 가게들이 배달해주는 것. 배민 라이더스는 배달하지 않던 가게들을 배민이 직접 배달의 민족 라이더스를 통해 배달해주는 것.
이게 조직이 아예 다르다보니까, 만들 당시에 배민 라이더스를 아예 별도로 만듦. 배달의 민족 시스템이랑 비슷하게.
완전히 똑같으면 상관이 없는데, 똑같은 듯 싶으면서도 몇개 다름. 그런 경우 참 합치기도 어렵고 분리하기도 애매함.
그런데 나중에 보니 배민 라이더스 시스템이 기존의 것이랑 비슷해서 시스템을 통합하기로 결정. 그래서 조직도 주문 관련된 부분이랑 합쳐지고 그럼. 시스템 통합을 하는데 통합이 잘 되나? 잘 안되지 당연히.
그래서 원래 아예 다른 테이블로 운영하고 있었는데, 새로 테이블을 설계를 하고, 이 두가지를 포함할 수 있게 만들기로함.
상위 애플리케이션 레벨이랑, 데이터베이스 시스템 일부만 좀 바꿔서 어쨌든 시스템 통합하고, 애플리케이션 레벨에서 분리함. 어쨌든 도메인은 하나로 정리
주문이 생성 접수 배달 취소 하는 동안 수많은 라이프사이클이 있음. 그동안 수많은 외부 API를 호출했음.
단적으로 배달의 민족 주문이 되고 나면 사장님이 접수하는 곳으로 넘어가야함. 또 배민 라이더스 시스템에도 넘어가야하고. 또 새로운 시스템을 만들면서 과거 DB도 싱크해주어야하고. 또 이건 약간 애매하긴 한데, 주문 완료하고 나면 리뷰 써달라고 푸쉬해주는 기능 있음. 리뷰 시스템 입장에서는 푸쉬 날릴려면 해당 주문이 배달 완료되었는지 알아야함. 이런 것들이 기존 시스템에서는 다 API로 되어있었음
따라서 기존 아키텍처같은 경우 리뷰 시스템에 장애가 나면, 주문 시스템도 영향을 받음. 되게 애매함.
생각해보면, API가 딱 끊기면 주문 시스템에서 미는 시스템에서 타임아웃 나거나, 에러가 날 것임.
따라서 아키텍처를 위와 같이 구성하기로 결정.
주문은 명확한 이벤트기반으로 주문의 라이프 사이클을 정의하자!!
그래서 이벤트 기반의 마이크로 서비스 아키텍처를 고민하자! 그래서 전사에서 이해할 수 있는 명확한 주문 이벤트를 정리함. 주문 생성, 접수, 배달완료, 취소 (이외에도 몇가지 더 있긴 함)
그래서 앞으로 주문 시스템에선 이벤트만 발행할거야. 그러니까 이제는 너희 시스템들이 원하면 이 이벤트를 가져가서, 서브스크립션(?)해서 너희들이 원하는 비즈니스 로직을 작성해!
이런식으로 완전히 비즈니스 로직을 분리함.
당시에는 AWS SQS나 SNS에 대한 노하우가 좀 있어서 이렇게 했음.
주문 시스템은 AWS SNS에 이벤트를 쏘고, (이벤트 안에 데이터가 있다. 이러한 이벤트 생성되었습니다. 접수되었습니다.) 각 시스템들에서 (리뷰, 레거시, 라이더스 시스템 등) 해당 이벤트들을 consume해서 가져가는 식으로 바꿔감.
이렇게하면, 위에서 든 예시처럼, 리뷰시스템이 죽어도 주문 시스템에선 아무런 문제가 안일어남. 주문 SNS하나 쏘고 끝나는 것이니까.
또 좋은점은, 리뷰 시스템이 죽었다 살아났을 때, 기존 API방식에서는 해당 이벤트가 날라가 버리는데, 이런 이벤트기반 시스템에서는 리뷰 시스템이 살아나는 순간, AWS SQS에 쌓여있는 이벤트들을 다시 consume할 수 있다. 그래서 앱 푸시를 다시 보낼 수 있는 것.
시스템들의 회복력이 굉장히 좋아짐.
또 중요한거 하나있음. 새로운 시스템이 주문 시스템 필요로 한다면, 그 시스템이 AWS SQS를 하나 만들고, 주문 SNS에 연결해서 시스템을 딱 꽂으면 된다. 주문 시스템이 제일 편해짐. 연동해달라고해서 따로 연동 구축할 필요없이 우리 주문 이벤트 이렇게 되어있으니 연결해서 쓰세요 하면 됨.
이렇게해서 2018년도 하반기부터 장애가 급격히 줄어듬. 주문이라는 되게 큰 시스템이 빠져나감.
남은 레거시 2대장(가게/업주, 광고)만 처리하면 됨
해당 기능들은 되게 역사가 오래된 기능. 배민의 처음부터 함께한 시스템들임.
위에서 말했듯이, 가게랑 광고랑 1:1임. 하나의 가게가 하나의 광고만 할 수 있음. 가게 테이블 안에 광고 데이터가 들어가있음. 그래서 1:1이라고함. 하나의 컬럼을 계속 추가한 것임. 그래서 컬럼이 거의 100개 가까이 됐음. 그래서 이걸 떼어내기가 굉장히 어려움.
가게를 손댈라해도, 광고를 멈춰야하고, 광고를 손댈라해도 가게를 멈춰야하고. 전체시스템에 영향있기 때문에 거의 비즈니스를 몇개월 멈춰두고 진행해야하는 작업이었던 것.
이때 회사에서는 이러한 의사결정을 함.
기술적으로 MSA로 가려면 프로젝트를 몇개월 중단시켜야함. 그런데 회사는 비즈니스를 해야하기 때문에 이것을 선택하기 굉장히 어려움. 그런데 이것을 기가막히게 접점을 만들어냄.
하나의 가게가 여러 개의 광고를 하게되면 비즈니스에 굉장히 도움된다. 하지만 이걸 하려면 먼저 시스템 기반이 안정화되어야하고 이걸 이렇게 바꿔야한다. 그래서 우리가 이걸 하기 위해선 앞으로 여기에 집중하고, 앞으로 세네달동안 여기에만 집중하도록 개발팀을 도와주자하고 의사결정을 봄. 이것이 바로 프로젝트 먼데이. 사업도 만족하고 비즈니스 개발팀도 만족하는 그런 프로젝트
가게상세, 가게목록 검색, 프론트서버, 사장님 사이트, 전체 어드민 모든 시스템들이 루비안의 가게랑 광고 데이터들을 보고있었음. 또 위에서 말했듯이 가게랑 광고 데이터가 막 섞여있었음. 따라서 거의 모든 시스템들이 같이 손을 봤어야 했음.
당시 개발 리더들끼리, 어차피 만들거 깔끔하게 가자고 결정함.
당시 가게 상세라는게 있는데, 이것을 가게 노출이라는 시스템으로 만들기로함. 이게 뭐냐면, 마이크로 서비스 공부하다보면 CQRS 모델이라는게 나오면서 쿼리 모델이라는 것이 있다.
서비스 조회용 가게 데이터를 가지고있는, 서비스에 fit하게 맞춘 가게 데이터를 가지고있는 쿼리 모델 만드는데, 그것이 바로 가게 노출 시스템.
또 가게목록이랑 검색 시스템이 하나로되어있었는데, 이거를 광고용 시스템과 검색시스템으로 명확히 분리하기로함.
결론적으로 광고용 시스템 마이크로 서비스로 만들고, 가게 업주 시스템 마이크로 서비스로 만든다음에, 필요한 시스템에서 API로 호출해서 사용하면 된다.
하지만 전사적인 레벨에서의 아키텍처를 새로 고민하는 단계였고, 여기서 가장 중요한 것이 장애를 어떻게 대응할 것인가였다.
기본적으로 고민했던 첫번째 안같은 경우, API로 개발하고, 단순히 API조회 하는 방식 서비스를 구성하자였다. 이게 가장 단순하고, 데이터 동기화 고민도 할 필요없다.
사장님들이 가게 업주 데이터들을 바꾸면 가게 업주 서비스 내부에 있는 데이터베이스의 데이터들만 바꾸고, 나머지 시스템들은 실시간으로 API를 찌르면된다. 그러면 단순하다.
그런데 위와 같은 문제가있다.
예를들어 광고시스템에 장애가 생겼다하면, 광고시스템의 API를 호출하는 모든 시스템에 연쇄적으로 장애가 전파된다.
가게 시스템도 마찬가지임. 가게시스템에 문제가있으면, 그 가게시스템의 데이터를 사용하는 모든 시스템들이 연쇄적으로 죽는 것.
배달의 민족 트래픽이 굉장히 늘어나고있었고, 또 이벤트같은 것을 많이해서 정말 대량의 트래픽이 순간적으로 막 몰려온다.
그러면 모니터링 툴이 순간적으로 피크를 확 친다. 평상시보다 트래픽 100배씩 들어온다. 이렇게 되면 API 호출방식을 사용할 경우 해당 트래픽이 모든 곳에 퍼지게된다.
그러면 가게노출, 광고리스팅 이런 것들이 대용량 트래픽을 잘 받을 수 있도록 설계할 수 있지만, 광고나 가게업주 시스템같은 경우 트래픽을 해결하는 것보다는 "정확하고 안정적으로 시스템을 운영하는 것"이 훨씬 중요하다.
그러므로 대용량 트래픽 들어오면 장애가 날 것이다.
가게나 광고같이 사장님이 관리하는 그런 서비스가 죽어도, 고객이 결제까지 해서 주문까지 들어올 수 있어야한다.
마이크로 서비스를 하게되면 데이터가 분산되게 되는데, 이를 어떻게 Sync 할 것인가.
CQRS 아키텍처를 선택하기로함
단순히 한 시스템만 CQRS로 가는게 아니라, 배달의 민족 전 시스템을 CQRS로 가기로 결정함.
사실 위의 아키텍처보다 시스템이 훨씬 많긴함 ㅎㅎ
광고랑 가게업주 같은 명령형 시스템, 즉 비즈니스 사이드의 시스템을 Command에 놓고, 조회형 시스템 (고객의 트래픽이 앞에서 들어오는 것들)을 Query에 놓음.
사장님 사이트에서 사장님이 가게의 이름이나 데이터를 바꿈. 그러면 가게업주에서 (내부적으로 DB있고) 변경됐다는 이벤트 (SNS, SQS 이벤트)를 쏜다.
이벤트를 쏘면 각 시스템들이 가게 업주의 데이터가 필요하면, 이벤트를 consume해서 조회나 검색에 있는 데이터들을 갱신한다.
사장님이 가게데이터 변경을 하면, 변경 이벤트를 발행하고, SNS SQS 모델로 이벤트를 싹 전파함.
그러면 해당 시스템에서 "본인의 데이터베이스의" 맞는 곳에 쫙 업데이트를 한다.
그 당시에는 AWS Managed Kafka가 없어서, 그리고 AWS SNS SQS에 대해 노하우가 있어서 이러한 방식을 선택했다.
가게에서 이벤트를 발행할 때, 메시지를 다 담아서 보내는 것이 아니라, 딱 가게 ID만 넣어서 보내는 것. 물론 몇가지 정보가 더 들어가기는 함.
이벤트가 발행되었으면, 이벤트를 수신하는 곳에서는, 이벤트안의 가게 ID를 보고, 이 가게의 데이터가 바뀌었구나 하고 위그림의 경우, 가게노출시스템과 가게 업주 시스템에서 서로 합의한 API를 호출한다. 그래서 해당 API의 데이터로 데이터를 채운다.
그러면 왜 변경된 데이터나, 전체 데이터를 채워서 보내지 않음?
변경된 데이터만 보내게 되면, 이벤트 순서를 고민해야함.
예를들어, 가게가 연락처를 A라했다가 B로 했다고하자. 그런데 이벤트는 B 다음에 A가 올 수도있다. 이 문제를 해결하려면 할 수는 있는데, 굉장히 많은 고민을 해야한다.
그래서 이걸 고민하지말자, 그냥 이벤트오면 이게 가장 최신이라고 보고, 항상 최신의 데이터를 갱신하자라고 결정.
변경된 데이터가 아니라 그냥 전체 데이터를 채워서 보내면 안되나?
각 시스템에서 원하는 데이터가 다 다르다. 가게노출에서 원하는 데이터랑, 광고 리스팅 시스템에서 원하는 데이터가 다르다. 동일 이벤트지만 다른 API를 만들어야한다.
그러면 그냥 모든 데이터 채워서 보내면 되지 않나? ➡ 테이블이 수십갠데 현실적이지 않다.
위의 두가지 문제 해결하기 위해서 딱 가게의 최소정보(거의 가게 ID만 보냄)만 넣어서 이벤트 발행하기로함.
나머지는 보통 common API를 만들어두고, specific한 API를 만든 다음에, 각 시스템들이 회신받은 이벤트면, 항상 API를 호출해서 최신의 데이터를 받아서 각 시스템들(앞단에 있는 시스템들)을 갱신하는 식으로 진행.
각 시스템들은 모든 데이터들을 보관하면 안되고, 본인에게 꼭 필요한 최소 데이터만 보관한다.
이게 물리적인 의존관계는 없지만, 데이터를 가지고있으면서 논리적인 의존관계가 생김. 그래서 가게 업주에서 무언가 데이터를 바꾸려할 때, 데이터를 다봐야함.
그래서 최소한만 가지고있자 결정을 함.
폴리글랏 데이터베이스
각 시스템들은 본인이 최대한 최적화할 수 있는 데이터베이스를 사용할 수 있게 된다.
가게업주는 RDBMS를 쓰고 이벤트를 보내면, 각 시스템들은 성능이 중요한 경우 redis나 dynamoDB를 쓰고, 검색이나 광고리스팅의 경우 ElasticSearch를 쓰는 등 본인의 시스템에 맞는 데이터를 동기화한다.
가게 노출 시스템이라는 것이 쿼리 모델로 되어있음.
이벤트들이 배달의 민족 상세화면 보면, 리뷰, 가게 상세, 메뉴 등등 데이터가 엄청 많음. 이것들을 빠르게 조회해야함.
뒷단에서 여러 시스템들로부터 이벤트들이 오면, 그거를 가게노출 시스템에서 키를 가게ID로 하고, 값을 그 데이터들로해서, 한줄로 집어넣음. 그래서 가게 ID만 넣으면 해당 상세 화면에 필요한 데이터들을 fit하게 맞추어 뿌릴 수 있게. (내가 네이버 인턴하면서 봤던 형식처럼 넣어놓는듯)
이런 식으로 데이터를 flat하게 만들어놓음. 이런식으로 가게 데이터를 관리하는 것이 쿼리 모델.
이렇게 해서 얻는 장점?
광고를 보거나, 검색을 하거나, 찜 리스트를 보거나, 이런 뒷단의 시스템들은 단순히 가게 ID만 반환해주면 됨. 물론 몇가지 데이터를 좀더 반환해주기는 함.
그러면 예를들어서 가게 리스트의 검색결과가 25개 정도 나왔다고 하자. 그러면 25개의 가게 ID만 반환해주는 것. 그러면 25개의 가게ID를 가지고 가게 노출시스템은 본인의 데이터베이스를 찔러봄.
그러면 위에서 말했듯이, 데이터가 flat하게 key value로 되어있음. 25개를 key value로 굉장히 빠르게 조회한 다음에 ID에다가 value를 씌움. 그래서 앱에서 렌더링할 수 있게 데이터를 씌운다음에 내보낸다.
가게 상세의 경우 모두 비동기 넌블로킹으로 되어있음. 따라서 세가지 시스템을 한번에 비동기 넌블로킹으로 조회하게 됨
CQRS로 바뀌고 나서는 트래픽이 들어오고 나면 뒷단을 아예 호출하지 않음.
그래서 트래픽이 들어와도, 조회용 쿼리와 관련된 시스템들만 트래픽을 받으면 된다.
광고나 가게업주 시스템이 죽어도, 장애가 격리되고, 고객은 주문까지 넘어갈 수 있다. 물론 주문이 죽으면 그건 안됨 ㅋㅋ
데이터 싱크할 때 장애가 나면 어떡하지?
전체 서비스들이 모놀리틱한 레거시 시스템에서 다 떨어져나옴
꼭 마이크로 서비스를 해야하나요?
이게 규모의 경제가 되어야 할 수 있다. 시스템 규모도 되야하고, 트래픽도 되야하고, 사람도 많아야한다.
정말 단순하게 생각해보면, 기존에는 테이블 조인하나 하면될 걸, 데이터 싱크 하고 맞추고 하면 비용이 10배정도는 더듦. 그런 것들을 상쇄하고 남을만큼의 가치가 있는경우에 마이크로 서비스로 넘어가는 것이 좋다.
Whether you're a fan of old-school arcade games or classic console titles, there's something for everyone. So get ready to take a trip down memory lane and play your favorite retro games online!
잘 읽었습니다. 좋은 글 감사합니다^^