Skip to content

[Design Doc] 랭킹 콜드 스타트 문제 해결하기 (9주차 · 4팀 · 이동준) #417

Description

@ddongjunn

TL;DR

TL;DR — 조회, 좋아요, 주문 이벤트를 Kafka에서 독립 consumer group으로 소비해, event-time 기준 Redis ZSET에 일별 랭킹을 미리 구축했다. Redis는 현재 랭킹을, MySQL ranking_snapshot은 확정된 과거와 재구축 기준을 맡는다.

본문

Introduction & Goals

  • Context / Background: 인기 상품은 조회량이 많고 항상 정렬이 필요하다. 요청마다 RDB에서 집계·정렬하면 DB 부하가 커지고, 전체 누적값만 쓰면 오래된 상품이 상단을 점유한다.
  • Goals:
    1. 이벤트가 들어올 때 점수를 누적해 랭킹 조회를 빠르게 만든다.
    2. 늦게 소비된 이벤트도 실제 발생일의 랭킹에 반영한다.
    3. Redis 장애·자정 콜드 스타트·과거 날짜 조회의 정책을 명확히 한다.

Detailed Design

System Architecture

flowchart LR
    API[commerce-api]
    K[Kafka<br/>catalog-events · order-events]
    S[commerce-streamer<br/>ranking consumer group]
    R[(Redis ZSET<br/>ranking:all:yyyyMMdd)]
    P[(MySQL<br/>product · like_count)]
    SCH[Ranking Scheduler]
    SNAP[(MySQL<br/>ranking_snapshot)]

    API -->|VIEW / LIKE / PAID<br/>occurredAt| K
    K -->|batch poll| S
    S -->|fold + pipeline ZINCRBY| R
    API -->|ZREVRANGE / ZREVRANK| R
    API <-->|상품 조합 · 장애 폴백| P
    SCH -->|23:50 carry-over<br/>00:30 finalize| R
    SCH -->|snapshot 저장| SNAP
    SNAP -->|수동 rebuild 기준| SCH
Loading
  • commerce-api는 이벤트를 발행하고, 완성된 ZSET을 읽어 상품 정보와 조합한다.
  • commerce-streamerranking-catalog, ranking-order group으로 기존 metrics와 독립적으로 이벤트를 소비한다.
  • 컨슈머는 event-time을 KST 날짜로 변환하고 (날짜, productId)별 점수를 fold한 뒤 Redis pipeline으로 적재한다.

Data Models

  • Redis key: ranking:all:{yyyyMMdd}, member: productId, score: VIEW(0.1) + LIKE(±0.2) + ORDER(0.6 × 수량).
  • Redis TTL: 날짜 기준 2일. 오늘·어제의 실시간 랭킹만 유지한다.
  • ranking_snapshot: stat_date, product_id, rank_position, score를 저장한다. 00:30에 어제 ZSET을 확정하며, 과거 조회와 재구축의 기준이다.

API Design

  • GET /api/v1/rankings?date=yyyyMMdd&page&size: 오늘·어제 ZSET에서 페이지를 읽고 상품 요약을 붙인다.
  • GET /api/v1/products/{id}: 오늘 ZSET의 ZREVRANKtodayRank로 추가한다. 순위가 없거나 Redis 장애면 null이다.
  • POST /api/v1/admin/rankings/rebuild: 어제 스냅샷 점수의 10%를 기준으로 오늘 ZSET을 재구축한다.

Constraints

  • 형식 오류는 400, 오늘, 과거 또는 미래 날짜는 404, window 빈 랭킹은 200 빈 목록이다.
  • Redis 읽기 장애 시 목록은 like_count 내림차순으로 폴백하고 degraded:true를 반환한다.
  • event-time 누락·미래 이벤트는 DLT, 너무 과거 이벤트는 드롭한다.
  • 재소비에 따른 중복 점수는 하루 단위의 근사 랭킹이라는 전제에서 허용한다.

Alternatives Considered

옵션 Pros Cons
RDB 일별 metrics 집계 내구성이 높다 조회마다 집계·정렬 부담이 크고, metrics의 책임이 커진다
Redis에만 저장 실시간 정렬과 조회가 빠르다 Redis 데이터 유실시 데이터 복구 기준이 없다
선택: Redis ZSET + snapshot 현재성·조회 성능과 과거 내구성을 분리한다 finalize 전 당일 이벤트는 완전 복구하지 못한다

선택 근거: 실시간 랭킹은 Redis ZSET의 정렬 능력을 활용하고, 확정된 과거만 MySQL에 남겨 저장소 책임을 분리했다. product_metrics 전체 누적값을 랭킹 원본으로 쓰지 않아 롱테일 문제도 피했다.

Cross-cutting Concerns

  • Scalability: batch listener에서 같은 상품·날짜의 이벤트를 fold하고 Redis pipeline으로 왕복 횟수를 줄인다.
  • Latency: 조회는 미리 정렬된 ZSET을 읽고, 상품 정보는 findAllById로 한 번에 조합해 N+1을 피한다.
  • Observability: DLT, too-old drop, Redis 폴백을 로그, 메트릭으로 관측한다.

Reference

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions