본문 바로가기

Server/Redis

[개발자를 위한 레디스] 5장 레디스를 캐시로 사용하기

반응형
SMALL

레디스와 캐시

캐시란?

캐시란?

  • 캐시란 데이터의 원본보다 더 빠르고 효율적으로 액세스할 수 있는 임시 데이터 저장소를 의미한다.
  • 사용자가 동일한 정보를 반복적으로 액세스할 때 원본이 아니라 캐시에서 데이터를 가지고 옴으로써 리소스를 줄일 수 있다.
  • 애플리케이션이 다음 조건을 만족시킨다면, 캐시를 도입했을 때 성능을 효과적으로 개선할 수 있다.
    • 원본 데이터 저장소에서 원하는 데이터를 찾기 위해 검색하는 시간이 오래 걸리거나, 매번 계산을 통해 데이터를 가져와야 한다.
    • 캐시에서 데이터를 가져오는 것이 원본 데이터 저장소 데이터를 요청하는 것보다 빨라야 한다.
    • 캐시에 저장된 데이터는 잘 변하지 않는 데이터다.
    • 캐시에 저장된 데이터는 자주 검색되는 데이터다.

 


캐시로서의 레디스

  • 레디스는 단순하게 키-값 형태로 저장하므로, 데이터를 저장하고 반환하는 것이 간단하다.
  • 자체적으로 다양한 자료 구조를 제공하기 때문에 애플리케이션에서 사용하던 list, hash 등의 자료 구조를 변환하는 과정 없이 레디스에 바로 저장할 수 있다.
  • 모든 데이터를 메모리에 저장하는 인메모리 데이터 저장소이기 때문에 데이터를 검색하고 반환하는 것이 상당히 빠르다.
  • 자체적으로 고가용성 기능을 가지고 있다.
  • 레디스의 클러스터를 사용하면 캐시의 스케일 아웃을 쉽게 처리할 수 있다.

 


캐싱 전략

레디스를 캐시로 사용할 때 레디스를 어떻게 배치할 것인지에 따라 서비스의 성능에 큰 영향을 끼칠 수 있다.

 

읽기 전략 - look aside

  • 레디스를 캐시로 사용할 때 가장 일반적으로 배치하는 방법이다.
  • 👍 레디스에 문제가 생기더라도 바로 서비스 장애로 이어지지 않고 데이터베이스에서 데이터를 가지고 올 수 있다.
  • 👎 기존 애플리케이션에서 레디스를 통해 데이터를 가져오는 연결이 매우 많았다면 모든 커넥션이 한번에 원본 데이터베이스로 몰려 많은 부하를 발생시킬 수 있다.
  • 찾고자 하는 데이터가 레디스에 없을 때에만 레디스에 데이터가 저장되기 때문에 lazy loading이라고도 부른다.

 

look aside 전략 (1)

  • 애플리케이션은 찾는 데이터가 먼저 캐시에 있는지를 확인한 뒤, 캐시에 데이터가 있으면 캐시에서 데이터를 읽어온다. (캐시 히트)

 

look aside 전략 (2)

  • 찾는 데이터가 없으면, 캐시 미스가 발생한다.
  • 애플리케이션은 직접 데이터베이스에 접근해 찾는 데이터를 가져온 후, 이를 다시 캐시에 저장하는 과정을 거친다.

 

캐시 워밍

  • 초기에 부하가 몰리는 것을 막기 위해, 미리 데이터베이스에서 캐시로 데이터를 밀어넣어주는 작업을 한다.
  • 이를 캐시 워밍이라고 한다.

 


쓰기 전략과 캐시의 일관성

캐시 불일치

  • 데이터가 변경될 때 원본 데이터베이스에만 업데이트되어 캐시에는 반영되지 않는다면 데이터 간 불일치가 일어난다. (캐시 불일치)
  • a라는 값은 데이터베이스에 28로 업데이트됐지만, 레디스에는 아직 3인 데이터가 저장되어 있다.

 

1. write through

write through

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

 

2. cache invalidation

cache invalidation

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

 

3. write behind(write back)

write behind

  • 쓰기가 빈번하게 발생하는 서비스라면 고려해볼만한 방식이다.
  • 먼저 데이터를 빠르게 접근할 수 있는 캐시에 업데이트한 뒤, 이후에는 건수나 특정 시간 간격 등에 따라 비동기적으로 데이터베이스에 업데이트한다.
  • 👎 캐시에 문제가 생겨 데이터가 날아갈 경우 특정 시간 동안의 데이터가 날아갈 수 있다는 위험성은 감수해야 한다.

 


캐시에서의 데이터 흐름

  • 레디스는 메모리에 모든 데이터를 저장하며, 기본적으로 메모리는 서버의 스토리지보다 훨씬 적은 양을 보관할 수밖에 없다.
  • 캐시는 가득 차지 않게 유지해야 하며 계속해서 새로운 데이터가 저장되고 기존 데이터는 삭제될 수 있도록 관리되어야 한다.
  • 캐시로 레디스를 사용할 때에는 데이터를 저장함과 동시에 적절한 시간의 TTL 값을 지정하는 것이 좋다.

 

만료 시간

  • TTL은 데이터가 얼마나 오래 저장될 것인지를 나타내는 시간 설정이다.
  • 만료 시간이 설정되면 해당 키와 관련된 데이터는 지정된 시간이 지난 후에 레디스에서 자동으로 삭제된다.

 

> SET a 100
"OK"

> EXPIRE a 60
(integer) 1

> TTL a
(integer) 58

🔼 a라는 키에 100을 저장한 뒤 EXPIRE 커맨드를 이용해 만료 시간을 60초로 설정했다.

 

> INCR a
(integer) 101

> TTL a
(integer) 51

> RENAME a apple
"OK"

> TTL apple
(integer) 41

🔼 데이터를 조작하거나 키의 이름을 바꿔도 설정된 만료 시간은 변경되지 않는다.

 

> SET b 100
"OK"

> EXPIRE b 60
(integer) 1

> TTL b
(integer) 57

> SET b banana
"OK"

> TTL b
(integer) -1

🔼 기존 키에 새로운 값을 저장해 덮어 쓸 때는 이전에 설정한 만료 시간은 사라진다.

 


메모리 관리와 maxmemory-policy 설정

  • 메모리의 용량을 초과하면 레디스는 내부 정책을 사용해 어떤 키를 삭제할지 결정한다.
    • maxmemory 설정: 데이터의 최대 저장 용량을 설정
    • maxmemory-policy 설정값: 용량을 초과할 때의 처리 방식을 결정

 

Noeviction

  • 기본값으로, 레디스에 데이터가 가득 차면 에러를 반환하는 설정이다. (데이터 삭제 X)
  • 장애 상황으로 이어질 수 있고, 관리자가 데이터를 직접 지워야 하기 때문에 레디스를 캐시로 사용할 때 권장되지 않는다.

 

LRU eviction

  • 레디스에 데이터가 가득 찼을 때, 가장 최근에 사용되지 않은 데이터부터 삭제하는 정책이다.
  • 레디스는 LRU 알고리즘을 이용한 2가지 설정값을 가지고 있다.
    • volatile-lru: 만료 시간이 설정되어 있는 키에 한해서 LRU 방식으로 삭제
    • allkeys-LRU: 모든 키에 대해 LRU 방식으로 삭제

 

LFU eviction

  • 레디스에 데이터가 가득 찼을 때 가장 자주 사용되지 않은 데이터부터 삭제하는 정책이다.
  • 2가지 설정값을 갖는다.
    • volatile-lfu: 만료 시간이 설정되어 있는 키에 한해서 LFU 방식으로 삭제
    • allkeys-lfu: 모든 키에 대해 LFU 방식으로 삭제

 

RANDOM eviction

  • 레디스에 저장된 키 중 하나를 임의로 골라내 삭제한다.
  • 알고리즘을 사용하지 않기 때문에 레디스의 부하를 줄여줄 수 있다. (미세한 차이)
  • 자주 사용하는 캐시를 삭제했다가 다시 넣어주는 작업이 더 크기 때문에 권장되지 않는다.
  • 2가지 설정값을 갖는다.
    • volatile-random: 만료 시간이 설정되어 있는 키에 한해 랜덤하게 삭제
    • allkeys-random: 모든 키에 대해 랜덤하게 삭제

 

volatile-ttl

  • 만료 시간이 가장 작은 키를 삭제한다.
  • 근사 알고리즘을 사용하기 때문에 간단하게 키를 찾을 수 있다.

 


캐시 스탬피드 현상

캐시 스탬피드 현상

  • 여러 개의 애플리케이션에서 바라보던 키가 만료되어 삭제된다면, 이 서버들은 한번에 데이터베이스에 가서 데이터를 읽어오는 과정을 거치는데, 이를 중복 읽기라 한다.
  • 이후 각 애플리케이션은 읽어온 데이터를 레디스에 쓰는데, 이 또한 여러 번 반복되기 때문에 중복 쓰기가 발생한다.

 

적절한 만료 시간 설정

  • 가장 간단한 방법은 만료 시간을 너무 짧지 않게 설정하는 것이다.
  • 여러 애플리케이션에서 한번에 접근해야 하고, 반복적으로 사용되는 데이터라면 만료 시간을 충분히 길게 설정해주는 것이 좋다.

 

선 계산

def fetch(key):
    value = redis.get(key)
    if(!value):
    	value = db.fetch(key)
        redis.set(value)
    return value
  • look aside 방식으로 캐시를 사용할 때 애플리케이션은 위 코드와 비슷하게 동작할 것이다.
    • 캐시에 데이터가 있는지 확인하고, 없으면 데이터베이스에서 데이터를 가져와 레디스에 저장
  • 만약 키가 실제로 만료되기 전에 이 값을 미리 갱신해준다면 여러 애플리케이션에서 한번에 데이터베이스에 접근해 데이터를 읽어오는 과정을 줄일 수 있을 것이다.

 

def fetch(key, expiry_gap):
    ttl = redis.ttl(key)
    
    if ttl - (random() * expiry_gap) > 0:
    	return redis.get(key)
    else:
    	value = db.fetch(key)
        redis.set(value, KEY_TTL)
        return value

# Usage
fetch('hello', 2)
  • 레디스가 실제로 만료되기 전 랜덤한 확률로 데이터베이스에 접근해서 데이터를 읽어와 캐시의 값을 갱신한다.
  • 상황에 따라 캐시 스탬피드 현상을 줄일 수 있기 때문에 전체적인 성능을 향상시킬 수도 있다.
  • 이때 expiry_gap 값은 적절히 설정해주는 것이 중요하다. (불필요한 작업이 늘어날 수 있음)

 

PER 알고리즘

  • 캐시 값이 만료되기 전에 언제 데이터베이스에 접근해서 값을 읽어오면 되는지 최적으로 계산할 수 있다.
  • 데이터를 가져오는 과정에서 GET 대신 아래 함수를 사용하는 것은 캐시 스탬피드 현상을 줄이고 성능을 최적화할 수 있다.

 

currentTime - ( timeToCompute * beta * log(rand()) ) > expiry
  • currentTime: 현재 남은 만료 시간
  • timeToCompute: 캐시된 값을 다시 계산하는 데 걸리는 시간
  • beta: 기본적으로 1.0보다 큰 값으로 설정 가능
  • rand(): 0과 1 사이의 랜덤 값을 반환하는 함수
  • expiry: 키를 재설정할 때 새로 넣어줄 만료 시간

 


세션 스토어로서의 레디스

세션이란?

  • 서비스를 사용하는 클라이언트의 상태 정보를 의미한다.
  • 레디스는 키-값 형식으로 사용이 간단하며 string, set, hash 등의 자료 구조를 제공하기 때문에 사용자 데이터를 저장하기에 용이하다.

 

세션 스토어가 필요한 이유

하나의 세션 스토어

  • 서비스 초창기 또는 프로토타입용 서비스에서는 굳이 세션 스토어가 필요하지 않다.

 

세션 스토어 - sticky session

  • 웹 서버가 늘어나면 여러 개의 웹 서버에 트래픽을 분배할 수 있기 때문에 더 많은 유저를 수용할 수 있다.
  • 이때 각 웹 서버별로 세션 스토어를 따로 관리한다면 유저는 유저의 세션 정보를 갖고 있는 웹 서버에 종속되어야 한다.
  • 👎 특정 웹 서버에 유저가 몰려 트래픽이 집중되는 상황이 발생해도 유저는 다른 서버를 사용할 수 없어, 트래픽을 분산시킬 수 없는 상황이 발생한다.

 

세션 스토어 - all-to-all

  • 유저의 세션 정보를 모든 웹 서버에 복제해서 저장하는 방법을 생각해볼 수 있다.
  • 👎 데이터가 복제되면서 불필요한 저장 공간을 차지하게 되고, 불필요한 네트워크 트래픽도 다수 발생하게 된다.

 

세션 스토어 - 데이터베이스를 이용

  • 데이터베이스를 세션 스토어로 사용하는 방법도 고려해볼 수 있다.
  • 👎 유저가 많아질수록 서비스의 전반적인 응답 속도를 저하시키는 요인이 될 수 있다.

 

세션 스토어 - 레디스를 이용

  • 레디스를 세션 스토어로 사용해 서버, 데이터베이스와 분리하고, 여러 서버에서 세션 스토어를 바라보도록 구성한다.
  • 👍 트래픽을 효율적으로 분산시킬 수 있고, 데이터의 일관성을 지킬 수 있다. (모든 웹 서버가 동일한 세션 스토어 사용)
  • 👍 관계형 데이터베이스보다 훨씬 빠르고 접근하기 편하다.

 

hash 자료 구조를 이용한 세션 데이터 저장

  • 레디스의 hash 자료 구조는 세션 데이터를 저장하기에 알맞은 형태다.

 

> HMSET usersession:1 Name Garimoo IP 10:20:104:30 Hits 1
OK

> HINCRBY usersession:1 Hits 1
1) "2"
  • 레디스는 key-value 형태의 저장소이고, 세션 또한 ID로 데이터를 저장하기 때문에 변환 없이 데이터를 그대로 저장할 수 있다.

 


캐시와 세션의 차이

캐시를 사용할 때의 데이터 흐름

  • 캐시는 데이터베이스의 완벽한 서브셋으로 동작한다. (look aside 전략)
  • 캐시에 저장된 데이터는 여러 애플리케이션에서 함께 사용할 수 있다.
  • 세션 스토어에 저장된 데이터는 여러 사용자 간 공유되지 않으며, 특정 사용자 ID에 한해 유효하다.

 

세션을 사용할 때의 데이터 흐름

  • 세션이 활성화되어 있는 동안에는 애플리케이션은 유저의 데이터를 세션 스토어에만 저장한다. (데이터베이스 X)
  • 유저가 로그아웃할 때 세션은 종료되며 이때 데이터의 종류에 따라 데이터베이스에 저장해 영구적으로 보관(Ex. 장바구니)할 것인지, 삭제(Ex. 최근 본 상품)할 것인지 결정된다.
  • 세션 스토어에 장애가 발생하면 내부 데이터가 손쇨될 수 있으므로 레디스를 세션 데이터로 활용할 때는 캐시로 사용할 때보다 더 신중한 운영이 필요하다.

 


References

개발자를 위한 레디스 (REDIS FOR DEVELOPERS)

반응형
LIST