proxy热key缓存能力
背景
本文主要考虑的是redis的代理架构,即客户端连接proxy,proxy通过一定的哈希算法将对应的key路由到对应redis-server上。
这种代理架构会将相同的key路由到同一个server上。而redis作为单线程程序,存在热key瓶颈问题,一般认为简单的string操作,热key的qps最多10w。
但是业务有时存在一种诉求,比如电商库存场景,业务可能短时间内存在大量的热key读请求,使得存在热点。
方案
在proxy上实现热key的自动探测和缓存能力,并能对外输出和展示热key列表和对应的qps。
考虑到在redis数据结构下,如果缓存的是热key,proxy势必需要实现一整套的redis数据结构。因此最终实现的是缓存热cmd(命令)的回复。
提供三个配置参数:
- 热cmd最小qps: min_qps
- 缓存过期时间: expire_ms
- 缓存命令条数:cache_count
只有命令的qps大于min_qps时,该命令才会被缓存与展示,一条命令缓存成功后可以最小持续expire_ms的时间,一共允许缓存cache_count的命令数量。
数据结构
一秒一次,通过统计红黑树hks_map
统计该1秒内的所有cmd的访问次数。即每秒都会清空hks_map
。
统计红黑树紧密关联的为缓存红黑树hkc_map
,其存放了缓存的热cmd对应的
- 收到回复时的时间戳
- rsp回复
- 该回复总延时,即发送过去时打个时间戳,收到时计算差值
- 是否已经发送了
backup_req
(后文讲解)
在每秒清空hks_map
的同时,会对其遍历,以生成一个新的hkc_map。查看是否有访问次数大于min_qps
的命令,如果有,将其导入hkc_map
,如果此时老的hkc_map
还有对应的rsp缓存,我们将其复制到新的以防止替换过程的缓存失效。
以上为缓存功能的数据结构,为了展示热cmd,还存在一个聚合红黑树hk_record_map
:
将每一秒的统计结果用求和的方式导入其中。假如每5秒,我们在hk_record_map
中已经存入了5秒的数据,可以将其以访问次数转换为从大到小的队列,都附带当前导出的时间戳,输出该5秒的qps情况出去,即可以存放到一个总队列里。总队列可以设置一个允许存放的最长时间戳,超过时清理队列的队尾,即最老时间戳的数据。
命令处理流程
一个请求到来时,有以下操作步骤
- 查看当前
hks_map
是否缓存了该热cmd,如果缓存了,直接将对应访问次数+1 - 如果没有缓存,并达到了当前
hks_map
限制的数量(如1024),直接将hks_map
访问次数少的一半清理掉,然后将当前cmd的访问次数置为1,并加入hks_map
。 - 查看
hkc_map
是否有该cmd条目,并缓存有相应rsp回复,如果有,增加该cmd的命中次数hit_qps,并直接将对应的rsp返回给请求方。 - 如果有该cmd条目,但没有rsp回复,说明请求没有命中缓存,如请求还在发送处理中,增加热key总qps次数但不增加命中次数。用于统计命中率。
一个回复到来时:
- 查看当前hkc_map是否有当前的cmd,如果有,直接更新其对应的rsp结构,并记录当前缓存时间,往返延迟。
缓存真实时间
考虑这样一种场景,某个热cmd同时是大key,那么其请求往还延时会很长,如果我们只按设置的缓存时间缓存这个rsp,势必导致很长的一段时间比例中,回复没有被缓存(过期后等待新的rsp)。
因此我们实际的缓存时间为,expire_ms + latency_ms
。
backup_req
backup_req是一种重要缓存保持机制。以上的方案当缓存过期时,会存在大量的流量穿透到redis-server,以获取新的rsp。
因此可以在每一次请求的处理中增加一环逻辑:
- 判断当前请求的rsp离过期时间是否大于一半的过期时间了,如果是的,我们可以把这次请求不用缓存返回,而是主动将其置为backup_req穿透到redis-server中,用于续约和获取新数据。
该机制在热key持续存在的情况下可以保持热key缓存一直存在,而不会引发缓存雪崩。