dev.to

Redis says the key is gone. The memory comes back 22 seconds later.

Redis는 키가 사라졌다고 답하지만 메모리는 22초 뒤에 돌아옵니다

Redis 키는 TTL이 끝나면 조회할 수 없지만, 메모리에서 즉시 제거되지는 않습니다. 만료 시점을 5백만 개 키에 맞춘 실험에서 유휴 서버는 메모리를 회수하는 데 22.8초가 걸렸고, 만료 시각을 분산하면 이 지연과 쓰기 실패 위험을 줄일 수 있습니다.

AI 요약

Redis에서 TTL이 지난 키는 GET에 nil을 반환하고 EXISTS 결과도 0이 됩니다. 하지만 논리적으로 만료된 시점과 실제 메모리를 회수하는 시점은 다릅니다. 이 글은 두 시점 사이에 얼마나 차이가 나는지 Redis 8.10.2에서 측정합니다.

만료 시각을 맞춰야 지연이 드러납니다

처음에는 SET ... EX 15로 백만 개 키를 만들었습니다. 로딩에 약 5초가 걸려 키마다 만료 시각이 흩어졌고, Redis는 마지막 키가 만료된 뒤 1.1초 만에 메모리를 회수했습니다. 지연이 거의 보이지 않았던 이유는 키가 한꺼번에 만료되지 않았기 때문입니다.

모든 키에 같은 절대 만료 시각을 지정해 다시 시험하자 결과가 달라졌습니다. Redis 8.10.2에서 5백만 개 키가 같은 초에 만료됐을 때, 만료 순간에도 DBSIZE는 4,980,513을 보고했고 만료된 데이터가 차지한 추가 메모리는 810MB였습니다. 키 수가 0이 되기까지 22.8초가 걸렸습니다. 이 시간 동안 키를 조회하면 모두 이미 만료된 것으로 나옵니다. DBSIZE는 아직 정리되지 않은 항목까지 세고 있습니다.

만료 처리는 조회와 백그라운드 작업이 나눠 맡습니다

Redis는 키를 조회하다 만료된 사실을 확인하면 그 키를 삭제하는 지연 만료(lazy expiration)를 수행합니다. 별도로 활성 만료(active expiration) 작업도 돕니다. 이 작업은 초당 hz회 실행되며, 만료 시간이 설정된 키 20개를 표본으로 뽑아 검사합니다. 표본의 4분의 1을 넘게 만료됐다면 추가 작업을 이어갑니다. 활성 작업이 서버를 독점하지 않도록 실행량에는 제한이 있습니다.

유휴 서버에서 기본 hz 값 10일 때 전체 회수 시간은 22.8초였습니다. hz를 100으로 올리면 13.7초로 줄었습니다. 주기가 열 배 잦아져도 회수 속도 개선은 약 1.7배에 그쳤습니다. 글쓴이는 실행 빈도보다 한 주기에서 처리하는 작업량 제한이 더 큰 영향을 줄 수 있다고 봅니다.

반면 단일 클라이언트가 SCAN으로 키 공간을 훑으며 읽게 하자 5백만 개 키를 정리하는 데 5.1초가 걸렸습니다. 조회 과정에서 만료된 키를 즉시 삭제하기 때문입니다. 다만 댓글은 이 결과에 조건을 붙입니다. 트래픽이 닿는 키는 지연 만료로 정리되지만, 아무도 읽지 않는 차가운 키는 활성 만료 작업을 기다립니다. 바쁜 서버라도 키 공간 일부는 유휴 서버처럼 정리가 늦을 수 있습니다.

maxmemory와 eviction 정책에 따라 쓰기 결과가 달라집니다

글쓴이는 만료된 키가 maxmemory를 차지하면 새 데이터를 쓰는 동안 살아 있는 키까지 퇴출될 것으로 예상했습니다. allkeys-lru 정책으로 4백만 개 키가 만료된 뒤 새 키 30만 개를 썼을 때 eviction은 334,076건 발생했지만 새로 쓴 살아 있는 키는 하나도 사라지지 않았습니다. 표본에서 만료 키가 살아 있는 키보다 10배 넘게 많았고, LRU 기준으로도 만료 키가 더 오래된 데이터였기 때문입니다.

하지만 기본 정책인 noeviction에서는 다른 문제가 생깁니다. 4백만 개 키가 504MB를 쓰고 maxmemory가 516MB인 설정에서 모든 키가 논리적으로 만료된 뒤 쓰기를 시도했습니다. 처음에는 메모리 사용량이 593MB였고, Redis는 maxmemory 초과를 이유로 쓰기를 거절했습니다. 만료 키가 정리되면서 약 2.5초 뒤 쓰기가 성공했습니다. 애플리케이션이 이미 없는 것으로 확인한 데이터가 메모리에 남아 있는 동안 OOM 오류가 발생할 수 있습니다.

관리형 Valkey에서는 조정 수단이 제한됩니다

DigitalOcean 관리형 Valkey 9에서도 백만 개 키의 회수 시간을 측정했습니다. 단일 vCPU 노드에서는 7.5초, 로컬 2코어 환경에서는 3.2초가 걸렸습니다. 글은 이 차이를 플랫폼의 만료 동작보다 하드웨어 차이로 설명합니다. 관리형 서비스에서는 CONFIG와 DEBUG가 비활성화돼 클라이언트에서 hz나 정책을 바꿀 수 없습니다. eviction 정책은 플랫폼 API에서 조회하며, 응답은 noeviction이었습니다. 설정 변경을 API로 제한하는 방식은 관리형 서비스의 운영 책임을 고려한 선택이지만, 자체 Redis에서 쓸 수 있는 hz 조정은 이용할 수 없습니다.

만료 키가 특정 시각에 몰리는 환경이라면 교체 시점에 이전 세대와 새 데이터를 함께 담을 메모리까지 고려해야 합니다. 작업 집합 크기와 비슷한 maxmemory 설정에서 noeviction을 쓰면 키 공간이 교체되는 순간 쓰기가 거절될 수 있습니다. 가능하다면 일일 만료 시각에 몇 분 정도 무작위 간격을 더해 만료를 분산하는 방법을 권합니다.

dev.to 반응

  • @kashif_manzer — 수치로 보여줘서 좋습니다. 다만 바쁜 서버의 결과에는 한 가지 조건이 있습니다. 지연 만료는 트래픽이 실제로 건드리는 키만 정리합니다. 자주 쓰는 키 공간은 SCAN 클라이언트처럼 스스로 정리되지만, 아무도 읽지 않는 차가운 키는 활성 만료 주기를 기다립니다. 실제로는 트래픽이 닿지 않는 데이터에서 바쁜 Redis도 글에서 본 유휴 서버와 똑같이 보일 수 있습니다. 그래서 마지막 제안이 실제 해결책입니다. 만료 시각을 분산하면 hz 조정으로 해결할 수 없는, 아무도 읽지 않는 키까지 도움이 됩니다.

원문: dev.to / 번역·요약: Trawling