Redis TTL 만료

7/15/2025

Redis 의 TTL 의 관리는 크게 두가지 방식으로 관리된다.

만료 키 관리 방식

  • 수동적 만료(게으른 만료: Lazy Expiration)

      클아이언트가 키를 조회하거나 접근할 때, 만료 시간을 체크하여 지났다면 그 시점에 삭제하고, nil(없음, null 과 동일)을 반환한다.

  • 능동적 만료(능동 만료: Active Expiration)

      Redis 내부적으로, 만료된 키를 주기적으로 스캔하여 삭제한다. 이때 백그라운드에서 일정 주기로 만료된 키의 일부를 샘플링하여 조회하고, 만료된 데이터는 삭제한다.

      기본적으로 메인 이벤트 루프에서 약 100ms 마다 한 번 만료된 키를 찾는 작업을 실행한다. 이 때 만약 너무 많은 만료 키가 쌓여있다면 한번 더 반복적으로 검사한다.

  • Redis 가 TTL 에 대해서 이렇게 관리하는 이유는 성능과 효율을 둘 다 잡기 위한 트레이드오프 때문이라고 한다.

    이유는 어느 한쪽만 사용한다고 했을 때를 예시로 들어 보면 알 수 있다.

    능동적 만료만 쓴다면?

    능동적 만료만 사용하면서 TTL 에 맞게 관리하려면, 모든 키에 대해 만료되는지에 대해서 주기적으로 스캔하여 만료시점에 삭제처리 해주어야 한다. 즉 전체 키에 대해서 아주 빠른 시간 내에 반복적으로 체크해주어야 하는 것이다. 아무리 특정지어 만료 가능성이 있는 키들을 샘플링하여 체크한다고 하더라도, 꽤나 많은 키에 대해서 지속적으로 체크하는 로직이 필요하게 되고, 이는 성능상에 영향을 줄 수 있다. Redis 는 기본적으로 싱글 스레드로 동작하기 때문에 이런 지속적이고 반복적인 다량의 키 체크하는 부분은 성능에 영향을 줄 가능성이 있다.

  • 모든 만료 키를 주기적으로 스캔하여 삭제하여 TTL 을 관리하는 방식은 잦은 조회처리로 CPU, I/O 자원을 낭바한다.
  • 기본적으로 싱글 스레드 구조인 Redis 에 대해서 이런 다량의 데이터 조회처리는 관리되는 데이터가 늘어날 수록 실제 서비스 성능에 저하로 이어질 수 있다.
  • 수동적 만료만 쓴다면?

    그럼 접근 시에만 데이터가 삭제되도록 하는 수동적 만료만 사용한다면, 한번 생성되고 추가적인 조회가 없는 데이터에 대해서는 TTL 이 만료되었더라도 지속적으로 메모리에 남을 수 있다는 문제가 발생한다. 이는 메모리 누수(memory leak) 문제로 이어지기 때문에 관리되는 메모리에 대해 최적화 하기 위해 능동적 만료가 같이 운영되는것이 맞다.

    TTL 잘 관리되는게 맞을까?

    뭔가 방식을 통해 TTL 이 잘 관리되는지 의문이 들 수 있다. 나 도한 조금 의문이 들긴한데, 명확하게 TTL 을 통해 이벤트 처리를 하는 pub/sub 이벤트 처리를 하는게 아니고, 데이터의 만료 시점으로 특정 시간 동안의 만료가 잘 되는가를 따진다면 문제없이 동작한다고 볼 수 있다.

    이는 만료 이벤트 처리를 통한 pub/sub 처리를 하게 된다면, 수동적 만료되지 않는 능동적 만료 처리에서 일부 오차가 발생할 수 있기 때문이다. TTL 을 통해 정확한 시간으로 처리되어야 하는 로직이라면 문제가 있을 순 있다.

    능동적 만료

    능동적 만료와 수동적 만료라는 방식으로 TTL 에 대해 잘 관리되지만, 보통 ms 단위로 잘 삭제되긴 하나, 싱글 스레드로 동작하는 Redis 에 대해 관리되는 만료 키가 많거나, 현재 부하가 많다면 수십 초까지 남이있는 경우도 드물게 발생할 수 있다.

    그럼 능동적 만료는 내부적으로 얼마나 자주 일어 나는가?

    이런 내용까지 충분히 궁금할 수 있다고 생각한다. 수동적 만료는 어떻게 보면 간단한데, 능동적 만료는 그럼 어떻게 동작하며 얼마나 자주 일어날까? (이런 부분을 알아야 어떻게 성능에 영향이 없는지까지 알 수 있으니까)

    위에도 언급했듯이 약 100ms 주기로 만료된 키를 찾기 위한 작업을 실행한다. 이에 대한 조금의 상세 설명이 있는 Redis 의 만료 시점처리하는 소스코드 내용을 참고해보는 것도 좋다.

    Unsupported block type: link_preview

    주석 본문
    Try to expire a few timed out keys. The algorithm used is adaptive and
    will use few CPU cycles if there are few expiring keys, otherwise
    it will get more aggressive to avoid that too much memory is used by
    keys that can be removed from the keyspace.
    
    Every expire cycle tests multiple databases: the next call will start
    again from the next db. No more than CRON_DBS_PER_CALL databases are
    tested at every iteration.
    
    The function can perform more or less work, depending on the "type"
    argument. It can execute a "fast cycle" or a "slow cycle". The slow
    cycle is the main way we collect expired cycles: this happens with
    the "server.hz" frequency (usually 10 hertz).
    
    However the slow cycle can exit for timeout, since it used too much time.
    For this reason the function is also invoked to perform a fast cycle
    at every event loop cycle, in the beforeSleep() function. The fast cycle
    will try to perform less work, but will do it much more often.
    
    The following are the details of the two expire cycles and their stop
    conditions:
    
    If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
    "fast" expire cycle that takes no longer than ACTIVE_EXPIRE_CYCLE_FAST_DURATION
    microseconds, and is not repeated again before the same amount of time.
    The cycle will also refuse to run at all if the latest slow cycle did not
    terminate because of a time limit condition.
    
    If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
    executed, where the time limit is a percentage of the REDIS_HZ period
    as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the
    fast cycle, the check of every database is interrupted once the number
    of already expired keys in the database is estimated to be lower than
    a given percentage, in order to avoid doing too much work to gain too
    little memory.
    
    The configured expire "effort" will modify the baseline parameters in
    order to do more work in both the fast and slow expire cycles.
    

    내용을 보고, 조금 더 쉽게 설명하면 다음과 같이 동작한다.

    TTL 만료된 메모리를 삭제하는 능동적 만료에 대해 청소라고 생각한다면, 이 내용은 다음과 같이 정리할 수 있다.

    똑똑한 청소부(적응형 알고리즘)

    개별 판단 기준에 따라 청소 강도를 조절하여 적응형 알고리즘이라 한다.

  • 이 청소부는 집(Redis)에 청소할 데이터(TTL 만료 키)가 별로 없다면, 설렁설렁하기도 하고, 청소할게 많다면 적극적으로 청소하기도 한다. (CPU 사용량 조절)
  • 이 때 설렁설렁한다는 의미는 청소를 최소한으로 작업하는, 청소 진행을 조기 종료한다는 의미이다. 이는 또 이후에 따로 다루자.
  • 여러 방을 순서대로 청소(여러 데이터베이스 확인)

  • Redis 는 데이터를 여러 방(Database) 에 나눠 저장하는데, 이 모든 방은 한번에 청소하는게 아닌 정해진 개수만큼만 방을 옮겨 다니며 청소한다. 다음번 청소는 이전 청소했던 다음 방부터 시작한다.
  • 청소 방식(주기)

    이 청소 방식은 특정 주기마다 가 청소 방식이 결정되는게 아니라, 각 청소 방식(주기)는 독립적으로 처리되고 있다고 보면 된다.

  • 느린 주기(메인청소, Slow Cycle)
    • 정기적으로 하는 대청소라고 보면된다. 1초에 10번 정도(10Hz) 니까, 100ms 라고 보면 된다.
    • 최대한 많이 청소(만료된 키 삭제)하는 걸 목표로 한다.
    • 이 과정에서 너무 많은 청소(만료 키 삭제)가 있다면 오래걸리므로 타임아웃이 발생한다.(특정 타임아웃 시점까지 만 청소한다는 뜻)
  • 빠른 주기(틈새청소, Fast Cycle)
    • 틈틈히 하는 간단한 청소라고 보면 된다. 느린주기(메인청소)에서 처리하지 못한 부분이 있을 수 있기 때문에, 평소에 짧은 시간 동안만 빠르게 처리한다. 이 처리는 특정 주기라기 보단 클라이언트 요청을 처리하고 유휴 상태가 되는 쉬러 가기 전에 한번씩 처리한다.
    • 짧은 시간 안에 최대한 많은 청소(만료된 키 삭제)하는 걸 목표로 한다.
    • 느린 주기(대청소)에서 시간 초과(타임아웃)로 중단된게 아니라면 실행되지 않는다.
  • 청소 강도 조절(Effort)

    Redis 설정을 통해 ‘effort’ 값으로 얼마나 열심히 청소할지(느린 주기나 빠른 주기) 강도를 바꿀 수 있다. 이 값을 높이면 만료 키를 지우기 위한 더 많은 작업을 수행한다. 하지만 기본적으로 싱글 스레드에서 동작하므로 다른 처리가 지연될 수 있음을 고려하자.

    만료 키 처리의 효율적 관리

    위에서 만료 키 처리를 청소로 비유했는데, 이 청소를 하는데에 설렁설렁하기도 하고, 적극적으로 한다고도 했는데 어떤 기준으로 어떻게 하는걸까? 주로 느린 주기에서 판단이 이뤄진다.

    이 적응형 알고리즘을 통해 만료 키를 삭제할 때에 설렁설렁하는 경우 다음과 같이 동작한다.

  • 일단 모든 주기에서 만료 키 삭제 처리는 진행된다.(절대 안하고 넘어가진 않는다) 최소한의 작업을 진행하는 것으로 이해하면 된다.
  • 판단은 느린 주기에서 이미 메모리에 여유가 있고, 만료 키의 수가 예상보다 낮은 특정 비율이라 판단하면, 조기 중단할 수 있다. Redis 는 적은 경우에 굳이 CPU 자원을 들여 모든 키를 삭제할 필요가 없다고 판단한다.
    • 현재 메모리 압박의 상태 확인
    • 만료를 위한 키 비중 확인
  • 이 판단 기준에 따라 최소한의 작업은 무작위 샘플링 방식(random sampling)을 통해 만료 시간이 설정된 키들 중에서 특정 갯수의 키를 무작위로 선택한다.
  • 무작위 선택된 키들에 대해서만 만료 여부 판단, 만료 처리 진행하고 조기 종료한다.
  • 그럼 적극적으로 하는 경우는 어떨까? 이 내용은 빠른 주기 처리에 대한 이해가 필요하다.

  • 빠른 주기 처리는 클라이언트 요청 처리 후 유휴 상태 진입에서 처리하며 이를 빠른 주기로 봤는데, 이 실행에는다음과 같은 조건이 있다.
    • 앞서 느린 주기처리에서 타임아웃이 발생했어야 실행한다. 발생하지 않았다면 당연 실행되지 않는다.
    • 마지막으로 이전 빠른 주기가 실행된 지 특정 시간만큼 지났는지 확인하여, 재실행 대기 시간(ACTIVE_EXPIRE_CYCLE_FAST_DURATION)에 걸려있지 않으면 실행한다. 너무 많은 빠른 주기 실행을 막기위해 존재한다.