Redis Timeout: Key 동시 만료 문제
StackExchange.Redis.RedisTimeoutException: Timeout awaiting response
이러한 문제가 발생했을 때, redis-cli에서 slowlog와 latency 명령을 확인해보면
redis-cli> SLOWLOG GET 10
redis-cli> LATENCY DOCTOR“Latency generated by expires”라는 문구를 확인할 수 있습니다.
이는 Redis에서 동시에 많은 키가 만료되어 발생하는 Timeout 문제로, 레디스가 기본적으로 passive와 active 방식을 혼합해 만료 키를 삭제하지만, 동시에 대량의 키가 만료되면 키 삭제 작업으로 인해 Redis가 일시적으로 블로킹되어 명령 수행이 지연될 수 있기 때문입니다.
특정 시점에 대량의 키가 동시에 만료되면 Redis 내부에서 블로킹 삭제 작업이 길어져 명령 응답이 지연됩니다.
StackExchange.Redis에서 타임아웃 에러(Timeout awaiting response)는 바로 이 지연 때문일 가능성이 큽니다.
왜 Redis가 블로킹?
레디스는 메모리 효율을 위해 수동/능동 만료를 혼합해 만료된 키를 삭제합니다.
핵심 문제는 Active Expiration이 모든 만료 키를 즉시 삭제하는 것이 아니라, ‘무작위 샘플링’ 방식으로 삭제합니다. 만약 특정 시간에 수만 개의 키가 동시에 만료되도록 설정되어 있다면, Active Expiration은 그중 일부만 처리하고 대다수를 놓치게 됩니다.
이후, 남겨진 대량의 만료 키들은 클라이언트의 접근(Passive)에 의해 한꺼번에 삭제되거나 다음 Active Expiration 주기에 발견되면서, Redis의 메인 스레드를 긴 시간 동안 점유(Blocking) 하여 다른 모든 명령어 처리를 지연시키고 결국 타임아웃을 유발합니다.
요약하자면:
Active expire 작업이 길어져 메인 스레드가 블로킹됨
다른 명령어 처리가 지연됨
클라이언트에서 Timeout 발생
해결 방안
1. TTL Jitter 추가
이 방법은 TTL에 랜덤 지터(jitter)를 추가해 만료 시간을 분산시켜 동시 만료를 방지하는 방법입니다.
키의 만료 시점을 분산시켜 Active Expiration이 무리 없이 처리할 수 있도록 만듭니다. 이는 특정 시점에 Redis가 블로킹되는 현상을 크게 줄여줍니다.
예를 들어, 60초 만료라면 60~90초 사이의 랜덤 값을 TTL로 지정해 여러 키가 동시에 만료되지 않도록 합니다.
2. 선제 캐시 갱신 (Proactive Refresh)
이 방법은 ‘대량 만료’ 문제보다는 ‘캐시 스탬피드’ 문제를 해결하는 데 더 특화된 전략입니다.
캐시 스탬피드 방지가 필요할 때
인기 있는 핫 데이터에 캐시 미스가 발생하면 안 될 때
데이터 생성 비용이 매우 높을 때(복잡한 쿼리, 외부 API 호출 등)
캐시 스탬피드란(Cache Stampede)?
매우 인기 있는(Hot) 데이터의 캐시가 만료되는 순간, 수많은 요청이 동시에 캐시 미스를 발생시켜 원본 DB로 몰려가 과부하를 일으키는 현상입니다.
선제 갱신은 TTL이 만료되기 약간 전에, 별도의 백그라운드 작업이 미리 캐시를 새로운 값으로 갱신합니다.
이렇게 하면 실제 사용자 요청은 캐시 미스를 겪을 확률이 낮아지게 됩니다.
🧐 다중 키 만료 문제를 해결하기 위해 재갱신 될 필요가 없는 키를 갱신해야 할 필요가 있을까?
만료 후 다시 생성하거나 사용할 필요가 없는 키라면, 재갱신(TTL을 연장하는 것)은 불필요하며 오히려 리소스 낭비로 이어질 수 있습니다.
이런 키는 애초에 TTL을 랜덤하게 지정하는 지터 방식을 사용하거나, 만료 시점 분산을 설계할 때 재생성 여부를 고려해 보는 것이 좋지 않을까라는 생각했습니다.
선제 갱신 장단점:
장점: 캐시 미스로 인한 동시 다발적 원본 요청을 방지해 시스템 안정성 및 응답 속도를 개선합니다.
단점: 캐시 갱신이 필요 없는 데이터까지 불필요하게 갱신될 수 있으며, 갱신 작업이 추가 리소스를 사용합니다.
그래서 선제 갱신은 주요 인기(핫) 데이터에 대해 TTL 만료 전에 백그라운드에서 정기적으로 갱신하는 전략으로, 접근 빈도가 매우 높은 특정 데이터에 선택적으로 적용하는 것이 좋습니다.
3. 명령어 최적화
GETDEL, UNLINK와 같은 명령어 호출 빈도를 조절해 Redis 부하를 줄일 수 있습니다.
애플리케이션 로직에서 명시적으로 키를 삭제할 때, DEL 대신 UNLINK를 사용하면 Redis의 블로킹을 줄일 수 있습니다.
DEL: 동기 방식키와 관련된 메모리 해제가 완료될 때까지 메인 스레드를 블로킹합니다.
값이 큰 키(예: 수만 개의 아이템을 가진 List, Hash)를 삭제할 때 지연이 길어질 수 있습니다.
UNLINK: 비동기 방식키 목록에서 해당 키를 즉시 제거하고, 실제 메모리 해제 작업은 별도의 백그라운드 스레드에 위임합니다.
따라서 메인 스레드의 블로킹을 최소화하여 전반적인 응답성을 높입니다.
정리
문제 상황
추천 해결책
특정 시간에 대량의 키가 만료되어 타임아웃 발생
TTL Jitter
인기 있는 캐시 만료 시 DB 부하 급증
선제적 캐시 갱신
큰 사이즈의 컬렉션 키를 자주 삭제해야 할 때
UNLINK 명령어 사용
데이터를 읽어온 직후 안전하게 삭제해야 할 때
GETDEL 명령어 사용
Last updated