秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统…
架构原则 - 4要1不要
数据量要尽量少
请求的数据量要少: 数据经过服务器处理,压缩和字符编码,需要消耗CPU
数据对系统的依赖要少: 减少服务器后台服务和数据库打交道,这些都要耗CPU
请求数要尽量少
比如js,css资源文件,http请求要经过三次握手,连接请求,还有有些资源需要串行加载
- 可以把多个文件合并压缩成一个文件
- 可以放到cdn中,减少对服务器的请求
- 请求不同域名下的资源,还涉及域名的DNS解析耗时
路径要尽量短
用户发出请求到返回数据这个过程中,需求经过的中间的节点数要少
依赖要尽量少
所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖
- 给系统分级,比如0级系统,1及系统…如此类推,0级是最重要的系统
- 为防止系统被拖垮,适当时候需要把一些系统降级,然后去掉
不要有单点
- 消除单点,是服务无状态化,让服务可以在服务器中随意移动
- 配置动态化,通过配置中心来推送
- 存储服务的无状态化,需要通过冗余多个备份方式,主备模式
架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都是空谈
不同场景下的不同架构案例
淘宝秒杀价购演进主线
架构升级都是要根据业务场景来具体问题具体分析的
第一版本,支持 QPS < 10w/s 的量级
如果你想快速搭建一个简单的秒杀系统,只需要把你的商品购买页面增加一个“定时上架”功能,仅在秒杀开始时才让用户看到购买按钮,当商品的库存卖完了也就结束了。
这就是当时第一个版本的秒杀系统实现方式
第二版本,支持 10w/s < QPS < 100w/s 的量级
10w级别,通常的瓶颈在数据读取上,增加缓存一般可以解决
相对于第一版本的的优化:
- 秒杀系统独立打造,可以有针对性做优化
- 减少页面复杂度,减少装饰功能的
- 单独部署一个机器集群,不影响正常商品的业务
- 独立缓存系统存放热点数据,提高“读性能”
- 增加“秒杀答题”,防止刷单
第三版本,支持 QPS > 100w/s 的量级
100w级别,通常瓶颈在于服务端网络带宽,需要把动静分离,把大部分静态资源放到cdn上,还有放到本地缓存中,减少对主服务器的请求量
相对第二版本的优化:
- 页面彻底动静分离,不刷新整个页面,降低数据量
- 本地缓存商品信息,减少服务器公共缓存集群的查询
- 增加限流保护
动静分离
秒杀的场景中,对于系统的要求其实就三个字:快、准、稳
理解动静数据
简单来说:“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie等私密数据
比如:
- 媒体新闻中,某一篇文章,不管谁访问都是一样的,他就是一个典型的静态数据
- 淘宝首页,会根据个人特征进行推荐的,可能每个人访问,不同时间访问都可能不一样,这些个性化的数据就是动态数据
动静分离后,分离出来的静态数据就可以放到缓存中,提高静态数据的访问效率
怎么对静态资源做缓存呢
- 静态资源应该缓存在里用户最近的地方
- 浏览器里,CDN,服务端的Cache中
- 静态化改造就是要直接缓存HTTP连接
- 静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据
- Web 代理服务器根据请求 URL,直接取出对应的 HTTP响应头和响应体然后直接返回
- 决定更好的方式来缓存静态数据
- 不同语言写的Cache软件处理缓存数据的效率也不相同,都有自身的弱点
- 选择在web服务器层做,如(Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。
如何做动静分离的改造
- URL唯一化
- URL唯一化为了缓存整个HTTP连接
- 比如商品详情,商品ID做唯一表示
- 分离浏览者相关的因素
- 浏览者相关因素:是否已登录,登录身份等,单独拆出来,通过动态请求来获取
- 分离时间的因素
- 服务器输出的时间也通过动态请求获取
- 异地化地域因素
- 异地化相关的内容也通过动态请求获取
- 去掉cookies
- 缓存的静态数据中不还有cookies
动静分离后,如何组织这些页面
- ESI(SSI): Server Side Includes
- 就是在服务器端做动态数据请求,然后插入到静态页面中,再把整个完整的页面返回给用户
- CSI: Client Side Includes
- 就是用户得到页面后,页面单独发起异步的javascript请求,向服务器拿到动态数据后,在插入到页面中,服务器性能好了,但是用户端页面可能延时,体验稍差
动静分离的几种架构方案
- 实体机单机部署
- 统一Cache层
- 上CDN
实体单机部署
就是把服务应用机子改成实体机,cache和应用共存,应用直接访问本机cache
优点:
- 没有网络瓶颈,而且能使用大内存
- 技能提升命中率,又能减少Gzip压缩
- 减少Cache失效压力
缺点:
- 既有应用服务,又有cache,造成运维复杂度,增加维护成本
统一的Cache层
就是将单机的Cache统一分离出来,做成一个独立的Cache集群
优点:
- 独立Cache,可以减少多个应用接入使用Cache的成本
- 统一的Cache方案更易于维护,方便做监控,自动化,升级扩展等
- 可以共享内存
缺点:
- Cache层内部交换成为瓶颈
- 缓存服务器的网卡也会成为瓶颈
- 机器较少时,风险较大,挂掉一台就影响很大一部分缓存数据
解决方案:对Cache机子做hash分组,做一致性哈希
上 CDN
就是将Cache移动到CDN上,因为CDN离用户最近,效果更好
问题:
- 失效问题
- 当数据有更新时,需要保证CDN可以在秒级内,让全国各地的Cache同时失效,对失效要求很高。
- 命中率问题
- 因为将数据放到全国各地的CDN上,必然导致Cache分散,而Cache分散又会降低命中率
- 发布更新问题
由于以上问题,想要放到全国的所有CDN节点不太现实,可以尝试使用几个节点来尝试实施,但需要满足几个条件:
- 靠近访问量比较集中的地区
- 离主站相对比较远
- 节点到主站的网络比较好,且稳定
- 节点容量比较大,不会占用其他CDN太多的资源
- 节点不要太多
CDN的二级Cache
二级cache是指cdn设置了多级回源机制,就是如果缓存没有命中再到二级缓存中去取,而不是直接回服务端来请求,本质是减少回原的请求量
选择CDN的耳机Cache比较适合,因为二级Cache数量偏少,容量大,让用户请求先回源的CDN的二级Cache中,如果没有命中再回源站获取数据
使用CDN的二级Cache作为缓存,可以达到和当前服务端静态化Cache类似的命中率,因为节点不多,Cache不是很分散,访问量比较集中,这样解决了命中率的问题,比较理想的一种CDN方案
特点:
- 把整个页面缓存在用户浏览器中
- 如果强制刷新整个页面,也会请求CDN
- 实际有效请求,只有用户对“刷新抢宝”按钮的点击
这样90%的静态数据缓存在了用户端或CDN中,性能自然提升不少
二八原则
有针对性地处理好系统的“热点数据”
关注热点数据
热点数据可能只占到所有数据的1%,却由于访问量大,很可能抢占99%的服务器资源。如果这些热点数据请求还没有价值,那么会对系统资源来说完全是浪费
什么是“热点”
热点操作
- 大量刷新页面,刷新购物车,0点下单
- 抽象为“读写请求”
热点数据
- 静态热点数据
- 可以提前预测的热点数据,如系统提前对热点商品打标、大数据分析发现
- 动态热点数据
- 不能提前预测到,是系统运行过程中临时产生的。如一个商品打广告,不知道哪天火了,短时间内转为热点商品
- 静态热点数据
发现热点数据
热点数据都知道需要缓存起来,那么你怎么知道哪些数据才是热点数据呢?
- 发现静态热点数据
- 发现动态热点数据
发现静态热点数据
提前预热,商品打标,大数据分析,top N商品
发现动态热点数据
静态热点数据虽然可以预测和分析得来,但是实时性都较差
如果系统能够在秒级内自动发现热点商品就完美了
动态热点数据发现具体实现:
- 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。
- 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx 模块统计的热点 URL。
- 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。
可以看到,上图通过对“站内导航”和“中间件”日志抓取后,分析平台调用,聚合分析,在推送到后台服务
异步采集日志,Zabbix了解一下
处理热点数据
- 优化
- 缓存热点数据
- LRU淘汰算法替换
- 限制
- 根据商品ID做一致性Hash,根据Hash做分桶
- 每个分桶设置一个队列处理,把请求限制在一个队列里,防止商品占用台多的服务器资源
- 例如对每个请求的商品id取模,让后根据取模的结果分别设置多个linkedhashmap,每个map当做一个队列
- 隔离
- 秒杀系统设计第一原则就是隔离热点数据,不要让1%的热点数据,影响99%的业务
- 业务隔离
- 把秒杀做成一个营销活动,提前预热,做好热点商品缓存
- 系统隔离
- 单独集群部署
- 单独域名请求
- 数据隔离
- 单独的Cache集群
- 单独的MySQL数据库存放
实现隔离有很多实现方式:
- 按照用户来区分,不同用户分配不同的Cookies,路由到不同的服务器接口中
- 在接入层针对URL中的不同Path来设置限流策略
流量削峰
秒杀系统中,秒杀开始前,流量几乎是一条直线,到了秒杀时刻,流量在时间上高度集中于一点,峰值对资源的消耗是瞬间的
为什么要削峰
峰值的出现,导致服务器忙不过来,但是闲时又没什么要处理
削峰的意义:
- 可以让服务端处理变得更加平稳
- 可以节省服务器资源的成本
削峰的本质就是用户请求迟缓发出,以便减少和过滤掉一些无效的额请求
削峰的思路
- 排队
- 答题
- 封层过滤
排队
使用消息队列来缓冲瞬时流量,把同步变成异步
除了消息队列,类似的排队还有:
- 利用线程池枷锁等待
- 先进先出,先进后出等常用的内存排队算法
- 把请求序列化到文件中,在顺序读文件(类似MySQL binlog的同步机制)
答题
就是点击秒杀按钮后,增加答题环节,答对了之后才进行下一步
目的:
- 防止秒杀机器作弊
- 延缓请求,把在一个时间点的请求(1s),转化成时间片的请求(2s~10s)
答题系统设计:
答题验证:
答题验证包括:
- 答案的正确性验证
- 用户身份验证,是否登录,资格验证等
- 提交答案时间验证,答题时间只花了1s,一般人为答题不不能达到,也能防止机器答题
分层过滤
假设请求分别经过:
- CDN
- 前台读系统(商品详情)
- 后台系统(交易系统)
- 数据库
过滤的过程:
- 大部分数据和流量在用户浏览器和CDN上获取,可以拦截大部分数据读请求
- 经过第二层(商品详情页)时,数据都走Cache,过滤掉无效请求
- 经过第三层(交易系统),做数据的二次校验,如登录,答题验证等,请求书进一步减少
- 最后一步,数据库完成强一致性校验,如库存不足
分层过滤的核心思想是:在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请求。
分成校验的基本原则:
- 将动态请求的读数据缓存(Cache)在web端,过滤掉无效的数据读
- 对读数据不做强一致性校验,减少因为一致性校验产生的瓶颈问题
- 对写数据进行基于时间片的合理分片,过滤掉失效的请求
- 对写数据做限流保护,过滤掉超出承载能力的请求
- 对写数据进行强一致性校验,保证数据的最终准确性(如库存不足)
提高系统的性能
影响性能的因素
服务端的性能指标:
QPS(Query Per Second,每秒请求数)
影响这个性能指标的两个因素:
- RT(Reponse Time),服务器响应时间
- 正常情况下,服务器处理请求的响应时间越短,一秒钟处理的请求数就越多
- 实际的测试得出:减少CPU的一半执行时间,可以增加一倍的QPS
- 处理请求的线程数
- QPS = RT * 线程数量
- 很多多线程的场景都有一个默认配置:线程数 = 2 x CPU核数 + 1
- 最佳实践得来的公式:线程数 = [(线程等待时间+线程CPU时间)/线程CPU时间]xCPU数量
- 当然,最好的办法是通过性能测试来发现最佳线程数
如何发现瓶颈
瓶颈可能是:CPU、内存(存储系统)、硬盘(I/O)和网络等
在秒杀场景中,瓶颈更多的发生在CPU中
使用CPU诊断工具:JProfiler和Yourkit,这些工具可以列出整个请求中每个函数的CPU执行时间
判断CPU是否是瓶颈
看当QPS达到极限时,你的服务器CPU使用率是不是超过95%,如果没有,则还有提升空间
如何优化系统
- 减少编码
- 就是基于把静态的字符串提前编码成字节并缓存,然后直接输出字节内容到页面,从而大大减少编码的性能消耗的,网页输出的性能比没有提前进行字符到字节转换时提升了 30% 左右。
- 减少序列化
- 序列化大部分发生在RPC调用中,可以减少RPC调用,通过把应用合并部署,避免远程RPC,在同一个台机子上,也不能走本机的socket
- 应用的极致优化
- 使用Web服务器做静态化改造,让大部分请求和数据直接在Web服务器上直接返回
- 直接处理请求,不使用MVC框架
- 直接输出流数据,使用json而不是模板引擎
- 并发读优化
- 采用应用层的LocalCache,秒杀系统单机上缓存商品的相关数据
- 商品标题和描述这些数据本身不变的数据,可以秒杀前全量推到秒杀机器上,并且一直缓存到秒杀结束
- 库存,动态数据会采用“被动失效“的方式,失效后再去拉取
读场景中,可以允许一定的脏数据,可以等到真正写数据库在保证最终一致性。
库存减扣
抛出问题:
商品到底用户下单了算卖出去,还是付了款才算卖出去?
减库存的几种方式
- 下单减库存
- 下单后马上减库存,最简单,控制最精确的一种方式
- 存在问题:会导致被人刷单不付款的问题
- 付款减库存
- 用户真正付款后才减库存。
- 存在问题:如果并发高,容易出现超卖
- 预扣库存
- 下单后,库存保留一定的时间(如10分钟),超过这个时间后,库存自动释放
- 当买家付款前,系统检测库存是否还有效,如果没有则尝试减库存,如果库存不足(尝试失败)则不允许继续付款,如果尝试减库存成功,则完成付款并且实际减库存
- 存在问题:仍然有刷单不付款和超卖的情况
解决方案
结合安全和反作弊措施
- 给经常下单不付款的买家进行打标(被打标的买家下单时不减库存)
- 给某些类目设置最大购买件数(一人3件等)
- 重复下单不付款的操作次数进行限制
业务应对超卖问题:
- 普通商品允许补货可以补货解决
- 完全不能超卖的(飞机票,酒店),只能在买家付款时提示库存不足
秒杀中的库存减扣-下单后减库存
经典的预扣库存的方案:买机票和电影票,下单后都有有效付款时间,超过时间,订单失效
秒杀中的库存减扣方式,采用”下单后减扣”比较合理
库存减扣的解决方案:
- 应用成许中通过事务判断来,即保证减扣后库存不能为负数,否则回滚
- 直接设置数据库中的字段数据为无符号,减扣时库存字段小于零,sql会报错
- 使用CASE WHEN来判断:
1 | UPDATE item SET inventory = CASE WHEN inventory >= xxx |
秒杀库存减扣的极致优化
库存的并发读问题:
- 库存的查询放到LocalCache中,解决并发读问题
库存的并发写问题:
如果秒杀商品的减库存逻辑单一,没有复杂的SKU库存和总库存的联动关系,可以使用缓存中减库存,但是如果有比较复杂的库存逻辑,或者需要使用事务,则还是需要在数据库中减库存
在缓存中减库存
在数据库中减库存
- 应用层排队
- 使用队列,减少同一台机器对数据库同一行记录进行操作的并发度
- 数据库层排队
- 对数据库做优化,让其支持并发写排队(阿里做了,艹,说个屁)
- 应用层排队
兜底方案
高可用建设的各个阶段:
- 架构阶段
- 编码阶段
- 测试阶段
- 发布阶段
- 运行阶段
- 故障发生
针对运行阶段处理方案:
- 降级
- 限流
- 拒绝服务
降级
降级,就是当系统容量达到一定程度时,限制或关闭某些非核心功能,把有限的资源留给合型业务
降级是需要提前做好预案和系统开关来实现,例如:
当秒杀流量达到5w/s时,通过一个开关,把成功交易从展示20条降级到展示5条
限流
- 客户端限流
- 就是减少客户端发出请求,缺点就是不好把控限流阀值
- 服务端限流
- 可以合理设置限流阀值,缺点是大量的请求还是到达了服务器,服务端处理不了的请求就是无用请求也会消耗服务器资源
方案:
测试最大的QPS,假设最大支持1W/s QPS,可以设置8000来进行限流保护
拒绝服务
系统负载达到一定的阀值:cpu超过90%等,系统直接拒绝所有请求
在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝HTTP请求,并返回503错误码
缓存失效的应对策略
应用层用队列接受请求,然后结果怎么返回的问题
也就是把用户的所有秒杀请求都放到一个队列进行排队,然后在队列里按照进入队列的顺序进行选择,先到先得
这种方案会有两个问题:
- 体验差,因为异步的方式,在页面中搞个倒计时,处理时间长
- 秒杀判断是根据进入队列时间,没意思
答案:
这种方案完全没有必要:
因为服务器接受请求本身就是按照请求顺序处理的,而这个处理在web层是实时同步的,处理结果也会立马返回给用户
我的理解:
把请求入队列,就是把用户信息放入一个队列中,队列满了,后来请求入不了队列的请求就返回“抢光了“
并不是说,把每个请求进入队列,然后一直等待队列处理再返回
缓存失效问题
有Cache的地方必然存在失效问题,因为需要保证数据的一致性
失效有两种方式:
- 被动失效
- 就是设置缓存的过期时间,针对某些时效性不是很高的数据,到期则自动失效
- 主动失效
- 一般有Cache的失效中心,通过监控数据库表的变化来发送失效请求