캐싱 도입 이후
캐싱 도입 이후에 발생한 문제들과 문제를 해결하면서서 겪은 경험을 공유하고자 글을 작성하였습니다 :b
1. 캐싱 도입 이후 발생한 문제들
캐싱 도입 이후에 DB로의 부하가 많이 줄어들었습니다.
대략적으로 발생헀던 문제들은 다음과 같습니다.
- 캐시 만료 시점에 다량의 DB 데이터 조회가 발생
- 데이터가 없는 경우 캐시 미스로 DB 조회가 추가적으로 발생
- 캐시가 장애 발생한 경우에 처리량 급격히 저하
- 핫키 만료시 다량의 DB 조회가 발생
- Redis 클러스터 구성에서 장애 노드 발생하여 클러스터 구성 정보 refresh
- DNS 구성된 노드의 변경시 장애 발생
이러한 문제들을 겪으면서 했던 고민과 해결과정을 공유합니다.
2. 캐시의 사용 이유
캐시의 사용 이유에 앞서 백엔드 개발자의 주요 고민을 한번 볼까요?
- 같은 리소스로 어떻게 더 많은 요청을 처리할까?
- 최소한의 리소스로 최대한 많은 요청을 처리해야 합니다.
- 어떻게 하면 서버의 부하를 줄일 수 있을까?
- 단, 데이터 베이스 및 인프라와 관련된 부하도 포함합니다.
- 어떻게 하면 서버의 응답 속도를 높일수 있을까?
- 같은 시간 내에 더 많은 양의 요청을 처리 해야합니다.
캐시의 사용 이유 역시 위의 고민들에서 비롯되었습니다.
- 캐시 사용 이유
- 주 저장소의 접근시간이 오래 걸리므로 보조 저장소에 필요로 하는 데이터를 설정합니다.
- 데이터 조회시 바로 주저장소에 조회를 하는게 아니라 보조 저장소 부터 조회하여 응답합니다.
- 보조 저장소에 데이터가 존재하는 경우 빠르게 응답할수 있습니다. (속도 향상)
- 요청마다 DB를 조회하게 되면 동일 데이터를 조회하는데에 그만큼의 리소스를 사용하게 됩니다.
3. 캐시의 조건
캐시를 사용하는데 여러가지를 고려해야합니다.
모든 상황에서 캐시가 좋은것은 아닙니다.
캐시를 도입하기전에 따져보야할 항목을 간단히 정리하였습니다.
- 데이터의 변화가 적은 데이터일수록 유리합니다.
- 변경이 적을 수록 캐시 관련 시스템에 가는 부하가 적어집니다.
- 서비스의 특성상 주로 발생하는 요청의 성격이 Write보다는 Read가 많은 서비스 일수록 유리합니다.
- Write가 많고 Read가 적으면 캐시를 도입해도 Read에서 가져가는 이점이 적습니다.
- 그리고 이 항목은 바로 위 항목의 내용과 일맥 상통합니다.
- 캐시 인프라의 안정성이 어느정도 확보되는 상태여야 합니다.
- 인프라의 불안정한 상황속에서 캐시 사용은 더 많은 이슈를 초래합니다.
- 고가용성을 위해 클러스터 구성을 반드시 필요로 합니다.
4. 인메모리 캐시를 사용하지 않는 이유
왜 인메모리 캐시가 아닌 별도 캐시 인프라 (Redis)를 사용 해야할까요?
- 서비스의 부하로 스케일 업 되어 동일 서비스가 10개가 구동중이라고 가정 합니다.
- 각자 서비스는 각자의 메모리에 데이터를 캐싱합니다.
- 조회 요청에 대해서는 캐시 데이터로 응답합니다.
- 데이터 갱신 요청 발생
- 갱신이 발생한 1개의 서비스를 제외한 나머지 9개의 서비스도 갱신이 필요합니다.
- 각자 서비스간 캐싱 데이터 갱신
- 각자 서비스 캐싱 데이터 갱신 완료시간이 서로 다름
- 이때 발생하는 요청들 사이에는 데이터 정합성이 깨지게 됩니다.
- 유저 test의 이름이 test1에서 test2로 갱신
- 갱신이 완료되지 않은 서비스는 갱신전 데이터인 test1을 전달 받습니다
- 갱신이 완료된 서비스는 갱신된 데이터인 test2를 전달 받습니다
자세한 내용은 캐싱을 도입하며 내가 경험 한 것들을 참고 바랍니다.
5. 캐시 데이터의 TTL 사용 이유
캐시 데이터에 TTL을 사용하는 이유는 무엇일까요?
사실 캐시 데이터 라고 해서 무조건 TTL 을 사용해야 하는 이유는 없습니다.
다만 인프라의 스펙은 정해져있으므로 모든 데이터를 캐시 할 수 없습니다.
한정적인 자원을 효율적으로 사용하기 위해 TTL을 사용합니다.
오래된 데이터의 경우 만료 하여 공간 확보를 합니다.
계속 조회되는 데이터의 경우 캐시 갱신을 통해 캐싱을 진행합니다.
6. 캐시의 기본 동작
읽기
- User ID가 test 인 유저를 조회하는 요청
- 캐시에 해당 데이터 존재하는지 확인
- 존재하는 경우
- 캐시 데이터로 요청 응답
- 존재하지 않는 경우
- 다음 스텝
- 존재하는 경우
- DB를 조회하여 데이터를 조회함
- DB에서 조회한 데이터를 캐싱
- 요청 응답
- 캐시에 해당 데이터 존재하는지 확인
쓰기
- Create
- User ID가 test1인 유저를 생성하는 경우
- 유저 생성 로직 수행
- 생성된 유저의 데이터 캐시
- 생성된 유저의 데이터 응답
- User ID가 test1인 유저를 생성하는 경우
- Update
- User ID가 test1에서 test2로 업데이트 되는 경우
- 유저 데이터갱신 로직 수행
- 변경된 유저의 캐시 데이터도 같이 갱신
- 변경된 유저의 데이터 응답
- User ID가 test1에서 test2로 업데이트 되는 경우
- Delete
- User ID가 test1인 유저를 삭제 하는 경우
- 유저의 데이터 갱신 로직 수행
- 삭제된 유저의 캐시 데이터도 삭제
- 삭제된 유저 데이터 응답
- User ID가 test1인 유저를 삭제 하는 경우
7. Write 요청에도 데이터를 캐싱 하는 이유
캐시 전략에는 여러가지 방안이 있습니다.
Read, Write 전략이 따로 있으며, 그 방법또한 여러가지 입니다.
Read 전략의 경우 캐시 조회하고 데이터가 없으면 DB 조회하는 Look-Aside 방식을 많이 씁니다.
Write 전략의 경우 Write-Through 방식을 많이 사용합니다.
Write-Through 방식은 DB의 데이터 갱신시 캐시도 같이 갱신하는 방법입니다.
이외에는 Write-Back, Write-Around 방식이 있습니다.
데이터 요청 종류 중 Delete 요청시에는 캐시에 남아있는 데이터가 반드시 삭제되어야 합니다.
- 데이터 정합성을 위해 삭제가 되어야합니다.
Update, Create시에는 데이터 캐시를 하지 않는 경우 데이터 삭제
- 조회시에만 데이터를 캐시
- 캐시 만료
- Update, Delete 요청
- TTL 만료
Read시에만 데이터를 캐시 하는 경우
- Read 요청이 단기간에 동시에 다발적으로 발생하는 경우
- 캐시 미스가 발생하므로 DB로의 요청이 급증합니다.
Update, Create에서 데이터를 캐시하는 경우
- 대부분의 서비스에서 Create 되지 않은 데이터에 대한 조회 요청의 발생은 적습니다.
- 조회 요청이 생성 이후 급증 하게 되는 경우 다량의 캐시 미스가 발생하지 않습니다.
- Create, Update 요청에 대해 동시성 처리가 되어있는 경우
- 보통의 Create는 insert if not exist의 형태입니다.
- 최초 생성 이후 캐싱 데이터가 존재하여 캐시 미스 발생이 적습니다.
Upate 요청에 대한 캐시
- Update 이전 데이터에 대한 요청이 발생하는 경우는 많을수 있습니다.
- Update 로직이 자주 수행되는 경우 캐싱 데이터도 자주 갱신됩니다.
- 이때 캐시 미스 혹은 갱신 이전 데이터를 받을수도 있습니다.
- 캐싱할 데이터의 날짜를 통해 최신 날짜만 갱신하도록 합니다.
- 동일 데이터 요청이 순차적으로 발생한 경우
- 나중에 들어온 요청이 먼저 수행되어 캐싱 데이터가 이전데이터로 갱신될수있습니다.
- 데이터 갱신시 캐시 데이터가 최신 데이터라면 캐싱을 하지 않습니다.
- 동일 데이터 요청이 순차적으로 발생한 경우
8. 캐싱 도입 이후 발생헀던 문제들
- 캐시 쇄도
- 앞서 말한 키 만료 및 조회 요청이 동시에 몰리는 경우 발생합니다.
- 발생 케이스
- 서비스 요청이 몰리는 시간이 오전 9시 이고, TTL 이 24시간 인 경우
- 다음날 9시에 다시 요청이 몰릴때 대량으로 캐시 만료되면서 캐시 미스가 다량으로 발생합니다.
- 이로 인해 DB에 요청이 몰리게 되며 DB의 부하가 증가합니다.
- 해결 방법
- 캐싱의 TTL을 값을 동일 값이 아닌 서로 미세하게 조정합니다.
- 24시간이 아니라 23시간 58분, 24시간 2분 이런식으로 미세하게 조정합니다
- 이렇게 되면 만료가 동시에 되지 않기 때문에 캐시 미스 발생 시간이 달라 DB에 요청이 몰리는 시간이 달라집니다.
- 캐싱의 TTL을 값을 동일 값이 아닌 서로 미세하게 조정합니다.
- 핫키 만료
- 핫키의 경우 조회가 많이되는 키를 말합니다
- 키 만료시 다량의 캐싱 미스가 발생해 DB 부하가 증가합니다
- 핫키의 경우 인지가 가능
- 핫키 만료 직전에 TTL을 갱신하는 방법이 있습니다.
- Prefix를 통해 같은 데이터라도 분산저장하여 만료에 대한 대처를 하는 방법이 존재합니다
- 캐싱 데이터가 없는 경우 캐싱 미스
- 데이터가 존재하지 않는 경우 캐시 미스 발생
- 다수의 요청에서 존재하지 않는 데이터를 조회하는 경우 캐시 미스 발생
- DB로의 요청이 증가
- 해결
- 데이터가 존재하지 않는 경우 "데이터 없는 상태"를 캐싱하여 캐시 미스가 발생하지 않도록 합니다
- 이경우 존재하지 않는 모든 데이터에 대한 처리를 할수 없습니다
- 존재하지 않는 데이터를 조회 하는 경우 만을 고려하여 캐싱 하도록 합니다.
- 데이터가 존재하지 않는 경우 캐시 미스 발생
- 캐시 장애
- 데이터 조회간 캐싱 시스템에 장애가 발생할수 있습니다
- 캐시 장애시 요청 자체의 fail 처리
- 이경우 client 단에서 재요청을 해야합니다
- 장애 복구가 단기간에 이뤄진다면 client에서 설정된 재시도 횟수 안에 정상 응답을 받습니다
- 장애 복구에 시간이 걸린다면 결국 DB를 조회하도록 해야합니다
- 캐시 장애시 요청 자체의 fail 처리
- 캐시 장애시 DB로 조회
- 앞단에 캐시가 없으므로 DB로의 요청이 증가하게 됩니다.
- 해결
- Redis의 경우 클러스터링을 통해 장애를 어느 정도 극복이 가능합니다
- master 노드 다운시 slave노드가 master로 승격되어 장애에 대한 대응이 가능합니다
- Redis의 경우 클러스터링을 통해 장애를 어느 정도 극복이 가능합니다
- 데이터 조회간 캐싱 시스템에 장애가 발생할수 있습니다
- 크기가 큰 데이터 저장시 문제
- 크기가 큰 데이터를 캐시 처리하는 경우 성능이 저하됩니다.
- 꼭 필요한 데이터만 선별하여 저장합니다
- 데이터 압축을 통해 조회 성능을 개선합니다.
- 크기가 큰 데이터를 캐시 처리하는 경우 성능이 저하됩니다.
9. Redis RedLock의 한계
캐시와 관련된 내용은 아니지만 Redis 분산락을 사용하면서 겪은 문제에 대해 공유합니다.
- Redis의 TTL은 timestamp를 기반으로 동작합니다.
- Redis standalone 구성이 아닌 cluster구성
- Lock 사용시 Timstampe 점프 발생
- Lock 사용시 TTL 을 같이 사용하여 무제한 Lock 점유 불가 하도록 합니다.
- 노드간 과반수의 Lock 점유 여부를 통해 Lock/Unlock을 결정 합니다.
- 이때 한개의 노드에서 timestamp가 점프 하여 TTL 만료된 경우
- 다른 노드에서 Lock을 점유 합니다.
- 과반수의 노드에서 Lock을 얻게 됩니다.
- 두개의 서비스에서 서로 다른 락을 점유 함으로 인해 동시 데이터 수정시 정합성 문제 발생할 수 있습니다.
- 해결
- 동일 데이터인 경우 중복 insert등의 문제는 db의 유니크키로도 해결할수 있습니다.
- Redis cluster 구성시 cordinater인 zookeeper를 통해 이를 해결합니다.
- zookeeper는 뗏목 알고리즘과 같은 합의 알고리즘을 통해 분산 환경에서 데이터 정합성을 보장합니다.
10. DNS 캐시와 클러스터 정보 갱신 (Cluster Tophology Refresh Option)
Redis 노드 정보가 DNS인 경우
노드만 교체되고 같은 DNS를 사용하는 경우 이전 노드의 IP로 접속하는 문제가 발생할 수 있습니다.-Dnetworkaddress.cache.ttl=10
위의 옵션을 통해 새로 변경된 IP로 접근이 가능하도록 DNS 캐싱 시간을 조절합니다.
Redis 클러스터 구성에서 노드 정보가 변경되는 경우
토폴로지 정보 갱신 옵션을 통해 클러스터 구성 변화를 감지하고 갱신하도록 합니다.
Spring에서 Lecttuce를 사용하는 경우 ClusterTopologyRefreshOptions
옵션을 통해 설정합니다.
11. 마무리
여기까지 좌충우돌 캐싱 도입 이후의 여정을 보셨습니다.
캐시 도입 이후 응답속도의 개선, 처리율 증가등의 이점을 보았지만 그만큼 많은 문제가 발생하였습니다.
이 글을 읽으시는 모든 분들 모두 성공적인 캐시 도입을 하시기 바라면서 이상 포스팅을 마칩니다!
긴글 읽어주셔서 감사합니다! 다음 포스팅에서 또 뵙겠습니다!
'TroubleShooting' 카테고리의 다른 글
Mongodb를 사용하며 내가 경험 한 것들 (0) | 2024.07.07 |
---|---|
캐싱을 도입하며 내가 경험 한 것들 (0) | 2024.07.05 |
댓글