21P by hiddenest 2020-12-24 | favorite | 댓글 2개

월 평균 이벤트 수가 100억 건이 넘는 환경에서, 빠른 시간 내에 데이터를 분석하여 유저 행동 기능 분석(Cohort)을 해줘야 하는 상황이 옴
(ex. 지난 6개월간 우리 앱에서 한 달에 10만원 이상 소비한 30대 여성 → 이들의 재방문율)

이런 환경에서 개발자가 쓰기만 했던 데이터스토어를 직접 구현하는 이야기를 담음.

# 유저 행동 분석 쿼리를 구현하기 위해서는…
- 사전에 미리 계산해놓지 않은 메트릭을 쿼리할 수 있어야 함 (+ 새로운 종류의 분석도 Re-indexing 없이 가능해야)
- 이벤트 데이터를 유저 별로 Group By할 때 High Cardinality Shuffle의 보틀넥은 적어야 함

# 기존 솔루션을 쓸까, 자체 솔루션을 만들까 고민함
- Druid는 다른 곳에서 사용중이었지만 Pre-Aggregation(계산된 값만 읽는 방식)의 한계로 기능 구현에 부적합
- Snowflake나 Redshift 등의 데이터 웨어하우스를 큰 규모로 운영할 수는 있지만, 특유의 범용성 때문에 목표 대비 너무 큰 규모의 클러스터를 운영해야되서 비쌈
- Funnel, ID 매칭 등 다양한 니즈를 커버하기 위해서는 SQL 기반 DB는 한계가 있음

# 결국 데이터스토어를 직접 만들다
- Luft = 처음부터 유저 ID 기준 Group By된 유저 행동 분석 쿼리를 빠르게 수행하는데 최적화된 데이터 스토어
- Golang을 기반으로 만들어짐
- 수십 TB 규모의 유저 데이터를 5대 이하의 노드만으로 평균 3초 ~ 최대 10초 사이에 분석
- 일반적인 RDBMS와 다르게 불변성을 가짐 (필요하다면 같은 기간의 데이터를 덮어씌움) → 심플한 클러스터 디자인, 복잡한 페이지 매니저 구현 없이 높은 성능, 원하는 데이터 저장 포맷 설계 가능

# 기술 기반 뜯어보기
- TrailDB (스토리지 엔진) - 유저 ID 파티셔닝에 최적화된 타임시리즈 이벤트 저장 Rowstore
ㅤ→ 값을 사전화시켜 그 ID만 저장
ㅤ→ 유저 이벤트를 시간순으로 정렬해 이전 이벤트 대비 늘어난 시간값, 바뀐 컬럼만 저장 (대부분의 사용자 속성은 변하지 않으므로)
ㅤ→ 인덱스 없음. 무조건 풀스캔해야 함.
ㅤ→ 근데 충격적으로 높은 압축률을 자랑 (CSV 13GB → ~TrailDB 300mb)
ㅤ→ 시간 복잡도는 O(n)이므로 공간 복잡도를 줄이면 되겠다고 생각
- LLVM (쿼리 엔진)
ㅤ→ 근데 TrailDB는 OR-AND 형식의 equals만 제공, Go에서 파싱된 쿼리를 C, C++로 전달해야 함
ㅤ→ PostgreSQL에서 쿼리를 LLVM JiT으로 컴파일한다는 것을 알게 됨
ㅤ→ 쿼리는 기능 확장이 빈번한데 C, C++로 짜면 개발 비용이 늘어나는 문제를 막을 수 있음 (Golang에서 LLVM IR만 생성해 넘기면 C, C++에서 JiT 컴파일에서 실행하면 됨)
- 연산 레이어 직접 만들기
ㅤ→ MapReduce가 많이 쓰이는데 Golang을 쓰기에 못씀
ㅤ→ Spark/Hadoop은 Long-running Job에 최적화되서 붙여봐도 성능이 잘 안나옴
ㅤ→ 이것도 직접 만듬 → https://github.com/ab180/lrmr
ㅤ→ gRPC + Protobuf + etcd 조합, 익숙한 Spark 디자인 많이 차용
ㅤ→ Resiliency를 포기하자 → 성능을 극한으로 높이면, 장애가 발생해도 처음부터 다시 해도 10초 미만
ㅤ→ 대규모 데이터 처리로 인한 버퍼 오버플로가 자주 발생 (Backpressure)하는 문제를 Pull-based Event Stream으로 변경 (Kafka, Armeria 등에서 채택)
- 샤딩 직접 구현하기
ㅤ→ 샤드 = 히스토리컬 노드
ㅤ→ 파티션의 날짜 범위를 샤딩 키값으로 사용하면?
ㅤㅤ→ 모든 쿼리에 시간이 있고 → 필터링 용이
ㅤㅤ→ 같은 시간 범위에는 비슷한 용량의 데이터가 있음 → 데이터 분산 용이
ㅤㅤ→ 분산 환경은 아름답지 않음…
ㅤㅤ→ 노드 다운되거나 새로 추가되면?
ㅤㅤ→ 저장 공간 꽉 차면?
ㅤㅤ→ 장애로 한 노드에만 쏠리면?
ㅤㅤ→ Druid의 Cost Function을 커스텀하여 파티션 날짜 범위 가깝고 겹칠수록 Cost가 높아지도록 만듬
ㅤ→ 샤드 가용성을 위해 아래의 일을 함
ㅤㅤ→ 샤드 정보에 TTL을 걸고 주기적으로 갱신 (etcd)
ㅤ→ S3에 파티션 저장, DynamoDB로 파티션 목록 관리

# 지금 프로덕션 상황
- 4대의 c5.2xlarge 인스턴스만으로 15초 내에 500GB 데이터 스캔

# 앞으로의 목표 (혹은 해야 할 일)
- 실시간 Funnel 분석을 10대 이내의 클러스터로 하고 싶음
- Spark를 지원해서 ML 연동 등을 지원하려 함
- TrailDB를 대체할 자체 컬럼스토어(Ziegel) 개발하고 있음
ㅤ→ SIMD와 멀티코어 최적화
ㅤ→ Bitmap Index로 사전에 유저 속성 기반 필터링

traildb 재밌죠. https://www.youtube.com/watch?v=-oPFxSwn0lM 재미있습니다. 오래묵은 영상이지만 traildb가 그동안 바뀐게 없을거에요.

지금 보니까 개발자 분의 블로그 글도 있네요,
https://engineering.ab180.co/stories/introducing-luft

TrailDB는 처음 들어봤는데 요런 친구고...
https://github.com/traildb/traildb