본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Backend

뉴스 피드 설계: 연결, 활동, 팬아웃

추가
목차7개 항목
출처와 참고

「checkcheckzz/system-design-interview」의 「Facebook 뉴스 피드·타임라인 설계」 문항이 가리키는 자료, Etsy의 「Activity Feeds Architecture」, Facebook의 「Building Timeline」(2012)과 그 요약 「Facebook Timeline: Brought To You By The Power Of Denormalization」, High Scalability의 「Tumblr Architecture」, 「Scaling Twitter」, 「Instagram Architecture」, 「Introduction to Architecting Systems for Scale」를 바탕으로 재구성한 노트다. karanpratapsingh의 「System Design」 강의 저장소를 바탕으로 EdgeRank와 Twitter 전체 설계(규모 추정, 데이터 모델, API, 리트윗, 검색·트렌드, 함께 아는 친구)를 보강했다.

피드가 어려운 이유

뉴스 피드는 사용자마다 다른 목록이다. 내가 팔로우하는 수백 명의 최근 활동을 모아 시간·관련도순으로 보여 줘야 하고, 읽기가 쓰기보다 훨씬 많으며, 스크롤 첫 화면은 수백 ms 안에 떠야 한다. 반대로 팔로워가 수천만 명인 계정이 글 하나를 쓰면 그만큼의 피드가 바뀐다.

피드의 재료는 둘이다.

재료뜻저장 모양
연결(connection)누가 누구(또는 어떤 가게·주제)를 따르나. 친구, 팔로우, 즐겨찾기, 구매 관계를 아우르는 방향 그래프간선을 두 번 저장한다. 나가는 간선 표(내가 따르는 대상)와 들어오는 간선 표(나를 따르는 사람). 간선마다 친밀도(affinity) 가중치
활동(activity)누가 무엇을 했나. 주어·동사·목적어(SVO)로 줄인 사건 기록[소유자, (주어, 동사, 목적어)]

두 가지 기본 전략

전략동작장점단점
읽을 때 모으기(pull, 스캐터·개더)피드를 열 때 따르는 사람들의 최근 활동을 각각 가져와 합친다쓰기가 싸다. 저장이 적다따르는 수만큼 읽기가 퍼진다. 읽기가 많은 서비스에서 비싸다
쓸 때 뿌리기(push, 팬아웃·받은편지함)글이 써지면 팔로워마다의 받은편지함(inbox)에 미리 넣는다. 피드는 내 받은편지함을 읽기만 한다읽기가 한 번에 끝난다팔로워 수만큼 쓰기가 늘어난다. 팔로워가 매우 많은 계정에서 폭발한다
  • Tumblr 대시보드는 스캐터·개더로 버티다 하루 5억 페이지 뷰의 70%가 대시보드가 되자 받은편지함 모델로 옮겼다. 초당 100만 쓰기, 5만 읽기를 목표로, 사용자 범위마다 HBase·서비스·Redis 캐시를 다 갖춘 셀에 받은편지함을 두고, 모든 셀이 사내 이벤트 버스(Kafka 위의 「파이어호스」)에서 모든 글을 받아 자기 사용자의 받은편지함에 넣는다. 파이어호스는 일주일 치를 보관해 원하는 시점부터 다시 읽을 수 있다.
  • 실제 대형 서비스는 혼합한다. 보통 계정은 팬아웃으로 뿌리고, 팔로워가 매우 많은 계정의 글은 읽을 때 따로 끌어와 합친다.

쓰기 경로: 활동을 복제하고 나중에 퍼뜨린다

Etsy는 활동 하나를 관심 가질 당사자 수만큼 복제한다. 「스티브가 카일을 서클에 추가했다」는 스티브의 샤드에 [스티브, (스티브, 연결, 카일)], 카일의 샤드에 [카일, (스티브, 연결, 카일)]로 두 번 쓴다. 「카일이 육포를 즐겨찾기했다」는 카일, 카일의 가게, 육포 상품 소유자 몫으로 세 번 쓴다. 각자의 샤드에서 자기 활동만 읽으면 되도록 데이터를 미리 흩어 두는 것이다. 쓰기가 많아지는 복제는 작업 큐(Gearman)로 뒤로 미룬다.

사용자 경험은 「요청 안에서는 보이는 만큼만 하고 나머지는 나중에」로 맞춘다. 글을 쓰면 내 타임라인에는 즉시 보이고, 팔로워 피드 갱신은 큐 워커가 뒤에서 한다. Instagram은 팬아웃을 Gearman 작업으로, Twitter 초기에는 친구 캐시 무효화를 메시지 큐로 백그라운드에 넘겼다.

읽기 경로: 집계와 표시

피드 생성은 집계(aggregation) 와 표시(display) 두 단계로 나뉜다. 집계는 DB의 활동을 사용자별 피드(캐시)로 바꾸는 일이고, 대개 오프라인 작업으로 돈다.

1. 연결 고르기   따르는 대상 전부가 아니라 실제로 활동을 가져올 대상을 고른다
                 (친밀도를 확률로 써서 가까운 연결을 더 자주 고르는 식)
2. 활동 집합     고른 연결별로 최근 활동을 가져와 점수와 플래그를 붙인다
                 플래그는 피드 주인 관점이다(「내 가게에 관한 것」 등)
3. 중복 제거     같은 사건이 여러 경로(친구 A와 B가 모두 연결됨)로 들어온 것을 SVO로 지운다
4. 점수와 정렬   기본은 시간 감쇠(오래될수록 낮게), 여기에 관계·종류 가중치
5. 저장          결과를 Memcached 같은 캐시에 사용자별 피드로 둔다
표시            캐시의 피드를 읽어 비슷한 활동을 묶고(「카일 외 3명이 좋아합니다」) 렌더링한다

따르는 대상이 피드 하나에 담기 어려울 만큼 많으면 1단계에서 표본을 고르거나, 따를 수 있는 수에 상한을 둔다.

4단계 점수의 고전적인 예가 Facebook이 쓰던 EdgeRank다. 피드 항목(edge)마다 점수 = 친밀도(affinity) × 가중치(weight) × 시간 감쇠(decay)를 곱한다. 친밀도는 내가 작성자와 얼마나 자주 주고받나(좋아요·댓글·메시지), 가중치는 행동 종류의 무게(댓글이 좋아요보다 무겁다), 감쇠는 오래될수록 줄어드는 값이다. 지금의 피드 랭킹은 이런 손 공식 대신 수천 가지 신호를 쓰는 머신러닝 모델이 한다.

오래된 기록까지: 비정규화와 캐시 수명

Facebook 타임라인은 최근 며칠이 아니라 평생의 기록을 보여 줘야 했다. 정규화된 기존 데이터는 화면 하나에 DB 왕복이 많았고, 캐시에 없으면 디스크 여기저기를 무작위로 읽었다.

  • 비정규화: 각 행이 활동 정보와 랭킹 메타데이터를 함께 갖게 다시 써서, 행을 고르거나 버리는 데 추가 조회가 필요 없게 했다. 디스크에는 (사용자, 시간) 순으로 정렬해 InnoDB가 한 사용자의 기록을 몇 번의 순차 I/O로 읽는다. 수십 가지 옛 데이터 형식은 변환 규칙 언어와 컴파일러로 옮겼다.
  • 계산을 데이터 옆에서: 랭킹·체크인 묶기 같은 집계기를 DB 서버마다 돌려 디스크를 끝까지 쓰고, 화면에 보일 데이터만 네트워크로 보낸다.
  • 캐시 수명을 데이터 성격에 맞춘다: 「2010년 전체 활동 순위」처럼 큰 질의의 결과는 작고 거의 바뀌지 않아 Memcached에 오래 둔다. 「최근 활동」은 자주 무효화되지만 다시 만들기가 빨라, 결과 캐시 대신 DB 행 캐시(InnoDB 버퍼 풀, 플래시 캐시)로 받친다.
  • 새 기반 기술을 만들기보다 사내에서 검증된 MySQL·Memcached·피드 집계기를 다시 써서 6개월 만에 출시했다.

캐시할 수 없는 큰 조인은 플래시 전용 DB 계층으로 모으고 여러 서버에 동시에 질의하는 프록시를 둬서 줄였다.

사례: Twitter 전체를 설계할 때

Twitter 전체 설계는 피드에 트윗 저장·검색·알림이 붙은 문제다.

구분내용
기능트윗 작성(텍스트·이미지·동영상), 팔로우, 팔로우한 사람들의 뉴스 피드, 트윗 검색
비기능고가용성과 낮은 지연, 확장성
확장지표·분석, 리트윗, 좋아요
사용자 10억, DAU 2억 × 하루 5트윗 = 하루 10억 트윗 → 초당 약 1.2만
텍스트: 10억 × 100B ≈ 하루 100GB
미디어: 트윗의 10% = 1억 개 × 50KB ≈ 하루 5TB
10년: 5.1TB × 365 × 10 ≈ 19PB, 수신 대역폭 ≈ 초당 60MB
  • 테이블: users, tweets(종류, 내용, 작성자), favorites(사용자-트윗 좋아요), followers(팔로워-팔로이 N:M), feeds(사용자별 피드), feeds_tweets(피드-트윗 N:M). 서비스마다 테이블을 나눠 갖는다.
  • API: postTweet(userID, content, mediaURL?), follow(followerID, followeeID)·unfollow, getNewsfeed(userID)(페이지 단위).
  • 서비스: 사용자, 뉴스 피드(생성·발행), 트윗(작성·좋아요), 검색, 미디어, 알림, 분석.
  • 피드 생성: ① 내가 따르는 사용자·해시태그·주제의 ID를 모으고 ② 각각의 트윗을 가져와 ③ 관련도·시간·참여도로 순위를 매기고 ④ 페이지 단위로 돌려준다. 팔로우가 많은 사용자는 오래 걸리므로 미리 만들어 캐시에 두고 주기적으로 갱신한다. 발행은 위 「두 가지 기본 전략」의 혼합(팔로워가 적은 계정은 푸시, 유명인은 풀)이다.
  • 리트윗: 새 트윗 행을 만들고 종류를 tweet, 내용을 원래 트윗 ID로 둔다. 리트윗이 많아지면 별도 테이블로 뺀다.
  • 검색과 트렌드: 트윗 검색은 Elasticsearch 같은 검색 엔진으로 한다(Elasticsearch 검색: 매핑·분석기·Query DSL). 트렌드는 최근 N초 동안 많이 검색·언급된 질의·해시태그를 세어 M초마다 배치로 갱신하고 캐시한다(실시간 트렌딩과 Top-K 집계).
  • 함께 아는 친구: 사용자를 노드, 팔로우를 방향 간선으로 둔 소셜 그래프에서 팔로워의 팔로워를 따라가 추천한다. 정확도는 추천 모델로 높인다.
  • 나머지: 모든 이벤트를 Kafka로 발행해 Spark로 분석하고, 알림은 브로커를 거쳐 FCM·APNs로 보낸다. 인기 상위 20% 트윗을 LRU 캐시에 두고, 미디어는 오브젝트 스토리지와 CDN으로 내려 준다.

정리

  • 피드 = 연결 그래프 + 활동 로그. 간선은 양방향으로, 활동은 관심 당사자별로 복제해 둔다.
  • 읽기가 많으면 쓸 때 받은편지함에 뿌리고(팬아웃), 팔로워가 매우 많은 계정만 읽을 때 합친다.
  • 무거운 팬아웃은 큐로 미루되, 쓴 사람 화면에는 즉시 보이게 한다.
  • 집계는 연결 고르기 → 활동 모으기 → 중복 제거 → 점수 → 캐시 순서다.
  • 결론: 피드는 「언제 일을 하느냐(쓸 때냐 읽을 때냐)」를 계정 규모에 따라 나눠 정하는 문제다.

관련 노트: 메시지 큐와 Kafka, 캐시 전략, Redis, 데이터베이스 샤딩, 알림 시스템 설계, 웹 확장성의 구성 요소, 사진 공유 서비스 설계

원본 기록9

댓글

GitHub 계정으로 댓글을 남길 수 있어요.