본문으로 건너뛰기
개발 머꼬
개발 노트Redis
hohyeon.dev21

TTL을 걸었는데 메모리가 줄지 않아 당황한 이유

  • #Debugging
  • #Engineering Note
  • #Redis

문제 발생

캐시 키에 전부 TTL을 걸었는데 used_memory가 기대만큼 떨어지지 않았습니다. TTL로 확인하면 만료된 키인데, 메모리 사용량은 한참 뒤에야 줄었습니다.

반대의 사고도 있었습니다. INCR로 카운터를 올리던 키가 어느 날부터 영원히 남기 시작했습니다.

원인 분석

두 가지 서로 다른 동작입니다.

첫째, 만료는 즉시 회수가 아닙니다. Redis 문서의 "How Redis expires keys" 절이 방식을 설명합니다 — 키는 수동(passive) 방식과 능동(active) 방식 두 가지로 만료된다. 클라이언트가 접근하려 할 때 시간이 지난 키는 수동으로 만료된다. 그러나 다시 접근되지 않을 만료 키들도 있으므로, 주기적으로 Redis는 만료가 설정된 키들 중 무작위로 몇 개를 검사해 이미 만료된 키를 키스페이스에서 삭제한다.

즉 만료 시각이 지났다고 그 순간 메모리가 반환되지는 않습니다. 아무도 접근하지 않는 키는 표본 추출에 걸릴 때까지 남습니다.

둘째, 명령에 따라 TTL이 유지되거나 사라집니다. 문서가 정확히 구분합니다 — 타임아웃은 키의 내용을 삭제하거나 덮어쓰는 명령(DEL, SET, GETSET, 모든 *STORE 명령)에 의해서만 지워진다. 키에 저장된 값을 새 값으로 대체하지 않고 개념적으로 변경하는 모든 연산은 타임아웃을 그대로 둔다. INCR, LPUSH, HSET이 그 예로 명시돼 있습니다.

그래서 SET key value로 값을 갱신하는 순간 TTL이 사라집니다 — 카운터가 영원히 남은 이유입니다.

해결 방안

  1. 값을 갱신하면서 TTL을 유지하려면 KEEPTTL을 씁니다.
SET cache:user:1 "..." KEEPTTL
  1. 갱신할 때마다 만료를 다시 겁니다. 세션처럼 "마지막 활동 이후 N분"이 필요하면 매 접근마다 EXPIRE를 새로 걸어 슬라이딩 만료로 만듭니다.

  2. 조건부 만료 옵션을 활용합니다. EXPIRENX(만료가 없을 때만), XX(있을 때만), GT/LT(더 크거나 작을 때만)로 의도를 코드에 드러냅니다. 문서 설명대로 non-volatile 키는 GT 판단에서 무한 TTL로 취급됩니다.

  3. 메모리 상한과 정책을 함께 정합니다. 만료만으로 메모리를 통제하려 하지 않습니다. maxmemory와 eviction 정책이 실제 안전장치입니다.

  4. 시계에 의존한다는 점을 인지합니다. 문서 경고대로 만료는 절대 유닉스 타임스탬프로 저장되므로 컴퓨터 시각이 크게 어긋나면 이상한 일이 벌어집니다 — 서버 시각을 미래로 옮기면 키가 즉시 만료됩니다.

  5. 복제 환경에서는 마스터가 판단합니다. 문서 설명대로 키가 만료되면 마스터가 DEL을 합성해 AOF와 복제본에 전파하고, 복제본은 스스로 만료시키지 않고 그 DEL을 기다립니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.