背景
对于一个互联网平台,特地是 toB 的 PaaS/SaaS 平台,热点 key 是一个绕不过来的问题。作为一个凋谢的零碎,平台每天要承载来自大量的内部零碎或者海量终端的申请,尽管所有的申请都须要满足开放平台定义好的鉴权规定,然而突发的申请或者异样的申请,却总是不期而至。
对于 toB 零碎来说,这样的申请至多包含以下几种类型:
- 客户谬误应用姿态导致的异样流量
PaaS 平台往往以 API 和 SDK 的形式对外提供服务,尽管咱们会提供各种 demo 和解决方案来领导客户以更优雅的形式应用咱们的服务,然而你永远无奈预测某个客户以一种你料想不到的形式调用咱们的 API。
- 来自未知攻击者的流量
以云信为例,咱们的服务器长年受到各种四层、七层攻打。四层流量个别不会间接到后盾服务器,然而对于七层流量就除了通用的 waf 防护之外,也须要在业务层能及时发现和定位起源。
- 客户压测
没错,客户常常会在没有当时告诉的状况给咱们一个大惊喜,为了爱护咱们的零碎不被从天而降的流量打垮,咱们必须疾速辨认这样的流量并作出适合的反馈。
- 客户的客户,谬误应用姿态导致的异样流量
对于 toB 产品,平台间接面对的往往是开发者或者说企业,客户的客户除了 C 端客户之外,也可能是其余企业,这种简单的关系导致不确定性大大增加。
- 客户被攻打产生的到平台的异样流量
咱们也不断收到来自客户的申请,反馈他们被黑产刷了,心愿咱们帮忙提供解决方案,这些黑产往往应用各种工具,也往往随同着大流量,PaaS/SaaS 厂商属于躺枪的角色。
为了应答上述各种突发和异样流量带来的零碎稳定性危险,开放平台往往会针对租户级别设置一些接口的 QPS 限度(频控系统),从而爱护零碎。然而频控往往是在入口层(API)设置规定,并不能残缺形容突发异样流量对系统产生压力的根本原因(比方某个 redis-key 或者某个数据库行),也就是说频控系统是面向内部视角的,而危险源却是面向外部资源的,从危险点自身看,频控系统的逻辑有时候显得有一些粗放。此外,因为各种各样的起因,频控配置的设置往往也不能笼罩残缺,而且随着零碎的一直演讲,相干参数也须要一直进行调整。
因而在频控系统之外,还须要一个热点探测平台,热点探测系统聚焦于产生零碎稳定性危险的热点自身,从而能够准确的评估和发现危险点。
为什么要自研
咱们调研了已有的热点探测的计划,比方京东的 hotkey(https://gitee.com/jd-platform-opensource/hotkey),搜狐的 hotCaffeine(https://github.com/sohutv/hotcaffeine),对内征询了网易云音乐的 hotCaffiene 外部 fork 版本,在剖析了性能特点和咱们的需要后,基于以下几个起因,咱们决定在参考上述计划的根底上进行自研。
上述开源计划均和 etcd 强绑定,etcd 在零碎中表演了配置核心和注册核心的角色,并且无奈应用其余服务代替,咱们冀望复用已有的配置核心和注册核心,而不须要再独自保护一套 etcd 集群。
上述开源计划更多侧重于热 key 缓存,而非监控自身。
咱们冀望框架尽可能的精简,有最小的依赖。
咱们心愿以插件化的形式凋谢相干接口,从而能够更不便的对接到不同部门的外部零碎中,以更不便的开发自定义业务逻辑
基于上述起因,咱们决定自研 camellia-hot-key 这样的热 key 探测框架。
零碎架构
架构图
架构原理
作为一个通用的热点探测平台,在设计时首先须要解决以下几个问题:
- 怎么收集和统计热点 key 的数量?
- 怎么定义和治理热点 key 的规定?
- 怎么应用热点 key 的探测后果?
怎么收集和统计热点 key 的数量?
因为热点 key 是一个全局维度的定义,因而必然须要一个中心化的服务器来汇总,第一反馈就是应用 redis 之类的中心化缓存来作为一个集中的计数器管理工具。然而面对海量的 key,redis 却存在一些很显著的性能瓶颈,即便不思考性能问题,产生的资源开销也是微小的,因而咱们须要设计一个低资源开销的计划。
当咱们从新扫视热 key 探测的场景能够发现,实际上咱们并不需要实时,而仅须要准实时(百 ms 级别)即可满足绝大部分场景。因而通过本地缓存和批量解决,将能够极大的升高中心化服务器的压力,而且也能通过灵便的调整缓存时长和批量大小来满足不同业务的不同需要。而对于服务器自身,参考 redis-cluster 的 slot 分片的思路,咱们能够将 hot-key-server 依照肯定规定进行 hash 分片,从而保障雷同的 key 路由到雷同的 server 节点,从而把热 key 的计算齐全本地化,配合后面说的本地缓存 + 批量解决,最终能够以较小的代价实现大量 key 的统计和计算。
除了中心化服务器之外,在收集和统计上还须要解决一个海量 key 的问题。显然咱们不能粗犷的在一个工夫窗口内记录下所有 key,一个简略的思路是应用 LRU/LFU 等算法来进行内存管制。为此,咱们考查了【ConcurrentLinkedHashMap/Guava/Caffeine】等开源框架(这几个是同一个作者在不同工夫实现的,对作者 @Ben Manes 示意敬意),Caffeine 号称过程缓存之王,其 W-TinyLFU 算法提供了最优缓存命中率,在热点探测场景下也能让咱们不会错过热 key,因而 Caffeine 无疑是咱们的第一抉择。但再进一步剖析后,咱们最终抉择了混用 ConcurrentLinkedHashMap 和 Caffeine,以达到最优的性能。
通过下面的剖析,根本的服务器架构就比拟清晰明了了,整个零碎包含 SDK 和 server 两局部:SDK 应用 Caffeine 来收集 key,并定时上报给 server;server 收到后,仍然应用 Caffeine 来进行收集和计算 key。而对于不同的 namespace,因为数量可预期,则应用 ConcurrentLinkedHashMap 来进行治理(仅须要简略的 lru 进行零碎爱护即可)。
怎么定义和治理热点 key 的规定?
热 key 探测显然是一个须要灵便变更热 key 规定的服务,热 key 规定次要包含 2 局部:
- 一个是什么 key 须要探测。
- 一个是什么样的 key 算热 key。
前者咱们提供了前缀匹配、齐全匹配、子串蕴含、匹配所有等不同的 key 规定匹配模式,不便业务进行不同维度的配置,并且提供规定列表的概念,依照定义的优先级进行匹配。
对于后者,咱们凋谢了工夫窗口、热 key 阈值两个参数来定义热 key。工夫窗口是一个滑动的窗口,以 1000ms 内 500 次这样的规定为例,框架外部会以 100ms 为一个滑动小窗口,取最近 10 个小窗口(100ms*10=1000ms)组成一个 1000ms 的指标窗口进行计数,这种形式一方面能够第一工夫辨认出热 key,另一方面对于热 key 的探测也不会存在跳跃的问题。
对于热 key 规定的应用,除了在 server 端之外,框架也会下发给 SDK,从而对于不须要的 key,能够间接在 SDK 侧抛弃,从而缩小不必要的网络传输。
作为一个通用的热 key 探测服务,显然会服务于不同的业务线,即便是同一个业务线,也会有不同的业务场景。因而对于热 key 规定之外,咱们定义了 namespace 的概念,每个 namespace 下能够定义 1 个或者多个规定,一个服务反对同时配置多个 namespace,各个 namespace 之间相互隔离。
怎么应用热点 key 的探测后果?
首先,框架内咱们预设了一个可选的 hot-key-cache 形式,这是 SDK+server 共同完成的。针对一些热点查问场景,在检测到热 key 后,server 会主动把检测后果下发给 SDK,SDK 会主动把后果缓存起来,从而防止查问申请的穿透,爱护后端的缓存 / 数据库服务,并且为了保障缓存的时效性,server 还会把缓存后果的更新 / 删除事件告诉给关联的 SDK 端,从而尽可能的保障缓存值在第一工夫取得更新,SDK 也会把缓存命中状况上报给 server,不便服务器进行数据统计。
除此之外,server 会被动把探测后果推送给业务定义好的回调中,业务能够自行进行自定义的解决,如报警、限流、加黑等。
在接入初期,你可能并不知道如何设定热 key 规定,设置的小了,可能会被热 key 告诉给吞没,设置的大了又可能施展不了成果,因而 server 还内置了一个热 key 的 topN 探测性能,把 namespace 下拜访申请最多的 key 告知业务,业务能够据此进行故障定位,或者以此为根据设置合乎业务理论的热 key 规定。
插件化和自定义扩大口
后面讲了热 key 探测框架的基本原理,而 camellia-hot-key 在设计之初就思考到了不同业务线的不同需要,因而采纳了插件化的设计准则,不便业务在不批改框架源码的前提下,能够更灵便的应用,也能更容易的和已有零碎进行买通。插件化次要体现在以下几点:
- 注册核心
不同于已有的开源热 key 服务,camellia-hot-key 不和任何注册核心进行绑定,你仅需实现相干接口,即可十分疾速的和已有的注册核心进行整合,如 zk(内置)、eureka(内置)、etcd、consul、nacos 等。
- 配置核心
camellia-hot-key 不和任何配置核心进行绑定,而是定义了 HotKeyConfigService 配置接口,你仅需实现该接口,即可十分快的把热 key 规定托管到你已有的配置核心中(camellia-hot-key 内置了本地配置文件 +nacos 两种形式)。
HotKeyConfigService 配置接口定义如下:
public abstract class HotKeyConfigService {
/**
* 获取 HotKeyConfig
* @param namespace namespace
* @return HotKeyConfig
*/
public abstract HotKeyConfig get(String namespace);
/**
* 初始化后会调用本办法,你能够重写本办法去获取到 HotKeyServerProperties 中的相干配置
* @param properties properties
*/
public void init(HotKeyServerProperties properties) { }
// 回调办法
protected final void invokeUpdate(String namespace) {//xxxx}
}
此外,在 camellia-hot-key 的设计中,配置核心仅须要和 server 进行交互,SDK 会通过 server 主动获取到配置(配置初始化 + 配置更新),而不须要和配置核心直连,从而尽可能的简化 SDK 的逻辑(让 SDK 更瘦)。
监控端点和自定义回调
camellia-hot-key 的 server 提供了 http 的监控端点(json 格局 /promethus 格局),用于裸露服务器等根本信息(如 qps、沉积等)。
此外还提供了丰盛的回调接口,包含但不限于:
- HotKeyCallback
热 key 探测回调,会把热 key 通过本回调实时推送给业务侧,除了推送热 key 自身以及以后计数外,还会同时把热 key 命中的规定和热 key 的起源一起回调给业务。
- HotKeyTopNCallback
热 key 的 topN 回调,这是一个全局维度的 topN 统计(会汇总多个 server 节点的数据),框架会定时回调给业务(默认 1 分钟)。
- HotKeyCacheStatsCallback
在启用热 key 缓存性能的状况下,SDK 会定时上报热 key 缓存的命中状况,服务器会通过这个回调接口把统计数据回调给业务。
敌对的 SDK 接口
为了适配不同的利用场景,框架对外提供了 CamelliaHotKeyMonitorSDK 和 CamelliaHotKeyCacheSDK 两种不同的 SDK。
- CamelliaHotKeyMonitorSDK
用于单纯的热 key 统计性能,探测后果的解决由业务自行实现,探测后果的解决能够在 SDK 侧进行,也能够在 server 侧进行。外围接口只有一个:
/**
* 推送一个 key 用于统计和检测热 key
* @param namespace namespace
* @param key key
* @param count count
* @return Result 后果
*/
Result push(String namespace, String key, long count);
- CamelliaHotKeyCacheSDK
封装了热 key 缓存的性能,SDK 会主动把探测到的热 key 进行本地缓存,业务接入方仅需实现 ValueLoader 接口即可。外围接口包含以下三个:
/**
* 获取一个 key 的 value
* 如果是热 key,则会优先获取本地缓存中的内容,如果获取不到则会走 loader 穿透
* 如果不是热 key,则通过 loader 获取到 value 后返回
*
* 如果 key 有更新了,hot-key-server 会播送给所有 sdk 去更新本地缓存,从而保障缓存值的时效性
*
* 如果 key 没有更新,sdk 也会在配置的 expireMillis 之前尝试刷新一下(单机只会穿透一次)*
* @param namespace namespace
* @param key key
* @param loader value loader
* @return value
*/
<T> T getValue(String namespace, String key, ValueLoader<T> loader);
/**
* key 的 value 被更新了,须要调用本办法给 hot-key-server,进而播送给所有人
* @param namespace namespace
* @param key key
*/
void keyUpdate(String namespace, String key);
/**
* key 的 value 被删除了,须要调用本办法给 hot-key-server,进而播送给所有人
* @param namespace namespace
* @param key key
*/
void keyDelete(String namespace, String key);
性能
对于 Camellia-hot-key,咱们进行了大量的优化和调优,次要包含如下:
- 传输协定
SDK 和 server 应用长链接(基于 netty4)和服务器进行交互,并且摒弃了 json/ 文本等协定,转而应用了更加精简的二进制协定,从而优化了性能(为了尽可能减少内部依赖,没有应用 pb 等第三方序列化库)。
- 无锁化设计
SDK 到 server 有一层 hash,保障雷同 key 被雷同 server 解决。此外,server 在收到音讯后,同样会依据 key 进行 hash 分流,从而雷同 key 只会被一个线程解决,极大的简化了滑动窗口的设计,也防止了锁(全流程无锁化)。
- JDK-17
咱们应用 jdk8 和 jdk17 别离进行测试,发现雷同吞吐量的状况下,jdk17 比 jdk8 有更低的 CPU 占用。
以下是一个简略的性能测试后果:
咱们都怎么应用
作为一个通用的热 key 探测框架,智企外部多条业务线均接入了该框架,并且通过相干自定义接口无缝的接入到了已有的零碎中,以 IM 为例,咱们在以下业务流程中接入了热点探测服务:
对于来自 IM-SDK 的申请,依据 uid+ 租户 id+ 接口以及租户 id+ 接口做了两个维度的探测,从而辨认异样的 C 端客户和异样的租户。
对于来自 IM-openAPI 的申请,依据租户 id+ 接口做了探测,从而辨认异样的租户和异样接口。
底层数据库层面,以 db 为例。咱们通过 mybatis 的 plugin 插件,做到了以业务不侵入的形式接入热点探测性能,探测的 key 咱们采纳如下形式进行拼装:type #租户 id#sql#param,这样做的益处,一方面能够在识出热点后,能够疾速定位起源租户,另一方面也能够对 select/update/insert/delete 等不同 SQL 操作类型以及不同的租户设置不同的规定
缓存方面,以 redis 为例。除了 redis-key 自身之外,为了不便定位起源,还会把租户 id 拼装到探测 key 外面,特地的,对于 dao_cache,还集成了 CamelliaHotKeyCacheSDK,从而在必要的时候开启 cache 性能爱护 redis。
下面讲的是输出,至于输入,基于框架提供的自定义接口,咱们对接到了外部的监控报警零碎,不便第一工夫感知到热点;并且也会把数据实时写入到数据平台,不便后续进行追溯。将来还会对接到频控流控系统,从而能够在感知到异样流量后第一工夫主动进行屏蔽。
总结
为什么要开源
在开发 camellia-hot-key 框架时,咱们就将其作为开源我的项目 camellia 的一部分进行设计,在不夹杂业务逻辑的状况下,精简内核,从而不便大家能够在不批改源码的状况下间接对接到各自的零碎中。
咱们冀望有更多的人应用,而不仅仅是被网易智企一家公司应用,咱们冀望能施展他的最大性能和价值。
因而欢送各位开源爱好者踊跃来找茬,大家一起对 camellia 进行一直的欠缺和改良,从而做到真正的共赢。
体验试用
camellia 是云信的一个开源我的项目,除了 hot-key 之外,还有 redis-proxy、id-gen、delay-queue 等诸多被生产环境充沛验证的组件,欢送大家一键三连:点赞(Star)、关注(PR)、评论(issue)!
github 地址:https://github.com/netease-im/camellia