캐시 설계 전략
서비스 응답시간에 큰 영향을 미치는 부분은 주로 네트워크 통신과 DB I/O이다. 캐싱을 통하면 DB I/O로 인해 발생하는 오버헤드를 줄일 수 있다.
서론
Caching?
캐시는 빠른 응답과 비용 절약을 위해 사용하는 임시 데이터다.
- 클라이언트의 요청으로 특정 데이터가 필요한 경우 서버는 DB에 질의한 뒤 결과를 반환한다.
- 만약 요청하는 데이터의 캐시가 존재한다면 DB에 질의하지 않고 캐시로부터 데이터가 반환된다.
- 캐기사 없다면? DB에 질의가 수행되어 반횐되고 그 결과를 캐싱한다.
캐시의 관리
캐시는 임시 데이터다. 따라서 캐시를 저장할 때 캐시가 만료되는 시간 (expire time || TTL; Time To Live)를 명시한다.
캐시는 해당 시간 내에서만 유효하고 만료 시간이 경과하면 사용할 수 없다.
캐시에 만료되는 시간을 두는 이유는 캐시가 실시간 데이터가 아니기 때문이다. 만약 원본 데이터가 바뀐다면 캐시된 내용도 바뀌어야 한다.
캐시를 사용하는 이유
캐시를 사용하면 DB에서 데이터를 읽을 때 발생하는 I/O 오버헤드를 줄일 수 있다. 캐시는 보통 Redis와 같은 In-Memory DB를 사용한다.
In-Memory DB는 메모리를 사용하기 때문에 일반적인 RDBMS보다 데이터를 읽는 속도가 빠르다. 따라서 캐시는 DB에서 데이터를 읽는 것 보다 월등히 속도가 빠르다.
모든 데이터를 캐싱하는 건?
In-Memory DB를 사용하면 요청을 빠르게 처리할 수 있지만, 메모리 특성상 데이터 소실의 위험이 있다. 또한 메모리 사용은 비용이 발생하기 때문에 방대한 양의 데이터를 모두 메모리에 적재하는 것은 큰 비용이 든다.
캐시 설계 전략이 필요한 이유
따라서 데이터의 특성에 따라 DB와 캐시를 적절히 사용하는 것이 매우 중요하다. 캐싱 전략을 올바르게 수립하면 적은 비용으로 큰 퍼포먼스 향상을 기대할 수 있다.
What to Cache
어떤 데이터를 캐싱하는 것이 좋을지 생각해 보아야 한다. 캐싱하기 좋은 데이터의 특성은 다음과 같다.
- 자주 바뀌지 않는 데이터
- 자주 사용되는 데이터
- 자주 같은 결과를 반환하는 데이터
- 오래 걸리는 연산의 결과
자주 바뀌지 않는 데이터
자주 바뀌지 않는 데이터의 경우, 한 번 캐시로 저장하면 메모리에서 읽어 빠르게 사용가능하다.
원본 데이터가 바뀌면 캐시도 바뀌어야 한다. 자주 바뀌지 않는 데이터의 캐시는 오랫동안 사용이 가능하기 때문에 캐싱하는 것이 효율적이다.
자주 사용되는 데이터
자주 사용되는 데이터는 캐싱하기 좋다. 한 번 캐싱해 놓으면 캐시를 사용하여 다수의 요청을 효율적으로 처리할 수 있다.
다만, 자주 사용될 지라도 매번 결과 값이 다르면 오히려 캐싱하지 않는 것이 낫다. 만약 일정시간 동안 데이터가 변하지 않는 것이 보장되면 해당 시간만큼의 TTL로 짧은 캐시를 생성하면 된다.
예를 들어 검색처럼 새로 요청하더라도 일정 시간 동안 같은 결과가 반환되는 경우에 해당한다.
자주 같은 결과를 반환하는 데이터
자주 같은 결과를 반환하는 데이터의 경우도 캐싱을 적용하기 좋다. 예를 들어 특정 arguments의 조합에 따라 결과가 일정한 경우 해당된다.
이런 경우는 각 arguments의 조합을 key로 연산 결과를 캐싱하면 된다.
오래 걸리는 연산의 결과
무거운 연산이 반복적으로 계산되어야 한다면 연산의 특성에 맞게 결과를 캐시하는 것이 효율적이다.
또는 사전에 별도의 프로세스에서 작업을 수행하여 결과를 미리 캐싱해 놓을 수 있다.
이 밖에도 일반적인 쿼리나 연산보다 캐싱할 때의 비용이 더 적다면 캐싱을 사용할 수 있다.
로그 분석이나 프로파일링을 통해 캐시를 적용할 함수나 API를 찾을 수 있다. 그러나 모든 사항을 고려하여 캐싱을 적용할지, TTL은 얼마나 적용할지 등은 개발자의 선택이 중요하다.
캐싱 전략 패턴의 종류
캐시를 이용하게 되면 닥쳐오는 문제점이 바로 데이터 정합성의 문제이다. 같은 종류의 데이터라도 두 저장소에 저장된 값이 서로 다른 현상이 일어날 수 밖에 없는 것이다.
따라서 적절한 캐시 읽기 전략(Read Cache Strategy)과 캐시 쓰기 전략(Write Cache Strategy)를 통해, 캐시와 DB간의 데이터 불일치 문제를 극복하면서도 빠른 성능을 잃지 않게 하기위해 연구를 할 필요가 있다.
캐시 읽기 전략 (Read Cache Strategy)
Look Aside Pattern
- Cache Aside 패턴이라고도 불림.
- 데이터를 찾을 때 우선 캐시에 저장된 데이터가 있는지 우선 확인, 캐시에 데이터가 없으면 DB에서 조회함.
- 반복적인 읽기가 많은 호출에 적합.
- 캐시와 DB가 분리되어 가용되기 때문에 원하는 데이터만 별도로 구성하여 캐시에 저장.
- 캐시와 DB가 분리되기 때문에 캐시 장애 대비 구성이 되어 있음.
만일 Cache Store가 다운되더라도 DB에서 데이터를 가져올 수 있어 서비스 자체는 문제가 없음.
- 대신 Cache Store에 붙어있던 connection이 많았다면, Cache Store가 다운된 순간 DB로 몰려 부하발생 가능.
일반적으로 사용되는 기본적인 캐시 전략. 이 방식은 캐시에 장애가 발생하더라도 DB에 질의를 실행함으로 캐시 장애로 인한 서비스 문제는 대비할 수 있지만, Cache Store와 DB간 정합성 유지 문제가 발생할 수 있음.
반복적으로 동일 쿼리를 수행하는 서비스에 적합, 단건 호출 빈도가 높은 서비스에는 비적합.
이런 경우 DB에서 캐시로 데이터를 미리 넣어주는 작업을 하기도 하는데 이를 Cache Warming이라고 함.
Read Through 패턴
- 캐시에서만 데이터를 읽어오는 전략 (inline cache)
- Look Aside와 비슷하지만 데이터 동기화를 라이브러리 또는 캐시 제공자에게 위임하는 방식이라는 차이가 있음.
- 따라서 데이터를 조회하는데 전체적으로 속도가 느림.
- 또한 데이터 조회를 전적으로 캐시에만 의지하므로 Cache Store가 다운될 경우 서비스 이용에 차질이 생길수 있음.
- 캐시와 DB간의 데이터 동기화가 항상 이루어져 데이터 정합성 문제에서 벗어날 수 있음.
- 읽기가 많은 호출에 적합
Cache Aside 방식과 비슷하지만, Cache Store에 저장하는 주체가 Server인가 혹은 Data Store 자체인가의 차이가 있음.
직접적인 DB 접근을 최소화 하고, Read에 대한 소모되는 자원을 최소화할 수 있음.
하지만 캐시에 문제가 발생하는 경우 바로 서비스 중단이 되기 때문에 Cache Store의 Replication 또는 Cluster구성하여 가용성을 높여야 함.
캐시 쓰기 전략 (Write Cache Strategy)
Write Back 패턴
- Write Behinde 패턴이라고도 불림.
- 캐시와 DB 동기화를 비동기하기 때문에 동기화 과정이 생략.
- 데이터를 저장할 때 DB에 바로 질의하지 않고, 캐시에 모아서 일정 주기 배치 작업을 통해 DB에 반영.
- 캐시에 모아놨다 DB에 쓰기 때문에 쓰기 커넥션 회수 비용과 부하를 줄일 수 있음.
- Write가 빈번하면 서 Read를 하는데 많은 양의 리소스가 소모되는 서비스에 적합.
- 데이터 정합성 확보.
- 자주 사용되지 않는 불필요할 리소스 저장.
- 캐시에서 오류발생시 데이터 영구소실의 가능성.
데이터를 저장할 때 DB가 아닌 캐시에 먼저 저장하여 모아놓았다가 특정 시점마다 DB로 쓰는 방식으로 일종의 Queue 역할을 겸하게 됨.
캐시에 데이터를 모았다 한 번에 DB에 저장하기 때문에 DB 쓰기 횟수 비용과 부하를 줄일 수 있지만, 데이터를 옮기기 전에 캐시 장애가 발생하면 데이터 유실이 발생할 수 있다는 단점이 존재.
반대로 DB에 장애가 발생하더라고 지속적인 서비스 제공을 보장하기도 함.
Replication이나 Cluster 구조를 적용하면 Cache Store 서비스의 가용성을 높일 수 있고, Read Through와 결함하변 가장 최근에 업데이트 된 데이터를 항상 캐시에서 사용할 수 있음.
Write Through 패턴
- DB와 Cache에 동시에 데이터를 저장하는 전략.
- 데이터를 저장할 때 먼저 캐시에 저장한 다음 DB에 저장.
- Read Trough와 마찬가지로 DB 동기화 작업을 캐시에 위임.
- DB와 캐시가 항상 동기화 되어 있어, 캐시의 데이터는 항상 최신 상태로 유지.
- 캐시와 백업 저장소에 업데이트를 같이 하여 데이터 일관성 유지.
- 데이터 유실이 발생하면 안 되는 상황에 적합.
- 자주 사용되지 않는 불필요한 리소스 저장.
- 매 요청마다 두번의 Write 가 발생함으로 빈번한 생성, 수정이 발생하는 서비스에서는 성능 이슈 발생.
- 기억장치 속도가 느릴 경우, 데이터를 기록할 때 CPU가 대기하는 시간이 필요하기 때문에 성능 감소.
Cache Store와 DB에 동시에 반영하는 방식. 항상 동기화 되어 있고 항상 최신정보를 가지고 있다는 장접이 있음.
저장할 때마다 2개 과정을 거치기 때문에 상대적으로 느림.
Write Around 패턴
- 모든 데이터는 DB에 저장
- Cache Miss가 발생하는 경우에만 DB와 캐시에 데이터 저장
- DB와 Cache Store의 데이터가 다를 수 있음.
Cache Miss가 발생하기 전에 DB에 저장된 데이터가 수정되었을 때, 사용자가 조회하는 Cache Store와 DB 간의 데이터 불일치 발생.
Cache 읽기 + 쓰기 전략 조합
- Look Aside + Write Around
- Read Through + Write Around
- Read Through + Write Through
캐시 저장시 참고
- Cache Hit Ratio : 캐시 사용의 정중도. 적중율이 높을 수록 CPU와 주기억장치 속도 차이로 인한 병목현상을 최소화할 수 있다.
자주 사용되면서 자주 변경되지 않는 데이터를 캐시에 저장할 경우 높은 성능 향상을 이뤄낼 수 있다.
- 지역성 (Locality) : Cache Hit Ratio는 캐시의 Locality, 즉 지역성에 의해 높아진다. 지역성이란 데이터 접근이 시간적 혹은 공간적으로 가깝게 일어나는 것을 의미함.
- 시간적 지역성 : 최근에 엑세스 된 프로그램이나 데이터가 가까운 미래에 다시 엑세스 될 가능성이 높은을 의미.
- 공간적 지역성 : 기억장치 내에 인접하여 저장된 데이터들이 연속적으로 엑세스 될 가능성이 높음을 의미.
- 순차적 지역성 : 분기가 발생하지 않는 이상 명령어들이 기억장치에 저장된 수서대로 인출되어 실행됨을 의미
- 일반적으로 캐시는 메모리에 저장되는 형태를 선호한다.
- 메모리 저장소(Cache Store)는 대표적으로 Redis와 MemCached가 있으며, 메모리를 1차 저장소로 사용하기 때문에 디스크와 달리 제약적인 저장 공간을 사용한다.
- 자주 사용되는 데이터를 어떻게 뽑아 캐시에 저장하고 자주 사용되지 않는 데이터는 어떻게 제거해 갈것이냐를 지속적으로 고민해야 할 필요성이 있다.
- 캐시는 자주 사용되며 자주 변경되지 않는 데이터를 기준으로 하는 것이 좋다.
- 캐시는 휘발성을 기본으로 하기 때문에 어느 정도 데이터 수집과 저장 주기를 가지도록 설계해야 한다.
- 유실 또는 정합성이 일부 깨질 수 있다는 점을 항상 고려해야 한다.
- 레파토 법칙 (8:2 법칙)
전체 결과의 80%가 전체 원인의 20%에서 일어나는 현상을 가리킨다.
80%의 활동을 20%의 유저가 하기 때문에 20%의 데이터만 캐시해도 서비스 대부분의 데이터를 커버할 수 있다는 의미다.
캐시 제거시 참고
- 캐시는 기본적으로 영구 저장소에 저장된 데이터의 복사본으로 동작하는 경우가 많다.
- 데이터 동기화 작업이 반드시 필요하다는 의미로 개발시 고려해야 한다.
- 캐시 만료 정책이 제대로 구현되지 않은 경우 클라이언트는 데이터가 변경되었음에도 오래된 정보가 캐싱되어 오래된 정보를 사용할 수 있다는 문제점이 있다.
- 따라서 캐시 구성시 기본 만료정책을 설정해야 한다.
- 만료 주기가 너무 짧으면 데이터는 너무 짤리 제거되고 캐시의 이점이 줄어든다.
- 만료 주기가 너무 길면 데이터 변경의 가능성과 메모리 부족현상, 자주 사용해야 하는 데이터가 제거되는 등의 역효과를 나타낼 수 있다.
Cache Stampede 현상
- TTL 값이 너무 작게 설정될 경우 발생할 수 있는 현상이다.
- Cache Miss로 DB에 데이터를 요청한 뒤, 다시 Cache Store에 저장하는 과정을 거칠 경우 모든 application에서 DB에서 값을 찾는 duplicate read가 발생한다.
- 읽어온 값을 각각 Cache Store에 저장하는 duplicate write도 발생하여 처리량도 다 같이 느려질 뿐 아니라 불필요한 작업이 굉장히 늘어나 폭주장애로 이어질 가능성이 있다.
캐시 공유시 참고
- 캐시는 application의 여러 인스턴스에서 공유하도록 설계한다.
- 각 application의 인스턴스가 캐시에서 데이터를 읽고 수정할 수 있다.
- 캐시 업데이트 방식
- 캐시 데이터의 변경 직전에 데이터가 검색된 후 변경되지 않았는지 확인해야 한다.
변경되지 않았다면 업데이트하고 변경되었다면 업데이트 여부를 어플리케이션 레벨에서 결정할 수 있어야 한다.
- 캐시 데이터를 업데이트 하기 전에 Lock을 잡는 방식.
lock을 사용할 경우 조회성 업무를 처리하는 서비스에 Lock으로 인해 대기현상이 발생할 수 있다.
캐시 가용성 참고
캐시를 구성하는 목적은 빠른 성능 확보와 데이터 전달에 있으며 데이터의 영속성 보장하기 위함은 아니다.
데이터의 영속성은 기존 RDBMS에 위임하고, 캐시는 데이터 읽기에 집중하는 것이 성능확보의 지침사항이다.
또한 Cache Store가 장애로 인해 다운 되었을 경우나 서비스가 불가능 할 경우에도 지속적인 서비스가 가능해야 한다. 이는 데이터가 결국 RDBMS에 동일하게 저장되고 유지된다는 점을 뒷바침 한다.