002 Redis Cache

레디스를 캐시로 사용하기 #

[캐시란?] #

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

[캐시로서의 레디스] #

레디스는 자체적으로 고가용성 기능을 가지고있다. 일부 캐싱 전략에서는 캐시에 접근할 수 없게 되면 이는 곧바로 서비스 장애로 이어질 수 있따. 캐시 저장소도 일반적인 데이터 저장소와 같이 안정적으로 운영될 수 있는 조건을 갖추는 것이 좋다. 레디스의 센티널, 클러스터 기능을 사용하면 마스터 노드의 장애를 자동으로 감지해 페일오버(Failover; 장애대비)를 발생시키기 때문에, 운영자의 개입 없이 캐시는 정상으로 유지될 수 있어 가용성이 높아진다. 레디스의 클러스터를 사용하면 캐시의 스케일 아웃 또한 쉽게 처리 가능하다. 서비스 규모에 따라 캐시 자체의 규모도 늘어나야할 상황이 발생할 수 있는데, 자체 샤딩 솔루션인 클러스터를 사용하면 수평 확장이 간단해진다. 레디스는 캐시 저장소 용도로 이상적이다.

[캐싱 전략] #

캐싱 전략은 캐싱되는 데이터의 유형과 데이터에 대한 엑세스 패턴에 따라 다르기 때문에 적절한 캐싱 전략을 선택해야한다.

[읽기 전략 - look aside] #

애플리케이션에서 데이터를 읽어갈때 주로 사용한다. 레디스를 사용할때 가장 일반적으로 배치하는 방법이다.

  1. 데이터가 먼저 캐시에 있는지 확인한다.
  2. 캐시에 있으면 캐시에서 데이터를 읽어온다. (=캐시 히트)
  3. 찾고자하는 데이터가 없을때에는 캐시 미스가 발생하며, 직접 데이터베이스에 접근해 찾고자하는 데이터를 가져온다.
  4. 찾고하자는 데이터가 레디스에 없을때에만 레디스에 저장하므로 lazy loading 이라고도 부른다.

장점

  • 레디스에 문제가 생겨 접근을 할 수 없는 상황이 발생하더라도 장애 발생이 아닌, 직접 데이터베이스에 다시 데이터를 가지고온다.
    • 모든 커넥션이 한꺼번에 원본 데이터베이스로 몰려 많으 부하를 발생시킬 수 있다.

미리 데이터베이스에서 캐시로 데이터를 밀어넣어주는 작업을 하기도하는데, 이를 캐시 워밍(cache warming)이라고도 한다.

[쓰기 전략과 데이터의 일관성] #

캐시 불일치 : 데이터가 변경될때 원본 데이터베이스에만 업데이트돼 캐시에는 변경된 값이 반영되지 않을때 발생하는 데이터 불일치

[쓰기 전략 - writh throwgh] #

데이터베이스에 업데이트할때마다 매번 캐시에도 데이터를 함께 업데이트시키는 방식이다. 캐시는 항상 최신 데이터를 가지고있을 수 있다. 데이터는 매번 2개의 저장소에 저장돼야 하기 때문에 데이터를 쓸 때마다 시간이 많이 소요될 수 있다.

다시 사용될만한 데이터가 아닌 경우에는? 무조건 캐시에도 저장되는건 리소스 낭비일 수도 있다. 따라서 위 방시을 사용할 경우 데이터를 저장할때 만료 시간을 사용할 것을 권장한다.

[cache invalidation] #

데이터베이스에 값을 업데이트 할때마다 캐시에는 데이터를 삭제한다. 신규 데이터 저장보다, 데이터 삭제가 리소스를 훨씬 적게 사용하기 때문이다.

[write behind(write back)] #

쓰기가 빈번하게 발생하는 시스템이라면 이 방식을 고려하자. 데이터베이스에 대량의 쓰기 작업이 발생하면 이는 많은 디스크I/O를 유말해, 성능 저하가 발생할 수 있다. 먼저 데이터를 빠르게 접근할 수 있는 캐시에 업데이트 한뒤, 이후에는 건수나 특정 시간 간격 등에 따라 비동기적으로 데이터베이스에 업데이트하는 것이다. 저장되는 데이터가 실시간으로 정확한 데이터가 아니어도 되는 경우 유용하다.

[캐시에서의 데이터 흐름] #

캐시는 메모리이기 때문에 기본적인 스토리지 보다 데이터를 적게 저장할 수 밖에 없다. 캐시는 가득차지 않게 일정 양의 데이터를 유지해야하며 관리되야한다. 캐시로 레디스를 사용할때에는 데이터를 저장함과 동시에 적절한 시간의 TTL 값을 저장하는 것이 좋다.

[만료시간] #

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

커맨드 : TTL, EXPIRE(초 단위), PTTL, PEXPIRE (밀리세컨드 단위)

// 키에 만료시간 지정
SET a 100
EXPIRE a 60
TTL a

INCR 커맨드로 데이터를 조작하거나, RENAME을 이용해 키의 이름을 바꾸더라도 만료시간은 그대로 유지된다. 그러나, 기존 키에 새로운 값을 저장해 키를 덮어쓸 때에는 이전에 설정한 마료시간은 유지되지 않고 사라진다.

SET b 100
EXPIRE b 60
TTL b // 57
SET b banana
TTL b // -1 (만료시간이 지정되지 않음)

[메모리 관리와 maxmemory-policy 설정] #

레디스의 메모리는 제한적이기 때문에 모든 키에 만료시간을 설정하더라도 너무 많은 키가 저장되면 메모리가 가득 차는 상황이 발생한다. 메모리의 용량을 초과하는 양의 데이터가 저장되면 레디스는 내부 정책을 사용해 어떤 키를 삭제할지 결정한다.

maxmemory 설정: 데이터의 최대 저장 용량 설정 maxmemory-policy 설정값 : 이 용량을 초과할 때의 처리 방식을 결정하는 설정값

[Noeviction] #

기본값이다. 레디스에 데이터가 가득 차더라도 임의로 데이터를 삭제하지 않고 더이상 레디스에 데이터를 저장할 수 없다는 에러를 반환한다.

[LRU eviction] #

LRU(Least-Recently-Used) evicition이란, 레디스에 데이터가 가득 찼을때 가장 최근에 사용되지 않은 데이터부터 삭제하는 정책이다.

  1. volatile-lru : 만료 시간이 설정돼있는 키에 한해서 LRU 방식으로 키를 삭제한다. 만약 모든 키에 만료시간이 지정돼있지 않다면, noeviction 상황과 동일하다.
  2. allkeys-LRU : 모든 키에 대해 LRU 알고리즘을 사용해서 데이터를 삭제한다.

[LFU eviction] #

LFU(Least-Frequently-Used) eviction이란, 레디스에 데이터가 가득 찼을때 가장 자주 사용되지 않은 데이터부터 삭제하는 정책이다. 사용 우선순위는 유동적으로 바뀌므로 특정 케이스에서는 LRU보다 더 효울적일 수 있다.

  1. volatile-lru : 만료 시간이 설정돼있는 키에 한해서 LFU 방식으로 키를 삭제한다. 만약 모든 키에 만료시간이 지정돼있지 않다면, noeviction 상황과 동일하다.
  2. allkeys-LRU : 모든 키에 대해 LFU 알고리즘을 이용해 데이터를 삭제한다.

[LANDOM eviction] #

레디스에 저장된 키 중 하나를 임의로 골라내 삭제한다. 랜덤으로 데이터를 삭제하기 때문에 나중에 사용할 수도 있는 데이터를 삭제할 가능성이 높아진다.

  1. volatile-random : 만료 시간이 설정돼있는 키에 한해 랜덤하게 키를 삭제한다.
  2. allkeys-random : 모든 키에 대해 랜덤하게 키를 삭제한다.

[voldatile-ttl] #

만료시간이 가장 작은 키를 삭제한다. 삭제 예정 시간이 얼마 남지 않은 키를 추출해 해당 키를 미리 삭제하는 옵션이다.

[캐시 스탬피드 현상] #

대규모 트래픽 환경에서 만료 시간을 어떻게 설정하느냐에 따라 캐시 스탬피드(cache-stampede)와 같은 예상치 못한 문제 상황이 발생할 수 있다. look aside 방식으로 레디스를 사용하고 있을때, 특정 키가 만료되는 시점에 키가 삭제된다면? 여러개의 어플리케이션에서 바라보던 키가 레디스에서 만료돼 삭제된다면 이 서버들은 한꺼번에 데이터베이스에 가서 데이터를 읽어오는 과정을 거친다. 이를 중복 읽기(duplicate read)라고 한다. 이후 각 애플리케이션에서는 읽어온 데이터를 레디스에 쓰게 되는데, 이 또한 여러번 반복되기 때문에 중복 쓰기(duplicate write)가 발생한다.

[적절한 만료시간 설정] #

캐시 스탬피드를 줄이기 위한 가장 간단한 방법은 만료 시간을 너무 짧지않게 설정하는 것이다. 여러 애플리케이션에서 한꺼번에 접근해야하는 데이터이며, 반복적으로 사용돼야하는 데이터라면 만료시간을 충분히 길게 설정한다.

[선 계산] #

look aside 방식으로 캐시를 사용할때 애플리케이션은 다음 코드와 비슷하게 동작할 것이다. 키가 실제로 만료되기 전에 이 값을 미리 갱신해준다면 여러 애플리케이션에서 한꺼번에 데이터베이스에 접근해 데이터를 읽어오는 과정을 줄여 불필요한 프로세스를 줄일 수 있다.

[PER 알고리즘] #

PER(Probabilistic Early Recomputation) 알고리즘 캐시 값이 만료되기 전에 언제 데이터베이스에 접근해서 값을 읽어오면 되는지 최적으로 계산할 수 있다.

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

위 알고리즘은 만료시간에 가까워질수록 true를 반환할 확률이 증가하므로, 이는 불필요한 재계산을 효과적으로 방지하는 가장 효율적인 방법일 수 있다.

[세션 스토어로서의 레디스] #

세션이란? 서비스를 사용하는 클라이언트의 상태 정보를 의미한다. 애플리케이션은 현재 서비스에 로그인돼 있는 클라이언트가 누구인지, 그 클라이언트가 어떤 활동을 하고 있는지 저장하고 있으며, 유저가 서비스를 떠나면 세션스토어에서 유저의 정보를 삭제한다. 많은 서비스에서 레디스를 세션 스토어로 사용하고 있다.

웹 서버가 여러대로 늘어나는 상황에서, 각 웹 서버별로 세션 스토어를 따로 관리한다면 유저는 유저의 세션 정보를 갖고있는 웹 서버에 종속되야한다. 따라서 레디스를 세션 스토어로 사용해 서버, 데이터베이스와 분리 해놓은 뒤 여러 서버에서 세션 스토어 (1개)를 바라보도록 구성해야한다. 유저는 세션 스토어에 구애받지 않고 어떤 웹 서버에 연결되더라도 동일한 세션 데이터를 조회할 수 있어 트래픽을 효율적으로 분산시킬 수 있으며, 데이터의 일관성도 고려할 필요가 없다. 또한 관계형 데이터베이스보다 훨씬 빠르고 접근하기도 간편하므로 데이터를 가볍게 저장할 수 있다.

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

HMSET usersession:1 Name Garimoo IP 10:20:104:30 Hits 1
HINCRBY userssession:2 Hits 1

[캐시와 세션의 차이] #

레디스를 캐시로 사용할때에의 가장 일반적인 look aside 전략을 이용할때 데이터는 데이터베이스의 서브셋으로 동작한다. 세션 스토어에 저장된 데이터는 여러 사용자간 공유되지 않으며, 특정 사용자 ID에 한해 유효하다. 일반적인 세션 스토어에서는 유저가 로그인하면 세션 데이터는 세션 스토어에 저장된다. 유저가 로그인해 있는 동안, 즉 세션이 활성화돼 있는 동안에는 애플리케이션은 유저의 데이터를 데이터베이스가 아닌 세션 스토어에만 저장한다.