《开源精选》是我们分享 Github、Gitee 等开源社区中优质项目的栏目,包括技术、学习、实用与各种有趣的内容。本期推荐的是京东 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个部分组成
etcd 集群
etcd 作为一个高性能的配置中心,可以以极小的资源占用,提供高效的监听订阅服务。主要用于存放规则配置,各 worker 的 ip 地址,以及探测出的热 key、手工添加的热 key 等。
client 端 jar 包
就是在服务中添加的引用 jar,引入后,就可以以便捷的方式去判断某 key 是否热 key。同时,该 jar 完成了 key 上报、监听 etcd 里的 rule 变化、worker 信息变化、热 key 变化,对热 key 进行本地 caffeine 缓存等。
worker 端集群
worker 端是一个独立部署的 Java 程序,启动后会连接 etcd,并定期上报自己的 ip 信息,供 client 端获取地址并进行长连接。之后,主要就是对各个 client 发来的待测 key 进行累加计算,当达到 etcd 里设定的 rule 阈值后,将热 key 推送到各个 client。
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 上报到探测集群进行数量探测。
最佳实践
判断用户是否是刷子
if (JdHotKeyStore.isHotKey(“pin__” + thePin)) {
//限流他,do your job
}
判断商品id是否是热点
Object skuInfo = JdHotKeyStore.getValue("skuId__" + skuId);
if(skuInfo == null) {
JdHotKeyStore.smartSet("skuId__" + skuId, theSkuInfo);
} else {
//使用缓存好的value即可
}
或者这样:
if (JdHotKeyStore.isHotKey(key)) {
//注意是get,不是getValue。getValue会获取并上报,get是纯粹的本地获取
Object skuInfo = JdHotKeyStore.get("skuId__" + skuId);
if(skuInfo == null) {
JdHotKeyStore.smartSet("skuId__" + skuId, theSkuInfo);
} else {
//使用缓存好的value即可
}
}
-END-
开源协议:Apache 2.0
开源地址:https://gitee.com/jd-platform-opensource/hotkey
原文始发于微信公众号(开源技术专栏):京东毫秒级热 key 探测框架设计与实践
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/65993.html