Key expiration (TTL 만료 메커니즘)

Redis는 TTL이 설정된 키를 즉시 삭제하지 않습니다. 대신 두 가지 방식을 조합해서 삭제합니다.

1. Passive (수동) - Lazy Expiration

원리:

  • 키에 접근하는 순간에 만료 여부를 체크

  • 만료되었으면 그때 삭제하고 "키가 없다"라고 응답

# 5초 TTL 설정
SET mykey "hello" EX 5

# 6초 후 (이미 만료됨)
GET mykey
# Redis: "앗, 이 키 만료됐네? 지우고 nil 반환!"# 결과: (nil)

핵심:

  • 아무도 안 읽었으면 지워지지 않음

  • 메모리에는 여전히 존재할 수 있음

  • CPU 효율적 (필요할 때만 체크하기 때문)

2. Active (능동) - Active Expiration

원리:

  • Redis가 백그라운드에서 주기적으로 만료된 키를 찾아서 삭제

  • 모든 키를 다 체크하면 너무 느리니까 랜덤 샘플링 사용

    • 즉, Redis는 백그라운드에서 주기적으로(ex: 초당 10회) 무작위로 만료 시간이 설정된 키들을 샘플링하여 만료된 키를 삭제합니다.

  • 해당 방식은 메모리를 점진적으로 회수하지만, 모든 만료 키를 즉시 찾아내지 못합니다.

    • 따라서 만료 시간이 지난 키가 잠시 동안 메모리에 남아있을 수 있습니다.

동작 방식

Redis의 능동 만료는 정해준 주기마다 실행되며, 한 번 실행될 때마다 다음 단계로 동작합니다.

  1. 실행주기: hz 설정값

Redis 설정 파일인 redis.conf에는 hz라는 값이 존재합니다. 이 값은 1초에 백그라운드 작업(예: 만료 키 삭제)을 몇 번 실행할지를 결정하며, 기본값은 10입니다.

  • activeExpireCycle 함수는 Redis 내부에서 만료된 키들을 주기적으로 탐색하여 삭제하는 역할을 합니다.

  • hz 10 (기본값): 초당 10번 실행 (약 100ms마다 한 번)

  • hz 100: 초당 100번 실행 (더 자주, 더 촘촘하게 검사하지만 CPU 사용량 증가)

  1. 샘플링: 무작위 키 검사

activeExpireCycle 함수가 한 번 실행될 때마다 TTL이 설정된 모든 키를 검사하지 않습니다.

모든 키를 검색하게 된다면 CPU가 100%로 치솟을 확률이 높아, 다음과 같은 방식으로 샘플링을 진행합니다.

  • 만료 시간이 설정된 키들 중에서 무작위로 20개의 키를 뽑아서 검사합니다.

  • 만료 시간이 지났다면 해당 키를 즉시 삭제합니다.

  1. 효율적인 검사 루프: 만료된 키가 많으면 더 많이 작업하기

단순히 20개(기본 값)만 검사하고 끝나면 만료된 키를 삭제하는 데 효율이 떨어질 수 있습니다.

만약 대량으로 쌓여있다면 더 많은 작업을 해야 하기 때문에 Redis는 동적으로 active expiration cycle을 갖습니다.

  • 각 검사 루프(Loop)에서는 현재 DB의 만료 키의 크기에 따라 샘플링 개수를 동적으로 제한합니다.

    • 뽑은 20개의 키 중에서 25% 이상(5개 이상)이 만료되었다면, “아직 만료된 키가 많군!”라고 판단하고 바로 다음 샘플링(20개)를 다시 시작합니다.

    • 만약 25% 미만이 완료되었다면, 루프를 멈추고 다음 hz 주기를 기다립니다.

  1. CPU 보호 장치: 작업 시간제한

만료된 키가 너무 많아서 루프가 계속 돈다면, 백그라운드 작업이 너무 오래 실행되어 메인 작업을 방해할 수 있습니다. 이를 방지하기 위해 시간제한이 설정되어 있습니다.

  • activeExpireCycle 함수는 한 번 실행될 때마다 정해진 CPU 시간 이상을 사용하지 않도록 설계되어 있습니다. 기본적으로 hz 주기에 할당된 시간의 25%를 넘지 않도록 제한됩니다.

    • 예를 들어 hz가 10이면 한 주기는 100ms이고, 이 중 25ms가 지나면 만료된 키가 더 남아있더라도 일단 작업을 중단하고 메인 스레드에 제어권을 넘겨줍니다. 남은 작업은 다음 주기에 처리됩니다.

  • 이렇게 샘플링 루프는 일정 시간제한(예:1ms)이나 만료 작업 시간 초과 조건에 따라 중단되어, Redis의 응답성 저하를 방지합니다.

예시:

Active 방식에서 지금처럼 스마트한 샘플링 방식이 없다면?

만약 Redis의 Active 만료 정책이 ‘100ms마다 만료된 키가 있는지 모든 키를 전부 확인한다.’라는 방식이었다면, CPU 사용률이 100%에 도달할 가능성이 존재합니다.

  1. 상황: 1억 개의 키 존재

  • Redis에 1억 개의 키가 저장되어 있고, 그중 상당수에 TTL이 설정되어 있습니다.

  1. Active 만료 사이클 시작

  • 정해진 주기(100ms)가 되어 백그라운드에서 만료 키를 정리하는 작업이 시작됩니다.

  1. 문제: 전체 스캔

  • Redis는 존재하는 1번 키부터 1억 번 키까지, 만료 시간이 지났는지 전부 확인하는 작업을 수행합니다.

  1. 결과: CPU 폭증

  • 이 1억 번의 체크가 끝날 때까지 엄청난 CPU 자원이 소모되고, 작업이 끝나기 전까지 Redis는 다른 어떤 클라이언트의 요청도 처리하지 못하고 멈춰버릴 수 있습니다.

하이브리드 방식으로 설계한 이유

Redis의 목표는 빠른 속도와 효율적인 메모리 관리 사이에서 최적의 균형을 맞추는 것입니다.

두 방식을 하이브리드로 사용하지 않고 하나만 선택한다면 균형이 깨질 수 있습니다.

🤔 수동(Passive) 방식만 사용한다면? 키를 읽을 때만 만료 여부를 체크한다면 어떻게 될까요?

상품을 판매하는 커머스 서비스에서 ‘1분간 유효한 구매 임시 토큰’을 Redis에 저장한다고 가정해 보겠습니다. 이 토큰은 재고를 1분간 확보하는 역할을 합니다.

  1. 토큰 생성

  • 사용자가 ‘구매하기’ 버튼을 누르면, 서버는 user:123:purchase_temp_token이라는 key를 60초 TTL로 설정하여 Redis에 저장합니다.

  1. 문제 발생

  • 해당 사용자가 갑자기 인터넷 연결이 끊어지거나 앱을 강제로 종료해서, 1분 안에 최종 결제 요청을 보내지 못했습니다. 즉. user:123:purchase_temp_token 키에 아무도 다시 접근하지 않습니다.

  1. 아무도 안 읽는 만료 키들이 메모리에 쌓임 → 메모리 낭비

  • 60초가 지나 키는 만료되었지만, Passive 방식만으로는 아무도 조회하지 않으니 키가 삭제되지 않습니다.

  • 이러한 상황이 수천, 수만 번 발생하면 만료된 키들이 삭제되지 않은 가비지 데이터가 메모리에 계속 쌓입니다.

결국, Passvie 방식만 사용하면 아무도 접근하지 않는 만료된 키가 삭제되지 않아 결국 메모리 누수로 이어질 수 있습니다.

🤔 능동(Active) 방식만 사용한다면? 백그라운드에서 만료 키를 삭제하는 방식만 쓰면 어떻게 될까요?

긴급 공지사항을 ‘정확히 10초 후에’ 사라지게 만드는 시스템을 개발한다고 가정해 보겠습니다. 10초가 지난 후에는 이 공지사항이 절대 노출되면 안 됩니다.

  1. 공지 토큰 생성

  • notice:critical 키를 10초 TTL로 Redis에 저장합니다.

  1. 문제 발생

  • 10초가 지났지만 Active 방식은 100ms마다 무작위로 샘플링하기 때문에 notice:critical 키를 발견하지 못할 수 있습니다. (여기서는 발견하지 못한 것으로 가정합니다.)

  • 10.1초가 지난 시점, 즉 만료 시간이 지났음에도 키는 아직 메모리에 남아있는 상황입니다.

  • 이때, 어떤 사용자가 해당 키를 가지고 공지사항을 조회합니다.

  1. 잘못된 데이터 제공

  • Redis에서는 Passive 방식이 없으므로, 키에 접근하는 순간 만료 여부를 체크할 수 없습니다.

  • Redis 메모리에는 키가 아직 남아있으므로, 이미 삭제되었어야 할 오래된 데이터를 사용자에게 반환합니다. 사용자는 잘못된 정보를 보게 됩니다.

즉, Active 방식만 사용하면 샘플링 과정에서 놓친 만료 키에 클라이언트가 접근했을 때, 삭제되었어야 할 오래된 데이터를 그대로 응답받는 데이터 부정합 문제가 발생할 수 있습니다.

두 방식을 조합한다면,

  • Passive: 읽을 때 확실히 걸러냄 (정확도 ↑)

  • Active: 아무도 안 읽는 키도 주기적으로 정리 (메모리 ↑)

마무리

Passive 방식은 클라이언트에게 만료된 데이터가 넘어가는 것을 철저히 막아 데이터의 정확성을 보장합니다. 동시에 Active 방식은 아무도 찾지 않는 만료 키들을 꾸준히 정리하며 메모리 누수를 방지합니다.

어느 하나만으로는 메모리 누수나 데이터 부정합이라는 심각한 문제를 피할 수 없습니다. 이 두 방식이 서로의 단점을 보완하며 협력하기 때문에, Redis는 빠르고 효율적인 인메모리 데이터 저장소로 활용할 수 있다고 생각했습니다.

따라서 Redis의 TTL을 사용할 때는 "정확히 n초 후에 삭제된다"가 아니라, "최소 n초의 생존을 보장하며, 그 이후에 효율적인 방식으로 삭제된다"라고 이해하는 과정이었습니다.

Last updated