에어브릿지는 웹, 앱을 넘나드는 사용자의 유입과 행동을 파악하여 광고 성과를 측정, 분석하는 제품입니다. 월 2000만 유저로부터 들어오는 100억 건 이상의 데이터 속에서 유저 행동을 실시간으로 분석한 애널리틱스를 제공하기 위해, AB180에서는 Luft라는 행동 분석에 특화된 OLAP DB를 자체적으로 개발해 코호트 분석 기능 및 각종 애널리틱스 제공에 활용하고 있습니다.
* 발표 영상: https://tv.naver.com/v/16969997
* 블로그 글: https://abit.ly/luft
* 채용 페이지: https://abit.ly/recruit-database-engineer
Spark 의 핵심은 무엇인가? RDD! (RDD paper review)Yongho Ha
요즘 Hadoop 보다 더 뜨고 있는 Spark.
그 Spark의 핵심을 이해하기 위해서는 핵심 자료구조인 Resilient Distributed Datasets (RDD)를 이해하는 것이 필요합니다.
RDD가 어떻게 동작하는지, 원 논문을 리뷰하며 살펴보도록 합시다.
http://www.cs.berkeley.edu/~matei/papers/2012/sigmod_shark_demo.pdf
Spark 의 핵심은 무엇인가? RDD! (RDD paper review)Yongho Ha
요즘 Hadoop 보다 더 뜨고 있는 Spark.
그 Spark의 핵심을 이해하기 위해서는 핵심 자료구조인 Resilient Distributed Datasets (RDD)를 이해하는 것이 필요합니다.
RDD가 어떻게 동작하는지, 원 논문을 리뷰하며 살펴보도록 합시다.
http://www.cs.berkeley.edu/~matei/papers/2012/sigmod_shark_demo.pdf
[NDC18] 야생의 땅 듀랑고의 데이터 엔지니어링 이야기: 로그 시스템 구축 경험 공유Hyojun Jeon
NDC18에서 발표하였습니다. 현재 보고 계신 슬라이드는 1부 입니다.(총 2부)
- 1부 링크: https://goo.gl/3v4DAa
- 2부 링크: https://goo.gl/wpoZpY
(SlideShare에 슬라이드 300장 제한으로 2부로 나누어 올렸습니다. 불편하시더라도 양해 부탁드립니다.)
[NDC18] 야생의 땅 듀랑고의 데이터 엔지니어링 이야기: 로그 시스템 구축 경험 공유Hyojun Jeon
NDC18에서 발표하였습니다. 현재 보고 계신 슬라이드는 1부 입니다.(총 2부)
- 1부 링크: https://goo.gl/3v4DAa
- 2부 링크: https://goo.gl/wpoZpY
(SlideShare에 슬라이드 300장 제한으로 2부로 나누어 올렸습니다. 불편하시더라도 양해 부탁드립니다.)
6. 1.1 Airbridge에 대해
웹과 앱에서의 유저 행동을 분석해 마케팅 성과를 측정하는 모바일 애트리뷰션 애널리틱스
누적 디바이스 수
5,000만+
월 평균 이벤트 수
100억+
7. 1.2 Airbridge의 새로운 기능 개발
코호트 분석 기능을 도입해, 폭넓고 유용한 유저 행동 분석 기능을 제공하자!
Retention
Funnel
TrendUsers who
Purchased
morethan
3
times
타겟 유저군을 자유자재로 잡아서, 다양한 분석 리포트 제공
8.
9. 타겟 유저군은 자유자재로 설정될 수 있어야 함
예시: 6월 한달동안 매 주마다 10000원 이상 소비했다가,
7월 이후로 한번도 앱을 열지 않은 “우수회원” 등급의 유저
기술적 니즈:
- 유저 ID에 대한 GROUP BY 쿼리가 자주, 빠르게 일어날 수 있어야 함
- 시계열 분석을 용이하게 할 수 있어야 함
1.4 기술적인 니즈 분석하기
설정하자마자 리포트가 즉시 보여져야 함
기술적 니즈: 실시간! 빠르게!
11. 1.5 기존 솔루션의 한계
Druid : 너무 느림 (16-20초 소요)
- 심지어 Pre-Aggregation 방식의 한계로 복잡한 행동분석 쿼리가 불가능
데이터 웨어하우스 : 비용 비효율적
- SQL로 행동분석 쿼리는 가능, 하지만 너무 범용적이라 여전히 느리다
- 빠르게 만들려면? 💸 🤭 💸
12. 1.6 자체 개발의 필요성
기존 솔루션을 테스트해보면서, 자체 개발의 필요성에 도달
비용 비효율성 해소
- 대규모 스케일의 데이터 웨어하우스로 원하는 성능을 낼 수 있지만 비용 비효율적
- 반면 자체 개발시 OLAP 쿼리 특수성으로 인해 최적화 여지가 무궁무진 (후술)
쿼리 다양성
- 퍼널 분석 등의 고도화된 행동 분석은 SQL로 표현하기에 한계가 존재함
- 따라서 높은 커스텀이 필요하지만, Spark 등을 쓰기엔 느림
15. 2.1. Luft
유저 행동 분석에 최적화된 실시간 OLAP 데이터스토어 by AB180
- Fast: 5~10초 내로 수억 이벤트를 스캔해 쿼리 제공
- Real-Time: 람다 아키텍쳐로 실시간 데이터 처리
- High Availability: 데이터는 샤딩되고 S3에 저장됨
- Cloud Native: 클라우드에 직접 연동되어 유연한 스케일링 및 확장
16. 2.2. Performance
- 코호트, 리텐션, 트렌드 분석 등을 10-15초 이내로 제공
- Druid와 비슷하거나 작은 규모 클러스터로 이를 제공
0초 2.5초 5초 7.5초 10초
9.31초
4.06초
0.66초
2020-05-27일부터 4주동안 구매 금액이 5000원 이상인 사람의 모수 추출 쿼리를 동일 클러스터 (4core * 4, 32GB) 스펙에서 캐시 없이 진행
17. 2.3. Key Points
- Immutability: Luft 내 데이터는 불변하다.
- 이벤트 데이터는 로그성 데이터처럼 한번 쌓이면 변하지 않기 때문
- 그래도 데이터 보정이 필요하다면 덮어쓰는 방식으로 수정 가능
- TrailDB: 이벤트 데이터 저장에 최적화된 데이터 저장 포맷
- 일반적인 DBMS는 데이터 수정을 위해 B+Tree, Skip-List 등을 사용
- 하지만 불변성을 전제한다면? 성능을 위한 특수 자료구조 적용 가능
18. 2.4. Architecture
Go로 개발되었으며, gRPC로 통신함
- 마스터 노드: API 서버 제공 및 쿼리 스케줄링 & 결과 취합
- 히스토리컬 노드: 샤딩된 데이터 저장 및 쿼리 연산
- 리얼타임 노드: 실시간 스트림 구독 및 쿼리 연산
- Lambda Architecture
Realtime Node
Kafka Consumer
Historical Node
Partition some-table/
2020-09-01_2020-09-02/
2020-09-05T18:43:01Z/1
Partition some-table/
2020-09-02_2020-09-03/
2020-09-06T18:43:01Z/1
Master
19. 이벤트 데이터는 유저별로 파티셔닝된 채로 인덱싱됨
Raw Data 유저 ID별 파티셔닝
installuser1
addToCartuser2
installuser3
purchaseuser2
installuser1
user2
user3
user4
purchase
addCart addCart purch
launch
install
like
addCart
25. 3.0. 프로젝트 시작과, 우리의 철학
우리에겐 오버엔지니어링할 여유가 없다.
하늘 아래 새로운 건 없기 때문에,
- 이미 솔루션이 있으면 최대한 차용
- 없다면, 기존에 왜 문제를 못 풀었을 지 생각하고
- 이제 무엇이 바뀌어 문제를 풀 수 있게 되었을지 분석
26. R&D를 통한 초기 방향성 잡기
기존에 왜 문제를 못 풀었는지 생각하자.
- Druid, Spark, Kafka, CockroachDB등의 오픈소스 분석 및 인사이트 추출
지금 무엇이 바뀌었기 때문에 문제를 풀 수 있을지 분석하자.
- 주요 변화: CPU 코어 수, SIMD, 메모리 용량, I/O 속도, 클라우드
27. 그러나 Pre-Aggregation 방식은 우리에게 맞지 않다!
“정해진 Metric을 시간 단위별로 사전에 미리 계산해놓고,
쿼리 시엔 필요한 값만 선택해 합산 (Roll-up)해서 결과를 빠르게 제공한다”
= High-Cardinal Shuffle에 취약
= 정해진 종류의 한정된 분석만 가능
따라서 Pre-Aggregation을 쓰지 않고, 그때그때 계산하는 게 베스트라는 결론
28. 그때그때 계산도 문제는 여전하다
유저 행동 분석 쿼리는 필연적으로 계산 결과를 유저별로 Group By하게 됨
한편 MapReduce에서의 가장 큰 Bottleneck은 Shuffling이다.
데이터가 흩어져 저장되어 있을수록 Shuffle 비용이 기하급수적으로 증가함
-> 파티션 키의 Cardinality가 높을수록 Shuffle 비용이 기하급수적으로 증가!
유저 ID는 대표적인 High-Cardinality 칼럼
= 따라서 Group By User를 수행할 시 Shuffle 비용이 매우 커진다.
29. Shuffle을 최적화하는 방법: 파티셔닝을 잘 해서 Shuffle을 국소적으로 만들자.
즉, 유저 ID로 파티셔닝될 수 있는 스토리지를 찾으면 되겠다!
30. 3.1. TrailDB의 발견
3.2. LLVM으로 쿼리 엔진 개발
3.3. 연산 레이어를 직접 만들기
3.4. 샤딩 구현하기
Query Engine
LRMR
Luft
Shard Controller
STORAGE LAYER
COMPUTATION LAYER
APPLICATION LAYER
We’re here! TrailDB
31. 3.1. TrailDB
· Adroll에서 2015년에 공개한 타임시리즈 이벤트 저장 Rowstore
· 유저 ID 기반 파티셔닝에 최적화된 유일한 스토리지 포맷
· 인덱스가 없다! 무조건 풀스캔해야 한다.
· 뭐 이런게 다 있ㅇ…😡
· 맥북 15” (Core i7, 2.6Ghz @ 6core) 기준 0.51초
· Snowflake에선 8.1초, Druid에선 1.1초
근데 풀스캔 성능이… 너무 좋네? 🤨
32. 충격적으로 높은 압축률이 비결
CSV로 13GB짜리 데이터가 TrailDB론 ~300mb
이런 압축률에도 여전히 Time Complexity는 O(N)이다.
어차피 Time Complexity가 O(N)일거, Space Complexity를 극한으로 줄여버리자.
사이즈가 작으면 메모리에도 잘 들어가요.
SSD고 뭐고, RAM Sequential Read가 최고
TrailDB, 왜 이렇게 빠를까?
98%
33. 유저 이벤트 특성을 반영해 설계한 데이터 구조
델타 인코딩: 유저의 이벤트를 시간순으로 정렬해,
이전 이벤트 대비 늘어난 시간 값만 저장
딕셔너리 인코딩: 값을 사전화시켜 그 ID만 저장
Event #
1
Event Timestamp: Delta-Encoded
Data: Dictionary-Encoded
…
Event #
2
Event Timestamp: Delta-Encoded
…
User ID
…
…
TrailDB, 왜 이렇게 빠를까?
35. Time view 100 TRUE
John
Doe
foo 72
like 150 FALSE bar
buy
Raw Data
Time view 100 TRUE
John
Doe
foo 72
Time like 150 FALSE
John
Doe
bar 72
Time buy 150 FALSE
John
Doe
bar 72
Edge-Encoding: 이전 이벤트 대비 바뀐 칼럼만 넣자.
어차피 대부분의 사용자 속성은 변하지 않으니까
TrailDB는 Columnstore같은 Rowstore
Event #1
Event #2
Event #3
36. Raw Data
Time view 100 TRUE
John
Doe
foo 72
Time like 150 FALSE
John
Doe
bar 72
Time buy 150 FALSE
John
Doe
bar 72
Δt
Event #1 Event #2 Event #3
Edge-Encoding: 이전 이벤트 대비 바뀐 칼럼만 넣자.
어차피 대부분의 사용자 속성은 변하지 않으니까
TrailDB는 Columnstore같은 Rowstore
Event #1
Event #2
Event #3
BuyΔt like 150 FALSE barTime view 100 TRUE
John
Doe
foo 72
37. Immutability: 수정 불가능함
물론 Mutability를 포기하고 다른걸 얻자는 아이디어는 이미 대세 (e.g. HDFS, RDD..)
Simplicity: Full-featured DB가 아닌 LevelDB같은 스토리지 포맷에 가깝다.
쿼리 엔진이나 샤딩, 복제, 클러스터링 같은 기능이 없음
그래도 이걸 스토리지 엔진으로 삼아 데이터스토어를 만들어볼 만 하다!
물론, 공짜 점심은 없다 🍔
38. · Less Space Complexity leads to Less Time Complexity
· Edge Encoding, Dict Encoding을 통한 중복 제거
· Rowstore에서 칼럼 순서는 상관없기 때문에,
최빈순으로 정렬해 Data Entropy를 줄임
· Immutability를 통해서 할 수 있는게 많아진다
· 기존의 RDBMS에서 할 수 없던 극한의 압축률
· 시간축으로 정렬해 저장해 Sequential I/O 속도 장점 살림
· OLAP 데이터스토어기 때문에, 수정 용이성을 과감히 버릴 수 있었기 때문
Lessons Learned from TrailDB
39. 3.1. TrailDB의 발견
3.2. LLVM으로 쿼리 엔진 개발
3.3. 연산 레이어를 직접 만들기
3.4. 샤딩 구현하기
TrailDB
Query Engine
LRMR
Luft
Shard Controller
STORAGE LAYER
COMPUTATION LAYER
APPLICATION LAYER
We’re here!
40. 3.2. LLVM으로 쿼리 엔진 개발
· TrailDB라는 스토리지가 생겼으니
쿼리 기능을 짜야 할 때
· Predicate Pushdown:
스토리지 레벨에 필터를 적용해
필요한 데이터만 읽는 기법
· Luft의 쿼리는 Go에서 파싱되므로,
C/C++단 TrailDB 쿼리 엔진에 넘겨야 함
41. · TrailDB는 OR-AND 형식의 Equals (=) 쿼리만을 지원한다!
· 난데없이 생겨버린 요구사항 2가지
1. TrailDB의 쿼리 엔진을 확장해 다양한 연산자 지원
2. Go로 파싱한 쿼리를 C언어로 가져오기
· 레이어화를 시킬 땐 Separation-of-Concern이 확실해야 한다.
· 따라서 이런 식으로 중복 코드를 절대 만들고 싶지 않았음
TrailDB 쿼리 기능의 한계
추후 기능 확장에 제약사항!
관리 포인트가 많이 늘어남.
42. · PostgreSQL에서 쿼리를 LLVM JiT으로 컴파일한다는 걸 리서치 중 발견
· LLVM JiT은 IR 코드를 주면 즉시 최적화 및 컴파일해 실행할 수 있음.
· 잠깐, 우리의 모든 문제점을 해결해줄 기술인데?
· Go 레벨에서 LLVM IR 코드만 생성해 넘기면
· C/C++에서 JiT 컴파일해서 실행해버리면 된다.
상상도 못한 돌파구, LLVM JiT
43. 쿼리는 기능 확장이 빈번하기 때문에 C/C++로 짜면 추후 개발 비용 .
근데 JiT 쿼리엔진은 한번만 만들어두면 나머진 Go로 짤 수 있음 .
용을 잡아볼만 하다!
45. · 일반적으론 성능 최적화 때문에 JiT을 도입하지만,
개발 비용때문에 JiT을 도입했다는 특수한 시나리오
· 상식: 복잡한 기술 스택 도입은 오버엔지니어링이다.
· 교훈: 복잡한 기술 스택 도입을 적시에 한다면 오히려 개발 비용 절감 가능
· 어차피 유지보수는 힘드니, 기능 확장성을 고려
Lessons Learned from JiT
46. 3.1. TrailDB
3.2. LLVM으로 쿼리 엔진 개발
3.3. 연산 레이어를 직접 만들기
3.4. 샤딩 구현하기
TrailDB
Query Engine
LRMR
Luft
Shard Controller
STORAGE LAYER
COMPUTATION LAYER
APPLICATION LAYER
We’re here!
47. · 연산 레이어에서 흩어진 데이터를 하나로 모음
· 이런 연산 레이어로 MapReduce가 많이 쓰여요.
· 근데 우리는 Go라서 못 써요.
· Spark를 겨우 붙여서 벤치마크해봤지만, 성능이 그닥
· 애초에 Spark / Hadoop은 Long-running Job에 최적화된 프레임워크
3.3. 연산 레이어를 직접 만들기
50. 빠른 개발을 위해
· gRPC + Protobuf + etcd라는 검증된 조합 선택
· 익숙한 Spark의 디자인을 많이 차용
쿼리와 같은 Short-running Job에 최적화하기 위해
- Idea: 과감히 Resiliency를 포기하자.
- 저널링이나 체크포인팅을 포기하는 대신, 성능 극한으로 높이고 개발 비용 절감
- 만약 장애가 발생하면?
- 처음부터 다시 하자! 어차피 10초 미만
Design Goals of LRMR
51. 구조적 문제 발생 : Stream Backpressure
프로덕션에서 대규모 데이터를 쿼리하다 보니, LRMR 단에서 버퍼 오버플로가 자주 발생
- Push-based Stream에서 흔히 볼 수 있는 Backpressure 문제
- 프로듀서 속도를 컨슈머가 따라오지 못해 버퍼에만 쌓이다 터진다.
🐰
Fast
Producer
🐢
Slow
Consumer
Push
Laaaaaaaaaag
52. 해결 : Pull-based Event Stream!
· Idea: 컨슈머가 처리할 수 있는 양만 그때그때 요청하면 어떨까?
· ✅ 속도에 맞게 요청할 수 있어 Backpressure 없음
· ✅ 스트림 오프셋을 컨슈머가 관리하므로, 추후 체크포인트나 저널링 도입도 용이
· Kafka, Reactive Streams, Armeria 등이 채택한 방식
🐰
Fast
Producer
Pull N tasks
🦄
Slow
Consumer
53. 3.1. TrailDB
3.2. LLVM으로 쿼리 엔진 개발
3.3. 연산 레이어를 직접 만들기
3.4. 샤딩 구현하기
TrailDB
Query Engine
LRMR
Luft
Shard Controller
STORAGE LAYER
COMPUTATION LAYER
APPLICATION LAYER
Finally!
54. 3.4. 샤딩 구현하기
Luft에서의 샤드 = “히스토리컬 노드”이다.
즉 샤딩 = 파티션을 여러 히스토리컬 노드에 분배하는 과정
Idea: 파티션의 날짜 범위를 샤딩 키값으로 사용하자
- 모든 쿼리에 반드시 시간 범위가 붙음 → 데이터 필터링 용이
- 같은 시간 범위엔 비슷한 용량의 데이터가 들어 있음 → 데이터 분산 용이
55. 새로운 파티션을 만들 때마다 Round-Robbin으로 각 샤드에 분배
문제: 분산 환경은 아름답지 않다.
- 노드가 다운되거나 새로 추가되면?
- 노드의 저장 공간이 꽉 차면?
- 일시적 장애로 인해 파티션이 한 샤드에 쏠려버리면?
Rebalance, Time Decay, Eviction 등의 고도화된 정책 니즈 제기됨
버전 1 : Vanilla Sharding
56. · 정책적 의도대로 Cost Function을 세우고, Cost가 제일 낮은 노드에 자원을 배치함
· 일반적으로 스케줄러를 짤 때 많이 사용되는 방법
· 우리의 정책적 의도 : 날짜 범위가 겹치지 않게 파티션을 분배하고 싶다!
· 두 파티션의 날짜 범위가 가깝고 겹칠수록 코스트가 높게 하면 된다.
· 이것까지 직접 만들긴 어렵다. Druid의 Cost Function 공식을 베이스 삼아 커스텀
버전 2 : Cost Function을 사용하자
57.
58. etcd로 샤드의 장애 상황을 효과적으로 관리
- Liveness Probe 패턴: 샤드 정보에 TTL을 걸고 주기적으로 갱신
- 장애 시엔 TTL 갱신이 안되니까 자연스럽게 샤드 목록에서 삭제됨
- Watch 기능으로 감지해 빠른 파티션 리밸런싱
클라우드 = 최고의 가용성 머신
- S3에 파티션을 저장해두고 필요한 것만 로드
- DynamoDB를 통해 파티션 목록을 관리
샤드의 가용성을 위한 끊임없는 노력
60. 4.1. 현재의 Luft
- Airbridge의 프로덕션에서 운영 중
- 실시간 코호트 모수 추출 및 리텐션 분석만을 현재 수행 중
- 4대의 c5.2xlarge 인스턴스만으로 최대 15초 내에 500GB 규모의 데이터 스캔
61. 4.1. 앞으로 해야 할 일
다양한 종류의 쿼리 지원
- 실시간 퍼널 분석을 10대 이내의 클러스터만으로 수행하는 것이 목표
- 고도화된 GROUP BY와 JOIN 기능
Spark 지원
- 데이터 분석의 확장성과 추후 ML 연동 등을 위해
그리고… Open Source!
62. 4.2. Ziegel : Columnstore by AB180
TrailDB를 대체할 자체 칼럼스토어
- TrailDB의 한계점을 극복하고 핵심 인사이트 계승
Design Goals
- 데이터 기반 데이터스토어 설계
- Bitmap Index 도입으로 사전에 유저 속성 기반 필터링
- SIMD와 멀티코어 최적화
63. 4.3. 다음에 다뤄보고 싶은 이야기
- 새로운 칼럼스토어 설계를 하며 얻은 데이터 엔지니어링적 인사이트
- 더 많은 프로덕션 경험과 장애 대응
- 구조적 SPOF를 줄여나간 이야기
- 극한의 Go 성능 최적화에 대한 경험
...만약 이런 이야기들을 직접 경험해보고 싶으시다면…?
64. Luft를 함께 만들어나가실 분을 찾습니다.
dev-recruit.ab180.co/database-engineer