출시·고도화 중
LRU 캐시 안내서 · 6/6
LRU나 그에 가까운 정책은 컴퓨터 시스템의 모든 층에서 돌아갑니다. 이 장에서는 그 층들을 차례로 살펴보고, 실제로 가져다 쓸 라이브러리와 캐시 버그 대부분을 일으키는 함정을 정리합니다.
Redis가 maxmemory에 도달하면 maxmemory-policy 설정이 무엇을 제거할지 정합니다. noeviction, allkeys-lru, volatile-lru, allkeys-lfu, volatile-lfu, allkeys-random, volatile-random, volatile-ttl 등이 있으며, volatile- 정책은 만료 시간이 설정된 키만 대상으로 합니다. Redis는 전역 연결 리스트를 두지 않고, 키 몇 개를 표본으로 뽑아(maxmemory-samples, 기본 5) 그중 가장 오래된 키를 제거하는 방식으로 LRU를 근사합니다.
redis-cli CONFIG SET maxmemory 256mb
redis-cli CONFIG SET maxmemory-policy allkeys-lruCDN 에지 서버도 디스크와 메모리가 한정되어 있어서, 드물게 요청되는 객체를 보통 LRU와 비슷한 정책으로 제거하고 Cache-Control: max-age의 TTL과 함께 씁니다. 브라우저의 HTTP 캐시도 디스크 할당량에 이르면 가장 오래전에 쓴 항목부터 지웁니다.
Python의 functools.lru_cache는 크기 제한이 있는 LRU로 함수를 메모이제이션합니다. 무제한 캐시는 maxsize=None(또는 functools.cache), 1과 1.0을 따로 캐시하려면 typed=True, 무효화에는 cache_clear()를 씁니다.
Java의 LinkedHashMap은 두 가지만 바꾸면 LRU 캐시가 됩니다. accessOrder = true를 넘겨 get이 항목을 맨 뒤로 옮기게 하고, removeEldestEntry를 재정의합니다.
import java.util.LinkedHashMap;
import java.util.Map;
class LruMap<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
LruMap(int capacity) {
super(16, 0.75f, true); // true = 접근 순서
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // 삽입할 때마다 넘치면 제거
}
}운영 환경의 Java 서비스에서는 만료와 동시성까지 갖춘 Caffeine(W-TinyLFU) 같은 라이브러리가 대개 더 낫습니다. Node.js에서는 lru-cache 패키지가 같은 역할을 합니다.
LRU 캐시에서는 읽기도 쓰기입니다. get이 리스트 순서를 바꾸기 때문입니다. 두 스레드가 동시에 노드를 옮기면 포인터가 망가질 수 있으므로 읽기-쓰기 잠금으로는 해결되지 않습니다. 가장 단순한 방법은 모든 연산을 잠금 하나로 감싸는 것이고, 경합이 심하면 캐시를 여러 조각(shard)으로 나눠 조각마다 잠금을 둡니다.
import threading
from collections import OrderedDict
class ThreadSafeLRU:
def __init__(self, capacity):
self.capacity = capacity
self.data = OrderedDict()
self.lock = threading.Lock()
def get(self, key, default=None):
with self.lock:
if key not in self.data:
return default
self.data.move_to_end(key)
return self.data[key]
def put(self, key, value):
with self.lock:
self.data[key] = value
self.data.move_to_end(key)
if len(self.data) > self.capacity:
self.data.popitem(last=False)functools.lru_cache는 여러 스레드에서도 내부 구조를 일관되게 유지하지만, 같은 키로 동시에 호출하면 감싼 함수가 두 번 이상 실행될 수 있습니다.
lru_cache를 붙이면 self도 계속 살아 있습니다.from functools import lru_cache
@lru_cache(maxsize=128)
def settings(user_id):
return {"theme": "light", "user": user_id}
s = settings(1)
s["theme"] = "dark" # 캐시된 객체를 바꿉니다
print(settings(1)["theme"]) # dark: 모든 호출자에게 뜻밖의 결과
@lru_cache(maxsize=128)
def settings_safe(user_id):
return ("light", user_id) # 바꿀 수 없는 값OrderedDict, lru_cache, LinkedHashMap, Caffeine, lru-cache 같은 라이브러리를 먼저 씁니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.