4. 本地缓存策略与存储管理:LRU缓存、磁盘配额、过期清理、存储路径规范
4.1 概述
动态分发系统中,本地缓存是降低网络延迟、减少后端压力的核心组件。不加控制的缓存会导致磁盘空间耗尽、性能退化、数据陈旧。本章围绕四种关键策略讲解实现:LRU淘汰(Least Recently Used)、磁盘配额(Disk Quota)、过期清理(Expiry Eviction)以及存储路径规范(Path Convention)。通过组合这些机制,构建一个可自愈、有上限、可审计的本地缓存层。
4.2 原理简述
| 策略 | 原理 | 解决的核心问题 |
|---|---|---|
| LRU 缓存 | 基于访问时间排序,优先淘汰最久未使用的数据 | 缓存容量固定时保留热点数据 |
| 磁盘配额 | 限制缓存目录的总大小或文件数,超出时触发清理 | 防止缓存膨胀撑满磁盘 |
| 过期清理 | 为缓存项设置 TTL,后台或按需清除过期条目 | 保证数据时效性,避免脏数据 |
| 存储路径规范 | 按来源、层级、哈希等组织缓存文件路径 | 便于管理、调试、快速定位 |
4.3 实现流程
- 初始化:创建缓存根目录,读取配额、TTL、LRU容量等配置。
- 写入流程:计算目标路径 → 检查配额 → 若超出则执行LRU或过期清理 → 写入文件 → 更新LRU索引 → 记录元数据。
- 读取流程:路径定位 → 文件存在且未过期? → 若过期则删除并回源;若有效则更新LRU访问时间 → 返回内容。
- 定期清理:定时任务扫描全量条目,删除过期文件;同时监控配额余量,触发大清理。
- 路径生成:对URL或资源Key做SHA256取前两级目录,分散文件,避免单目录过多。
关键决策:LRU与过期清理的优先级。推荐将过期清理作为第一道防线(删除无效数据),LRU作为第二道防线(在配额边界剔除冷数据)。
4.4 代码实现(核心片段)
LRU缓存索引(Python + lru-dict):
from lru import LRU
cache_index = LRU(maxsize=5000) # 最大缓存条目数
def put_cache(key, filepath, size):
# 淘汰时会被自动调用 evict 回调
cache_index[key] = {'path': filepath, 'size': size, 'ctime': time.time()}
def get_cache(key):
if key in cache_index:
entry = cache_index[key]
entry['ctime'] = time.time() # 模拟LRU,实际访问会内置更新
return entry['path']
return None
磁盘配额检查与清理:
import shutil, os
CACHE_ROOT = '/data/cache'
QUOTA_BYTES = 10 * 1024 * 1024 * 1024 # 10GB
def ensure_quota(needed_bytes):
while get_dir_size(CACHE_ROOT) + needed_bytes > QUOTA_BYTES:
# 从LRU中获取最久未使用条目
oldest_key = get_lru_oldest()
if not oldest_key:
break
remove_cache_file(oldest_key)
过期清理扫描:
import time, glob
TTL_SECONDS = 3600 * 24 # 1天
def cleanup_expired(root):
now = time.time()
for meta_file in glob.glob(f'{root}/**/.meta.*', recursive=True):
with open(meta_file) as f:
meta = json.load(f)
if now - meta['write_time'] > TTL_SECONDS:
cache_file = meta_file.replace('.meta.', '')
os.remove(cache_file)
os.remove(meta_file)
存储路径规范函数:
import hashlib, os
CACHE_ROOT = '/data/cache'
def get_cache_path(url):
# 对URL做SHA256,取前4字符作为两级目录
h = hashlib.sha256(url.encode()).hexdigest()
dir1, dir2 = h[:2], h[2:4]
safe_name = h[4:] + '.cache'
return os.path.join(CACHE_ROOT, dir1, dir2, safe_name)
4.5 配置示例(YAML)
local_cache:
enabled: true
root: /data/cache
quota:
max_bytes: 10737418240 # 10GB
max_files: 100000
cleanup_policy: LRU_FIRST
ttl:
default_seconds: 86400
per_type:
images: 3600
manifests: 600
path_strategy: HASH_FLAT_TWO_LEVEL
lru_capacity: 5000
4.6 调试与验证
使用以下命令或脚本检查缓存状态:
# 查看缓存目录大小
du -sh /data/cache
# 统计文件数量
find /data/cache -type f | wc -l
# 监控LRU命中率(需在代码中暴露计数器)
curl http://localhost:8080/__cache_stats
# 返回示例:
# {"hits": 1234, "misses": 56, "evictions": 12, "quota_utilization": 0.73}
调试技巧:在开发环境开启详细日志(如每次淘汰记录原因),生产环境使用采样日志避免性能影响。
4.7 性能与稳定性优化
- LRU索引持久化:定期将内存LRU表写入磁盘快照,重启后可恢复热点状态。
- 批量清理:当配额超出时,不要逐条删除,一次淘汰一批(例如按照LRU顺序删除总大小的10%)。
- 异步IO:文件写入、删除操作使用异步队列,避免阻塞主线程。
- 目录分片:使用两级哈希目录后,每个目录下文件数可控(通常小于1000),避免ls/glob延迟。
- 并发锁:对LRU索引和元数据文件使用读写锁,细粒度避免竞态。
4.8 常见错误与避坑
| 错误场景 | 原因 | 解决方案 |
|---|---|---|
| 缓存目录创建失败 | 权限不足或父目录不存在 | 启动时检查并创建,设置umask,以Systemd服务形式运行 |
| 配额清理后仍超限 | 多个进程同时写入,清理不及时 | 使用文件锁或基于目录的临时预留 |
| LRU索引丢失所有数据 | 进程崩溃未持久化 | 启动时扫描文件系统重建索引(代价较高,可做增量恢复) |
| 过期清理后文件还在 | 元数据扩展属性未写或者时间戳错误 | 采用原子写入:先写临时文件再rename |
| 路径哈希冲突 | 不同URL生成了相同哈希前缀 | 使用完整哈希作为文件名,且保留尾部扩展名以便区分类型 |
重要:不要在每次读请求时都扫全量目录,LRU索引必须常驻内存。对于超大规模缓存(百万级文件),考虑用
rocksdb等嵌入式KV存储替代文件系统。