CDN边缘节点作为内容分发的最后一公里,其本地缓存容量始终有限。当新资源需要写入而剩余空间不足时,缓存剔除策略直接决定了后续请求的命中率与回源压力。LRU、LFU与ARC是三种最具代表性的剔除算法,它们在边缘节点上的实现方式和适用场景存在明显差异,理解这些差异是做好节点性能调优的前提。

LRU算法在边缘节点的实现与局限
LRU(Least Recently Used)的核心思想是淘汰最长时间未被访问的缓存对象。在边缘节点中,通常用一个双向链表配合哈希表来维护缓存顺序:每次访问命中时将节点移到链表头部,写入新对象时若超限则移除链表尾部节点。这种结构使得插入、删除、访问定位都能在常数时间完成,对高并发的边缘请求非常友好。
下面是一段简化版的LRU缓存实现,用Python演示了基本的存取与剔除逻辑:
class Node:
def __init__(self, key, value):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = {}
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _add(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def get(self, key):
if key in self.cache:
node = self.cache[key]
self._remove(node)
self._add(node)
return node.value
return -1
def put(self, key, value):
if key in self.cache:
self._remove(self.cache[key])
node = Node(key, value)
self._add(node)
self.cache[key] = node
if len(self.cache) > self.capacity:
lru = self.tail.prev
self._remove(lru)
del self.cache[lru.key]
LRU在边缘节点落地时最大的问题是“缓存污染”。当爬虫或批量刷新导致大量冷资源被一次性请求,这些对象会挤掉真正的热点内容,因为LRU只认最近访问而不管访问频率。此外,视频大文件的分片若被当作独立对象处理,也会加速热点小文件的淘汰。因此在纯静态小文件且访问分布均匀的CDN业务中LRU表现良好,但在混合流量下命中率波动明显。
LFU算法的频次统计与边缘适配难点
LFU(Least Frequently Used)以访问次数为剔除依据,为每个缓存对象维护一个计数器,淘汰计数值最小的对象。相比LRU,它能长期保留真正被反复请求的热点,避免偶发流量冲刷缓存。在边缘节点服务热门新闻图片或常驻API响应时,LFU可以显著提升命中率。
但LFU在边缘环境有两大痛点。其一是新写入对象计数为零,容易被立刻淘汰,难以应对突发流量;其二是计数器只增不减,早期热点若过气仍长期占据缓存。工程上常用“定期衰减”或“分段计数”缓解,例如每十分钟所有计数右移一位。下面示例展示带衰减逻辑的LFU剔除判断:
public class LFUEdge {
private Map<String, Integer> freq = new HashMap<>();
private int capacity;
public LFUEdge(int capacity) {
this.capacity = capacity;
}
public void access(String key) {
freq.put(key, freq.getOrDefault(key, 0) + 1);
}
public void decay() {
for (String k : freq.keySet()) {
freq.put(k, freq.get(k) >> 1);
}
}
public String evict() {
String minKey = null;
int min = Integer.MAX_VALUE;
for (Map.Entry<String, Integer> e : freq.entrySet()) {
if (e.getValue() < min) {
min = e.getValue();
minKey = e.getKey();
}
}
freq.remove(minKey);
return minKey;
}
}
在边缘节点使用LFU还需考虑内存开销:每个对象额外计数与排序结构会占用元数据存储。对于千万级缓存项的节点,LFU的维护成本高于LRU。同时,访问频次统计在多线程回源场景下需要加锁或原子变量,会影响高并发读性能。因此很多CDN厂商仅在特定频道启用LFU,而非全局默认。
ARC算法如何自适应融合两者优势
ARC(Adaptive Replacement Cache)由IBM提出,将缓存划分为LRU部分(T1+T2)与LFU部分(B1+B2的幽灵列表),并根据近期剔除对象的重新访问情况动态调节两部分尺寸。若刚从LFU区剔出的对象很快又被访问,说明频次策略有效,便扩大LFU区;反之则扩大LRU区。这种自适应机制让边缘节点在未知流量模式时也能保持较高命中率。
以下伪代码描述了ARC的核心调节过程,其中p为LFU目标大小,c为总容量:
void arc_access(cache *c, item *x) {
if (in_t1(x) || in_t2(x)) {
move_to_t2(x);
} else if (in_b1(x)) {
p = min(p + max(1, len_b2()/len_b1()), c);
replace(c, p);
move_b1_to_t2(x);
} else if (in_b2(x)) {
p = max(p - max(1, len_b1()/len_b2()), 0);
replace(c, p);
move_b2_to_t2(x);
} else {
if (len_t1()+len_b1() == c) {
if (len_t1() < c) { evict_b1(); replace(c, p); }
else { evict_t1(); }
} else if (len_t1()+len_t2()+len_b1()+len_b2() >= c) {
if (len_t1()+len_t2()+len_b1()+len_b2() == 2*c) evict_b2();
}
add_t1(x);
}
}
在边缘节点部署ARC时,幽灵列表会占用少量额外内存,但换来的是面对突发直播切片与常驻图片混合请求时的稳定表现。实测中,同等容量下ARC命中率比LRU高约百分之十到二十,尤其在流量模式切换时劣化更慢。对于不支持动态调参的老旧边缘设备,也可固定p值做静态ARC以折中复杂度和效果。
边缘节点算法选型与参数建议
选择剔除算法首先要看业务访问模型。如果边缘节点主要承载版本固定的安装包或长期热门素材,LFU或ARC更合适;若内容生命周期极短、以临时分发为主,LRU的简洁和低延迟反而占优。对于综合型CDN,可按域名或路径配置不同策略,而非全节点统一。
参数方面,LRU无需调参但需注意对象粒度,建议将大文件分片与元数据分开管理;LFU的衰减周期应贴近业务热点更替频率,视频类可设较长、资讯类设较短;ARC的p初始值可设为总容量一半,再根据监控中B1、B2命中比动态微调。配合节点级命中率与回源带宽看板,才能验证算法是否真正贴合边缘场景。