Java 아키텍처·Spring 용어 사전
캐시

캐시 어밸런치

많은 키가 한꺼번에 만료되거나 Redis 자체가 내려가 부하가 통째로 DB에 쏠린다.

스탬피드가 키 하나의 문제라면 어밸런치는 캐시 전체가 한꺼번에 빠지는 것이다. 부하가 통째로 DB 로 쏠린다.

두 가지 경로

① 많은 키가 동시에 만료된다

배포 직후나 캐시 워밍업 때 수만 개 키를 한꺼번에 채우면 TTL 도 같이 걸린다. 정확히 TTL 뒤에 그것들이 한꺼번에 만료된다. 캐시를 미리 채워 두려던 조치가 시한폭탄이 되는 셈이다.

더 고약한 것은 주기가 생긴다는 점이다. 다시 채운 것들도 또 같이 만료되므로, TTL 간격으로 DB 부하가 규칙적으로 치솟는다. "매시 정각마다 느려진다" 는 증상이 이 모양이다.

→ 채울 때부터 TTL 에 지터를 준다. 스탬피드와 같은 처방이지만 목적이 다르다 — 저기서는 한 키를 노리는 요청들을 흩뜨리는 것이고, 여기서는 키들끼리 만료 시각이 겹치지 않게 하는 것이다.

② Redis 자체가 내려간다

노드 장애, 메모리 초과, 네트워크 단절, 잘못된 FLUSHALL. 캐시 적중률이 순간 0 이 되고 평소 캐시가 받아 내던 트래픽 전부가 DB 로 간다.

여기서 계산해 볼 것이 있다. 적중률 95% 로 초당 1만 요청을 받고 있었다면 DB 는 초당 500건만 보고 있었다. 캐시가 빠지는 순간 초당 1만 건이 된다. 20배다. 캐시가 잘 듣고 있었을수록 배수가 크다.

무엇을 보고 알아채나

터진 다음에 원인을 찾으면 늦다. 평소에 봐야 할 것은 셋이다.

지표무엇을 말하나
캐시 적중률갑자기 떨어지면 어밸런치가 시작된 것이다
evicted_keys계속 오르면 메모리가 부족해 조용히 캐시가 비어 가는 중이다
DB 커넥션 풀 대기캐시에서 새어 나온 부하가 DB 에 닿았다는 신호다

특히 적중률과 DB 부하를 같은 화면에 그려 두면 인과가 눈에 보인다. 적중률이 떨어진 시각과 DB 지연이 오른 시각이 겹치면 진단이 끝난다.

대비

  • 복제·클러스터로 단일 장애점을 없앤다 — 마스터가 죽어도 복제본이 승격한다
  • 로컬 캐시를 한 겹 둔다 — Redis 가 없어도 자주 쓰는 것은 JVM 안에서 막아 준다. 어밸런치 때 완충재가 된다
  • DB 앞에 문을 단다 — 동시 호출 수를 제한해(벌크헤드) DB 가 통째로 무너지는 대신 초과분만 실패하게 한다. 서킷 브레이커로 회로를 열어 빠르게 실패시키는 것도 같은 발상이다. 전부 죽는 것보다 일부가 죽는 편이 낫다
  • 복구를 서서히 한다 — 캐시가 돌아왔다고 한꺼번에 다시 채우면 그 자체가 다시 부하다
  • 캐시 없이 견딜 수 있는지 알고 있어야 한다 — Redis 를 끊고 부하를 걸어 보는 것이 유일하게 확실한 확인이다

워밍업이 곧 위험이다

배포 때 캐시를 미리 채우는 것은 좋은 관행이지만, 채우는 방식이 곧 다음 어밸런치의 원인이 된다. 한꺼번에 채우면 한꺼번에 만료되고, 채우는 과정 자체가 DB 에 대량 조회를 날린다. 나눠서 천천히, TTL 을 흩뜨려서 채워야 한다.

캐시를 어디까지 믿을 것인가

캐시 어사이드가 "캐시가 죽어도 DB 로 동작이 유지된다" 는 장점을 갖는다고 하지만, 그것은 기능이 유지된다는 뜻이지 용량이 유지된다는 뜻이 아니다. 어밸런치는 정확히 그 차이가 드러나는 순간이다.

캐시를 전제로 DB 용량을 잡은 순간, 캐시는 "있으면 좋은 것" 이 아니라 필수 경로가 된다. 그렇다면 캐시도 DB 만큼의 가용성 설계를 받아야 한다.

면접 함정

  • "캐시는 있으면 좋은 것이라 없어도 된다" → 위와 같다. 용량 산정에 캐시를 넣었다면 필수 구성 요소다.
  • "복제를 붙였으니 안전하다" → 동시 만료로 인한 어밸런치는 Redis 가 멀쩡해도 일어난다.
  • "미리 캐시를 채워 두면 안전하다" → 채우는 방식이 나쁘면 그것이 원인이 된다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 캐시·Redis — 어사이드·쓰기 전략