背景

本文主要考虑的是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情况出去,即可以存放到一个总队列里。总队列可以设置一个允许存放的最长时间戳,超过时清理队列的队尾,即最老时间戳的数据。

命令处理流程

一个请求到来时,有以下操作步骤

  1. 查看当前hks_map是否缓存了该热cmd,如果缓存了,直接将对应访问次数+1
  2. 如果没有缓存,并达到了当前hks_map限制的数量(如1024),直接将hks_map访问次数少的一半清理掉,然后将当前cmd的访问次数置为1,并加入hks_map
  3. 查看hkc_map是否有该cmd条目,并缓存有相应rsp回复,如果有,增加该cmd的命中次数hit_qps,并直接将对应的rsp返回给请求方。
  4. 如果有该cmd条目,但没有rsp回复,说明请求没有命中缓存,如请求还在发送处理中,增加热key总qps次数但不增加命中次数。用于统计命中率。

一个回复到来时:

  1. 查看当前hkc_map是否有当前的cmd,如果有,直接更新其对应的rsp结构,并记录当前缓存时间,往返延迟。

缓存真实时间

考虑这样一种场景,某个热cmd同时是大key,那么其请求往还延时会很长,如果我们只按设置的缓存时间缓存这个rsp,势必导致很长的一段时间比例中,回复没有被缓存(过期后等待新的rsp)。

因此我们实际的缓存时间为,expire_ms + latency_ms

backup_req

backup_req是一种重要缓存保持机制。以上的方案当缓存过期时,会存在大量的流量穿透到redis-server,以获取新的rsp。

因此可以在每一次请求的处理中增加一环逻辑:

  1. 判断当前请求的rsp离过期时间是否大于一半的过期时间了,如果是的,我们可以把这次请求不用缓存返回,而是主动将其置为backup_req穿透到redis-server中,用于续约和获取新数据。

该机制在热key持续存在的情况下可以保持热key缓存一直存在,而不会引发缓存雪崩。