缓存
缓存存放的是临时数据,相比主存,缓存往往速度更快,容量更小。
缓存往往是多级的,由内而外,cpu cache -> 内存 -> ssd -> hdd(单机) -> 服务器/多节点/oss/nas等
只要有缓存就会有一致性问题
写缓存
写缓存有两种策略,write back和write through。
write back情况下,缓存不会立刻更新到主存,这让write back是单对象的行为。如果是多对象操作缓存,write back必然存在缓存不可见问题。例如page cache,一个进程的写入对另一个进程可能不可见。
一般选择的写缓存策略是write through。 相当于没有写缓存。
读缓存
为了一致性,读写一般使用同一个缓存。读缓存需要考虑的就是缓存失效问题。 读缓存需要处理,如果写操作导致大量客户端读缓存失效,产生的缓存穿透问题。
nfs3 close-and-open一致性表示,文件close后,写缓存刷到后台,文件open后,本地读缓存全部失效,需要从后台重新拿。此外,nfs3通过ctime表示缓存有效性,每次读操作nfs3 都会向后台执行getattr,如果ctime和缓存一致,则直接读本地,否则读后台。
在有lease功能后,lease 的版本id,起到ctime的作用。数据写入服务端会lease++,客户端每次读操作会从后台获取lease,如果lease 和本地一致,则直接读本地,否则本地读缓存失效,读后台。
缓存在生效前需要先预热下,也就是预读
缓存的更新
缓存本质是KV map,需要加锁维护。缓存可能遇到大量请求同时更新,更新时的加锁可能严重影响性能。
缓存更新有两种
- 缓存key存在,value失效了需要更新,这种不需要加锁,很快速
- 缓存key被淘汰了/不存在,需要插入,这种更新需要加锁,比较重
缓存还可以根据状态更新,典型的是cpu cacheline的MSI协议(Modified, Shared和Invalid)。这个要求每个cache line能感知到其他cache line发生写操作,同时能通知其他cache line写内存。例如cpu1能感知到cpu2发生了写cache操作,于是更新自己状态为unvalid,并让cpu2 刷到内存。java的valitale 关键字就是通过让cpu将对变量的修改直接刷内存
使用范围很小。
缓存穿透,缓存雪崩
读缓存场景,如果数据被写入会导致所有客户端读缓存失效,瞬时压力可能给到服务器。
如果只是写操作导致的缓存失效,则缓存穿透也可以理解
如果是过期引起的缓存失效,使用LRU同时防止缓存大规模同时过期
主存前面可以加队列和流控来缓解压力
流控可以考虑权重公平流控,后台统计每个client的请求数量,当总数量达到阈值,通过权重计算得到每个client的能放进去的请求数量(最小值和最大值,也不能没请求)
4k对齐
场景的缓存多是对齐的,例如page cache是4K对齐
write through策略,每笔写入都会写到后台,即使不是4K写。
客户端读缓存也设置成4K对齐的,使page cache可以命中
自定义fuse客户最好自行管理缓存
文件元数据缓存
主要的元数据缓存
- inode缓存,缓存inode 元数据,atime, ctime, mtime, link, mode, size等信息,这个缓存会经常更新
- inode 数据缓存,
- dentry缓存,缓存目录项存的文件名,方便readdir,文件创建删除时需要更新
ganesha 元数据缓存,
genasha每个目录项会缓存目录中子文件name->filehandle映射,目的是方便lookup。如果lookup某个name缓存未命中, 会持有目录写锁 访问后台,拿到(name, filehandle)后释放目录写锁。
这样的原因是不给后台太多压力,防止多个请求同时打向后台(加了写锁只有一个lookup请求能打到后台),问题是产生了lookup串行化影响性能。
缓存应该有预读的功能,即缓存失效后尽量从后台拿到局部性多的数据。
本文标题:缓存
文章作者:Infinity
发布时间:2024-12-22
最后更新:2025-01-04
原始链接:https://larrystd.github.io/2024/12/22/%E7%BC%93%E5%AD%98/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!