背景
在请求具有一定的重复性的业务场景下,客户端或者服务端将请求结果保存在缓存中,可以大大减少服务端压力和延时(尤其是请求方采用负载均衡策略,比如IP一致性哈希时)。通常缓存的大小是固定的,在大部分情况下缓存应该尽量保存热度较高的请求结果以提高缓存命中率,有多种缓存淘汰策略如:LRU、定期淘汰等,在服务运行过程中缓存淘汰操作会在适当的时机进行
服务在稳定运行时,其缓存命中率也处于稳定状态。不合理的缓存淘汰机制或者由于其他原因导致缓存命中率大幅下降,将会对服务性能产生很大挑战,甚至导致请求方熔断;另外,如果服务启动时缓存并没有“热”起来甚至已经被清空,那么冷启动时其延时会比稳定运行时更高,解决这个问题就需要在服务启动前对缓存进行预热,即warmup操作
高并发服务通常会采用本地+远程双层缓存的架构,远程缓存可以由 codis 集群或者其他 KV 集群实现,本地缓存位于服务所在机器的内存中
基本功能
-
并发读写
- 缓存需要对多个读写操作提供并发支持,并且最大限度的减少各操作的耗时(使用时也需要注意数据类型对耗时的影响
- 比如使用int64_t代替std::string类型作为缓存的key会降低查询耗时)
- 分桶存储,锁粒度的控制
- 缓存需要对多个读写操作提供并发支持,并且最大限度的减少各操作的耗时(使用时也需要注意数据类型对耗时的影响
-
强制失效
- 插入、读取是缓存的常规操作,另外也有业务场景需要提供强制让某条缓存失效的功能,同样需要并发安全
-
淘汰
- LRU、定期过期等策略
-
预热
- 本地缓存预热会增加单个服务启动时间
- 远程缓存预热决定整体服务可上线时间
- 如果是重构的新服务,逐渐增加服务的流量(另一方面是稳定性的考虑)来预热缓存
- 通常远程缓存需要高可用
本地缓存
实现
C++ 实现的本地缓存可参考 facebook 的:LRU缓存、分桶 LRU 缓存,以及基于上述缓存实现的定期淘汰缓存
预热方案
本地缓存位于内存中,服务停止后会其占用内存也会失效。预热通常发生在(时间较短的)服务上线过程,在可认为上线前的缓存是”热“的场景下,对本地缓存进行预热有以下两种思路。预热缓存除了会增加服务停止、启动时长,也会增加服务上下线等部署相关操作的复杂度,例如使用 K8s 部署有状态服务(状态与宿主机 IP 相关)时,服务不一定被部署到上次部署的宿主机上,这时则可以考虑通过其他方法如负载均衡控制 QPS 以达到预热缓存的目的
回放请求
如果只是由于服务启动后、接收请求前,某些模块未初始化导致的耗时较高,只需要在服务启动后发起下请求以完成懒初始化
如果是由于缓存为空导致耗时较高,可以在服务启动后解析最近请求日志并回放;另一种方法是在服务停止前将本地缓存中的key全部dump出来,服务启动后读取这些key并构造请求,计算对应的value写入缓存
回放请求的方法比较简单,但是当服务在本地缓存命中率较低时处理warmup请求耗时也较高,进而增加了服务启动的时间,降低上线效率。有时受限于上线时长,无法达到预热目标数量
dump & load
如果服务在本地缓存命中率较低时处理warmup请求耗时较高,可以考虑在服务停止前将本地缓存整个dump到磁盘或者分布式存储系统等等,服务启动后将dump内容再load到缓存
local-cache-warmup项目中实现了对上述 定期淘汰缓存 执行预热的工具,用户需要自己实现对本地缓存内容的序列化与反序列化操作(本地缓存经常会保存一些复杂的资源型数据结构),工具内部将并发执行dump和load操作,并且争取最大程度降低其耗时。针对 定期淘汰缓存,如果其load操作比正常请求结果写入缓存快很多的话,还需要避免缓存集中在较短时间内失效的情况(这可能对服务性能影响较大),工具中也提供了warmup接口额外指定一个浮动秒数 ,代表本次写入的缓存将在策略期限以内或以外浮动秒数的区间失效
负载均衡
通过适当的负载均衡策略,减少服务冷启动时接收到的 QPS
分布式缓存
待补充
参考
How to write a large buffer into a binary file in C++, fast?
mmap, memcpy to copy file from A to B