무엇을 버릴지 고르는 기준은 지역성입니다. 최근에 쓴 것이 또 쓰일 것이라 보면 LRU, 자주 쓰인 것이 또 쓰일 것이라 보면 LFU 입니다.
캐시 교체 정책
주요 정책
| 정책 | 버리는 대상 | 잘 맞는 상황 |
|---|
| LRU | 가장 오래 안 쓴 키 | 최근 접근이 재접근을 예측할 때 |
| LFU | 가장 드물게 쓴 키 | 인기 키가 꾸준히 고정적일 때 |
| Random | 무작위 | 접근 빈도가 대체로 균등할 때 |
| TTL 우선 | 남은 TTL이 짧은 키 | 만료 후보를 직접 관리할 때 |
Redis 설정
maxmemory 4gb
maxmemory-policy allkeys-lru
allkeys-*: 모든 키가 제거 후보
volatile-*: TTL이 설정된 키만 후보 (해당 키가 없으면 쓰기가 오류)
noeviction: 제거하지 않고 쓰기 명령에 오류를 반환
어느 정책이 맞는지는 접근 모양이 정한다
정책의 우열이 아니라 그 데이터를 어떻게 읽느냐가 답을 정합니다.
| 읽는 모양 | 맞는 정책 | 왜 |
|---|
| 최근에 본 것을 또 본다 | 최근 사용 기준 | 시간적 지역성이 있다 |
| 소수가 계속 인기 있다 | 사용 횟수 기준 | 오래돼도 여전히 자주 읽힌다 |
| 한 번씩 훑고 지나간다 | 어느 쪽도 도움 안 된다 | 캐시가 오히려 방해다 |
셋째가 중요합니다. 전체를 순서대로 훑는 배치가 돌면 캐시가 그 데이터로 다 채워지고
정작 필요한 것이 밀려납니다. 그런 경로는 캐시를 지나가지 않게 만듭니다.
왜 정확한 최근 순서를 안 쓰나
정확히 하려면 읽을 때마다 순서를 고쳐야 하고, 그것이 읽기마다 붙는 비용입니다.
접근할 때마다 목록에서 위치를 옮긴다
그 목록도 여러 곳에서 동시에 고치면 잠금이 필요하다
그래서 실제 구현은 몇 개를 무작위로 뽑아 그중 가장 오래된 것을 버리는 근사 방식을 씁니다.
정확하지 않지만 충분하고, 읽기 경로에 비용을 거의 안 붙입니다.
실무 포인트
- Redis의 LRU/LFU는 정확한 구현이 아니라 표본 기반 근사다(
maxmemory-samples). 실무 체감 차이는 작다
- 캐시 전용 인스턴스에
noeviction을 두면 메모리가 차는 순간 쓰기가 실패한다
- 캐시와 영구 보관 키를 한 인스턴스에 섞지 않는다. 섞으면 지워지면 안 되는 키가 제거될 수 있다
evicted_keys가 계속 늘면 메모리 증설 또는 TTL 단축을 검토한다
Q.LRU와 LFU의 차이는 무엇이고 어떤 상황에 각각 유리한가요?
최근성을 보는지 빈도를 보는지가 다릅니다.
| 항목 | LRU | LFU |
|---|
| 기준 | 가장 오래 안 쓴 것을 버린다 | 가장 적게 쓴 것을 버린다 |
| 잘 맞는 경우 | 최근 접근이 곧 다시 올 때 | 인기 항목이 꾸준할 때 |
| 약점 | 한 번 훑고 지나가는 조회에 캐시가 밀린다 | 옛 인기 항목이 계속 남는다 |
각자 불리한 상황을 구체적으로 보면 이렇습니다.
| 정책 | 불리한 상황 |
|---|
| LRU | 배치가 전체 데이터를 한 번 스캔하면 캐시가 전부 교체된다 |
| LFU | 지난달 인기 상품이 이번 달에도 자리를 차지한다 |
LFU 의 문제를 줄이려고 시간이 지나면 횟수를 감쇠시키는 방식을 씁니다.
흔한 실수: 둘 중 하나를 고르는 문제로 보는 것. Redis 의 LFU 는 감쇠를 포함하고, 리눅스 페이지 캐시는 최근성과 빈도를 함께 봅니다. 실제 구현은 대개 혼합입니다.
Q.Redis에서 maxmemory-policy를 어떻게 정하나요?
캐시 전용인지, 잃으면 안 되는 데이터가 섞여 있는지로 갈립니다.
| 정책 | 동작 | 언제 |
|---|
| noeviction | 쓰기를 거부한다 | 캐시가 아니라 저장소로 쓸 때 |
| allkeys-lru | 모든 키 중 오래 안 쓴 것 | 순수 캐시. 가장 흔하다 |
| allkeys-lfu | 모든 키 중 적게 쓴 것 | 인기 편중이 뚜렷할 때 |
| volatile-lru | 만료가 설정된 키만 대상 | 캐시와 영속 데이터가 섞여 있을 때 |
| allkeys-random | 무작위 | 접근 패턴이 고를 때. 계산이 가장 싸다 |
기본값은 noeviction 입니다. 캐시로 쓰면서 이 값을 그대로 두면 메모리가 차는 순간 쓰기가 전부 실패합니다.
흔한 실수: 캐시 용도인데 기본값을 그대로 두는 것. 배포 후 며칠 뒤 메모리가 차면서 갑자기 쓰기 오류가 터집니다. 캐시로 쓸 것이면 명시적으로 축출 정책을 정해야 합니다.
Q.volatile-lru를 쓸 때 주의할 점은?
만료가 설정된 키만 축출 대상이라, 만료 없는 키가 쌓이면 축출할 것이 없어집니다.
| 상황 | 결과 |
|---|
| 만료 있는 키가 충분하다 | 정상 동작 |
| 만료 없는 키가 메모리를 채웠다 | 축출할 대상이 없어 쓰기가 거부된다 |
이 정책을 쓰는 이유는 캐시와 영속 데이터를 한 인스턴스에 섞어 두었을 때 영속 데이터가 지워지는 것을 막기 위해서입니다. 목적은 맞지만 전제가 있습니다. 캐시 키에 반드시 만료를 걸어야 합니다.
점검할 것입니다.
| 확인 | 방법 |
|---|
| 만료 없는 키 비율 | 샘플링해 확인한다 |
| 축출 실패 | 쓰기 오류와 메모리 상한 도달을 함께 본다 |
| 분리 검토 | 캐시와 영속 데이터를 다른 인스턴스로 나누는 것이 근본 |
흔한 실수: 만료를 빠뜨린 캐시 키가 생기는 것. 코드 경로마다 만료 설정을 반복하면 놓치기 쉬우니, 캐시 접근을 감싸는 함수에서 만료를 필수 인자로 만드는 편이 안전합니다.
Q.캐시 메모리가 부족하다는 신호를 어떤 지표로 판단하나요?
| 지표 | 부족 신호 |
|---|
| 축출 건수 | 0 이 아니고 계속 늘어난다 |
| 사용 메모리 | 상한에 붙어 있다 |
| 히트율 | 떨어진다. 담기 전에 밀려나기 때문 |
| 평균 TTL 잔여 | 만료 전에 축출되는 비율이 높다 |
| 쓰기 오류 | noeviction 정책이면 쓰기가 거부된다 |
축출 건수가 가장 직접적입니다. 만료로 사라지는 것과 자리가 없어 밀려나는 것은 다른 지표이고, 후자가 늘면 용량 문제입니다.
메모리 단편화도 함께 봅니다. 실제 데이터보다 운영체제가 잡은 메모리가 훨씬 크면 단편화가 심한 것이고, 재시작이나 활성 조각 정리 설정으로 다룹니다.
| 대응 | 순서 |
|---|
| 1 | 큰 값과 불필요한 키를 찾아 정리한다 |
| 2 | TTL 을 줄여 회전을 빠르게 한다 |
| 3 | 직렬화 형식을 줄인다 |
| 4 | 증설하거나 샤딩한다 |
흔한 실수: 히트율만 보고 메모리 문제를 놓치는 것. 히트율 하락의 원인이 키 설계인지 용량인지는 축출 지표로 갈립니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
캐시/Redis 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.