本期推荐的是京东App后台中间件,毫秒级探测热点数据,毫秒级推送至服务器集群内存,大幅降低热key对数据层查询压力。
项目介绍
对任意突发性的无法预先感知的热点数据,包括并不限于热点数据(如突发大量请求同一个商品)、热用户(如恶意爬虫刷子)、热接口(突发海量请求同一个接口)等,进行毫秒级精准探测到。然后对这些热数据、热用户等,推送到所有服务端JVM内存中,以大幅减轻对后端数据存储层的冲击,并可以由使用者决定如何分配、使用这些热key(譬如对热商品做本地缓存、对热用户进行拒绝访问、对热接口进行熔断或返回默认值)。这些热数据在整个服务端集群内保持一致性,并且业务隔离,worker端性能强悍。
该框架历经多次压测,性能指标主要有两个:
1 探测性能:8核单机worker端每秒可接收处理16万个key探测任务,16核单机至少每秒平稳处理30万以上,实际压测达到37万,CPU平稳支撑,框架无异常。
2 推送性能:在高并发写入的同时,对外推送目前性能约平稳推送每秒10-12万次,譬如有1千台server,一台worker上每秒产生了100个热key,那么这1秒会平稳推送100 * 1000 = 10万次,10万次推送会明确在1s内全部送达。如果是写入少,推送多,以纯推送来计数的话,该框架每秒可稳定对外推送40-60万次平稳,80万次极限可撑几秒。
每秒单机吞吐量(写入+对外推送)目前在70万左右稳定。
核心功能:
热数据探测并推送至集群各个服务器
适用场景:
- mysql热数据本地缓存
- redis热数据本地缓存
- 黑名单用户本地缓存
- 爬虫用户限流
- 接口、用户维度限流
- 单机接口、用户维度限流
- 集群用户维度限流
- 集群接口维度限流
什么是热key
- MySQL等数据库会被频繁访问的热数据
- 如爆款商品的skuId。
- redis的被密集访问的key
- 如爆款商品的各维度信息,skuId、shopId等。
- 机器人、爬虫、刷子用户
- 如用户的userId、uuid、ip等。
- 某个接口地址
- 如/sku/query或者更精细维度的。
- 用户id+接口信息
- 如userId + /sku/query,这代表某个用户访问某个接口的频率。
- 服务器id+接口信息
- 如ip + /sku/query,这代表某台服务器某个接口被访问的频率。
- 用户id+接口信息+具体商品
- 如userId + /sku/query + skuId,这代表某个用户访问某个商品的频率。
以往热key问题怎么解决
我们分别以redis的热key、刷子用户、限流等典型的场景来看。
redis热key:
这种以往的解决方式比较百花齐放,比较常见的有:
- 上二级缓存,读取到redis的key-value信息后,就直接写入到jvm缓存一份,设置个过期时间,设置个淘汰策略譬如队列满时淘汰最先加入的。或者使用guava cache或caffeine cache进行单机本地缓存,整体命中率偏低。
- 改写redis源码加入热点探测功能,有热key时推送到jvm。问题主要是不通用,且有一定的难度。
- 改写jedis、letture等redis客户端的jar,通过本地计算来探测热点key,是热key的就本地缓存起来并通知集群内其他机器。
刷子爬虫用户:
- 日常累积后,将这批黑名单通过配置中心推送到jvm内存。存在滞后无法实时感知的问题。
- 通过本地累加,进行实时计算,单位时间内超过阈值的算刷子。如果服务器比较多,存在用户请求被分散,本地计算达不到甄别刷子的问题。
- 引入其他组件如redis,进行集中式累加计算,超过阈值的拉取到本地内存。问题就是需要频繁读写redis,依旧存在redis的性能瓶颈问题。
限流:
- 单机维度的接口限流多采用本地累加计数
- 集群维度的多采用第三方中间件,如sentinel
- 网关层的,如Nginx+lua
综上,我们会发现虽然它们都可以归结到热key这个领域内,但是并没有一个统一的解决方案,我们更期望于有一个统一的框架,它能解决所有的对热key有实时感知的场景,最好是无论是什么key、是什么维度,只要我拼接好这个字符串,把它交给框架去探测,设定好判定为热的阈值(如2秒该字符串出现20次),则毫秒时间内,该热key就能进入到应用的jvm内存中,并且在整个服务集群内保持一致性,要有都有,要删全删。
该框架主要由4个部分组成
1、etcd集群
etcd作为一个高性能的配置中心,可以以极小的资源占用,提供高效的监听订阅服务。主要用于存放规则配置,各worker的ip地址,以及探测出的热key、手工添加的热key等。
2、client端jar包
就是在服务中添加的引用jar,引入后,就可以以便捷的方式去判断某key是否热key。同时,该jar完成了key上报、监听etcd里的rule变化、worker信息变化、热key变化,对热key进行本地caffeine缓存等。
3、worker端集群
worker端是一个独立部署的Java程序,启动后会连接etcd,并定期上报自己的ip信息,供client端获取地址并进行长连接。之后,主要就是对各个client发来的待测key进行累加计算,当达到etcd里设定的rule阈值后,将热key推送到各个client。
4、dashboard控制台
控制台是一个带可视化界面的Java程序,也是连接到etcd,之后在控制台设置各个APP的key规则,譬如2秒出现20次算热key。然后当worker探测出来热key后,会将key发往etcd,dashboard也会监听热key信息,进行入库保存记录。同时,dashboard也可以手工添加、删除热key,供各个client端监听。
综上,可以看到该框架没有依赖于任何定制化的组件,与redis更是毫无关系,核心就是靠netty连接,client端送出待测key,然后由各个worker完成分布式计算,算出热key后,就直接推送到client端,非常轻量级。
worker端强悍的性能表现
每10秒打印一行,totalDealCount代表处理过的key总量,可以看到每10秒处理量在270万-310万之间,对应每秒30万左右QPS。
采用protobuf序列化后性能进一步得到提升。在秒级36万以上时,能稳定在CPU 60%,压测持续时长超过5小时,未见任何异常。30万时,压测时长超过数日,未见任何异常。
安装教程
安装etcd
1.在etcd下载页面下载对应操作系统的etcd,
https://github.com/etcd-io/etcd/releases 使用3.4.x以上。
2.启动worker(集群) 下载并编译好代码,将worker打包为jar,启动即可。如:
java -jar $JAVA_OPTS worker-0.0.1-SNAPSHOT.jar --etcd.server=${etcdServer}
worker可供配置项如下:
etcdServer为etcd集群的地址,用逗号分隔
JAVA_OPTS是配置的JVM相关,可根据实际情况配置
threadCount为处理key的线程数,不指定时由程序来计算。
workerPath代表该worker为哪个应用提供计算服务,譬如不同的应用appName需要用不同的worker进行隔离,以避免资源竞争。
3.启动控制台
下载并编译好dashboard项目,创建数据库并导入resource下db.sql文件。 配置一下application.yml里的数据库相关和etcdServer地址。
启动dashboard项目,访问ip:8081,即可看到界面。
其中节点信息里,即是当前已启动的worker列表。
规则配置就是为各app设置规则的地方,初次使用时需要先添加APP。在用户管理菜单中,添加一个新用户,设置他的APP名字,如sample。之后新添加的这个用户就可以登录dashboard给自己的APP设置规则了,登录密码默认123456。
如图就是一组规则,譬如其中as__开头的热key的规则就是interval-2秒内出现了threshold-10次就认为它是热key,它就会被推送到jvm内存中,并缓存60秒,prefix-true代表前缀匹配。那么在应用中,就可以把一组key,都用as__开头,用来探测。
4.client端接入使用
引入client的pom依赖。
在应用启动的地方初始化HotKey,譬如
@PostConstruct
public void initHotkey() {
ClientStarter.Builder builder = new ClientStarter.Builder();
ClientStarter starter = builder.setAppName("appName").setEtcdServer("http://1.8.8.4:2379,http://1.1.4.4:2379,http://1.1.1.1:2379").build();
starter.startPipeline();
}
其中还可以setCaffeineSize(int size)设置本地缓存最大数量,默认5万,setPushPeriod(Long period)设置批量推送key的间隔时间,默认500ms,该值越小,上报热key越频繁,响应越及时,建议根据实际情况调整,如单机每秒qps10个,那么0.5秒上报一次即可,否则是空跑。该值最小为1,即1ms上报一次。
注意:
如果原有项目里使用了guava,需要升级guava为以下版本,否则过低的guava版本可能发生jar包冲突。或者删除自己项目里的guava的maven依赖,guava升级不会影响原有任何逻辑。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
<scope>compile</scope>
</dependency>
有时可能项目里没有直接依赖guava,但是引入的某个pom里引了guava,也需要将guava排除掉。
使用
主要有如下4个方法可供使用
boolean JdHotKeyStore.isHotKey(String key)
Object JdHotKeyStore.get(String key)
void JdHotKeyStore.smartSet(String key, Object value)
Object JdHotKeyStore.getValue(String key)
1 boolean isHotKey(String key) ,该方法会返回该key是否是热key,如果是返回true,如果不是返回false,并且会将key上报到探测集群进行数量计算。该方法通常用于判断只需要判断key是否热、不需要缓存value的场景,如刷子用户、接口访问频率等。
2 Object get(String key),该方法返回该key本地缓存的value值,可用于判断是热key后,再去获取本地缓存的value值,通常用于redis热key缓存
3 void smartSet(String key, Object value),方法给热key赋值value,如果是热key,该方法才会赋值,非热key,什么也不做
4 Object getValue(String key),该方法是一个整合方法,相当于isHotKey和get两个方法的整合,该方法直接返回本地缓存的value。 如果是热key,则存在两种情况,1是返回value,2是返回null。返回null是因为尚未给它set真正的value,返回非null说明已经调用过set方法了,本地缓存value有值了。 如果不是热key,则返回null,并且将key上报到探测集群进行数量探测。