并发读
并发写
秒杀整体架构:稳准快
稳:系统架构高可用,保障活动顺利完成
准:不超卖
快:rt低,整体链路协同优化
高性能:
涉及大量秒杀的并发读和并发写,如何支持高并发?
1.设计数据动静分离
2.热点的发现与隔离
3.请求的削峰与分层过滤
4.服务端的极致优化
一致性:
核心:如何设计秒杀减库存方案
高可用:
planB兜底
设计秒杀系统的5个架构原则:4要1不要
1.数据尽量少
1.1 用户请求数据能少就少,请求的数据包括上传给系统的数据和系统返回给用户的数据(网页页面数据),由于数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器做处理,比如序列化、压缩、字符编码等等,这些处理都是非常消耗cpu的行为,所以减少传输的数据量可以显著的减少cpu的使用。例如简化秒杀页面大小,去掉不必要的页面装修效果等等。
1.2 依赖的数据越少越好,很多客户端/前端同学希望尽可能调用少的接口获取更多的信息,没有按需请求,如页面仅展示歌曲信息,但接口除去歌曲信息之外,还返回了标签、风格等等不必要的信息,调用其他服务会涉及序列化和反序列化,这也是cpu一大杀手,同时整个请求的rt也会上升,rt慢了,机器负载load也会上升,引起连锁反应。
2.请求数尽量少
用户请求的页面返回后,浏览器渲染依赖多个css/javascript/图片等等文件,这类请求相比起页面所需的核心数据,将其定义为“额外请求”(不需要每次重新请求下载,定值),可以通过合并css文件和JavaScript文件,把多个 JavaScript 文件合并成一个文件,在 URL 中用逗号隔开(https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js)。这种方式在服务端仍然是单个文件各自存放,只是服务端会有一个组件解析这个 URL,然后动态把这些文件合并起来一起返回。
3.路径要尽量短
把多个相互强依赖的应用合并部署在一期,把远程RPC调用变成JVM内部之间的方法调用,假如节点服务可用性是99.9%,假如一次请求经过五个节点,那么整个请求的可用性就变成了99.9%的5次方,约等于99.5%。
4.依赖尽量少
简单说就是我们要对系统依赖的服务按照重要性进行分级,比如getSongs获取歌曲信息接口,那么歌曲服务就是0级,艺人/专辑信息数据1级,标签/曲风流派等服务属于2级,在大促期间,可以对非0级的服务进行降级,保障核心服务的稳定性。
5.分布式,不要有单点
单点意味着无备份,风险不可控,对于任何服务都要分布式化,避免将服务的状态和机器绑定。
秒杀系统演绎历程
1W QPS
商品购买页 -> 服务端 -> 数据库增删改查
10W QPS
核心:秒杀系统升级
系统部署上做独立集群,抵挡流量洪峰
库存数据放入缓存,提高读性能
增加秒杀答题,防止有秒杀器抢单
100W QPS
页面动静分离,尽可能减少页面刷新量和动态资源请求。
服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,进一步降低服务端压力
增加系统限流保护
通过策略的方式减少系统压力
如何做好动静分离?有哪些方案可选
如何区分动态数据和静态数据?
主要区别是看页面中输出的数据是否和URL、浏览器、时间、地域相关,以及是否含有Cookie等私密数据,即页面展示的数据中是否包含个性化的数据。举两个例子:
1.新闻类的网站,千人一面,用户、时间、地域等信息的改变并不会影响页面的展示,这类数据数据静态页面。
2.手淘首页,千人千面,基本全是个性化的数据,这类页面属于动态页面。
如何对静态数据做缓存?
1.静态化改造,简而言之是直接缓存http连接,比如静态数据存入CDN,将内容放置到离用户最近的服务器中,减少骨干网和城域网之间的网络时延。
2.选择一种合适的方式缓存静态数据,不同语言写的Cache处理缓存的效率各不相同,java系统本身不适合处理大量连接请求,每个连接消耗的内存较多,servlet容器解析http协议较慢,可以不在java系统这一层做缓存,而在Web服务器上做,屏蔽java语言层面的一些弱点,可以在nginx,apache层进行全局管控,Web服务器也更擅长处理大并发的静态文件请求。
动静分离的几种架构方案?
前面通过改造把静态数据和动态数据做了分离,那么如何在系统架构上进一步对这些动态和静态数据重新组合,再完整的输出给用户呢?
三种方案可选:
1.实体机单机部署
2.统一cache层
3.上cdn
除此之外,还可以:
1.整个页面缓存在用户的浏览器中
2.实际有效请求减少,只是用户对刷新抢宝按钮的点击
静态资源走cdn缓存
动态内容服务端cache
什么是热点?
热点分为热点操作和热点数据
热点操作:
如加入购物车、商品详情、交易下单等等
热点数据:
如热门商品等等
如何处理热点数据?
处理热点数据通常有几种思路:一是优化,二是限制,三是隔离。
优化:
1.缓存lru
2.例如对被访问商品的 ID 做一致性 Hash,然后根据 Hash 做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。(Q:分桶?怎么做)
3.隔离
业务隔离。把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热。
系统隔离。系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外 99% 分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。
数据隔离。秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想 0.01% 的数据有机会影响 99.99% 数据。
思路:
无损:排队、答题、分层过滤
有损:限流、机器负载保护等强制措施
排队(简单说就是一步变多步):
消息队列缓冲瞬时流量
线程池加锁等待
先进先出。先进后出等内存排队算法
请求序列化文件,再顺序读取文件,类似mysql binlog
答题:
功效:防止刷量作弊
延缓请求
分层过滤:尽量减少无用请求,如减少不必要的一致性校验等
一般是两个因素:一次响应的服务端耗时和线程数
响应时间一般都是由cpu执行时间和线程等待时间组成,线程等待时间通过测试发现用处不大,因为这个可以通过增加线程数来弥补,主要还是cpu执行时间对性能有真正的影响,因为cpu的执行真正消耗了服务器的资源,如果减少cpu一半的执行时间,就可以增加一倍的qps
需要注意的是:不同的系统对瓶颈关注度不同,例如对于缓存系统而言,制约它的是内存,而对存储型系统来说I/O更容易是瓶颈。
对于秒杀系统而言,瓶颈更多地发生在CPU上
如何优化系统
1.减少编码
java编码运行较慢,是java语言本身所决定的,很多场景下,只要涉及字符串的操作(如输入输出操作,I/O操作)都比较浩cpu资源,无论是磁盘i/o还是网络i/o,因为都需要将字符转换成字节,而这个过程必须转码。
同时,每个字符的编码都需要查表,而这种查表的操作非常耗资源,所以减少字符和字节之间的转换卓有成效。
如何减少编码?提前把一些数据转换成字节,减少静态数据编码转换
2.减少序列化
序列化也是Java性能的一大天敌,因为序列化和编码往往是同时发生的,减少序列化也就减少了编码。
序列化大部分是在RPC中发生的,因此要减少应用之间RPC调用,一种比较常见的方案是合并部署,将多个关联性比较强的应用合并部署,举例来说,就是将两个原本在不同机器上的不同应用合并部署到一台机器上,同一个tomcat中,且不能走本机的socket,这样才能避免序列化的产生。
3.Java极致优化
大部分请求直接在Nginx服务器或者Web代理服务器返回,这样可以减少数据的序列化和反序列化,java层只处理少量数据的动态请求。
减少数据。事实上,有两个地方特别影响性能,一是服务端在处理数据时不可避免地存在字符到字节的相互转化,二是 HTTP 请求时要做 Gzip 压缩,还有网络传输的耗时,这些都和数据大小密切相关。
数据分级。也就是要保证首屏为先、重要信息为先,次要信息则异步加载,以这种方式提升用户获取数据的体验
4.并发读优化
当前这边采用的加锁方案是通过tair去做的,除此之外是否还有其他的方案呢?
应用层LocalCache,在秒杀系统的单机上缓存商品相关的数据。一般划分成动态数据和静态数据分别处理。
静态数据:比如商品标题、描述这些本身不会改变的数据,秒杀前全量推送到秒杀机器上,并一直缓存到秒杀结束。
动态数据:比如库存这类动态数据,会采用”被动失效“的方式缓存一定时间(一般是数秒),失效后再去缓存拉取最新的数据。
这里有个常人无法理解的事情,像库存这种频繁更新的数据,一旦数据不一致,会不会导致超卖?
这就要用到前面介绍的数据的分层校验原则,读的场景可以允许一定的脏数据,因为这里的误判只会到你少量原本无库存的下单请求被误认为有库存,可以等到真正写数据时再保证最终的一致性,通过在数据的高可用性和一致性之间平衡,来解决高并发的数据读取问题。
此外,要做好优化,你还需要做好应用基线,比如性能基线(何时性能突然下降)、成本基线(去年双 11 用了多少台机器)、链路基线(我们的系统发生了哪些变化),你可以通过这些基线持续关注系统的性能,做到在代码上提升编码质量,在业务上改掉不合理的调用,在架构和调用链路上不断的改进。
性能优化的核心就一个字-减
如果还继续减的
1:异步化-减少等待响应的时间
2:降日志-减本地磁盘的交互
3:多级缓存-再减少获取数据路径
4:减功能-非核心功能或后补功能去掉
分库分表,ID路由
购买一般分两步:下单和付款
减库存一般有如下几个方式:
下单减库存:可能存在的问题是恶意下单,从而影响卖家的商品销售
付款减库存:可能存在的问题:库存超卖,因为下单和付款是割裂的,可能会造成很多卖家下单成功,导致下单数量超出库存,只能在付款时提示失败,影响购物体验。
预扣库存:将两次操作合并,下单时先预扣,在规定时间内不付款再释放库存,即采用“预扣库存”的方式,虽然一定程度上缓解了上面的问题,但是针对恶意下单的情况,虽然把有效付款时间设置为10分钟,但是恶意买家完全可以十分钟后再次下单,或者采用一次下单很多件的方式来把库存减完。针对这种情况,解决办法还是要结合安全和反作弊措施制止。
例如,给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存)、给某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复下单不付款的操作进行次数限制等。针对“库存超卖”这种情况,在 10 分钟时间内下单的数量仍然有可能超过库存数量,遇到这种情况我们只能区别对待:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。
大型秒杀系统中如何减库存?
确定适合的场景:下单减库存
下单减库存在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般有以下几种解决方案:
在应用程序中通过事务来判断,保证减库存后库存不能为负数;
直接设置数据库的字段数据为无符号证书,这样减库存字段值小于零时会直接执行SQL语句报错;
使用case when 判断语句
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
秒杀系统减库存的极致优化
后续整理
高可用建设应该从哪里着手
架构阶段:
架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题,做好多机房单元化部署,异地容灾。在系统不同阶段选择不同的设计结构,比如qps只有10时,可能简单增删改查就足够了,当系统流量上去之后,需要分布式以及微服务拆分等措施来保障系统的高可用性。
编码阶段:
编码阶段最重要的是保障代码的健壮性,有防御式编程的思想,例如涉及远程调用问题时,设置合理的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有预期,防止结果超出系统处理范围,对自己负责的系统要有足够的了解,哪些服务,哪些数据是核心服务、核心数据,哪些是可被降级、异常可被允许的,哪些服务需要兜底数据,保障服务24小时正常可用。
测试阶段:
要保障测试用例的覆盖度,常用极端数据对系统健壮性进行测试。
发布阶段:
分批发布,安全生产环境小流量验证,发布阶段要支持回滚机制。
运行阶段:
监控是核心,要能够及时发现并定位问题。
故障阶段:
故障发生时首先要考虑的是止损,先止损,再恢复服务,最后再对数据清洗。
特殊时期:
如大促、秒杀等场景,参考全篇。
当应用无法承受大流量洪峰时,该如何针对运行阶段进行处理?
三种方式:降级、限流和拒绝服务
1.降级
降级预案制定的前提是,我们要对自己负责的系统足够熟悉,包括核心功能、依赖的第三方服务、依赖服务按照重要登记进行分层划分。降级一般分为两种类型:业务降级、数据降级和服务降级。
业务降级是针对的是某个产品而言,将部分产品功能进行降级,比如淘宝双十一大促时节,核心流量聚焦在少数核心功能上,此时可以将部分非核心产品下线,提升系统抵抗力。
数据降级指的是返回数据总量进行一定程度上的降级、比如返回歌曲总数由50条降级为10条,数据量的减少也就意味着数据编码耗时会降低、线程处理时间降低,cpu降低。
服务降级指的是系统依赖的第三方服务进行降级,比如获取商品详情,本身商品会获取除商品本身之外的其他附加信息(如商品评价,依赖评论系统),这些第三方服务的调用会增加额外的网络开销,同时更多的数据意味着数据序列化和编码也需要耗费更多的cpu,影响性能。
所以在系统达到一定容量时,我们需要针对不同的系统设立不同的“预案”,它是一个有目的,有计划的执行过程,可以通过预案系统来实现降级。
2.限流
相比起降级这种方式,限流行为更加极端一些,当系统容量到达瓶颈时,降级也无法解决问题,此时可以通过限制一部分流量来保护系统。
系统上线前,我们必须要通过压测提前对自己的系统水位有一个预估,将限流阈值控制在这个水位线之下。
常见的限流手段有基于QPS和线程数的限流。可以在java层做,更好的是在上层web服务器,如nginx中做限流。
3.拒绝服务
拒绝服务是抵挡流量洪峰的最极端的一种手段,用以防止最坏的情况发生,防止因服务器压垮而长时间彻底无法提供服务。比如在nginx上设置过载保护,当机器负载达到某个值时直接拒绝http的请求,并返回503错误码。
nigin网关层 + lua脚本,读取tair/redis/diamond,限流动态化配置
抽奖系统防超卖
第一步,拿商品key
第二步,写db,key是唯一主键,写中奖纪录时其他key写不进去
淘宝的直接db减库存保证,问题是商品量大,并发高时,写数据库只写这一行的数据,db压力非常大,排队无队可排
一个key,1000万人,怎么搞?
一个key,分到1000个桶,999个是空的,只要奖品发出去就ok,不在乎先后时间顺序
vip商品量大的,通过redis原子锁去搞