作为一名程序员,谁还不关注几个技术公众号?
今天某公众号推送了一篇文章,标题为“京东的热点key探测系统发布,单机 QPS 提升至 37 万”。后台开发同学都知道,单机QPS 37万是什么概念,多么充满噱头的标题啊(老营销号了)。果然吸引我点了进去。
原来这个标题不是玩笑,在京东618大促期间,单机极限支撑30万QPS!
怎么做到的?下面,我们从高并发系统实现思路开始,一步步揭秘京东单机高并发的实现原理。
高并发实现思路
以商品抢购系统为例,当大促活动开始时,可能有上亿个用户会进入商品详情页,准备抢购商品。可能要发送数亿次请求来获取商品数据。
数据是从哪里获取的呢?归根结底是数据库。
比如经典的MySQL数据库,但受限于磁盘IO读写、并发连接数等因素,仅支持百~千级qps。因此通常数据库是整个后端系统性能的瓶颈。数据量大时,虽然可通过分库分表和读写分离提升性能,但远远无法满足流量需求。
因此,想要实现高并发,必须利用其它技术,减少实际对数据库的请求。比如应用分层、缓存等。
根据请求的生命周期,可以得到如下互联网流量漏斗图:
互联网流量漏斗图
由上至下分析流量漏斗过程和用到的技术:
- 首先,用户在客户端(前端)发起抢购请求。为防止流量过大,可以使用有损服务随机拒绝部分请求,或者采用业务限流(比如加验证码,随机等待)
- 请求域名被DNS解析成IP或CNAME(CDN 静态化)
- 请求发送至解析到的负载均衡机器(L5、LVS等)
- 请求被负载均衡器转发至接入层网关(Nginx、HAProxy等)
- 请求被转发至应用服务器实例(如果底层是微服务,可能还有一层应用层网关,比如Zuul),应用服务负责读取数据、执行业务逻辑。在服务中耗时的操作可以采用消息队列进行异步化。qps过高时,可采用服务熔断和降级。
- 服务先从缓存层(如redis集群)中读取数据,如果命中缓存,直接读取缓存
- 没命中缓存,则查询数据库
经过层层过滤和转发,最后,单个应用服务实例承载的qps在万级至十万级,对数据库的压力控制在千级别qps内。
总结一下,实现高并发的要点:静态化、有损服务、负载均衡、缓存、队列。
似乎一切都很完美,一套架构下来,剩下要做的事情就是加机器、加机器、加机器。。。
机器不要钱么,有没有什么方法进一步提升应用服务性能,节约机器成本呢?
一种常用方案是检测热点数据,并针对热数据进行特殊处理。比如采用多级缓存机制,在缓存层之上,针对热点数据再做一层本地缓存。直接从本地内存中读取数据,可进一步提升性能。
那么什么是热数据?为什么要检测热数据?如何检测热数据呢?
热数据探测技术
什么是热数据??
顾名思义,热数据是指很热门、频繁被访问的数据。
热数据可分为两类:
- 有预期:比如大促活动中某些网红代言的爆款商品
- 无预期:比如恶意攻击、爬虫、突然火爆的商品
而热key实际上是一个频繁被访问的字符串,例如:
- MySQL等数据库中被频繁访问的数据,如爆款商品的skuId
- KV缓存系统中经常被访问的key
- 机器人、爬虫、刷子用户,如用户的userId、uuid、ip等
- 某接口地址,如商品查询/sku/query
- 统计用户访问某个接口的频率,如userId /sku/query
- 统计某台服务器某个接口被访问的频率,如ip /sku/query
- 统计某用户访问某个商品的频率,如userId /sku/query skuId
为什么要检测热数据?
我们检测热数据的原因很简单:
1. 提升性能
如上所说,对热点数据进行本地缓存,可大幅提升机器数据读取性能,减轻下层缓存集群压力。
热数据多级缓存读取流程
缓存级数越多,意味着更新操作越复杂,数据不一致的风险越大。
2. 规避风险
对于无预期的热数据(热key),可能会对业务带来极大的风险,可分为两个层次:
- 对数据层的风险 正常情况下,Redis缓存单机可支持十万左右qps,并可通过集群增大并发度。并发量一般的系统,用Redis做缓存就足够了。但是如果有一个商品突然爆火,或者收到恶意请求,对该数据key的访问qps可能飙升到百万、千万量级,在redis单线程的工作方式下,会导致正常的请求排队,无法及时响应,严重时会导致整个分片集群瘫痪。 还有一种情况,某热点key突然过期,直接导致大量请求砸向DB,直接导致DB挂掉!
- 对应用服务的风险 我们的应用单位时间所能接受和处理的请求量是有限的,如果受到恶意请求(爬虫等),某个恶意用户独自占用大量请求处理资源,会导致其他正常用户的请求无法及时响应。
恶意请求导致的请求排队
因此,需要一套动态热key检测机制,检测出无预期的热点数据,以针对这些数据进行不同的处理,比如本地缓存、拒绝恶意用户、接口限流/降级等。在提升数据访问性能的同时规避可能的风险。
那么如何检测热数据呢?
如何检测热数据?
通常,我们需要为“热”定义一个阈值或规则,比如1秒内访问1000次的数据算热数据。
对于单机应用,检测热数据很简单,直接在本地为每个key创建一个滑动窗口计数器,统计单位时间内的访问总数(频率),并通过一个集合存放检测到的热key。
滑动窗口
而对于分布式应用,对热key的访问是分散在不同的机器上的,无法在本地独立地进行计算,因此,需要一个集中的热key计算单元。可将热数据探测工作分为配置规则、热key上报、热key统计、热key推送四个步骤:
- 配置规则:指定热key的上报条件
- 热key上报:各应用实例上报访问到的key至集中计算单元
- 热key统计:收集各应用实例上报的信息,使用滑动窗口算法计算key的热度
- 热key推送:当key的热度达到设定值时,推送热key信息至所有应用实例,各应用实例将key值进行本地缓存
通过上述步骤,一套基本的热key检测机制就完成了。但是,为满足高并发场景,在设计热key探测框架时,还应满足如下指标:
- 实时性:考虑到热key的突发性,必须能够实时发现热key并推送
- 高性能:框架应保持轻量且高性能,能够有效降低成本
- 准确性:精准探测符合规则的热key,不漏报,不误报
- 一致性:保证应用实例本地缓存的热key一致,否则可能出现数据错误
- 可扩展:要计算的key数量级很大时,集中计算集群应便于扩展
优秀的热key探测框架还应满足易接入、业务无侵入、动态配置、可视化管理等特性。
下面来看看京东毫秒级热key探测框架的实现。
其实通过Redis本身也可以实现热key探测功能,比如用monitor命令监控key的访问并进行统计,或者利用v4.0.3后redis-cli自带的-hotkeys选项查看热key。但是这两个命令在key较多时,执行缓慢,且会降低redis的性能。因此,自实现热key探测框架是必要的。
京东毫秒级热key探测框架分析
JdHotkey是京东研发的通用轻量级热key探测框架。
官方给出的架构图如下:
JdHotkey架构图
该框架分为四个核心部分:
- Etcd集群:高可用强一致的 Key/Value 存储系统,主要用于共享配置和服务发现。此处存放计算集群(worker)的地址、热key规则、已检测出的热key等。
- Worker计算集群:用java实现的计算程序,通过Etcd供客户端发现并建立连接,主要负责收集和计算热key的访问频率,并且将符合规则的热key推送至客户端。
- 客户端:引入jar包,负责与Worker建立链接并上报key(先在本地累加,周期性批量上报)、监听key上报规则、缓存热key。
- Dashboard控制台:通过读写Etcd集群完成对Worker、Client、配置规则、热Key的监控,并支持持久化数据至MySQL。
通过这四部分的交互,实现了完整的热数据探测机制(规则配置 => 热key上报 => 统计 => 推送),详细过程如下:
1. 依次启动Etcd和Dashboard控制台,配置key上报规则:
通过Dashboard配置规则
Dashboard体验地址:http://hotkey.tianyalei.com:9001/
2. 启动Worker集群,与Etcd建立连接,Worker将自身信息上报至Etcd并拉取热key规则,维持心跳
3. 客户端与Etcd建立连接,发现Worker并拉取Key上报规则
4. 客户端与Worker建立长连接并上报符合规则的key,通过hash算法决定上报至哪台Worker
通过hash将key上报至不同Worker
5. Worker使用滑动窗口算法计算key访问频率,并将符合热key规则的key推送至所有的客户端实例,同时也推送至Etcd供Dashboard查看。
滑动窗口源码 使用 AtomicInteger[] 循环队列实现,感兴趣的同学可以看下。
6. 客户端实例接收到新的热key信息,使用Caffeine(高性能内存缓存库)进行缓存。 7. 可以通过Dashboard更改Etcd中存储的key规则,Worker和Client通过Etcd提供的watch api监听到规则的改变。
经压测,该热Key探测系统的性能很高,16核单机worker端每秒可接收处理30万个key探测任务,每秒可稳定对外推送40-60万次热key。
除了京东HotKey外,业界也有一些热key探测的优秀实现,比如有赞TMC。
有赞TMC
TMC是有赞的透明多级缓存,在原分布式缓存的基础上,增加了应用层热点探测及本地缓存等功能。
TMC本地缓存的整体架构如图:
TMC本地缓存架构
TMC通过自封装Jedis-Client(和原生Jedis-Client接口一致)来实现多级缓存对原有应用的透明,此处我们主要关注其热点探测部分(红圈)。
类比JdHotkey的架构,TMC热点探测核心组件如下:
1. Apollo配置中心负责存储服务信息、Etcd集群信息、热key规则配置。类似JdHotkey的Dashboard(可视化配置) Etcd(规则存储)。
2. Hermes计算集群会从Apollo配置中心拉取信息(如热key规则),它负责收集客户端上报的key访问数据并进行周期性分析计算,并将检测到的热key推送至Etcd集群,客户端通过监听Etcd集群来接受热key的推送。类似于JdHotkey的Worker,但是其热度统计使用的是时间轮算法,周期性批量推送热key:
热度统计过程中的时间轮
3. Etcd集群负责存储热key,客户端通过监听Etcd集群实现热key的发现和失效。
4. 客户端Hermes-SDK通过rsyslog Kafka上报key访问事件,从Etcd拉取热key并维护本地缓存等。
整体热key发现步骤如下:
热key发现完整步骤
对比JdHotkey和有赞TMC两套热key发现架构,最大的区别体现在热key上报和推送机制上。
对于JdHotkey,计算集群通过长连接收集key上报并将热key直接推送给客户端;而对于TMC,客户端通过Kafka上报key,服务端通过Etcd推送热key,将计算集群和客户端完全解耦。
学习完TMC的架构后,对JdHotkey的设计有了一些思考和疑问。
JdHotkey设计思考
对于JdHotkey框架,在Worker检测到热key后,会将热key推送至所有客户端实例以及Etcd,如下图:
Worker推送热key至Client和Etcd
上述方式会进行两次推送,感觉是有一些性能浪费。那么能否像TMC一样,让Worker仅推送key至Etcd,客户端直接监听Etcd来获取热key?如下图:
Worker推送热key,Client监听Etcd获取热key
JdHotkey的团队对此给出了回答:
为什么是worker推送,而不是worker发送热key到etcd,客户端直接监听etcd获取热key?
- worker和client是长连接,产生热key后,直接推送过去,链路短,耗时少。如果是发到etcd,客户端再通过etcd获取,多了一层中转,耗时明显增加。
- etcd性能不够,存在单点风险。譬如我有5000台client,每秒产生100个热key,那么每秒就对应50万次推送。我用2台worker即可轻松完成,随着worker的横向扩展,每秒的推送上限线性增加。但无论是etcd、redis等等任何组件,都不可能做到1秒50万次拉取或推送,会瞬间cpu爆满卡死。因为worker是各自隔离的,而etcd是单点的。实际情况下,也不止5000台client,每秒也不止100个热key,只有当前的架构能支撑。虽然可以扩容Etcd集群,但同样会增加成本。对于watch Api,还要考虑对内存的占用。
Etcdv3 watch 内存占用官方压测
那为什么有赞TMC能够使用Etcd进行热key推送呢?
答案很简单,二者对实时性的要求不同,因此计算集群的工作机制也不同。TMC的热key是周期性推送,计算集群以3秒一个周期完成热度滑窗统计工作,对Etcd的压力并不大;而考虑到京东的业务场景,JdHotkey需要支持毫秒级精准探测,毕竟对于爆款秒杀商品,连1秒的时间都等不了!
以上就是对热key探测技术的讲述。总之,没有最好的架构,只有最适合的架构。在做技术选型时,我们也要评估系统是否需要热key探测及本地缓存,毕竟多一层缓存,就多一份数据不一致的风险。
但多学习一些思路总是好的~ 参考资源
1. 京东毫秒级热key探测框架设计与实践 2. 有赞透明多级缓存解决方案(TMC) 3. 京东 hotkey 源码 4. redis4.0之基于LFU的热点key发现机制