Redis TTL 만료
7/15/2025
Redis 의 TTL 의 관리는 크게 두가지 방식으로 관리된다.
만료 키 관리 방식
클아이언트가 키를 조회하거나 접근할 때, 만료 시간을 체크하여 지났다면 그 시점에 삭제하고, nil(없음, null 과 동일)을 반환한다.
Redis 내부적으로, 만료된 키를 주기적으로 스캔하여 삭제한다. 이때 백그라운드에서 일정 주기로 만료된 키의 일부를 샘플링하여 조회하고, 만료된 데이터는 삭제한다.
기본적으로 메인 이벤트 루프에서 약 100ms 마다 한 번 만료된 키를 찾는 작업을 실행한다. 이 때 만약 너무 많은 만료 키가 쌓여있다면 한번 더 반복적으로 검사한다.
Redis 가 TTL 에 대해서 이렇게 관리하는 이유는 성능과 효율을 둘 다 잡기 위한 트레이드오프 때문이라고 한다.
이유는 어느 한쪽만 사용한다고 했을 때를 예시로 들어 보면 알 수 있다.
능동적 만료만 쓴다면?
능동적 만료만 사용하면서 TTL 에 맞게 관리하려면, 모든 키에 대해 만료되는지에 대해서 주기적으로 스캔하여 만료시점에 삭제처리 해주어야 한다. 즉 전체 키에 대해서 아주 빠른 시간 내에 반복적으로 체크해주어야 하는 것이다. 아무리 특정지어 만료 가능성이 있는 키들을 샘플링하여 체크한다고 하더라도, 꽤나 많은 키에 대해서 지속적으로 체크하는 로직이 필요하게 되고, 이는 성능상에 영향을 줄 수 있다. 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 만료된 메모리를 삭제하는 능동적 만료에 대해 청소라고 생각한다면, 이 내용은 다음과 같이 정리할 수 있다.
똑똑한 청소부(적응형 알고리즘)
개별 판단 기준에 따라 청소 강도를 조절하여 적응형 알고리즘이라 한다.
여러 방을 순서대로 청소(여러 데이터베이스 확인)
청소 방식(주기)
이 청소 방식은 특정 주기마다 가 청소 방식이 결정되는게 아니라, 각 청소 방식(주기)는 독립적으로 처리되고 있다고 보면 된다.
- 정기적으로 하는 대청소라고 보면된다. 1초에 10번 정도(10Hz) 니까, 100ms 라고 보면 된다.
- 최대한 많이 청소(만료된 키 삭제)하는 걸 목표로 한다.
- 이 과정에서 너무 많은 청소(만료 키 삭제)가 있다면 오래걸리므로 타임아웃이 발생한다.(특정 타임아웃 시점까지 만 청소한다는 뜻)
- 틈틈히 하는 간단한 청소라고 보면 된다. 느린주기(메인청소)에서 처리하지 못한 부분이 있을 수 있기 때문에, 평소에 짧은 시간 동안만 빠르게 처리한다. 이 처리는 특정 주기라기 보단 클라이언트 요청을 처리하고 유휴 상태가 되는 쉬러 가기 전에 한번씩 처리한다.
- 짧은 시간 안에 최대한 많은 청소(만료된 키 삭제)하는 걸 목표로 한다.
- 느린 주기(대청소)에서 시간 초과(타임아웃)로 중단된게 아니라면 실행되지 않는다.
청소 강도 조절(Effort)
Redis 설정을 통해 ‘effort’ 값으로 얼마나 열심히 청소할지(느린 주기나 빠른 주기) 강도를 바꿀 수 있다. 이 값을 높이면 만료 키를 지우기 위한 더 많은 작업을 수행한다. 하지만 기본적으로 싱글 스레드에서 동작하므로 다른 처리가 지연될 수 있음을 고려하자.
만료 키 처리의 효율적 관리
위에서 만료 키 처리를 청소로 비유했는데, 이 청소를 하는데에 설렁설렁하기도 하고, 적극적으로 한다고도 했는데 어떤 기준으로 어떻게 하는걸까? 주로 느린 주기에서 판단이 이뤄진다.
이 적응형 알고리즘을 통해 만료 키를 삭제할 때에 설렁설렁하는 경우 다음과 같이 동작한다.
- 현재 메모리 압박의 상태 확인
- 만료를 위한 키 비중 확인
그럼 적극적으로 하는 경우는 어떨까? 이 내용은 빠른 주기 처리에 대한 이해가 필요하다.
- 앞서 느린 주기처리에서 타임아웃이 발생했어야 실행한다. 발생하지 않았다면 당연 실행되지 않는다.
- 마지막으로 이전 빠른 주기가 실행된 지 특정 시간만큼 지났는지 확인하여, 재실행 대기 시간(
ACTIVE_EXPIRE_CYCLE_FAST_DURATION
)에 걸려있지 않으면 실행한다. 너무 많은 빠른 주기 실행을 막기위해 존재한다.