- 시스템 설계시 완벽한 일관성, 가용성, 낮은 지연시간, 높은 처리량을 동시에 만족하기는 사실상 어려운 과제임
- 애플리케이션에 맞는 균형점을 찾는 방식으로 적합한 설계를 찾는 것이 중요함
- Bluesky의 팔로잉 피드/타임라인 설계에서 쓰기 성능을 개선하기 위해 일관성을 일부 희생하는 트레이드오프를 적용함
- 그 결과 사용자에게 부정적인 영향을 주지 않으면서 P99 지연시간이 96% 이상 감소하는 효과를 봄
타임라인 팬아웃
- Bluesky에서 사용자가 게시물을 올리면, 시스템에 의해 인덱싱되고 데이터베이스에 저장되어 API 응답으로 제공됨
- 동시에 이 게시물을 팔로워 각각의 타임라인 테이블에 삽입하는 “팬아웃” 과정을 거침
- 이는 팔로워 목록을 조회한 뒤, 각 팔로워의 타임라인 테이블에 역순 삽입을 수행함
- 타임라인 테이블은 사용자별로 파티셔닝되어 분산 DB(ScyllaDB)에 저장되고, 고가용성을 위해 여러 샤드에 복제됨
- 사용자마다 다른 샤드에 할당될 수 있음
- 저장 공간 절약을 위해 일정 길이를 넘어가는 타임라인은 오래된 게시물 레퍼런스를 정기적으로 삭제함
핫 샤드 문제
- Bluesky는 약 3,200만 명의 사용자가 있으며, 타임라인 데이터베이스는 수백 개의 샤드로 나뉨
- 수백만 명이 사용하는 시스템에서, 극단적으로 많은 팔로잉 관계를 가진 사용자가 존재할 수 있음
- 예: 수십만 명을 팔로우하는 사용자
- 한 샤드에는 다수의 사용자 타임라인이 함께 저장됨
- 특정 사용자가 매우 많은 쓰기를 유발하면 해당 샤드가 과부하 상태(“핫 샤드”)가 됨
- 이러한 핫 샤드는 쓰기나 읽기 연산이 집중되어, 같은 샤드의 다른 사용자들에게도 지연이 전파되는 문제가 생김
지연시간 누적
- 한 사용자가 2,000,000명의 팔로워를 가질 경우, 순차적으로 쓰기하면 20분 이상 걸릴 수 있음
- 이를 줄이기 위해 팬아웃을 병렬화하면 평균 지연시간은 짧아짐
- 그러나 P99 지연시간(약 15밀리초 이상)이 여러 번 발생하여 병렬 작업 전체를 지연시킬 수 있음
- 팔로워가 매우 많은 경우, P99 혹은 P99.9 지연으로 인해 전체 팬아웃 시간이 최악의 경우 수 분에서 수십 분까지도 늘어날 수 있음
Lossy(손실) 타임라인
- 팔로잉 수가 지나치게 많은 사용자에게는 모든 게시물을 정확히 순서대로 보여주는 것이 현실적으로 불가능함
- 사람이 실제로 모든 게시물을 소비하기도 어려움
- 따라서 일정 기준치(예: ‘reasonable_limit’)를 넘는 팔로잉 수를 가진 사용자의 타임라인에는 일부 쓰기를 확률적으로 “드롭”하는 방식을 도입함
- loss_factor = min(reasonable_limit / num_follows, 1) 공식을 사용함
- 팬아웃 시에 무작위 값을 생성하여 loss_factor보다 큰 경우 타임라인 쓰기를 생략함
- 이를 통해 특정 사용자 타임라인에 대한 과도한 쓰기를 제한하고, 샤드 전체 성능 저하를 방지함
캐싱에 대하여
- 타임라인 쓰기는 초당 백만 건 이상 발생하기 때문에, 각 쓰기마다 사용자 팔로잉 수를 DB에서 직접 조회하면 부하가 매우 커짐
- 대신 Redis에 팔로잉 수가 높은 계정을 정렬 세트(sorted set)로 캐싱함
- 팬아웃 서비스 인스턴스는 30초마다 이 캐싱 정보를 메모리에 로드함
- 결과적으로 팬아웃 과정 중에도 고팔로잉 사용자 정보를 빠르게 조회할 수 있음
- 캐싱 정보가 완벽히 최신일 필요는 없으므로, 약간의 불완전성을 감수하며 성능과 확장성을 높이는 접근임
결과
- 로시 타임라인을 도입한 후 Timelines 데이터베이스에서 핫 샤드가 사실상 사라짐
- 팬아웃 한 페이지를 처리하는 데 걸리는 P99 지연이 90% 이상 줄어듦
- 전체 팬아웃 작업 시간을 보았을 때, P99 기준으로 5~10분 걸리던 작업이 10초 미만으로 단축됨
- 일관성을 일부 희생해도 서비스 이용자 기대치를 충분히 만족하며 대규모 확장성을 유지할 수 있음을 보여줌
- Bluesky 타임라인 아키텍처에는 여전히 개선 여지가 있지만, 이번 변경으로 쓰기 처리량과 확장성을 크게 향상시켰음