왜 갑자기 지연 스파이크가 생기는가

Redis는 단일 스레드로 명령을 처리한다. 그래서 하나의 작업이 오래 걸리면 그 뒤에 줄 선 모든 명령이 함께 밀린다. 평소 P99가 1ms인데 특정 순간에만 수십~수백 ms로 튀는 스파이크가 관측된다면, 상당수는 대용량 키의 삭제·만료가 메인 스레드를 점유했기 때문이다. 특히 수백만 개 필드를 가진 Hash나 큰 Set/Sorted Set을 DEL하거나, TTL이 한꺼번에 만료될 때 문제가 드러난다.

만료는 언제 실제로 일어나는가

Redis의 키 만료는 두 가지 경로로 동작한다. 첫째는 lazy expiration으로, 클라이언트가 해당 키에 접근하는 순간 만료 여부를 확인하고 삭제한다. 둘째는 active expiration으로, 백그라운드에서 주기적으로 TTL이 지난 키를 샘플링해 지운다. 문제는 만료 대상이 크거나 한꺼번에 몰릴 때다. 삭제 자체가 메인 스레드에서 동기적으로 수행되면, 큰 컬렉션의 모든 원소 메모리를 해제하는 동안 이벤트 루프가 멈춘다.

lazy free의 핵심 아이디어

lazy free는 "메모리 해제를 백그라운드 스레드에 위임"하는 기능이다. 키를 키스페이스에서 즉시 언링크(unlink)해 논리적으로는 바로 사라지게 하되, 실제 메모리 회수는 별도 스레드가 처리한다. 덕분에 메인 스레드는 큰 객체를 지우느라 블로킹되지 않는다. 명령 수준에서는 DEL 대신 UNLINK를 쓰면 되고, 만료·eviction·rename 등 내부 삭제 경로는 설정으로 제어한다.

설정 예시

# redis.conf — 삭제 경로별 lazy free 활성화
lazyfree-lazy-expire yes      # TTL 만료 시 백그라운드 해제
lazyfree-lazy-eviction yes    # maxmemory eviction 시 백그라운드 해제
lazyfree-lazy-server-del yes  # rename 등 내부 암묵 삭제 시
lazyfree-lazy-user-del yes    # DEL을 UNLINK처럼 동작시킴
lazyfree-lazy-user-flush yes  # FLUSHALL/FLUSHDB ASYNC 기본화

운영 중이라면 재시작 없이 CONFIG SET으로 적용할 수 있다.

redis-cli CONFIG SET lazyfree-lazy-expire yes
redis-cli CONFIG SET lazyfree-lazy-user-del yes
# 애플리케이션에서는 큰 키를 지울 때 UNLINK 사용
redis-cli UNLINK big:hash:1 big:zset:2

DEL vs UNLINK 비교

항목DELUNLINK
키스페이스 제거즉시즉시
메모리 해제메인 스레드(동기)백그라운드 스레드(비동기)
큰 키 삭제 시 블로킹있음없음/미미
작은 키차이 없음차이 없음(내부적으로 동기 처리)

작은 키는 UNLINK도 동기로 해제한다. lazy free는 원소 수가 임계치를 넘는 큰 객체에서만 이득이 크다.

TTL 몰림을 분산하기

설정만으로 부족한 경우가 있다. 배치로 같은 시각에 대량 키를 넣으면 TTL도 같은 순간 만료되어 active expiration이 한 번에 폭주한다. 만료 시각에 지터(jitter)를 주어 분산하는 것이 안전하다.

import random

def set_with_jitter(r, key, value, base_ttl=3600, jitter=300):
    # base_ttl ± jitter 범위로 만료 시각을 흩뿌린다
    ttl = base_ttl + random.randint(-jitter, jitter)
    r.set(key, value, ex=ttl)

확인과 주의점

효과는 지표로 검증한다. redis-cli --latency와 INFO stats의 expired_keys, SLOWLOG를 함께 본다. lazy free는 CPU 코어를 추가로 사용하므로, 코어가 1~2개뿐인 인스턴스에서는 백그라운드 스레드 경합으로 오히려 손해일 수 있다. 또한 언링크 후 실제 메모리 반환까지 짧은 지연이 있어, used_memory가 순간적으로 늦게 줄어드는 점을 모니터링 기준에 반영해야 한다. 마지막으로 근본 해결은 애초에 초대형 키를 만들지 않는 것이다. 큰 컬렉션은 해시 슬롯이나 접두어 기준으로 샤딩해 두면, 삭제·만료 비용 자체가 작아진다.