[Redis]레디스를 캐시로 사용하기

Jeongyeon Kim·2024년 1월 2일
1

Redis

목록 보기
4/7
post-thumbnail

레디스와 캐시


캐시란?

캐시란 데이터의 원본보다 더 빠르고 효율적으로 액세스할 수 있는 임시 데이터 저장소

애플리케이션이 다음 조건을 만족시킨다면 캐시를 도입했을 때 성능을 효과적으로 개선할 수 있음

  • 원본 데이터 저장소에서 원하는 데이터를 찾기 위해 검색하는 시간이 오래 걸리거나, 매번 계산을 통해 데이터를 가져와야 함
  • 캐시에서 데이터를 가져오는 것이 원본 데이터 저장소 데이터를 요청하는 것보다 빨라야 함
  • 캐시에 저장된 데이터는 잘 변하지 않는 데이터
  • 캐시에 저장된 데이터는 자주 검색되는 데이터

위 조건을 만족하는 이상적인 캐시는 애플리케이션이 직면하게 되는 많은 문제점 해결 가능

  • 원본 데이터 저장소 데이터를 가지고 오는 시간을 단축시키기 때문에 애플리케이션의 응답 속도 줄일 수 있음
  • 캐시는 데이터의 복제본을 저장하는 저장소이기 때문에 원본 데이터 저장소에서 데이터를 읽는 커넥션을 줄일 수 있음
  • 캐시를 적절하게 배치함으로써 애플리케이션의 확장 가능
  • 원본 데이터 저장소에서 데이터를 가져올 때 CPU와 메모리 등의 리소스를 많이 사용했다면 캐시를 사용함으로 애플리케이션 자체의 리소스를 줄일 수 있음
    • 같은 값을 도출하기 위해 같은 계산을 할 필요가 없으므로 리소스 최적화
  • 중요한 데이터를 캐시에 올려두고 사용할 때 원본 데이터 저장소에 장애가 발생해 접근할 수 없은 상황이 발생하더라도 캐시에서 데이터를 가지고 올 수 있기 때문에 장애 시간을 줄일 수 있음

캐시로서의 레디스

1. 사용이 간단

  • 키-값 형태로 저장하므로 데이터 저장 및 반환 간단
  • 자체적으로 다양한 자료 구조를 제공해 애플리케이션에서 사용하던 자료 구조 변환 없이 바로 저장

2. 모든 데이터를 메모리에 저장하는 인메모리 데이터 저장소이기 때문에 데이터를 검색하고 반환하는 것이 빠름

  • 평균 읽기 및 쓰기 작업 속도가 1ms 미만, 초당 수백만 건의 작업 가능

3. 자체적으로 고가용성 기능을 가지고 있는 솔루션

  • 일부 캐싱 전략에서는 캐시에 접근할 수 없게 되면 서비스의 장애로 이어짐 ➡️ 캐시 저장소도 일반적인 데이터 저장소와 같이 안정적으로 운영될 수 있는 조건을 갖추는 것이 좋음
  • 레디스의 센티널 또는 클러스터 기능을 사용하면 마스터 노드의 장애를 자동으로 감지해 페일오버를 발생시키기 때문에 운영자의 개입 없이 캐시는 정상으로 유지될 수 있어 가용성 높음

4. 클러스터를 사용하면 캐시의 스케일 아웃 쉽게 처리 가능

  • 자체 샤딩 솔루션인 클러스터를 사용하면 수평 확장이 간단해짐

캐싱 전략

읽기 전략 - look aside


  • 애플리케이션에서 데이터를 읽어갈 때 주로 사용하는 전략
  • 캐시 히트(cache hit): 애플리케이션은 찾고자 하는 데이터가 먼저 캐시에 있는지를 확인한 뒤, 캐시에 데이터가 있으면 캐시에서 데이터를 읽어옴
  • 캐시 미스(cache miss): 찾고자 하는 데이터가 캐시에 없음
  • 레디스에 문제가 생겨 접근을 할 수 없는 상황이 발생하더라도 바로 서비스 장애로 이어지지 않고 데이터베이스에서 데이터를 가지고 올 수 있음
  • 기존에 애플리케이션에서 레디스를 통해 데이터를 가져오는 연결이 매우 많았다면 모든 커넥션에 한꺼번에 원본 데이터베이스로 몰려 많은 부하 발생 ➡️ 원본 데이터베이스의 응답이 느려지거나 리소스를 많이 차지하는 이슈
  • lazy loading: 찾고자 하는 데이터가 레디스에 없을 때에만 레디스에 데이터가 저장됨
  • 기존 사용 중인 서비스에 처음 레디스를 투입하거나 데이터베이스에만 새로운 데이터를 저장하는 경우 매번 캐시 미스가 일어나 성능 저하 ➡️ 캐시 워밍을 통해 해결
    • 캐시 워밍(cache warming): 미리 데이터베이스에서 캐시로 데이터를 밀어넣어주는 작업

쓰기 전략과 캐시의 일관성

캐시에 원본 데이터와 동일한 값을 갖도록 유지하는 것이 필수적!
캐시 불일치(cache inconsitency): 데이터가 변경될 때 원본 데이터베이스에만 업데이트 되어 캐시에는 변경된 값이 반영되지 않아 데이터 간 불일치가 발생한 것

1. write through

  • 데이터베이스에 업데이트할 때마다 매번 캐시에도 데이터를 함께 업데이트 시키는 방식
  • 캐시는 항상 최신 데이터를 가지고 있을 수 있다는 장점
  • 데이터는 매번 2개의 저장소에 저장되어야 하기 때문에 데이터를 쓸 때마다 시간이 많이 소요될 수 있다는 단점
  • 다시 사용되지 않을 데이터가 매번 업데이트 될 수 있기 때문에 데이터를 저장할 때 만료 시간을 사용하는 것이 좋음

2. cache invalidation

  • 데이터베이스에 값을 업데이트할 때마다 캐시에서는 데이터를 삭제하는 전략
  • 저장소에서 특정 데이털르 삭제하는 것이 새로운 데이터를 저장하는 것보다 훨씬 리소스를 적게 사용

3. write behind(write back)

  • 먼저 데이터를 빠르게 접근할 수 있는 캐시에 업데이트한 뒤, 이후에는 건수나 특정 시간 간격 등에 따라 비동기적으로 데이터베이스에 업데이트
  • 저장되는 데이터가 실시간으로 정확한 데이터가 아니어도 되는 경우에 사용

캐시에서의 데이터 흐름

캐시는 가득 차지 않게 일정 양의 데이터를 유지해야 하며 계속해서 새로운 데이터가 저장되고 기존 데이터는 삭제될 수 있도록 관리해야 함 ➡️ 적절한 시간의 TTL을 지정하는 것이 좋음


만료 시간

  • 초(second) 단위로 표현
  • 키에 만료 시간을 설정하면 데이터의 수명을 관리하고 메모리 공간을 효율적으로 사용하는 데 도움을 줌
  • 만료 시간 설정
    • EXPIRE 커맨드: 만료 시간 설정(초 단위로 동작)
    • SET 커맨드의 EX 옵션: 데이터의 저장과 동시에 만료 시간을 설정
    • TTL 커맨드: 만료 시간 확인(초 단위로 동작)
    • PTTL, PEXPIRE 커맨드는 밀리세컨드 단위로 동작
    • INCR 커맨드로 데이터를 조작하거나 RENAME을 이용해 키의 이름을 바꾸더라도 설정된 만료 시간은 변경되지 않음
    • 기존 키에 새로운 값을 저장해 키를 덮어 쓸 때에는 이전에 설정한 만료 시간은 유지되지 않고 사라짐

레디스에서 키가 만료되었다고 해도 바로 메모리에서 삭제되는 것은 아님.
만료된 키를 곧바로 삭제하지 않기 때문에 키를 삭제하는 데 들어가는 리소스를 줄일 수 있지만, 그 만큼 메모리를 더 사용할 가능성 존재.

  • passive 방식
    • 클라이언트가 키에 접근하고자 할 때 키가 만료되었다면 메모리에서 수동적으로 삭제
  • active 방식
    • TTL 값이 있는 키 중 20개를 랜덤하게 뽑아낸 뒤, 만료된 키를 모두 메모리에서 삭제
    • 만약 25% 이상의 키가 삭제되었다면 다시 20개의 키를 랜덤하게 뽑은 뒤 확인하고, 아니라면 뽑아놓은 20개의 키 집합에서 다시 확인

메모리 관리와 maxmemory-policy 설정

Noeviction

  • 기본값
  • 레디스에 데이터가 가득 차더라도 임의로 데이터를 삭제하지 않고 더 이상 레디스에 데이터를 저장할 수 없다는 에러 반환
  • 캐시에 데이터를 저장하지 못해 에러가 발생할 경우 관리자가 데이터를 직접 지워야 함
  • 데이터의 관리를 캐시에 맡기지 않고, 애플리케이션 측에서 관리

LRU(Least-Recently Used) eviction

  • 레디스에 데이터가 가득 찼을 때 가장 최근에 사용되지 않은 데이터부터 삭제하는 정책
  • 최근에 액세스 되지 않은 데이터는 나중에도 액세스 될 가능성이 낮을 것이라는 가정을 전제
  • 근사 알고리즘 이용
  • volatile-lru
    • 만료 시간이 설정되어 있는 키에 한해서 LRU 방식으로 키를 삭제
    • 만약 레디스 내부에 저장된 키에 모두 만료 시간이 지정되어 있지 않다면 noeviction 상황과 동일
  • allkeys-lru
    • 모든 키에 대해 LRU 알고리즘을 이용해 데이터를 삭제하기 때문에 메모리가 꽉 차있을 때 장애가 발생할 상황 방지

LFU(Least-Frequently Used) eviction

  • 레디스에 데이터가 가득 찼을 때 가장 자주 사용되지 않은 데이터부터 삭제하는 정책
  • 자주 사용되지 않은 데이터는 나중에도 액세스될 가능성이 낮을 것이라는 가정 전제
  • 근사 알고리즘 이용
  • volatile-lfu
    • 만료 시간이 설정되어 있는 키에 한해서 LFU 방식으로 키를 삭제
  • allkeys-lfu
    • 모든 키에 대해 LFU 알고리즘을 이용해 데이터 삭제

RANDOM eviction

  • 레디스에 저장된 키 중 하나를 임의로 골라내 삭제
  • 삭제될 키 값을 계산하지 않아도 된다는 점에서 부하를 줄여줌
  • 랜덤으로 데이터를 삭제하기 때문에 나중에 사용될 수도 있는 데이터를 삭제할 가능성이 높아짐 ➡️ 데이터 저장소에서 다시 데이터를 가지고 와서 캐시에 넣어주는 작업 불필요
  • 굳이 레디스의 부하를 줄이기 위한다는 이유로 사용하는 것을 권장하지 않음
  • volatile-random: 만료 시간이 설정되어 있는 키에 한해 랜덤하게 키 삭제
  • allkeys-random: 모든 키에 대해 랜덤하게 키 삭제

volatile-ttl

  • 만료 시간이 가장 작은 키 삭제
  • 근사 알고리즘 이용

캐시 스탬피드 현상

캐시 스탬피드(cache stampede): 캐시 공간은 한정되어 있으므로 저장된 데이터에 만료 시간을 설정하는데, 해당 데이터에 계속 읽기 요청이 들어오고 있을 때 캐시 만료시간이 닥치면 순간적으로 데이터베이스에 그 읽기 요청이 집중되고 그게 다시 레디스에 중복된 쓰기 요청으로 몰리게 됨

  • 중복 읽기(duplicate read): 여러 애플리케이션에서 바라보던 키가 만료되어 삭제된다면 이 서버들은 한꺼번에 데이터베이스에 가서 데이터를 읽어옴
  • 중복 쓰기(duplicate write): 중복 읽기 이후 각 애플리케이션에서 읽어온 데이터를 레디스에 쓰게 되면 여러 번 반복해서 쓰게 됨

한번 캐시 스탬피드 현상이 발생하면 결과적으로 더 많은 데이터가 이 현상의 영향을 받게 됨 ➡️ 계단식 실패(cascading failure)

계단식 실패를 줄이기 위한 방법

1. 적절한 만료 시간 설정

  • 여러 애플리케이션에서 한꺼번에 접근해야 하는 데이터이며, 반복적으로 사용되어야 하는 데이터라면 저장 시점부터 만료 시간을 충분히 길게 설정

2. 선 계산

  • 키가 실제로 만료되기 전에 이 값을 미리 갱신해준다면 여러 애플리케이션에서 한꺼번에 데이터베이스에 접근해 데이터를 읽어오는 과정을 줄여 불필요한 프로세스 줄임

3. PER 알고리즘

  • PER(Probabilistic Early Recomputation) 알고리즘을 이용하면 캐시 값이 만료되기 전에 언제 데이터베이스에 접근해서 값을 읽어오면 되는지 최적으로 계산할 수 있음
  • currentTime - ( timeToCompute * beta * log(rand()) ) > expiry
    • currentTime: 현재 남은 만료 시간
    • timeToCompute: 캐시된 값을 다시 계산하는 데 걸리는 시간
    • beta: 기본적으로 1.0 보다 큰 값으로 설정 가능
    • rand(): 0과 1 사이의 랜덤 값을 반환하는 함수
    • expiry: 키를 재설정할 때 새로 넣어줄 만료 시간
    • currentTime에서 timeToCompute x beta x log(rand())를 빼서 얻은 값이 expiry 보다 크면 조건은 거짓(False)
    • currentTime에서 timeToCompute x beta x log(rand())를 빼서 얻은 값이 expiry 보다 작으면 조건은 참(True)
      • 데이터를 다시 계산하기 위해 데이터베이스로 이동
      • 만료 시간이 가까워질수록 currentTime과 expiry 사이의 차이가 작아지며, rand() 함수가 반환한 무작위 값에 의존하기 때문에 조건이 참이 될 확률 높아짐
    • 데이터를 가져오는 과정에서 GET 대신 이 함수를 사용하는 것은 캐시 스탬피드 현상을 줄이고 성능을 최적화하는데 도움

세션 스토어로서의 레디스


세션이란?

세션(session): 서비스를 사용하는 클라이언트의 상태 정보

세션 스토어가 필요한 이유

  • sticky session: 특정 웹 서버에 유저가 몰려 트래픽이 집중되는 상황이 발생하더라도 유저는 다른 서버를 사용할 수 없어, 결국 트래픽을 분산시킬 수 없는 상황
  • all-to-all 방법: 유저의 세션 정보를 모든 웹 서버에 복제해서 저장하는 방법
    • 유저를 여러 웹 서버에 분산시킬 수 있지만, 유저의 세션 데이터를 여러 서버로 복사되어 저장되기 때문에 불필요한 저장 공간 차지
    • 하나의 유저는 한 번에 하나의 웹 서버에만 접속하기 때문에 다른 웹 서버에 저장된 유저의 세션 정보는 무의미
    • 데이터를 복제하는 과정에서 불필요한 네트워크 트래픽 다수 발생
  • 데이터베이스를 세션 스토어로 이용
    • 세션 스토어의 응답 속도가 느려지면 클라이언트의 응답 속도 저하로 이어짐
  • 레디스를 세션 스토어로 이용
    • 레디스를 세션 스토어로 사용하고 서버, 데이터베이스와 분리시켜 놓은 뒤 여러 서버에서 세션 스토어를 바라보도록 구성
    • 유저는 세션 스토어에 구애받지 않고 어떤 웹 서버에 연결되더라도 동일한 세션 데이터 조회 가능 ➡️ 트래픽 효율적 분산, 데이터의 일관성 고려할 필요X
    • 레디스는RDBMS보다 훨씬 빠르고 접근하기 간편하므로 데이터를 가볍게 저장할 수 있음
    • 레디스의 hash 자료 구조는 세션 데이터 저장에 알맞음

캐시와 세션의 차이

  • 캐시에 저장된 데이터는 여러 애플리케이션에서 함께 사용
  • 세션 스토어에 저장된 데이터는 여러 사용자 간 공유되지 않으며, 특정 사용자 ID에 한해 유효


profile
Backend Developer👩🏻‍💻

0개의 댓글