Search
📚

[if(kakaoAI)2024] 선물하기 서비스 캐싱 전략

Tags
Architcture
Meet Up / Conference
Study
Caching
Last edited time
2024/12/24 00:48
2 more properties

1. 개선 배경

1.1. 개요

선물하기 서비스는 이벤트 시즌(예: 빼빼로데이, 화이트데이)마다 트래픽이 증가
특히, KBO 프로야구와의 콜라보로 굿즈 프로모션을 진행했을 때, 평소 트래픽의 10배 이상, 이벤트 시즌 기준의 3배에 달하는 트래픽이 발생
결과적으로, 주요 서비스뿐만 아니라 관련된 외부 API를 사용하는 서비스에도 과부하가 전파되었고, 이에 따라 서비스 장애가 발생

1.2. 기존 서비스 구조와 문제점

선물하기 서비스 기존 구조
사용자 화면에 표시할 데이터를 API를 통해 외부 서비스에서 조회하고 이를 캐싱하는 구조
캐시 스탬피드(Cash Stampede) 현상 발생
서버 부하 증가 및 성능 저하를 초래할 수 있는 현상
캐시 데이터가 만료되면 수많은 클라이언트가 동시에 데이터를 요청하여 API 서버와 DB에 과부하 발생
캐시 장벽이 사라지는 순간 외부 서버로 흘러들어가는 트래픽

1.3. 기존 해결 시도

1.3.1. 캐시 TTL(Time-to-Live) 증가

캐시 데이터의 만료 시간을 늘려, 동일한 리소스에 대한 반복 요청 빈도를 줄이는 방법
한계점
이벤트 상품처럼 특정 시점에 업데이트가 필요한 데이터가 최신 상태로 보이지 않는 문제가 발생
사용자가 오래된 데이터를 보고 불만족스러운 경험을 할 가능성이 큼

1.3.2. 분산 락(Distributed Lock)

캐시가 만료된 시점에 여러 클라이언트 요청이 들어오더라도, 단 하나의 요청만 캐시 갱신 작업을 처리하도록 락을 적용
한계점
락을 대기하는 요청의 응답 지연 문제가 발생
실시간성을 요구하는 선물하기 서비스 특성상 적합하지 않음

1.3.3. 캐시 웜업 (Cache Warm Up)

웜업 방식
이벤트 데이터가 만료되기 전에 사전에 배치(batch) 작업을 통해 캐시에 데이터를 미리 로드.
이를 통해, 캐시 만료 시점에도 클라이언트 요청이 데이터베이스나 외부 API로 전달되지 않도록 방지.
선물하기 서비스에서는 1분 주기배치를 최신 상품 정보 반영
효과
빠른 응답 시간 유지
다른 서비스로 트래픽 전파 차단
문제점
캐시 서버 부하
대부분의 데이터를 리모트 캐시에 저장하면서, 캐시 서버 자체가 과부하로 응답 지연 발생 가능
대상 누락
캐시 웜업 대상이 누락될 경우, 여전히 캐시 스탬피드 문제가 재발
모든 데이터를 캐시에 로드하면 메모리와 리소스를 과도하게 사용

1.4. 해결 방안: Hybrid + Auto

Hybrid Cache
부하는 줄이고 응답은 빠르게
Auto Cache Warm Up
웜업 대상 자동 선별 → 캐시 웜업 대상 누락 이슈 해결
캐시 장벽 자동화

2. Hybrid Cache

2.1. 개요

기존의 문제
웜업된 리모트 캐시 서버에 의존 → 부하 증가 → 응답 지연 발생
변화가 낮고, 빈번하게 조회되는 데이터에 로컬 캐시 활용
예) 홈화면의 홈카드, LNB, Navigator 영역의 데이터
개인화 X, 업데이트 빈도 낮음, 데이터 크기 작음, 조회 키 범위 적음
그러나 빈번하게 조회되는 데이터
리모트 캐시 → 로컬 캐시 활용

2.2. 하이브리드 캐시 구조의 구성

2.2.1. 로컬 캐시(Local Cache)

각 서버의 메모리에 자주 조회되는 데이터를 저장
서버 내부에서 데이터를 처리하기 때문에 네트워크 비용이 발생하지 않고, 응답 속도가 매우 빠름

2.2.2. 리모트 캐시(Remote Cache)

중앙화된 캐시 시스템으로, 모든 서버에서 최신 상태의 데이터를 유지
여러 서버 간 데이터 일관성을 보장하고, 캐시 데이터의 중앙 관리 역할을 수행
캐시 웜업의 저장소 형태로 사용 가능
예시

2.3. 하이브리드 캐시 동작 방식

2.3.1. 요청 처리 흐름

사용자의 요청이 들어왔을 때의 요청 처리 흐름
1.
로컬 캐시에서 데이터 조회
요청된 데이터가 로컬 캐시에 존재하면 바로 응답
2.
리모트 캐시에서 데이터 조회
로컬 캐시에 데이터가 없을 경우, 리모트 캐시에서 데이터를 가져와 응답한 뒤 로컬 캐시에 저장
3.
데이터베이스(DB) 및 외부 API 호출
리모트 캐시에도 데이터가 없을 경우, 데이터베이스와 외부 API를 통해 데이터를 조회
조회된 데이터는 리모트 캐시와 로컬 캐시에 모두 저장

2.2.2. 캐시 갱신

로컬 캐시는 주기적으로 리모트 캐시 데이터를 참고하여 업데이트
데이터가 변경되면, 리모트 캐시를 먼저 갱신하고 로컬 캐시가 이를 동기화

2.4. 데이터 일관성 문제 해결

하이브리드 캐시는 여러 서버에서 각각의 로컬 캐시를 사용 → 데이터 변경 시 일관성 문제가 발생
각 서버의 캐시 만료 시간이 다르기 때문에 사용자에게 다른 데이터가 제공될 위험 존재
→ 주키퍼(Zookeeper)를 활용하여 해결

2.4.1. Zookeeper 활용

Zookeeper의 특징
서버간의 동기화를 위한 분산 코디네이션 기능
Znode 기반 트리구조로 데이터 관리
고가용성, 고성능
활용 이유
Watch 기능을 이용한 변경 이벤트 감지
클라이언트에게 쉽게 변경 이벤트 전달 가능
작고 가벼운 이벤트 처리에 사용 용이
기존 인프라 활용하여 운영 관리 비용 낮음

2.4.2. 동작 방식

Zookeeper의 노드는 각 로컬 캐시의 이름에 해당
각 노드는 특정 값을 저장하고 있음
특정한 값은 실제 캐시에 저장하는 값이 아님
단순히 이벤트를 발생시키기 위한 용도로 사용되는 값
변경이 발생했을 때의 타임스탬프를 사용
Zookeeper가 저장하고 있는 값을 변화시켜 각 서버들에게 데이터 변경 이벤트 전달

2.4.3. 조회 흐름 (1)

1.
Admin 서버에서 데이터 서버 발생
특정 데이터에 해당하는 노드의 값을 현재 시간으로 변경
2.
주키퍼는 특정 노드에 저장된 데이터를 변경하고 변경 이벤트를 각 서버로 전달
3.
이벤트를 전달 받은 서버들은 각 인스턴스에 있는 캐시 만료 처리

2.4.4. 조회 흐름 (2)

1.
원본 데이터의 변경 발생
2.
리모트 캐시 데이터 만료 처리
3.
주키퍼를 통해 해당 캐시가 만료되었다는 이벤트 전달
4.
이벤트를 전달 받은 서버들은 각각 로컬 캐시 만료 처리
5.
새로운 요청이 들어왔을 때, 만료된 캐시가 최신 상태의 캐시로 갱신됨
리모트 / 로컬 캐시에 각각 갱신

2.5. Hybrid Cache 결과

13.01% TPS 성능 향상
Remote Cache CPU 사용량 99% 개선

3. Auto Cache Warm Up

캐시 대상이 누락되는 경우 캐시 스탬피드 현상이 다시 발생할 수 있음 → 자동화

3.1. 개요

3.1.1. 리모트 캐시 & 기본적인 캐시 웜업

선물하기 > 프로모션 페이지
상품, 이미지, 텍스트 등으로 구성
실시간으로 변경 가능
로컬 캐시 기반의 하이브리드 캐시 사용 불가
상품 리스팅에서 다양한 상품 노출
평균 1.5MB의 응답 데이터
잦은 GC & 인스턴스 메모리 부하
→ 리모트 캐시 & 기본적인 캐시 웜업 사용

3.1.2. AS IS: 캐시 웜업 대상 수동 등록

활성 상태 프로모션 22,000개
활성 상태 전체 프로모션 대상 캐시 웜업 불가
캐시 웜업시
불필요하게 30GB 사용
오랜 시간 소요
불필요한 자원 사용 방지를 위해 대용량 트래픽 예고된 프로모션 페이지만 수동 등록하여 캐시 웜업
수동 처리 누락으로 인해한 이슈
대용량 트래픽으로 예상하지 못한 프로모션의 캐시 웜업이 안된 경우 → 캐시 스탬피드 문제 발생
캐시 갱신 자동화 필요

3.2. 해결책 (1) PER 알고리즘

3.2.1. PER 알고리즘

캐시 스탬피드 해결로 유명한 PER (Probabilistic Early Recomputation) 알고리즘
캐시 만료 전 일정한 확률로 캐시를 새로 갱신하는 방법
Pseudo Code
value: 응답 데이터
delta: 캐시 갱신까지 걸리는 델타 값
expiry: 만료 시간
function get(key, timeToLive, beta) value, delta, expiry = readCache(key) if !value or nowTime() - delta * beta * log(random(0, 1)) >= expiry then start = nowTime() value = compute(key) # 최신데이터 조회 delta = nowTime() - start # 최신 데이터 조회까지 걸린 캐시 갱신필요한 시간 계산 writeCache(key, (value, delta), timeToLive) # 캐시 최신 데이터로 갱신 end return value end
Lua
복사

3.2.2. 한계점

캐시 갱신 여부를 캐시 만료 전 요청에만 의존
프로모션 페이지는 일정한 캐시 갱신 시간(delta)를 기대하기 어려움
→ 프로모션만의 특성을 담은 캐시 웜업 자동화 방식 필요

3.3. 해결책 (2) HOT Promotion Collector

3.3.1. Auto Cache Warm Up

선물하기의 Hot 프로모션
Steady: 꾸준히 호출되는 프로모션
Spike: 급격하게 트래픽이 증가한 프로모션
Hot 프로모션을 자동 수집 & 주기적으로 캐시 웜업
→ 대용량 트래픽 방어 & 항상 빠른 응답 속도
HOT Promotion Collector를 통해 HOT 프로모션 자동 감지

3.3.2. HOT Promotion Collector 설계

프로모션 Page ID와 최근 호출시간, 누적 호출 횟수 저장
HOT Promotion?
최근 호출 시간이 최근이며 누적 호출횟수가 높은 프로모션

3.3.3. HOT Promotion Collector - Redis

HOT Promotion Collector 기술 스택 선정 기준을 모두 만족
일관성 있는 결과 → 원격 저장소 사용
속도 지연 X → 인메모리 저장소
통계 및 수집 용이 → 자료구조 지원
Sorted Set을 활용하여 최근 호출된 프로모션 리스트 저장
Hash를 활용하여 프로모션 누적 호출 횟수 리스트 저장
Hash의 성능은 O(n) → 키 내부 페이지 수가 많을 수록 성능 저하
→ 누적 호출 횟수를 시간(요일) 별로 설정

3.3.4. HOT Promotion Collector - 가중치

Steady Promotion과 Spike Promotion의 중요도가 다름
Steady
예) 유명 커피 브랜드 / 생일 선물 추천 프로모션
누적 호출이 많은 프로모션 페이지에 가중치
Spike
예) 한정판 KBO x 캐릭터 프로모션 / 복날 맞이 치킨 교환건 프로모션
최근 호출된 프로모션 페이지에 가중치
실시간 호출 횟수가 많은 프로모션에 가중치
최종 스코어 계산 공식
시간별 누적 횟수 스코어
(실시간 누적 횟수 X 실시간 가중치) + (1시간 전 누적 횟수 X 1시간 전 가중치) + …
최종 누적 횟수 스코어
시간별 누적 횟수 스코어 X 누적 횟수 스코어 가중치
최근 호출 시간 최종 스코어
최근 호출 시간 기준 정렬 스코어 X 최근 호출 시간 가중치
HOT 프로모션 최종 스코어
최종 누적 횟수 스코어 + 최종 호출 시간 스코어
totalScore=(hour=0daycounthweighth)weightc+scorerweightr\text{totalScore} = \left( \sum_{hour=0}^{day} \text{count}_h \cdot \text{weight}_h \right) \cdot \text{weight}_c + \text{score}_r \cdot \text{weight}_r
프로모션 ID 별로 스코어 계산하여 특정 수 만큼만 HOT Promotion으로 정의

3.3.5. HOT Promotion Collector - traffic

대용량 트래픽 요청을 대비하여 Collector 분리
1.
사용자 요청이 들어올 경우, 프로모션 서버 각 인스턴스 로컬 스토리지에 id, 누적 호출 횟수 임시 저장
2.
1초, 10초, 30초 같은 특정 시간 마다 HOT Promotion Collector에게 주기적으로 데이터 전달
a.
로컬 스토리지는 초기화
3.
HOT Promotion Collector는 수집하여 재료 데이터를 저장하고, HOT Promotion 계산

3.3.6. HOT Promotion Collector 요약

1.
유저가 프로모션 페이지 요청
2.
서버 로컬 스토리지에 페이지 id와 호출 횟수 임시 저장
3.
최신 상품 상태가 반영된 버전 캐시 조회 → 유저 응답
4.
특정 시간이 지나면 핫 프로모션 수집기에 로컬 스토리지 데이터 전송
5.
핫 프로모션 수집기는 자체 연산을 통해 핫 프로모션 저장

3.4. 캐시 웜업 자동화 배치

1.
특정 시간이 되어 배치 실행
2.
핫 프로모션 리스트 조회
3.
핫 프로모션의 최신 상품 상태를 외부 서비스 API를 통해 조회
4.
캐시에 최신 데이터 갱신

3.5. Auto Cache Warm Up 결과

핫 프로모션 대상 발탁 이후 전반적인 응답 속도 개선
API Call 36.8% 감소
DB Query Call 99.51% 감소

4. 초 단위 Cache Warm Up

4.1. 개요

현재 캐시 갱신 주기는 1분
특정 경우에는 1분이라는 캐시 갱신 주기가 길 수 도 있음 → 초단위 캐시 웜업 도입
예) 중요 프로모션 제품의 판매 상태 정보 업데이트 딜레이 → 캐시 갱신 주기 감소 필요

4.2. RabbitMQ를 활용한 초단위 트리거

4.2.1. 개요

RabbitMQ
300K 개의 메세지 처리 가능한 브로커
Dead Letter Queue System를 이용한 Failover 처리 가능
다양한 exchange type 지원
토픽, 팬아웃, 다이렉트
Dead Letter Queue System 을 활용하여 초단위 트리거 구현

4.2.2. RabbitMQ의 DLQ

1.
Publisher가 TTL 1초로 메세지 발행
2.
바인딩 된 큐로 이동
3.
1초 뒤에 메세지 만료 → DLQ 재발행
4.
바인딩 된 DLQ로 이동
5.
DLQ 구독 컨슈머는 실제 발행 시점 기준 1초 후에 메세지 구독

4.3. 초 단위 Cache Warm Up

4.3.1. Publisher가 분당 60개 메세지 발행

message properties에 TTL 설정
각 메세지는 1초씩 차이나도록 설정
각 메세지는 동시에 큐에 발행

4.3.2. 매 초 마다 DLQ Consumer가 캐시 웜업 수행

첫번째 메세지 (TTL = 1s)가 1초 뒤 DLQ로 이동 → 메세지 구독 → 캐시 웜업 수행
두번째 메세지 (TTL = 2s)가 2초 뒤 DLQ로 이동 → 메세지 구독 → 캐시 웜업 수행
동일한 방식으로 초당 한건씩 DLQ로 이동하여 1초당 메세지 1건 구독하여 캐시 웜업 수행

5. 정리

Hybrid Cache
리모트 캐시 부하 분산
주키퍼를 활용한 동기화
Auto Cache Warm Up
Hot Promotion Collector를 이용하여 캐시 웜업 자동화
초단위 Cache Warm Up
RabbitMQ DLQ를 이용하여 초단위 Cache Warm Up

6. Reference