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번씩 수행
- passive 방식
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 알고리듬을 이용해 데이터를 삭제하기 때문에 적어도 메모리가 꽉 찼을 때 장애가 발생할 상황은 방지할 수 있다
- volatile-lru
- LFU eviction
- LFU(Least-Frequently Used) eviction이란 레디스에 데이터가 가득 찼을 때 가장 자주 사용되지 않은 데이터부터 삭제하는 정책
- LFU는 LRU와 유사하지만 키를 엑세스하는 패턴에 따라 우선순위가 유동적으로 바뀐다는 점에서 특정 케이스에서는 LRU보다 더 효율적일 수 있다
- LFU 역시 두 가지 설정값을 가짐
- volatile-lfu
- 만료 시간이 설정돼 있는 키에 한해서 LFU 방식으로 키를 삭제
- allkeys-lfu
- 모든 키에 대해 LFU 알고리듬을 이용해 데이터를 삭제
- volatile-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.