Post

Redis 캐시 전략: 읽기/쓰기 패턴과 메모리 관리

캐시 도입 효과와 전략, 데이터 만료와 삭제 정책, 캐시 스탬피드 해결 방안

Redis 캐시 전략: 읽기/쓰기 패턴과 메모리 관리

1. 레디스와 캐시

1-1. 캐시란?

  • 캐시 도입 시 성능을 개선할 수 있는 애플리케이션 조건
    • 원본 데이터 저장소에서 원하는 데이터를 찾기 위해 검색하는 시간이 오래 걸리거나, 매번 계산을 통해 데이터를 가져와야 한다
    • 캐시에서 데이터를 가져오는 것이 원본 데이터 저장소 데이터를 요청하는 것보다 빨라야 한다
    • 캐시에 저장된 데이터는 잘 변하지 않는 데이터이다
    • 캐시에 저장된 데이터는 자주 검색되는 데이터이다
  • 캐시는 데이터의 복제본을 저장하는 저장소이기 때문에 원본 데이터 저장소에서 데이터를 읽는 커넥션을 줄일 수 있다



2.캐싱 전략

2-1. 읽기 전략 - look aside

  • 캐시 히트
    • 애플리케이션은 찾고자 하는 데이터가 먼저 캐시에 있는지를 확인한 뒤, 캐시에 데이터가 있으면 캐시에서 데이터를 읽어온다
    • 레디스로부터 데이터가 없다는 응답을 받은 애플리케이션은 직접 데이터베이스에 접근해 찾고자 하는 데이터를 가져온다. 그 뒤 애플리케이션은 이를 다시 캐시에 저장한다
  • 캐시 미스
    • 찾고자 하는 데이터가 없는 경우
    • look aside 구조의 장점은 레디스에 문제가 생겨 접근할 수 없는 상황이 발생하더라도 바로 서비스 장애로 이어지지 않고 데이터베이스에서 데이터를 가지고 올 수 있다
  • 캐시 워밍
    • 기존에 운영중인 서비스에 처음 레디스를 투입하거나, 데이터베이스에만 새로운 데이터를 저장하는 등의 경우 캐시 미스가 발생하는 것을 방지하기 위해 미리 데이터베이스에서 캐시로 데이터를 밀어넣어주는 작업

2-2. 쓰기 전략과 캐시의 일관성

  • 캐시 불일치
    • 데이터가 변경될 때 원본 데이터베이스에만 업데이트돼 캐시에는 변경된 값이 반영되지 않아 데이터 간 불일치가 일어나는 경우
  • write through
    • write through 방식은 데이터베이스에 업데이트할 때마다 매번 캐시에도 데이터를 함께 업데이트 하는 방식
    • 캐시는 항상 최신 데이터를 가진다는 장점이 있지만, 데이터는 매번 2개의 저장소에 저장돼야 하기 때문에 데이터를 쓸 때마다 시간이 많이 소요될 수 있다
    • 다시 사용되지 않을 데이터일 수도 있는데, 무조건 캐시에도 저장되기 때문에 리소스 낭비가 발생할 수 있다. 따라서 이 방식을 사용하는 경우 데이터 저장 시 만료 시간 설정을 권장
  • cache invalidation
    • cache invalidation은 데이터베이스에 값을 업데이트할 때마다 캐시에서 데이터를 삭제하는 전략
    • 저장소에서 특정 데이터를 삭제하는 것이 새로운 데이터를 저장하는 것보다 리소스를 적게 사용하기 때문에 write-through의 단점을 보완할 수 있다
  • write behind(write back)
    • 쓰기가 빈번하게 발생하는 서비스라면 write behind 방식을 고려할 수 있다
    • 데이터베이스에 대량의 쓰기 작업이 발생하면 많은 디스크 I/O를 유발해 성능 저하가 발생할 수 있다
    • 따라서 먼저 데이터를 빠르게 접근할 수 있는 캐시에 업데이트한 뒤, 이후에는 건수나 특정 시간 간격 등에 따라 비동기적으로 데이터베이스에 업데이트
    • 저장되는 데이터가 실시간으로 정확한 데이터가 아니어도 되는 경우 유용한 방법



3. 캐시에서의 데이터 흐름

3-1. 만료 시간

  • 레디스에서 만료 시간(TTL)은 데이터가 얼마나 오래 저장될 것인지를 나타내는 시간 설정
  • 특정 키에 대한 만료 시간을 설정할 수 있으며, 데이터의 유효 기간 또는 만료 시간을 정의
  • EXPIRE 커맨드
    • 저장된 키에 만료 시간 설정
  • SET 커맨드
    • EX 옵션을 함께 사용해 데이터의 저장과 동시에 만료 시간을 설정할 수도 있다
  • TTL 커맨드
    • 키에 대한 만료 시간 확인
    • 만료 시간이 남아 있다면 남은 시간을 반환, 키가 존재하지 않을 때는 -2, 키에 대해 만료 시간이 지정되지 않았을 때는 -1
  • TTL과 EXPIRE 커맨드는 초 단위로 동작하며, PTTL와 PEXPIRE 커맨드는 밀리세컨드 단위로 동작
  • 레디스에서 키가 만료됐다고 해도 바로 메모리에서 삭제되는 건 아니다. 키는 passive 방식과 active 방식 두 가지로 삭제된다
    • passive 방식
      • 클라이언트가 키에 접근하고자 할 때 키가 만료됐다면 메모리에서 수동적으로 삭제
      • 사용자가 접근할 때만 수동적으로 삭제하기 때문에 이를 passive 방식 만료라고 한다
      • 그러나 사용자가 다시 접근하지 않는 만료된 키도 있어 이 방식만으로는 충분하지 않다
    • active 방식
      • TTL 값이 있는 키 중 20개를 랜덤하게 뽑아낸 뒤, 만료된 키를 모두 메모리에서 삭제
      • 만약 25% 이상의 키가 삭제됐다면 다시 20개의 키를 랜덤하게 뽑은 뒤 확인하고, 아니라면 뽑아놓은 20개의 키 집합에서 다시 확인
      • 이러한 과정을 1초에 10번씩 수행

3-2. 메모리 관리와 maxmemory-policy 설정

  • 메모리 용량을 초과하는 양의 데이터가 저장되면 레디스는 내부 정책을 사용해 어떤 키를 삭제할지 결정
  • 레디스에서는 데이터의 최대 저장 용량을 설정하는 maxmemory 설정과 이 용량을 초과할 때의 처리 방식을 결정하는 maxmemory-policy 설정값을 사용해 메모리를 관리한다
  • Noeviction
    • maxmemory-policy의 기본 설정값
    • 레디스에 데이터가 가득 차더라도 임의로 데이터를 삭제하지 않고 더 이상 레디스에 데이터를 저장할 수 없다는 에러를 반환
      • 데이터의 관리를 캐시에게 맡기지 않고, 애플리케이션 측에서 관리함을 의미
      • 데이터가 가득 차더라도 캐시 내부적 판단으로 데이터를 삭제하는 것이 위험하다고 판단될 때 이 옵션을 사용할 수 있다
    • 하지만 캐시에 데이터를 저장하지 못해 에러가 발생한 경우 로직에 따라 장애 상황으로 이어질 수 있으며, 이런 상황에서는 관리자가 레디스의 데이터를 직접 지워야 하기 때문에 레디스를 캐시로 사용할 때 권장하지 않는 설정값
  • LRU eviction
    • LRU(Least-Recently Used) eviction이란 레디스에 데이터가 가득 찼을 때 가장 최근에 사용되지 않은 데이터부터 삭제하는 정책
    • 최근에 엑세스되지 않은 데이터는 나중에도 엑세스될 가능성이 낮을 것이라는 가정을 전제
    • 레디스는 LRU 알고리듬을 이용한 두 가지 설정값을 가짐
      • volatile-lru
        • 만료 시간이 설정돼 있는 키에 한해서 LRU 방식으로 키를 삭제
        • 만약 레디스를 사용할 때 임의적인 방식으로 삭제되면 안 되는 값에 대해서는 만료 시간을 지정하지 않는다면 volatile-lru 방식을 사용하는 것이 적합
      • allkeys-LRU
        • 모든 키에 대해 LRU 알고리듬을 이용해 데이터를 삭제하기 때문에 적어도 메모리가 꽉 찼을 때 장애가 발생할 상황은 방지할 수 있다
  • LFU eviction
    • LFU(Least-Frequently Used) eviction이란 레디스에 데이터가 가득 찼을 때 가장 자주 사용되지 않은 데이터부터 삭제하는 정책
    • LFU는 LRU와 유사하지만 키를 엑세스하는 패턴에 따라 우선순위가 유동적으로 바뀐다는 점에서 특정 케이스에서는 LRU보다 더 효율적일 수 있다
    • LFU 역시 두 가지 설정값을 가짐
      • volatile-lfu
        • 만료 시간이 설정돼 있는 키에 한해서 LFU 방식으로 키를 삭제
      • allkeys-lfu
        • 모든 키에 대해 LFU 알고리듬을 이용해 데이터를 삭제
    • 레디스에서 키를 삭제하기 위해 사용되는 LRU와 LFU 알고리듬은 모두 근사 알고리듬으로 구현됐다. 정확한 키를 계산하는 것보다 특정 키를 근사치로 찾아내 효율적으로 데이터를 삭제하는 방식으로 동작
  • RANDOM eviction
    • 레디스에 저장된 키 중 하나를 임의로 골라서 삭제
    • 이 방법은 랜덤으로 데이터를 삭제하기 때문에 나중에 사용될 수도 있는 데이터를 삭제할 가능성이 높다. 이럴 경우 데이터 저장소에서 다시 데이터를 갖고 와서 캐시에 넣어주는 작업이 오히려 불필요함을 유발
    • 또한 LRU, LFU의 경우에도 근사 알고리즘을 사용하기 때문에 큰 리소스랄 사용하지 않는다.
    • 따라서 굳이 레디스의 부하를 줄인다는 이유로 random eviction을 사용하는 것은 권장하지 않는다
  • volatile-ttl
    • volatile-ttl 방식은 만료 시간이 가장 작은 키를 삭제
    • 즉, 삭제 예정 시간이 얼마 남지 않은 키를 추출해 해당 키를 미리 삭제하는 옵션
    • 근사 알고리듬을 이용해 만료 시간을 비교한다

3-3. 캐시 스탬피드 현상

  • 캐시 스탬피드
    • 여러 애플리케이션에서 동시에 만료된 캐시 데이터를 요청할 때 발생하는 현상
    • 중복 읽기와 중복 쓰기가 일어나며 데이터 소스에 과부하 발생
  • 적절한 만료 시간 설정
    • 캐시 스탬피드를 줄이기 위한 가장 간단한 방법은 만료 시간을 너무 짧지 않게 설정하는 것
    • 여러 애플리케이션에서 한꺼번에 접근해야 하는 데이터이며, 반복적으로 사용해야 한다면 저장 시점부터 만료 시간을 충분히 길게 설정하는 것이 좋다
  • 선 계산
    • 캐시 스탬피드가 문제되는 이유는 데이터가 만료되는 시점에 여러 애플리케이션에서 동시다발적으로 이를 인지하고, 이후 작업을 동시에 진행하기 때문
    • 키가 실제로 만료되기 전에 이 값을 미리 갱신해준다면 여러 애플리케이션에서 한꺼번에 데이터베이스에 접근해 데이터를 읽어오는 과정을 줄여 불필요한 프로세스를 줄일 수 있다
  • PER 알고리듬
    • 캐시 스탬피드 현상을 완화시킬 수 있는 확률적 조기 재계산 알고리듬
    • PER(Probabilistic Early Recomputation) 알고리듬을 사용하면 캐시 값이 만료되기 전에 언제 데이터베이스에 접근해서 값을 읽어오면 되는지 최적으로 계산할 수 있다
    • currentTime - ( timeToCompute * beta * log(rand()) ) > expiry
      • currentTime: 현재 남은 만료 시간
      • timeTOCompute: 캐시된 값을 다시 계산하는 데 걸리는 시간
      • beta: default 1로 설정. 0보다 큰 값으로 설정 가능
      • rand(): 0과 1 사이의 랜덤 값을 반환하는 함수
      • expiry: 키를 재설정할 때 새로 넣어줄 만료 시간
    • 만료 시간이 가까워질수록 currentTime와 expire 사이의 차이가 작아지며, rand() 함수가 반환한 무작위 값에 의존하기 때문에 조건이 참이 될 확률이 높아진다
    • 만료 시간이 점점 다가올 때 더 자주 만료된 캐시 항목을 확인하는 것을 의미
This post is licensed under CC BY 4.0 by the author.