이번 글은 성능을 향상시키기 위해서 캐시를 사용할려는데 캐시사용에도 여러 전략이 존재합니다.
그래서 여러전략들을 정리하도록하겠습니다.
캐싱전략이란?
캐싱(Caching)은 애플리케이션의 처리 속도를 높여준다. 이미 가져온 데이터나 계산된 결과값의 복사본을 저장함으로써 처리 속도를 향상시키며, 이를 통해 향후 요청을 더 빠르게 처리할 수 있습니다
로컬 캐싱 vs 글로벌 캐싱
로컬 캐싱은 서버 내부 저장소에 캐시 데이터를 저장하는 것이다.
만약 scacle out을 한다면 데이터가 공유가 안되므로 서버마다 캐시 데이터가 다를수있습니다 그런데
서버내에서 작동하기 때문에 속도가 빠릅니다.
예를 들어, 사용자가 같은 리소스에 대한 요청을 반복해서 보내더라도 A 서버에서는 이전 데이터를, B 서버에서는 최신 데이터를 반환하여 각 캐시가 서로 다른 상태를 가질 수도 있다. 즉, 일관성 문제가 발생할 수 있다는 것이다. 이외에도 서버 별 중복된 캐시 데이터로 인한 서버 자원 낭비, 힙 영역에 저장된 데이터로 발생하는 GC에 대한 문제 등을 고려해야 한다.
반면에 글로벌 캐싱은 서버 내부 저장소가 아닌 별도의 캐시 서버를 두어 각 서버에서 캐시 서버를 참조하는 것이다.
캐시 데이터를 얻으려 할 때마다 캐시 서버로의 네트워크 트래픽이 발생하기 때문에 로컬 캐싱보다 속도는 느리다.
하지만 서버 간에 캐시 데이터를 쉽게 공유할 수 있기 때문에 위에서 언급한 로컬 캐싱의 문제를 해결할 수 있다.
왜 캐시 전략을 고려해야하는가?
Redis로 캐시로 사용할 때 어떻게 배치할 것이냐는 캐싱 전략이 필요하다. 이에 따라 성능에 영향을 끼치기 때문에 상황(데이터 유형, 데이터 액세스 패턴)에 맞게 적절한 전략을 사용해줘야 한다.
캐싱 전략 5가지
- Look-Aside(Cache-Aside) 읽기 전략
- Read-Through 읽기 전략
- Write-Through 쓰기 전략
- Write-Around 쓰기 전략
- Write-Back 쓰기 전략
📌Look-Aside(Cache-Aside) 읽기 전략
앱에서 데이터를 읽는 전략이 많을 때 사용하는 전략이다.
이 구조는 Redis를 캐시로 쓸 때 가장 일반적으로 사용하는 방법이다. 캐시는 찾는 데이터가 없을 때 DB에 직접 조회해서 입력되기 때문에 Lazy Loading이라고 한다.
- 이 방식은 가장 범용적이며 읽기 요청이 많은 경우에 적합하다. 이 구조는 Redis가 다운되더라도 바로 장애로 이어지지 않고 DB에서 데이터를 가져올 수 있다는 장점이 있다.
- 그런데 만약 캐시에 많은 커넥션이 붙어있는 상태에서 다운이 발생하면 동시에 DB로 그 커넥션이 다 붙기 때문에 갑자기 DB 부하가 많이 몰릴 수 있다.
Look-Aside 전략을 사용할 때 가장 일반적인 쓰기 전략은 DB에 직접 쓰는 Write-Around 쓰기 전략이다.
이 경우 캐시와 DB의 데이터가 일치하지 않을 수 있다. 이를 해결하기 위해 TTL을 사용하고 TTL이 만료될 때 까지는 변경되지 않은 캐시 데이터를 계속 제공한다. 데이터 최신성을 보장해야 하는 경우에는 캐시 entry를 무효화하거나 더 적절한 쓰기 전략(Cache를 거친 쓰기 전략)을 이용해야 한다.
-> 새로 쓴다면 캐시에 모든걸 비우고 캐시를 채워주는방법이 있을것같다
🎈TTL(Time To Live)이란?
TTL이란 저장된 콘텐츠의 유효시간이다. TTL을 길게 설정하면 원본서버의 부하는 줄어들지만 변경사항이 늦게 반영된다. 반대로 짧게 설정하면 너무 잦은 변경확인 요청으로 원본서버 부하가 높아진다.
과정
- 앱은 데이터를 찾을 때 캐시를 먼저 확인한다.
- 캐시에 데이터가 있으면 해당 데이터를 읽어오는 작업을 반복한다.
- 만약 Redis에 해당 키가 존재하지 않는다면 (Cache miss) 앱은 DB에 접근해서 데이터를 직접 가지고 온 뒤 다시 Redis에 저장하는 과정을 거친다. 그래서 동일 데이터에 대한 후속 읽기 결과 Cache Hit이 된다.
🌱Read-Through 읽기 전략 (Through는 거치다 통과하다)
Read-Through 전략은 데이터를 읽을 때 오직 캐시로만 데이터를 읽어온다.
만약 Cache Miss가 발생하면 DB에서 해당 데이터를 캐시에 바로 저장한다.
즉 캐시는 앱과 DB 중간에 위치해 앱은 캐시만 바라보게 되고 DB는 캐시만 바라보게 된다. 항상 첫 요청은 항상 Cache Miss가 발생하여 데이터가 로딩된다.
- 뉴스 기사 읽기와 같이 동일한 데이터가 여러 번 읽기 요청이 되는 경우에 적합하다. 디스크 읽기 비용을 절약할 수 있다.
- 첫 요청은 항상 Cache Miss가 발생하여 데이터가 로딩된다.
- Look-Aside와 다른 점은 읽기에 대한 앱의 관점이다. Look-Aside의 경우 Cache Miss가 나면 앱이 직접 DB에 데이터를 조회한 반면, Read-Through는 캐시에서 DB에 데이터를 직접 조회하여 로드한다. 그리고 Read-Through는 캐시의 데이터 모델과 DB 데이터 모델이 다를 수 없다.
과정
- 앱은 모든 데이터를 캐시로만 읽어온다.
- Cache Miss가 발생한 경우 캐시에서 직접 DB 데이터를 읽어온다. (즉 캐시가 DB에 접근하는게 Look-Aside와 다르다)
🍓Cache Warming - 데이터 미리 캐싱해두기
Look-Aside에서 캐시가 다운되면 다시 새로운 캐시를 투입해야 하고 DB에 새로운 데이터를 넣으면 커넥션은 자연스레 DB로 몰리게 된다. 이러한 경우 초기에 Cache Miss가 엄청 발생해서 성능 저하가 올 수 있다.
매번 첫 요청에 Cache Miss가 발생하는 Read-Through도 마찬가지이다.
이럴 때는 캐시로 미리 데이터를 밀어넣어주는 Cache Warming 작업을 해주면 된다.
실제로 실무에서는 특정 이벤트 상품 오픈 전 해당 상품의 조회수가 몰릴 것을 대비해 상품 정보를 미리 DB에서 캐시로 올려주는 작업을 처리한다고 한다.
+ 미리 몰릴것을 예측한 데이터를 캐시에 넣어두는 작업을 하는 것이다
🧊Write-Around 쓰기 전략
DB에만 데이터를 저장하고 읽은 데이터만 캐시에 저장하는 방식이다.
일단 모든 데이터는 DB에 저장되고 Cache Miss가 발생한 경우에만 DB에서 캐시로 데이터를 끌어오게 된다.
쓰기 요청이 캐시를 거치지 않고, 데이터베이스에 직접 전달된다. 데이터는 쓰기 오퍼레이션에 의해 캐시에 업데이트 되지 않는다. 캐시에 로딩되는 시점은 캐시에 요청이 되고 miss가 발생했을때다. 즉 읽은 데이터만 캐시에 로딩된다. 쓰기만한다면 캐시에 로딩되지않는다.
- 자주 읽히지 않는 데이터는 캐시에 로드되지 않으니 리소스를 절약할 수 있다.
- 그러나 캐시 내의 데이터와 DB내의 데이터가 다를 수 있다는 단점이 있다. (ex. 캐시에 이미 로드된 데이터를 수정할 경우)
과정
- 앱은 모든 데이터를 DB에 저장한다.
- Cache Miss가 발생한 경우에만 DB에서 캐시로 데이터를 저장한다.
그래서 Write-Around는 데이터가 한 번 쓰여지고 덜 자주 읽히거나 읽지 않는 상황에서 좋은 성능을 제공할 수 있다.
이 경우 읽기 전략은 크게 상관없다. (Read-Through나 Look-Aside와 모두 결합 가능)
기록하는 데이터가 자주 사용되지 않는 경우에 적합하다(DB에만저장하기때문에)
- 실시간 로그
- 채팅방 메시지
🍧Write-Through 쓰기 전략
Read-Through와 같다. (캐시로만)
즉 캐시에 저장하고 -> 캐시에 기록된 내용을 DB에 저장
Write-Through 전략은 DB에 데이터를 저장할 때 먼저 캐시에 기록된 다음 DB에 저장되는 방식이다.
- 캐시는 항상 최신 정보를 가지고 있고 DB와 동기화되어 데이터 일관성을 보장한다는 장점이 있지만
- 저장할 때 마다 두 단계 스텝(앱 → Redis → DB)을 거쳐야 하기 때문에 추가 쓰기 시간이 발생하여 상대적으로 느리다는 단점이 있다. 그리고 저장하는 데이터가 재사용하지 않을 수도 있는데 무조건 캐시에 넣는 것은 리소스 낭비이다.
따라서 이렇게 데이터를 저장할 때에는 얼마 동안만 캐시에 보관하고 있겠다는 TTL을 설정해주는 것이 좋다.
과정
- 앱은 모든 데이터를 캐시에 저장한 후 DB에 저장한다.
그런데 만약 캐시 전략을 잘못 조합하면 성능이 안좋아진다.
예를 들어 실시간 로그를 기록하는 시스템에서 Read-Through / Write-Through 전략을 사용한다면 자주 읽지도 않는 데이터가 캐시에 적재되기 때문에 리소스 낭비가 발생하게 된다. (기록을 자주하는데 캐시-> DB이렇게 걸쳐서 리소스낭비가 심함)
이러한 경우에는 Read-Through / Write-Around 전략을 사용하는 게 적합하다.
🌈Write-Back 쓰기 전략
Write-Back 전략은 쓰기 작업을 캐시에 먼저 저장했다가 특정 시점마다 DB에 저장하는 방식이다. 디스크 기반 DB로 치면 데이터를 굉장히 많이 써야되기 때문에 무조건 디스크에 저장해야 하는 상황에서 주로 사용한다. 매번 디스크 쓰기를 실행하면 시간이 오래 걸리기 때문에 캐시에 저장해두었다가 묶음으로 디스크 쓰기 작업을 실행해주는 것이다.
- 쓰기가 많은 경우에 적합하다. DB 디스크 쓰기 비용을 절약할 수 있다. 그리고 특정 시점에만 DB에 쓰기 요청을 하기 때문에 DB 오류에도 탄력적이다.
- 바꿔서 생각해보면 데이터를 캐시에 모두 모아뒀다가 DB에 옮기기 때문에 도중 캐시에 장애가 발생하면 데이터 유실이 크게 발생할 수 있다.
대부분의 RDB 스토리지 엔진 내부에는 Write-Back 캐시 기능을 갖고 있다. 쿼리는 먼저 메모리에 기록되다가 특정 시점에 한 번에 Disk에 flush된다.
+ (한번에 처리할때 랭킹같은것에 좋을것같다)
과정
- 앱은 모든 데이터를 캐시에 저장한다.
- 특정 시점이 되면 캐시에 저장된 데이터를 DB에 저장한다.
- 그렇게 DB에 저장된 데이터는 캐시에서 삭제해준다.
Write-Back은 Read-Through와 결합하면 가장 최근 업데이트되고 액세스 된 데이터를 항상 캐시에서 사용할 수 있다.
개인적의견
Write-Around를 하고 Cache Warming을하고 Read-Trhough를 적용
DB에 변경된걸 저장하고 DB에것을 반영시켜야하니 Cache Warming하고
읽는것은 캐시로만 읽게 하는것
Write Around + Look-Aside 가 진짜로 많이 사용되는것처럼 제일 적합한것같다.
Write Around를 먼저하고 통해 캐시 저장소를 비우고 Cache Warming후 Look Aside를 진행하는 방법이 최고이다
캐시 메모리
캐시는 성능을 높이는데 많은 역할을 하지만 데이터를 제대로 관리하지 못하면 문제가 발생할 수 밖에 없다.
예를 들어, 개발하고 있는 SNS 서비스를 많은 사람들이 이용한다고 가정했을 때, 짧은 시간 안에 Redis 서버에는 다양한 캐시 데이터들로 가득 차게 될 것이다.
이때 메모리 사용에 대한 어떠한 제한이 없다면 캐시 데이터가 메모리를 계속 차지하게 되므로 어플리케이션의 전체적인 성능까지도 영향을 끼칠 수 있다. 이러한 상황을 막기 위해 Redis에서는 다양한 Eviction 정책을 제공한다.
Redis 공식 문서에 따르면 32bit 시스템에서는 메모리 제한 사용량 기본값이 3GB로 설정되어 있지만, 64bit 시스템에서는 0으로 설정되어 있다. 즉, 메모리 사용량에 대해 상한선이 없다는 뜻이다.
물리적인 메모리의 용량 이상을 사용하게 되면 스왑 영역까지 사용하게 된다. 스왑 영역이란 물리 메모리에 공간이 부족할 때 빈 공간을 만들기 위해 현재 사용 중인 메모리 일부분을 저장하는데 사용하는 저장 장치의 한 구역이다.
32비트는 레디스 용량을 제한하지만 64비트부터는 무한이라서 가상메모리가 잡혀있따면 swap영역 까지 사용합니다.
(가상영역은 디스크입니다)
스왑영역을 사용하면 메모리용량보다 많은 양의 데이터를 저장할 수 있는 장점이있지만 스왑영역은 RAM이 아니라
성능을 저하시킵니다 왜냐하면 메모리에 접근하는 속도가 차이가 나기때문입니다.
maxmemory 옵션
redis.conf 내에 maxmemory옵션을 사용하여 maxmemory 에 설정한 용량만큼 사용했다면 Eviction policy 에 따라 데이터를 제거하여 설정한 용량이하로 유지합니다.
32비트 시스템인 경우 3GB 가 기본값이며 64비트인 경우 0이 기본값입니다. 0인 경우 Eviction policy 를 사용하지 않습니다.
Redis에서는 다양한 Eviction 정책 = Redis Cache Eviction Policy
Redis가 사용하는 메모리가 maxmemory 에 지정한 크기보다 커지면 Redis는 사용자가 지정한 Eviction 정책에 따라 저장되어 있는 데이터를 제거한 후, 새로운 데이터를 저장하게 됩니다.
Redis에서 제공하는 Eviction 정책
noevitcion
maxmemory 에 도달한 상태에서 클라이언트가 새로운 데이터를 저장하려고 할 때 기존 캐시 데이터를 지우지 않고 에러를 발생
LRU
교체 전략에서 봤던 LRU입니다
LRU(Least Recently Used) 알고리즘은 사용한지 가장 오래된 데이터부터 삭제하는 알고리즘이다.
이론 상의 LRU 알고리즘을 구현하려면 메모리가 많이 필요하다. 그래서 Redis에서는 maxmemory-samples 옵션에서 지정한 수의 키로 샘플링하여 LRU 알고리즘의 근사치를 계산한다. 샘플 수가 많을수록 메모리를 많이 사용하지만 알고리즘의 정밀도가 올라간다.
- allkeys-lru : 모든 키를 대상으로 LRU 알고리즘을 적용하여 키를 삭제
- volatile-lru : EXPIRE SET 안에 있는 키를 대상으로 LRU 알고리즘을 적용하여 키를 삭제
- (EXPIRE SET 안에 있는 키: 만료 기간이 설정된 키)
Random
무작위로 데이터를 삭제한다.
- allkeys-random : 모든 키를 대상으로 무작위로 데이터를 삭제
- volatile-random : EXPIRE SET 안에 있는 키를 대상으로 무작위로 데이터를 삭제
TTL(캐시 유효시간)
TTL이 짧은 데이터부터 삭제합니다
- volatile-ttl : EXPIRE SET 안에 있는 키를 대상으로 TTL이 짧은 데이터부터 삭제
LFU
LFU(Least Frequently Used) 알고리즘은 사용 빈도수가 가장 적은 데이터부터 삭제하는 알고리즘이다. LRU와 달리 최근에 사용된 데이터라도 자주 사용되지 않는다면 제거 대상이 될 수 있다.
- allkeys-lfu : 모든 키를 대상으로 LFU 알고리즘을 적용하여 키를 삭제
- volatile-lfu : EXPIRE SET 안에 있는 키를 대상으로 LFU 알고리즘을 적용하여 키를 삭제
어플리케이션 특성에 맞게 Eviction 정책을 선택하여 redis.conf 파일의 maxmemory-policy 옵션에 지정한다.
maxmemory-policy allkeys-lru
정책 설정이 중요한 이유
프로젝트에서는 RDB의 부하를 줄이고와 성능을 위해 위해 Redis를 캐시를 사용하게되는데
하지만 서비스가 커지고 사용자가 많아지면서 자연스레 캐시의 용량도 증가하게 되면서 간혹 레디스가 죽으면서 서비스도 죽는 현상이 발생하게 되었습니다.
+ (글에서가져온내용)캐시서버에 의해 서비스가 죽으면 캐시의 TTL을 조정하거나 메모리를 늘리는 식으로 관리했지만 언제까지고 이런식으로 관리할 수는 없다고 생각을 하게 되었고 레디스에 대해서 공부를 하기 시작했습니다.
어떤 Eviction 정책이 적합할까? (Liiot님 글)
어플리케이션에 따라 데이터 액세스 패턴이 다르기 때문에 최대한 어플리케이션 특성을 고려하여 올바른 Eviction 정책을 선택해야 한다. 첫 문단에 언급한 것처럼 나는 현재 개발하고 있는 SNS 서비스를 많은 사람들이 이용하는 상황을 가정하고 있다. 그만큼 빠르게 증가하는 캐시 데이터를 효율적으로 관리하면서도 요청이 많은 데이터를 선별적으로 보존해야 한다는 것이다.
먼저, noeviction 과 volatile-* 정책은 위의 상황에 적합하지 않은 정책이라고 생각했다. noeviction 정책은 기존의 데이터를 삭제하지 않기 때문에 어플리케이션을 데이터 보존용으로 운영할 때 적합하다고 판단했다. volatile-* 정책들은 "만료 기간이 설정된 데이터"라는 전제 조건을 만족한 데이터들만 제거 대상으로 선정한다. 만약 전제 조건을 만족하는 데이터들이 없다면 noeviction 정책처럼 처리되기 때문에 후보에서 제외시켰다.
나머지 allkeys-lru , allkeys-random , allkeys-lfu 중에서 나는 allkeys-lfu 정책을 선택했다. allkeys-lru 는 최근에 사용되었는지에 따라 데이터 보존 여부가 나뉘기 때문에 사람들이 잘 찾지 않는 리소스더라도 어떤 사람이 최근에 요청했다면 삭제 대상에서 제외된다. 즉, 재요청될 가능성이 높은 데이터가 순위에 밀려 삭제가 될 수 있다는 것이다. 요청이 많이 들어오는 데이터가 삭제된다면 그만큼 데이터베이스에 접근하는 횟수가 높아질 수 있다. 따라서, allkeys-lfu 정책을 적용하여 위와 같은 문제 상황이 없도록 했다.
스프링에 적용시 참고할것
https://chagokx2.tistory.com/98
https://loosie.tistory.com/800
https://chagokx2.tistory.com/102
'Delivery' 카테고리의 다른 글
Redis를 이용한 장바구니 - 1 (0) | 2022.08.15 |
---|---|
리펙토링 디비조회 (0) | 2022.08.09 |
레디스를 선택한 이유, 세션클러스터링 (0) | 2022.08.05 |
레디스에 대해 알아보자 (0) | 2022.08.05 |
Mybatis batch처리 성능과 트랜잭션 (0) | 2022.08.01 |