备注:有需要存储海量日志的场景,秒级GB级或数十GB级,可关注我的另一个开源项目JLog,较ELK系列套件在处理日志方面提升10倍性能,且存储降低70%以上。

以下为hotkey相关

![输入图片说明](https://images.gitee.com/uploads/images/2020/0616/105737_e5b876cd_303698.png “redis热key探测及缓存到JVM (1)
京东APP后台热数据探测框架能够对突发性的热点请求进行毫秒级的精准探测,包括热点商品、恶意爬虫用户和高负载接口等。该框架将这些热数据推送到所有服务端JVM内存中,以减轻后端数据存储层的压力。使用者可以决定如何分配和使用这些热key,例如本地缓存热商品、拒绝访问热用户或对热接口进行熔断。在多次高压压测和大促期间的考验中,该框架表现出色,每天处理数十亿个key,大幅降低了热数据对数据层的查询压力,提升了应用性能。在大促期间,hotkey的worker集群秒级吞吐量达到1500万级别,本地缓存占应用总访问量的50%以上。性能指标显示,8核单机worker端每秒可处理16万个key探测任务,16核单机至少每秒平稳处理30万以上。推送性能方面,在高并发写入的同时,对外推送性能约平稳推送每秒10-12万次。每秒单机吞吐量(写入+对外推送)目前在70万左右稳定。该框架的开发得到了京东数科、京东零售等多个部门的支持和贡献。

输入图片说明
屏幕截图.png

输入图片说明
屏幕截图.png

采用protobuf序列化后性能进一步得到提升。在秒级36万以上时,能稳定在CPU 60%,压测持续时长超过5小时,未见任何异常。30万时,压测时长超过数日,未见任何异常。

输入图片说明
屏幕截图.png

输入图片说明
屏幕截图.png

输入图片说明
屏幕截图.png

界面效果

输入图片说明
屏幕截图.png

加微信入群讨论问题,1群已满,请加2群

输入图片说明
12.png

常见问题

1 worker挂了怎么办

client根据worker的数量对key进行hash后分发,同一个key一定会被发往同一个worker。譬如4台,挂了一台,key就自动hash到另外3台。那么这个过程中,就会丢失最多一个探测周期内的所有发来的key,譬如2秒10次算热,那么就可能全部被rehash,丢失这2秒的数据。

它的影响是什么呢?我要不要去存下来所有发来的key呢?很多人都会问的问题。

首先挂机,那是极其罕见的事件,即便挂了,对于特别热的key,完全不影响,hash丢几秒,不影响它继续瞬间变热。对于不热的key,它挂不挂,它也热不了。对于那些将热未热的,可能会这次让它热不起来,但没有什么影响,业务服务完全可以吃下这个热key。而加上一堆别的组件如存储、worker间通信传输key等,它的复杂度,性能都会影响很大。

所以它挂了对系统没有任何影响

2 为什么全部要worker汇总计算,而不是客户端自己计算

首先,客户端是会本地累加的,在固定的上报周期内,如500ms内,本地就是在累加,每500ms批量上报给worker一次。如果上报频率很高,如10ms一次,那么大概率本地同一个key是没有累加。

有人会说,把这个间隔拉长,譬如本地计算3秒后,本地判定热key,再上报给其他机器。那么这种场景首先对于京东是不可行的,哪怕1秒都不行。譬如一个用户刷子,它在非常频繁地刷接口,一秒刷了500次,而正常用户一秒最多点5次,它已经是非常严重的刷子了。但我们本地还是判断不出来它是不是刷子。为什么?机器多。

随便一个app小组都有数千台机器,一秒500次请求,一个机器连1次都平均不到,大部分是0次,本地如何判断它是刷子呢?总不能访问我一次就算它刷吧。

然后抢购场景,有些秒杀商品,1-2秒就没了,流量就停了,你本地计算了3秒,才去上报,那活动已经结束了,你的热key再推送已经没价值了。我们就要在活动即将开始之前的可能在10ms内,就要该商品被推送到所有client的jvm里去,根本等不了1秒。

3 为什么是worker推送,而不是worker发送热key到etcd,客户端直接监听etcd获取热key

(1) worker和client是长连接,产生热key后,直接推送过去,链路短,耗时少。如果是发到etcd,客户端再通过etcd获取,多了一层中转,耗时明显增加。

(2) etcd性能不够,存在单点风险。譬如我有5000台client,每秒产生100个热key,那么每秒就对应50万次推送。我用2台worker即可轻松完成,随着worker的横向扩展,每秒的推送上限线性增加。但无论是etcd、redis等等任何组件,都不可能做到1秒50万次拉取或推送,会瞬间cpu爆满卡死。因为worker是各自隔离的,而etcd是单点的。实际情况下,也不止5000台client,每秒也不止100个热key,只有当前的架构能支撑。

4 为什么是etcd,不是zookeeper之类的

etcd里面具备一个过期删除的功能,你可以设置一个key几秒过期,etcd会自动删除它,删除时还会给所有监听的client回调,这个功能在框架里是在用的,别的配置中心没有这个功能。

etcd的性能和稳定性、低负载等各项指标非常优异,完全满足我们的需求。而zk在很多暴涨流量前和高负载下,并不是那么稳定,性能也差的远。

安装教程

  1. 安装etcd

    在etcd下载页面下载对应操作系统的etcd,https://github.com/etcd-io/etcd/releases 使用3.4.x以上。相关搭建细节,及常见问题会发布到CSDN博客内。

  2. 启动worker(集群) 下载并编译好代码,将worker打包为jar,启动即可。如:

    java -jar $JAVA_OPTS worker-0.0.1-SNAPSHOT.jar --etcd.server=${etcdServer}

    worker可供配置项如下:

    输入图片说明
    屏幕截图.png

    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。

    输入图片说明
    屏幕截图.png

热键检测规则说明

热键检测是一种用于识别在短时间内频繁访问的键值对的技术。以下是一组热键检测规则的示例:

  • 前缀匹配: prefix-true 表示启用前缀匹配功能。

  • 热键规则: as__ 开头的热键规则,其中 interval-2秒 表示在2秒内,threshold-10次 表示触发条件为出现10次,则认为它是热键。

  • 缓存时间: 热键一旦被检测到,会被推送到JVM内存中,并缓存60秒。

客户端接入使用指南

  1. 引入依赖: 在项目的 pom.xml 文件中添加客户端依赖。

  2. 初始化热键检测: 在应用启动的地方初始化 HotKey。 示例代码:

1
2
3
4
// 引入依赖
// 在pom.xml中添加
// 初始化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();
}

aaaaaaa aaaaaaa a在配置本地缓存时,可以通过setCaffeineSize(int size)方法来设置缓存的最大数量,默认值为5万。此外,可以使用setPushPeriod(Long period)方法来定义批量推送key的间隔时间,该参数默认设置为500毫秒。

  • setCaffeineSize(int size): 设置本地缓存中能够存储的最大数据项数量。默认情况下,这个值被设定为50,000。

  • setPushPeriod(Long period): 控制将热key推送到远程服务的时间间隔。更小的数值意味着更高的上报频率和更快的响应速度。推荐根据实际应用场景调整此参数;例如,如果单机每秒处理查询(QPS)大约10个,那么可以考虑设置为500毫秒上报一次,否则过高的上报频率可能会导致不必要的资源消耗。

  • 注意setPushPeriod的最小值为1毫秒。 注意事项:

  • 如果你的项目已经依赖了Guava库,为了兼容性和避免jar包冲突,请确保升级到指定版本的Guava。或者,你可以从自己的项目中移除Guava的Maven依赖。这样做不会对现有逻辑造成影响。 aaaaaaa a

<dependency>
 <groupId>com.google.guava</groupId>
 <artifactId>guava</artifactId>
 <version>28.2-jre</version>
 <scope>compile</scope>
</dependency>

Maven依赖管理

排除Guava依赖

有时项目可能没有直接依赖Guava,但引入的某个POM文件中包含了Guava依赖。在这种情况下,您需要在项目的pom.xml文件中显式排除Guava。

降级Fastjson版本

如果项目中使用了Fastjson,并且需要降级到2.0.0版本以下,请确保执行此操作。因为在2.0.0及以上版本中,com.alibaba.fastjson.serializer.JSONLibDataFormatSerializer类已经被删除。这会导致JSON工具类com.jd.platform.hotkey.common.tool.FastJsonUtils在初始化时找不到该类,进而影响规则配置的JSON转换。

推荐版本

建议使用与HotKey相同的Fastjson版本,以确保兼容性和稳定性。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.70</version>
</dependency>

aaaaaaa 使用JdHotKeyStore类进行热key管理的说明

在处理高并发请求或识别频繁访问的资源时,可以使用JdHotKeyStore类来帮助管理和识别所谓的’热key’。以下是该类提供的主要方法及其应用场景:

方法列表

  • boolean isHotKey(String key)

  • 功能:判断给定的key是否为热key。

  • 返回值:如果是热key返回true,否则返回false

  • 行为:当调用此方法时,会将key上报至探测集群以统计访问次数。

  • 应用场景:适合于仅需判断key热度而无需获取具体缓存值的情况,比如识别刷子用户行为、监测API接口访问频率等。

  • Object get(String key)

  • 功能:获取与给定key关联的本地缓存值。

  • 返回值:返回key对应的value,如果未找到则可能返回null。

  • 应用场景:通常在确认了某个key为热key之后,用于从本地缓存中获取数据,例如结合Redis缓存使用。

  • void smartSet(String key, Object value)

  • 功能:仅为被标记为热key的项设置value。

  • 行为:若指定的key是热key,则执行赋值操作;反之不做任何改变。

  • Object getValue(String key)

  • 功能:综合检查并尝试返回指定key的本地缓存值。

  • 返回情况:对于热key有两种结果——已设置了value时直接返回之,或者从未设置过value则返回null;非热key总是返回null,并同时向探测系统报告此次查询。

  • 应用场景:提供了一种简便的方式来同时检查key状态和获取其对应的数据。

最佳实践示例

判断用户是否为刷子用户

  • 可通过定期或触发式地调用isHotKey方法来监控特定用户的活动频率。

  • 若发现某用户ID对应的key频繁出现(即isHotKey返回true),则可进一步采取措施如限制访问速率、增加验证码验证等手段,以防止滥用服务。

    if (JdHotKeyStore.isHotKey(“pin__” + thePin)) {
        //限流他,do your job
    } 

2 判断商品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即可
              }

         }