1、背景
Tair是集体内部广泛使用的分布式K-V缓存系统,在该系统上进行读写时,会根据key的hash计算出一台固定的server来存取该K-V。因此在实际应用中,某些高峰时段,会大量请求同一个Key(可能对应应用的某个促销商品、热点新闻、热点评论等),根据key的hash,所有读请求都将落到同一个server上,该机器的负载就会严重加剧,此时整个系统增加server也没有任何用处,因为根据hash算法,同一个key的请求还是会落到同一台新机器上,该机器成为系统瓶颈。这个问题称为“热点key”问题。
2、热点key问题解决原理
解决热点问题有很多办法,如果不能从业务的角度将热点拆分,那么只能采取多份数据的形式分担(C/S 分摊,服务端线程间、机器间、集群间分摊)。比如用户如果提前知道某些key可能成为热点,那么客户端可以提前拆分热点key;也可以搭建一个备用集群,写的时候双写,然后随机双读。这些方案都有各自的缺点。下面给出的是自动运行的解决方案:
本方案在原有集群的基础上做修改,不改变集群部署,对应用透明。主要分为两个模块服务端热点统计和客户端Local-cache逻辑。
即在data-server上面运行热点统计逻辑,对每个到达的读请求,提取出key进行统计,并计算出当前请求中的热点key,同时当客户端访问的正好是热key,则向客户端回一个热点反馈feedback包。客户端收到feedback包之后,就将携带的hot-key写入客户端local-cache,同时拒绝非热key写入local-cache。
2.1 服务端热点统计
2.1.1 如何启动
在Tair3.2.4的DS上面增加HotKey模块,默认DS启动后就开启,可通过接口随时控制启停热点统计功能。该功能启动后,会做相关初始化,比较重要的有:设定统计热点key时要关注的opcode列表,设定采样次数,设定热点QPS阈值,设定服务器端热点key容量上限,以及每个热点key在服务器的生命周期。
2.1.2 流程
服务器识别热点的HotKey逻辑,是加在worker线程里,每个到达服务器的读请求(写请求不需要统计)首先竞争一把try锁,获取锁成功后,会经历三个阶段,失败的请求不做统计:
采样阶段
判断本请求的opcode是否在配置的opcode列表里面;
维护1024个桶,将每个请求上的pick-key哈希到桶,记录每个桶的命中数,记录当前采样次数(tair每个请求中的key形式会有pkey-skey、multi-key、single-key等,如何对他们中的有效部分做统计,后面详细叙述,这里统称有效key为pick-key);
当采样次数达到sample_max(服务器配置)时,计算这些桶的标准差和均值,同时记下最大的桶号;
如果这些桶的标准差大于均值的N倍(设定系数),则认为存在热点,当最大的桶号的QPS大于阈值,那么最大的桶号就是当前热桶,进入定位阶段,同时进入下一个采样周期。
即热点现象判断公式为:
所有桶的count计数的标准差 > 这些桶的count计数的均值 * N(系数)
定位阶段
接下来会再统计reap_max次命中了热桶的请求,计算出热桶中的每个pick-key的访问次数,统计reap_max次停止;
挑选计数最大的X个pick-key,如果X个中的某些pick-key的QPS大于阈值,则这些key被认定为热 key;
将判定为hot的pick-key加入服务器热点LRU链表,同时进入下一个定位周期,释放本次try锁。
查询阶段
对到达服务器的每一个读请求,都会取出pick-key,然后查询服务器LRU链表;
如果命中,则对该pick-key标记为热点;
该请求结束后,会额外发送一个feedback包,携带热点key,通知客户端。
至此,服务器hot-key逻辑结束。
2.1.3如何获取pick-key
这部分需要有tair使用经验。下面针对不同操作有不同的热点key统计策略。当前热点统计功能覆盖了所有读操作,共10个接口如下:
单key请求
get、prefix-get、get-hidden、prefix-get-hidden这类接口的key是普通单key或mixed-key,在服务器端hot-key逻辑看来,跟普通单key没有区别。直接取出key或mixed-key做统计。
prefix请求
prefix-gets、prefix-get-hiddens、simple-prefixgets(当前只有一个pkey)这类接口是一个pkey下面包含多个skey。服务器端统计时只取其pkey部分,pkey的命中次数以skey的个数作为权值。如果该pkey成为热点,服务器会通过feedback控制客户端,缓存该pkey下所有skey。
即:带pkey的操作产生热点时,会将该pkey下所有key写入Local-cache。
mprefix-gets、mprefix-gethiddens这类接口是有多个pkey,并且每个pkey下面有多个skey。但是到达服务端还是多个请求分开的,服务器端会把每个pkey都统计一遍,只返回其中热点的pkey。
多key请求
Mget这类请求会一次性带多个key,服务端会迭代统计该请求上每个key,并只返回其中热点部分。
2.2客户端Local-cache逻辑
在热点key系统中,客户端唯一需要做的是:当服务器端反馈有热点产生时,客户端首先将该feedback包中的key写入local-cache,同时开启hot-running模式,该模式下只能写入服务端反馈的带热点标记的key,local-cache中非热点的key将逐步被淘汰。
2.2.1 Localcache介绍
本解决方案依赖于tair客户端的Local-cache,它是tair客户端自带的缓存功能,能将写入服务器的key-value,缓存在本地读取,其容量非常有限。使用local-cache需注意以下几个细节问题:
1. Localcache与tair-server间如何同步
当客户端打开cache功能后,每次写操作强制删除本地cache的key,读操作时,更新后端服务器的值到本地cache。
2. Localcache数据淘汰
由于客户端机器配置有限,Localcache只能缓存部分K-V。因此客户端Localcache有两种淘汰机制以防内存消耗过多,过期淘汰和容量逐出。
过期淘汰
即当读server操作成功后,更新Localcache时,会将每个key设置一个在Localcache中合理的过期时间(默认30ms)。应用使用tair客户端时,大多是多线程访问。试想当多个线程发现某个key的存活超过过期时间时,会做什么处理呢?会有大量请求击穿Localcache,访问后端tair。
为了解决这样的击穿问题,这里加了一个过期时间续期的操作。当多个线程并发读取某个key1时,第一个竞争到锁的线程会判断key的过期时间,如果已经过期,则返回null,并对该key续期(即延长过期时间),由于返回的是null,该线程会击穿Localcache继续向server发起请求,并更新本地Localcache。其他线程访问key时,已经是续期之后,这些线程的访问该key时,就不会发现已经过期,读到的是老的value,等到第一个线程完成server端key-value到Localcache的更新结束后,所有线程都将读到新value。这样的好处是不会在某个key过期时间一到就击穿Localcache,并大量的访问server,对server造成压力。
如图,只有thread-1会击穿Localcache,其他线程拿到的要么是老的value,要么是最新value。
容量逐出
客户端Localcache容量较小,默认只能存30个key。超过之后就会触发LRU淘汰。LRU的实现我们用的是google的ConcurrentLinkedHashMap,比LinkedHashMap的实现性能更好,读写性能提升20%。它更新LRU链表是异步的,且合并了多次读操作需要的更新。
3. clone机制
Clone要解决的问题是:当客户端需要将key对应的value写入到Localcache时,很多场景需要深clone,以防止同一个对象在Localcache之外被修改,从而造成Localcache中的value与后端server的value不一致。
但是clone操作本身很耗费CPU,如果每次往Localcache中写value都需要clone的话,非常影响性能。并且大多数情况,用户访问一个key的机会只有一次,比如一个for迭代循环,每次访问不同(key + i),就需要把每个key对应的value,clone一次再写入Local-cache中。这样做有两个问题:一是非常耗时;二是:并写入Localcache中的K-V没有第二次命中的机会,这是不必要的clone。
因此我们clone的机制是,第一次写Localcache时,只把null当做value写入。当Localcache中该key还有第二次机会被访问时,客户端拿到null就会重新去服务器读最新数据,然后我们就真实的clone一次服务端读到的value并写入Localcache中。我们认为该key有第二次读到就会有更多次机会被读到。
2.2.1 如何使用
本方案依赖local-cache功能,因此使用时需要先打开local-cache。
Tair2客户端
使用tair2客户端的应用可以手动开启热点功能。该功能默认不开启,需要在Tair2客户端调用enableLocalCacheImprove接口。该接口刚被调用之后,无任何影响,直到第一次收到服务器的feedback包,此时客户端进入hot-running状态,即local-cache只能写入带有hot标记的key,其他的key只能逐步被淘汰(LRU或者过期)。Hot-running的持续时间是距离最近一次收到feedback之后的一分钟,此后就会回到初始enableLocalCacheImprove状态。
客户端调用disableLocalCacheImprove接口,就可以关闭local-cache的热点增强功能。
Mc客户端
使用Mc客户端的应用除了可以调用上面tair2的接口之外,还可以通过修改diamond配置的local-cache参数来调节。
需注意的是,当用户通过mc打开某个area的local-cache之后,如果在diamond里添加了同样area的local-cache,那么该local-cache的管理就有diamond配置接管,当diamond关闭或修改cache配置,就会同步生效。
3、兼容升级方案
本方案同时修改了客户端和服务器,不同时升级的话会有不兼容现象,因此我们有一个协议协商机制。
协商机制解决的问题是:
- 新的服务器在发现热点后会给客户端回一个feedback包,该协议在老的客户端上解析时会抛出异常。于是在客户端上加了一个register_client_version的协议,每次当客户端和服务端第一次建立连接时,就发送该协议,只有新版客户端发送的client-version才会触发服务端回feedback包,老版本客户端是不会发送该协议,服务端是默认不会回包的。
- 同样反过来,新版客户端建立连接时会发送register_client_version协议到老的服务器,也有decode相关问题。因此客户端有setNeedRegisterClientVersion接口来控制是否需要发送注册协议,默认是不发送。