반응형

캐시는 다음과 같은 이유로 주로 사용된다.

 

  • 성능 개선: 자주 요청되는 데이터를 빠르게 제공하여 응답 속도를 높임.
  • 부하 감소: 데이터베이스, API 서버 등 원본 소스에 대한 요청을 줄여 리소스 절약.
  • 비용 최적화: 외부 API 호출이나 고비용 데이터 처리 작업을 줄임.
  • 안정성 강화: 원본 데이터 소스 장애 시 캐시 데이터를 활용해 서비스 지속 가능.
  • 복잡한 연산 제거: 고비용 연산 결과를 캐싱하여 반복 작업 최소화.
  • 사용자 경험 향상: 빠르고 개인화된 데이터를 제공해 UX 개선.
  • 분산 시스템 효율성: 로컬 캐시와 CDN을 통해 네트워크 지연 감소.

 

따라서 캐시와 관련된 문제들은 애플리케이션 성능에 영향을 미칠 수 있다. 어떠한 문제들이 있을 수 있을까.

 

1. 캐시 미스(Cache Miss)

  • 정의: 애플리케이션이 필요한 데이터가 캐시에 없는 상황
  • 유형:
    • Cold Miss: 캐시가 초기화되거나 처음 사용될 때 발생하는 미스. 캐시에 아무 데이터가 없어서 모든 요청이 미스 처리
    • Capacity Miss: 캐시의 용량이 부족해 더 이상 데이터를 저장할 수 없는 상황에서 기존 데이터가 삭제되면서 발생하는 미스.
    • Conflict Miss: 특정 데이터가 저장될 수 있는 캐시 공간이 제한되어 있을 때 발생하는 미스입니다. 캐시에 이미 동일한 위치에 저장된 데이터가 있을 때 교체되면서 발생
  • 해결 방안: 캐시 크기 조절(키워서 미스 안 나게), 효율적인 캐싱 전략 설정, LRU(Least Recently Used)나 LFU(Least Frequently Used) 같은 캐시 교체 알고리즘 도입 등

2. 캐시 스탬피드(Cache Stampede)

  • 정의: 캐시가 만료된 경우 다수의 요청이 동시에 캐시 미스를 일으켜 데이터베이스나 원본 서버에 과부하를 유발
  • 예시: 특정 데이터의 캐시가 만료되었을 때, 많은 사용자나 프로세스가 동시에 캐시 된 데이터를 요청하며 원본 서버에 큰 부하가 걸림
  • 해결 방안:
    • 락(Locking): 캐시가 만료되었을 때 첫 번째 요청자만 원본 데이터에 접근하도록 락을 걸어 다른 요청자가 대기하도록.. 대기하고 나서는 캐시에서 가져가게
    • 백오프(Backoff): 캐시 업데이트를 요청하는 다수의 주체들이 동일한 시점에 갱신 요청을 보내는 것을 방지하기 위해, 재시도 전에 일정한 대기 시간을 설정하는 방식
      • API 호출 실패 후 1초 → 2초 → 4초로 대기 시간을 증가시키며 재요청.
      • 캐시가 만료된 데이터를 가져오기 위해 여러 클라이언트가 요청을 보낼 때, 요청 간의 간격을 두어 스탬피드를 방지.
    • 예상 만료 처리(Early Expiration): 캐시의 데이터가 만료되기 전에 미리 갱신 작업을 수행하여 캐시 미스 상황을 방지하는 방식
      • TTL(Time To Live) 설정 시, 실제 만료 시간(T)보다 짧은 시간(T-E)으로 갱신 작업을 예약.
      • 백그라운드 작업을 통해 갱신 작업 수행.
    • 탄력적 TTL: 캐시의 TTL을 랜덤하게 설정하여 여러 캐시 항목이 동시에 만료되지 않도록 조정(TTL skew)

3. 캐시 독(Cache Thundering Herd)

  • 정의: 특정 캐시 키에 대한 동시 접근이 일어나거나 캐시가 갱신되어야 할 때 한 번에 모든 스레드가 동일한 데이터를 요청하는 문제입니다. 캐시 스탬피드와 유사하지만 주로 특정 데이터에 집중된 대량 요청
  • 문제상황
    • 백엔드 부하 증가: 모든 요청이 원본 소스(DB, API 등)에 쏠려 장애를 유발.
    • 성능 저하: 응답 속도가 느려지고, 전체 시스템 성능이 저하.
    • 서비스 가용성 위협: 심한 경우 서비스 중단으로 이어질 수 있음.
  • 해결 방안: 분산 락, 지연 갱신(Lazy Update) 같은 접근을 통해 특정 리소스에 동시 접근하는 것을 방지

 

위와 같이 여러 해결책이 있지만 대체적으로 즉각적인 해결책이 되지 못한다.(응답 지연의 가능성)

캐시 웜업(Cache Warming)

캐시 웜업은 애플리케이션이나 서비스가 시작되거나 재시작될 때, 캐시를 미리 채워두는 작업. 이를 통해 서비스 초기 상태에서 캐시 미스(Cache Miss)로 인한 성능 저하를 방지하고, 원본 데이터 소스(DB, API 등)에 대한 부하를 줄일 수 있다.

캐시 웜업의 필요성

 

  • 초기 캐시 미스 문제 방지 -> 초기 성능 향상
    • 애플리케이션이 시작된 직후 캐시가 비어 있는 상태에서는 모든 요청이 원본 데이터 소스로 전달됨.
    • 갑작스러운 트래픽 증가로 원본 소스(DB/API)가 과부하 상태에 빠질 수 있음.
  • 서비스 안정성 향상, 사용자 경험 개선
    • 캐시 웜업으로 자주 사용되는 데이터를 미리 준비해 두면 초기 요청 처리 속도가 빨라지고, 사용자 경험이 개선됨.
  • 트래픽 분산
    • 캐시가 미리 채워져 있으면 요청이 분산되므로, 백엔드 부하가 줄어듦.

 

캐시 웜업의 일반적인 방법

  1. 프리로드(Preloading): 시스템 초기화나 캐시 시작 시점에 미리 정해진 데이터 집합을 캐시에 로드. 예를 들어, 자주 요청되는 데이터나 중요한 설정값을 미리 캐시에 넣어둔다.
  2. 배치 처리: 배치 작업을 통해 일정 시간 간격으로 특정 데이터를 캐시에 로드하여 캐시가 항상 최신 상태를 유지하도록 한다.
    • @Scheduled(fixedDelay = 300000) 혹은 별도의 배치 프로그램 
    • 만료 전 갱신으로 캐시만을 사용하여 응답하게 끔하여 트래픽 전이 되지 않고 응답 지연 없게 처리; 1분 주기 배치
      • 캐시 웜업 대상 누락 시 cache stampede 발생!
      • 캐시 웜업 대상 조회의 자동화 필요
  3. API 또는 관리자 도구 활용: 관리자 페이지나 별도의 API를 통해 수동으로 캐시를 웜업
  4. 온디맨드 웜업(On-Demand Warming): 실제 사용자가 특정 데이터를 요청하기 직전에, 예측 분석을 통해 해당 데이터가 자주 요청될 것으로 판단될 경우 이를 캐시에 미리 로드한다.
    1. 데이터 분석 시 "호출 횟수(view count) / 최근 호출 시간" 통계화 고려(날짜-id-view)
    2. 로컬 디비에 관련 데이터 쌓고(레디스 부하 발생 가능)
    3. 주기적으로 레디스에 올려
    4. 관련하여 레디스 자료구조 활용(분산 자료구조인 hash)
    5. 레디스에 올리고 로컬 삭제
    6. 해당 데이터 기반으로 캐시를 갱신한다

프리로드 예시(ApplicationRunner 이용)

@Component
public class CacheWarmupRunner implements ApplicationRunner {
    @Autowired
    private CacheService cacheService;
    @Autowired
    private DataService dataService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<Data> dataList = dataService.getFrequentlyAccessedData();
        dataList.forEach(data -> cacheService.put(data.getKey(), data));
    }
}

캐시 웜업의 장단점

  • 장점: 캐시 미스로 인한 부하를 줄이고, 사용자에게 보다 빠른 응답 시간을 제공
  • 단점: 데이터 신선도 이, 필요성이 낮은 데이터의 경우 불필요한 리소스 사용이 될 수 있음

 

하이브리드 캐시

: 로컬 캐시 + 리모트 캐시

  • 로컬 캐시로: 빈번하게 조회되고/공통 데이터/데이터 크기가 작고/업데이트 빈도가 낮은 데이터
  • 리모트 캐시: 최신 상태 데이터/중앙 관리(마스터 캐시)
  • 디비 -> 리모트 -> 로컬 순으로 갱신

주의사항

  • 로컬 캐시마다 TTL이 다 다르기 때문에 데이터 일관성을 유지하기 힘듦
  • 데이터 변경 시 누가 old로 내리는지 찾기 힘듦
  • 즉시 반영 시에는 별도의 만료 프로세스 필요
    • 주키퍼 활용해서 데이터 변경 시 이벤트 발행하여 동기화 가능
    • 변경 이벤트 ->리모트 캐시 만료 처리 -> 로컬 캐시 만료 처리

 

참고

초단위 캐시 웜업(준 실시간)

로컬 캐시 TTL 다 달라.. 초단위 웜업 시 rabbit 의 DLQ 활용 가능

1~60초 각각의 만료시간을 가진 메세지 발행

https://youtu.be/BUV4A2F9i7w?si=eVdHEBzJ3h8bZOIu


캐시 갱신 정책

캐시와 원본 데이터 간의 동기화 방법.

  1. Write-Through
    • 캐시에 쓰는 동시에 원본 데이터도 즉시 갱신.
    • 장점: 데이터 일관성 보장.
    • 단점: 쓰기 성능 저하.
  2. Write-Behind (Write-Back)
    • 캐시에 먼저 쓰고 나중에 원본 데이터 갱신.
    • 장점: 쓰기 성능 향상.
    • 단점: 데이터 손실 가능성.
  3. Read-Through
    • 캐시에 없을 때 원본 데이터를 읽고 캐시에 저장.
    • 장점: 자동 갱신.
    • 단점: 초기 요청 지연.
  4. Manual Update
    • 명시적으로 캐시를 갱신.
    • 장점: 제어 용이.
    • 단점: 관리 복잡.

 

캐시 만료 정책

캐시 데이터의 유효 기간을 설정하는 방법.

  1. TTL (Time-To-Live)
    • 데이터가 캐시에 저장된 후 유지 시간 설정.
    • 예: 30초 후 만료.
  2. TTI (Time-To-Idle)
    • 마지막 접근 후 일정 시간 지나면 만료.
    • 예: 10분 동안 미사용 시 삭제.
  3. Fixed Expiry
    • 특정 시간에 모든 데이터 만료.
    • 예: 매일 자정 초기화.
  4. Forever Cache
    • 만료 없이 지속적으로 유지.
    • 데이터 변경이 거의 없을 때 사용.
728x90
반응형

'architecture > micro service' 카테고리의 다른 글

[Dead Letter] PDL, CDL  (0) 2024.11.14
E2E(end to end) 테스트  (0) 2024.11.13
대용량 데이터 처리 고민  (1) 2024.11.10
transaction outbox pattern + polling publisher pattern  (0) 2024.11.07
2PC vs 2PL  (1) 2024.11.06

+ Recent posts