高并发实现思路
以商品抢购系统为例,当大促活动开始时,可能有上亿个用户会进入商品详情页,准备抢购商品。可能要发送数亿次请求来获取商品数据。数据是从哪里获取的呢?归根结底是数据库。比如经典的MySQL数据库,但受限于磁盘IO读写、并发连接数等因素,仅支持百~千级qps。因此通常数据库是整个后端系统性能的瓶颈。数据量大时,虽然可通过分库分表和读写分离提升性能,但远远无法满足流量需求。因此,想要实现高并发,必须利用其它技术,减少实际对数据库的请求。比如应用分层、缓存等。根据请求的生命周期,可以得到如下互联网流量漏斗图:互联网流量漏斗图由上至下分析流量漏斗过程和用到的技术:- 首先,用户在客户端(前端)发起抢购请求。为防止流量过大,可以使用有损服务随机拒绝部分请求,或者采用业务限流(比如加验证码,随机等待)
- 请求域名被DNS解析成IP或CNAME(CDN + 静态化)
- 请求发送至解析到的负载均衡机器(L5、LVS等)
- 请求被负载均衡器转发至接入层网关(Nginx、HAProxy等)
- 请求被转发至应用服务器实例(如果底层是微服务,可能还有一层应用层网关,比如Zuul),应用服务负责读取数据、执行业务逻辑。在服务中耗时的操作可以采用消息队列进行异步化。qps过高时,可采用服务熔断和降级。
- 服务先从缓存层(如redis集群)中读取数据,如果命中缓存,直接读取缓存
- 没命中缓存,则查询数据库
热数据探测技术
什么是热数据?????
顾名思义,热数据是指很热门、频繁被访问的数据。热数据可分为两类:- 有预期:比如大促活动中某些网红代言的爆款商品
- 无预期:比如恶意攻击、爬虫、突然火爆的商品
- 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挂掉!
-
对应用服务的风险我们的应用单位时间所能接受和处理的请求量是有限的,如果受到恶意请求(爬虫等),某个恶意用户独自占用大量请求处理资源,会导致其他正常用户的请求无法及时响应。
恶意请求导致的请求排队
如何检测热数据?
通常,我们需要为“热”定义一个阈值或规则,比如1秒内访问1000次的数据算热数据。对于单机应用,检测热数据很简单,直接在本地为每个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。
Dashboard体验地址:http://hotkey.tianyalei.com:9001/
2. 启动Worker集群,与Etcd建立连接,Worker将自身信息上报至Etcd并拉取热key规则,维持心跳
3. 客户端与Etcd建立连接,发现Worker并拉取Key上报规则
4. 客户端与Worker建立长连接并上报符合规则的key,通过hash算法决定上报至哪台Worker
通过hash将key上报至不同Worker5. Worker使用滑动窗口算法计算key访问频率,并将符合热key规则的key推送至所有的客户端实例,同时也推送至Etcd供Dashboard查看。6. 客户端实例接收到新的热key信息,使用Caffeine(高性能内存缓存库)进行缓存。滑动窗口源码 使用 AtomicInteger[] 循环队列实现,感兴趣的同学可以看下。
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探测及本地缓存,毕竟多一层缓存,就多一份数据不一致的风险。但多学习一些思路总是好的~