背??景在Redis中,热键指的是那些在一段时间内访问频率比较高的键值。具体来说,在业务方面,包括商品限时抢购,即时新闻热点,或者某些全球资源极有可能产生热键。热键的出现可能会影响系统的稳定性和可用性。比如对应节点的网卡带宽被充分利用,出现丢包重传,请求波动耗时大幅增加,甚至影响业务的正常使用,造成用户恐慌。不满意。因此,在日常工作中,我们需要重点避免这种情况,比如在设计和编码阶段避免引入全局热键,或者在设计时考虑出现热键时的解决方案。可能的解决热点key即使我们在设计和开发的时候已经尽量避免了,但是在实际的生产环境中,它们还是有可能存在的。其持续出现的原因如下:一些边界情况没有考虑异常或非由于预期的流量无法完全避免,我们需要一种方法来快速定位是否有热键,如果有热键是什么问题,帮助业务快速排查问题,定位问题根源。如果我们要设计一个定位方案,我们可以从Redis请求路径上的节点开始,比如客户端,中间层,服务端,具体如下:1.客户端收集变化上报给RedisSDK,记录每一个请求,收集到的数据会定期上报,然后通过统一的服务聚合计算。解决方案直观简单,但无法适应多语言架构。一方面,多语言SDK的对齐是个问题。另一方面,后期SDK的维护和升级会面临比较大的困难和高昂的成本。2.代理层收集和上报如果所有的Redis请求都被代理了,可以考虑更改Proxy代码来收集。思路和client基本类似。该方案对用户完全透明,可以解决客户端SDK的语言异构和版本升级问题,但开发成本会高于客户端。3、定期扫描Redis数据Redis在4.0版本后增加了热键搜索功能[1],可以直接使用redis-cli--hotkeys获取当前keyspace的热键,通过scan+objectfreq实现.该方案不需要二次开发,直接使用现成的工具即可。但是由于需要扫描整个keyspace,实时性比较差。此外,扫描时间与按键数量呈正相关。如果键的数量很多,那么耗时可能会非常耗时。长的。4、Redis节点抓包分析在可能存在hotkey(流量倾斜判断)的节点上,通过tcpdump抓取一段时间内的流量并上报,然后通过外部程序进行分析、聚合、计算。该方案不需要侵入现有的SDK或Proxy中间件,开发和维护成本可控,但也有缺点。具体来说,热点关键节点的网络流量和系统负载已经比较高,抓包可能会进一步恶化。Redis的Monitor命令没有考虑,因为开销比较大。单个监控客户端会使系统吞吐量降低50%。更多详情参见:https://redis.io/commands/monitor我们的选择是由于饿了么在内部,所有的Redis请求都是通过透明代理Samaritan[2]传递的,代理是我们自己开发和维护的。代理层的改造成本是完全可控的,所以我们选择了第二种方案,即在代理层收集上报。大方向确定后,还需要考虑具体的细节问题,比如:记录所有请求如何保证不占用过多内存甚至OOM?记录所有的请求如何保证代理的性能,请求时间不会大幅增加?对于第一点,由于我们只关心热键而不是统计所有键的计数器,那么我们可以使用LFU只保留访问频率最高的,第二点需要结合具体实现来考虑中介。下图是proxy的内部实现,省略了一些不相关的细节:注:每个redis节点都会创建一个对应的唯一client,其上的所有请求都会通过pipeline执行。每个客户端都有自己内部的不同收集器,相互独立。HotkeyCollector的内部结构如下,包括LFUCounter、Syncer和EtraceClient:Etrace是一个内部应用监控平台,类似的开源产品是CAT的基本工作流程[3]是的,LFUCounter负责记录key的访问频率,Syncer会周期性的通过EtraceClient向远程服务器发送统计数据。另外,为了避免向服务器发送过多的无效数据,内部预设了一个阈值,只有超过阈值的才会发送给服务器。按照前期的设计,我们会有一个实时计算服务拉取Etrace上的数据,进行聚合计算得到当前的hotkey。遗憾的是,在代理中间件改造上线后,这个实时计算服务的开发迟迟没有提上日程。分析表明,投资回报率低和维护成本高是主要原因。因此,业务中如果想查看热键,只能手动戳Etrace上的事件碰碰运气。比如:因为使用起来很麻烦,用户基本体验了第一次就放弃了,不会再用第二次了,连我们自己都不愿意用……那时候我们急需找到更好的解决用户体验和系统复杂度的问题,让这个特性真正为业务赋能。如果最终的方案是对之前的方案进行优化,我们可以从以下两个方面着手:如何在不增加实时计算组件成本的情况下,高效聚合数据?如何提升用户体验,方便用户使用?对于第一点,当时的第一个想法是能不能把聚合逻辑放在agent进程中,这样就不需要依赖任何外部组件,可以降低整个系统的复杂度和维护成本。但是这里会有一个问题。设计外部聚合组件的初衷是聚合来自不同机器的数据。现在用单机数据会不会有什么问题?逻辑站得住脚吗?经过深思熟虑,逻辑成立。因为到达业务的流量是负载均衡的,所以不同实例上的流量比较均匀,不会相差太多。基于这个部分可以代表整体的原则,那么单个实例上的热键就可以代表一个全局的情况。.另外,在易用性和用户体验方面,如果聚合数据在处理过程中,我们可以提供类似HOTKEY的自定义命令,让用户直接通过redis-cli获取。最终的解决方案如下,不相关的细节省略:在实现上,每个集群都会有一个全局的HotkeyCollector,每个client都有自己独立的Counter。TheCounter仍然使用前面提到的LFU[4]算法,而Collector会定期收集每个Counter的数据并进行聚合。聚合时不会使用真实的计数,而是概率计数[5],并且为了适应访问方式的变化,计数器的值会随着时间衰减,总体上与redislfu[6]。下面是一个生产环境的真实例子,展示了最近一段时间的热键:注:默认logfactorfactor为10,counter值每分钟衰减一半。Collector的默认容量是32,只记录最频繁的请求32个key的输出和redis-cli--hotkeys的输出很像。counter的具体含义可以参考UsingRedisasanLRUcache[7]。并没有真正解决热键本身带来的问题。它仍然需要业务方对其进行修改或将这些热键调度到单独的节点。成本比较高,有些商家甚至自己做本地缓存。本着更好服务客户的原则,后面会考虑在proxy中实现hotkey缓存。但是,要在代理中实现缓存,需要解决内存使用、数据一致性和性能等问题。目前还没有很好的解决办法,还在研究中。好消息是Redis6计划实现服务器辅助客户端缓存[8]。如果可能,我们会考虑尽快对接。最后,实时热键采集功能已经上线并开源。相关源码可以在撒玛利亚人中找到。有兴趣的朋友可以试试。如果您有任何问题或想法,请提交问题或直接与??我交流。关于作者:你饿了吗?韩亮,CacheGroup,CI框架工具部链接:[1]https://github.com/antirez/redis/pull/4392[2]https://github.com/samaritan-proxy/samaritan[3]httpshttps://github.com/dianping/cat[4]https://en.wikipedia.org/wiki/Least_frequently_used[5]https://en.wikipedia.org/wiki/Approximate_counting_algorithm[6]http://antirez.com/news/109[7]https://redis.io/topics/lru-cache[8]https://redis.io/topics/client-side-caching
