![[Grab] 수백만건의 주문을 저장하고 처리하는 노하우 (feat. DynamoDB)](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYPjyg%2Fbtr8J4aeUbu%2FLJcUnnuIAj5ehTPWK4tcvK%2Fimg.png)
들어가며
- Grab은 스마트폰 기반의 동남아시아 승차 공유 서비스이다(- 위키 백과 -).
- 지금은 그랩페이, 익스프래스, 프레쉬... 등 다양한 사업 분야 진출해 있는 소위 말하는 "슈퍼앱"이다.
- 지금은 처분했지만 한동안 그랩의 "주주"였다.
- 그래서, 조금은 뜬금없지만 언젠가 재진입할 날을 고대하며, 그랩의 테크 블로그 글 들에 대해 번역해보려 한다.
Grab Food
- 오늘 번역하려고 하는 글을 Grab의 여러 서비스들 중 Grab Food를 대상으로 한다.
https://www.grab.com/global/ko/food/
Grab Food | 동남아시아에서 간편한 음식 배달을 원하시면 다운로드하세요
동남아시아에서 여러분의 입맛을 사로잡을 음식을 GrabFood를 통해 모두 주문하세요. 즉시 배달이나 예약 배달, 방문 포장 중에서 선택하세요.
www.grab.com
- 즉시배달, 포장.. 흡사 동남아의 "배달의 민족" 같다. 그런데, 특이한 점이 두 가지 정도 있다.
- 하나는 예약 배달이 된다는 것이고, 다른 하나는 "믹스 앤 매치" = 배달 한 건에 여러 음식점의 음식을 맛 볼 수 있는 서비스.
- 그래서 배달료는? - 안알랴즘
원문
https://engineering.grab.com/how-we-store-millions-orders
How we store and process millions of orders daily
The Grab Order Platform is a distributed system that processes millions of GrabFood or GrabMart orders every day. Learn about how the Grab order platform stores food order data to serve transactional (OLTP) and analytical (OLAP) queries.
engineering.grab.com
- Grab에서 OLTP와 OLAP를 어떤 식으로 구성했는지에 관련된 글
- DynamoDB를 OLTP로 MySQL RDS를 OLAP로 운영하고 있다고 함.
글 소개
- Grab App에서 음식 주문 -> 음식점에서 주문을 준비 -> 드라이버가 음식을 찾아서 고객에게 배송
- 이런 일반적인 음식 배달 서비스의 백엔드 시스템이 어떻게 구성되어 있는지 생각해본 적이 있는가?
- Grab의 주문 시스템 데이터 베이스는 효율적으로 나뉘어져 있다. 오늘은 이 시스템을 만들기 전까지 있었던 여정에 대해 다루어 보려 한다.
- 원문에는 "database solution that powers the order platform"이라는 말이 있었는데, database solution을 컴퓨터 전원과 같은 power로 설명한게 인상깊었음...
배경 지식 설명
Query Pattern
Write(쓰기) | Read(읽기) |
1. 주문 생성하기! | 1. 주문 id 조회! |
2. 주문 업데이트 하기! | 2. 고객id로 현재 진행중인 주문 알기! |
3. 과거 주문 조회하기! | |
4. 주문 통계 조회하기! |
- 여기서 Write + R1 + R2를 Transactional Query라고 하고 R3 + R4를 Analytical Query라고 하는데...
- 가장 큰 차이점은? Analytic Query는 당장 서비스를 "운영"하는데 있어서는 필요하지 않을 수도 있다!(지금 피자를 주문하는데, 옛날에 치킨을 먹었던 사실이 무슨 상관인가!)
- 반면에, Transactional Query는 당장 운영하는데 필요하다.. 예를 들어 R2.(고객id로 현재 주문 알기) 주문에 문제가 생기면.. 현재 진행중인 주문을 먼저 살펴봐야 한다.
- Transactional과 Analytic이 좀 더 궁금하신 분들은.. 아래 글을 읽어봐도 좋다..!(ㅎ헷)
데이터 마트에서는 뭘 파나요?(feat. OLTP, OLAP)
자~데이터 담으러 가자(feat.안내상님, 송곳) - 들뜬 마음으로 마트에 방문한 A씨. - 어라? 그런데 구매하려고 했던 바나나가 없다. - 바나나 다 팔렸나요? 질문을 남긴 안씨. 직원C씨는 알아 보고 오
gibles-deepmind.tistory.com
Traffic Pattern
- 여기서 Traffric이란 위에서 말한 쓰기/읽기 쿼리 패턴 중 뭐가 더 많이 들어오냐!이다.
- 일단, 바쁠 때는 새로운 주문이 겁내 많이 들어 온다. 쓰기 쿼리가 읽기 쿼리보다 3배 가량 많다.
- 여기서 QPS라는 용어가 나오는데, Qquery Per Second의 약자로. 말 그대로 초당 쿼리 횟수이다.
- 즉, 위에서 새로운 주문이 겁내 많이 들어온다 = QPS가 3배다 라는 의미로, 초당 읽기 쿼리보다 쓰기 쿼리가 3배 가량 더 들어온다. 이런 의미이다.
https://jjingho.tistory.com/154
- 위 블로그에서 QPS 추정에 대한 대략적인 감을 잡을 수 있다. QPS는 사용자와 사용자의 액션에 대한 함수로, 사용자가 많고 사용자가 하는 행동이 많아질수록 QPS 값이 커진다.
- 그리고 범위 쿼리(Range Query)가 겁내 많아진다는 얘기가 같이 나오는데, 고객 단위로 현재 진행중인 주문이 몇 건인지를 살펴보는 것을 말하는 것 같다. (고객이 여러 건을 주문할 수 있으니, 고객 자체를 범위로 볼 수 있음)
- 여튼, 바쁠 때는 새로운 주문이 겁내 많이 들어와서 Write쿼리량이 > Read 쿼리량 보다 압도적이다.
그래서, DB 디자인은 어떻게?
<Stability>
- 요약하자면.. Peak시간에 무수히 쏟아져 들어오는 Write/Read쿼리도 견딜 수 잇어야 한다!
- 그리고, 일부 시스템이 죽더라도.. 고객이 주문만큼은 꼭 완료할 수 있게 해야 함.
<Scalability and cost>
- 비즈니스가 성장하는 만큼 database solution도 성장해야 한다. 여기서 성장이란 = 그만큼 더 많은 쿼리를 처리하라는 뜻!
- 근데, 이게 끝이 아님. 그러면서도 사장님 가라사대 cost effective = 돈은 덜써라!
<Consistency>
- 일단 Transactional 쿼리 = 운영계(=주문관련) 먼저 잘 처리하고! Analytic 쿼리 = 분석계(=분석해서 의사결정에 활용) 이것도 흔들림 없이 잘 처리해라!
DB솔루션!
- 일단 OLTP와 OLAP에 서로 다른 DB를 쓰자!
- OLTP DB는 주문 처리 할 때 필수!(=주문 생성, 진행중인 주문 조회 등) 그렇지만 완료된 주문까지 다 저장할 필요는 없기 때문에 상대적으로 보유 기간이 짧음!(= 금방 삭제 한다는 뜻)
- 반면, OLAP DB는 과거 데이터와! 통계치를 저장하고 있어야 하기 때문에 상대적으로 보유 기간이 길다!(=금방 삭제 하지 않는 다는 뜻)
- 왜 서로 다른 DB를 쓰는 걸까? = 독립적인 운영
- 예를 들어, 엄청 좁은 주차 공간을 두고 두 차량이 경쟁을 하는 상황을 생각해보자.
- 똑같이 일이 Database내에서도 일어 난다. 좁은 공간을 두고 두 차 모두 이러지도 저러지도 못하는 상황이 연출되는데 이를 DeadLock이라고 한다. DeadLock이 생겨버리면 저 두 차가 나가지 못하는 것처럼.. 어떤 작업도 끝낼 수 없다.
- 10분 테코톡 케빈님의 영상을 보면 "소심한"이라는 단어가 붙어 있다. 즉, 서로 쓰고 싶은 운동 기구를 상대편이 사용하고 있는데, 둘 다 소심해서.. 다 썼냐는 말을 못하면 하루 종일 똑같은 기구만 쓰다가.. 집에 간다는 그런 얘기
https://www.youtube.com/watch?v=Ry_gB34cvwc&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC
- 왜 이런 일이 일어 날까? 한정된 주차장 공간처럼 본질적으로 서버의 자원은 유한하기 때문이다.
- 그래서, OLAP보고는 저쪽가서 혼자 놀라고 독립적인 공간을 떼어 주는 것이다.
- 그럼, OLAP가 어떤 난리를 피워도 OLTP에는 영향을 주지 않을테니까.
- 왜 이렇게 OLAP만 미워 하냐고 할 수 있지만.. 근본적으로 소를 키우는건 OLTP이다.(주문을 받아야 돈을 벌지 않겠음?)
- 그리고, OLTP는 그때 그때 들어오는 주문을 잘 처리해야 하니까 상대적으로 가볍게 유지하고 옛날 데이터는 OLAP에 저장해둔다.
- 대충 아래와 같은 방식으로 처리한다고 한다.
- 주문 플랫폼에서 고객이 액션을 하면 OLTP로 들어가서 주문을 처리하고 동시에, 일정 과정(=Data Ingestion)을 거쳐서 OLAP도 저장한다는 의미
상세 구조 - OLTP database
- 크게 두 가지 종류의 쿼리를 날린다.
- 하나는 주문 번호로 주문을 생성하는 것이고 key(주문 번호)-value(주문 내용) query
- 다른 하나는 배치 쿼리로 고객 아이디로 진행중인 주문 목록을 보여 주는 것.( 고객 아이디가 직접 노출되는건 아니지만 패스트푸드점에 대기중인 주문 번호 리스트 전광판 정도로 생각할 수 있을 것 같다.)
- 그리고 OLTP용 DB로 DynamoDB를 사용한다.
DynamoDB란?
- AWS의 NoSQL 서비스이다. 여기서 NoSQL이란? 수평적 확장이 가능한 구조의 DB를 얘기한다.
- RDB는 수직적 확장을 지원한다. 즉, 주문 번호 / 주문 내용 / 주문 고객 등과 같은 고정된 컬럼 값에 대하여 행을 지속적으로 추가하는 것이다.
- 반면, 수평적 확장이란 고정된 컬럼 값 외에 다른 값도 들어올 수 있다는 것을 의미한다.
- 위 그림의 Attributes를 보면 행 별로 OrgName + SubscriptionLevel과 UserName + Role이라는 서로 다른 컬럼 값을 가진다. 이게 만약 RDB였다면..
Primary Key | Sort Key | OrgName | SubscriptionLevel | UserName | Role |
... | ... | ... | ... | NULL | NULL |
... | ... | NULL | NULL | ... | .... |
- 위와 같이 출력할 수 있었을 것이다. 하지만, 이것도 어디까지나 고정된 컬럼값을 사전에 정의해두어야 볼 수 있는 부분이다.
- NoSQL은 속성값을 사전에 정의 하지 않고도 다 때려 박을 수 있다.(좀 더 궁금하면 아래 영상을 참조)
https://www.youtube.com/watch?v=1nE609C6-SI&ab_channel=AmazonWebServicesKorea
- 여하튼 이런 DynamoDB를 사용하는 이유는, 확장 가능성이 좋고 사용성도 좋다고 한다.
사용 이유1. three-way replication이란?
- 우선, two-way를 먼저 알아보자.
- Master DB를 본 떠서 Slave DB를 만들어두고 CRUD에서 CRD인 Insert, Update, Delete만 Master DB에서 하고
- Read(=Select)는 Slave에서 진행하여 DB 부하를 줄이는 방식.
- 반면, three-way replication은 각각의 마스터를 세 개를 만드는 것과 유사하다.
- 여하튼, 이걸 three-way나 한다는 건 replication을 기본적으로 세 번 한다는 의미. 정확히는 모르겠지만 아래 글에 보면 서로 다른 Zone으로 copy를 세 번 한다는 얘기가 나온다.
https://www.amazon.science/blog/lessons-learned-from-10-years-of-dynamodb
- 당연히, 카피 할 때마다 돈도 많이 나올텐데 왜 세 번이나..? replication을 할까?
- 킹오브 파이터를 하면 세 명의 캐릭터를 고를 수 있는데.. 두 명이 먼저 죽어도 나머지 한명으로 게임을 진행할 수 있다.
- DB도 마찬가지 아닐까? 두명의 OLTP 마스터가 쓰러지면.. 나머지 OLTP 마스터라도 주문을 완료하자는 강력한 의지가 반영된게 아닐까 싶다. 그만큼 안정적이라는 의미.
https://learn.microsoft.com/en-us/azure/storage/common/storage-redundancy
https://www.quora.com/Why-is-replication-factor-always-3-for-building-distributed-systems
- 위 글들에서도 비슷한 의미를 찾을 수 있다.
사용 이유2. strong consistency란?
- 정보의 일관성으로 생각할 수 있다. Dynamo DB에서 strong consistency를 모드를 사용하면
- 항상, 최신 데이터를 읽을 수 있다는 의미이다.
- 이게 무슨말이냐면, 데이터를 업데이트 하고 있는 중에 데이터를 조회하면 변경 전 데이터 버젼을 볼 수도 있고, 이로 인해 정보의 일관성이 떨어질 수 있다.
- 반면, strong consistency모드를 사용하면 데이터 업데이트를 먼저하고 읽기를 진행(= 즉 읽기가 후순위)하기 때문에 항상 최신 정보를 일관성있게 확인할 수 있다.
사용 이유3. adaptive capacity
- 우선, partition key가 뭔지 정렬 key가 뭔지 대략 알고 있어야 한다.
- partition key는 고객, sort key는 주문 일자이다. 그리고, 속성으로 orderid가 있는데 nosql이니까 상품명 주문량등 다양한 정보가 들어올 수 있다.
- 여튼, partition key를 고객이라 두었으니 예를 들어 customername을 삼성전자라고 둬보자. 23년 1월 국민연금 가입 기준 임직원 수가 11만 8천명 정도이다.
- 삼성전자에서 grab food로 대량 주문을 했다고 가정하자. 그러면, 삼성전자 partition으로 traffic(=쿼리 요청)이 어마어마하게 증가할 것이다.
- 이때, DynamoDB를 사용하면 유연하게 Partition의 처리 용량을 조절할 수 있다고 한다.
- 위 그림에 잘 설명되어 있듯이, 삼성전자(=partition4)의 쿼리 요청량이 급증하면 그만큼 유연하게 partition4의 읽기/쓰기 쿼리 처리 용량을 늘려준다.
GSI사용법
- GSI는 Globaly Second Index의 약자이다. 일단, 왜 이런 구조가 나왔는지 먼저 이해를 해야 하는데
- 앞서 얘기 했듯 Attribute는 고정된 값이 아니기 때문에 기본적으로 쿼리는 Key단위로 날릴 수 있다.
- 그런데, Attribute값을 기준으로 쿼리를 날려야 하는 경우가 있다.
- 예를 들어서, 위와 같은 데이터가 있다고 가정하면, Award 속성의 Champ를 먹은 경우만 출력한다고 해보자.
- 이를 위해서 Award속성을 partition Key로 바꿔 주는 것이다.
- 그럼 짜잔 이렇게 champ를 먹은 경기만 알 수 있다.
- 여하튼 GSI를 쓰면 이렇게 Attribute를 partition key로 가지는 테이블을 복제해서 좀 더 편리하게 쿼리를 날릴 수 있다.
Sparse Index를 유지하는 전략
- 위 예시에서 sort key는 game_id이고 gsi로 award를 사용했는데, 자세히 살펴보면 모든 game_id마다 award가 있지는 않다. 이러한 구조를 sparse index라고 한다. award가 듬성 듬성 있다는 의미
- 여튼, sparse index면 조회 범위가 적으니 당연히 더 빨리 쿼리를 실행할 수 있다.
- grab food에서 특정 고객의 현재 진행중인 주문을 살펴보는 쿼리를 생각해보자.
- 여기서 gsi를 쓴다는건 pax_id를 gsi -> pk로 바꾼다는 의미인데 pax_id를 그대로 pk로 사용하지 않고 pax_id_gsi를 pk로 사용한다고 한다.
- 그 이유는 completed된 경우 pax_id_gsi에서 삭제하기 때문이다.(completed 됐다고 해서 바로 pax_id를 삭제할 수는 없다.)
- 이렇게 하면 앞서 말한 것 처럼 ongoing 주문이 듬성 듬성 있게 되고 -> sparse index 구조의 장점을 살려 더 효율적으로 쿼리를 날릴 수 있게 된다.
TTL 적용 전략
- TTL이란? Time to Live의 약자로 data 만료 기간을 자동으로 설정해주는 기능이다.
- 그런데, big table에 TTL 기능을 진행하면 그 자체로도 큰 비용이 들 수 있다.
- 그렇기 때문에 big table 같은 경우에는 TTL 적용 전 수동으로 오래된 기간 데이터를 삭제하고 나면
- 더 줄어든 데이터에 TTL을 적용해서 비용을 아낄 수 있음.
상세 구조 - OLAP database
- Aurora 대신에 MySQL RDS를 사용함.
- Aurora에 큰 돈을 쓸만큼 비즈니스 요구량이 크지 않음(Aurora가 MySQL보다 비쌈)
- RDS를 이용하여 저장소 비용만 내고 조회 비용은 최소화
- Partitioning은 Monthly 단위로 하고 6개월 지난 데이터는 삭제
Data ingestion pipeline
- kafka를 사용함. 99.95% SLA 때문에(대충 10000분 중에 9995분 간은 서비스 작동을 보장한다는 의미)
- 작동 안하는 5분은? stream 데이터를 받지 못함. 이 때는 Amazon Simple Queue Service(SQS)를 이용해서 한번 더 시도해보고
- 그래도 안되면 DLQ에 저장해 두었다가 추후 재요청
- 중복 메시지랑 잘못된 메시지를 처리하는 것도 중요
- 중복 메시지는 database에서 order id + createdate와 같은 unique key 조합으로 처리
- 잘못된 메시지의 경우 두 과정으로 처리
- 1. microseconds 단위로 데이터를 최신 상태로 유지
- 2. Upsert를 사용 = 중복되는 값이 있으면 Update를 하고 중복되는 값이 없으면 Insert를 함.
결론 및 향후 계획
- Aurora에서 RDS + table size조절 전략을 통해 비용을 많이 줄임
- DynamoDB를 쓰니까 확실히 OLTP의 안정성과 용이성을 보장해줌.
- OLAP로 RDS를 사용하면서 늘어나는 비즈니스 요구사항에 효과적으로(=비용 절감) 대응함
- data 유지 기간을 설정하면서 비용을 절감함
- OLTP / OLAP DB는 분리되어 있지만, Source는 하나로 사용하며 일관성을 유지함
- MySQL RDS를 OLAP로 쓰고 있는데, Text 서치가 어려움. ElasticSearch같은 NoSQL 데이터베이스로 넘어갈까 고민.
제 블로그에 와주셔서 감사합니다! 다들 오늘 하루도 좋은 일 있으시길~~
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!