秒杀系统设计(一) 概述和原则
秒杀系统设计(二) 高性能01-动静分离与热点缓存
秒杀系统设计(三) 高性能02-流量削峰与服务端优化
秒杀系统设计(四) 一致性-库存
秒杀系统设计(五) 高可用-兜底方案
动静分离
让系统“快”起来:
1、提高单次请求的效率
2、减少没必要的请求
“动静分离”就是瞄着这个大方向去的。所谓“动静分离”,其实就是把用户请求的数据(如HTML页面)划分为“动态数据”和“静态数据”。简单来说,“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和URL、浏览者、时间、地域相关,以及是否含有Cookie等私密数据。
比如说:
1、很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面
2、我们如果现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据了
也就是所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据
如何做静态数据缓存
1、应该把静态数据缓存到离用户最近的地方
静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。常见的就三种: 用户浏览器里、CDN上或者在服务端的Cache中。应该根据实际情况,把它们尽量缓存到离用户最近的地方
2、静态化改造就是要直接缓存HTTP连接
静态化改造是直接缓存HTTP连接而不是仅仅缓存数据,Web代理服务器根据请求URL,直接取出对应的HTTP响应头和响应体然后直接返回,这个响应过程简单得连HTTP协议都不用重新组装,甚至连HTTP请求头也不需要解析
3、让谁来缓存静态数据也很重要
不同语言写的Cache软件处理缓存数据的效率也各不相同。以Java为例,因为Java系统本身也有其弱点(比如不擅长处理大量连接请求,每个连接消耗的内存较多,Servlet容器解析HTTP协议较慢),所以你可以不在Java层做缓存,而是直接在Web服务器层上做,这样你就可以屏蔽Java语言层面的一些弱点;而相比起来,Web服务器(如Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求
如何做动静分离改造
以典型的商品详情系统为例来详细介绍,可以从以下5个方面来分离出动态内容:
- URL唯一化: 商品详情系统天然地就可以做到URL唯一化,比如每个商品都由ID来标识,那么
http://item.xxx.com/item.htm?id=xxxx
就可以作为唯一的URL标识。为什么要URL唯一呢?前面说了我们是要缓存整个HTTP连接,那么以什么作为Key呢?就以URL作为缓存的Key,例如以id=xxx这个格式进行区分 - 分离浏览者相关的因素: 浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取
- 分离时间因素: 服务端输出的时间也通过动态请求获取
- 异步化地域因素: 详情页面上与地域相关的因素可做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适
- 去掉Cookie: 服务端输出的页面包含的Cookie可以通过代码软件来删除,如Web服务器Varnish可以通过unset req.http.cookie 命令去掉Cookie。注意,这里说的去掉Cookie并不是用户端收到的页面就不含Cookie了,而是说,在缓存的静态数据中不含有Cookie
分离出动态内容之后,如何组织这些内容页就变得非常关键了。这里需要提醒一点,因为这其中很多动态内容都会被页面中的其他模块用到,如判断该用户是否已登录、用户ID是否匹配等,所以这个时候我们应该将这些信息JSON化(用JSON格式组织这些数据),以方便前端获取
可以用上面介绍的缓存的方式来处理静态数据
动态内容的处理通常有两种方案:
- ESI(Edge Side Includes)(或者SSI)方案: 即在Web代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好
- CSI(Client Side Include)方案: 即单独发起一个异步JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差
动静分离的几种架构方案
前面通过改造把静态数据和动态数据做了分离,那么如何在系统架构上进一步对这些动态和静态数据重新组合,再完整地输出给用户呢?
这就涉及对用户请求路径进行合理的架构了。根据架构上的复杂度,有3种方案可选
方案1、实体机单机部署
这种方案是将虚拟机改为实体机,以增大Cache的容量,并且采用了一致性Hash分组的方式来提升命中率。这里将Cache分成若干组,是希望能达到命中率和访问热点的平衡。Hash分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致Cache被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题
这里我给出了实体机单机部署方案的结构图,如下:
Nginx+Cache+Java结构实体机单机部署
实体机单机部署有以下几个优点:
1、没有网络瓶颈,而且能使用大内存
2、既能提升命中率,又能减少Gzip压缩
3、减少Cache失效压力,因为采用定时失效方式,例如只缓存3秒钟,过期即自动失效
不足之处:
1、这个方案中,虽然把通常只需要虚拟机或者容器运行的Java应用换成实体机,优势很明显,它会增加单机的内存容量,但是一定程度上也造成了CPU的浪费,因为单个的Java进程很难用完整个实体机的CPU
2、另外就是,一个实体机上部署了Java应用又作为Cache来使用,这造成了运维上的高复杂度,所以这是一个折中的方案。如果你的公司里,没有更多的系统有类似需求,那么这样做也比较合适,如果你们有多个业务系统都有静态化改造的需求,那还是建议把Cache层单独抽出来公用比较合理
方案2.统一Cache层
所谓统一Cache层,就是将单机的Cache统一分离出来,形成一个单独的Cache集群。统一Cache层是个更理想的可推广方案,该方案的结构图如下:
将Cache层单独拿出来统一管理可以减少运维成本,同时也方便接入其他静态化系统。此外,它还有一些优点:
1、单独一个Cache层,可以减少多个应用接入时使用Cache的成本。这样接入的应用只要维护自己的Java系统就好,不需要单独维护Cache,而只关心如何使用即可
2、统一Cache的方案更易于维护,如后面加强监控、配置的自动化,只需要一套解决方案就行,统一起来维护升级也比较方便
3、可以共享内存,最大化利用内存,不同系统之间的内存可以动态切换,从而能够有效应对各种攻击
这种方案虽然维护上更方便了,但是也带来了其他一些问题,比如缓存更加集中,导致:
1、Cache层内部交换网络成为瓶颈
2、缓存服务器的网卡也会是瓶颈
3、机器少风险较大,挂掉一台就会影响很大一部分缓存数据
要解决上面这些问题,可以再对Cache做Hash分组,即一组Cache缓存的内容相同,这样能够避免热点数据过度集中导致新的瓶颈产生
方案3.上CDN
在将整个系统做动静分离后,我们自然会想到更进一步的方案,就是将Cache进一步前移到CDN上,因为CDN离用户最近,效果会更好。但是要想这么做,有以下几个问题需要解决:
1、失效问题,就是缓存时效的问题。谈到静态数据时,说过一个关键词叫“相对不变”,它的言外之意是“可能会变化”。比如一篇文章,现在不变,但如果你发现个错别字,是不是就会变化了?如果你的缓存时效很长,那用户端在很长一段时间内看到的都是错的。所以,这个方案中也是,我们需要保证CDN可以在秒级时间内,让分布在全国各地的Cache同时失效,这对CDN的失效系统要求很高
2、命中率问题。Cache最重要的一个衡量指标就是“高命中率”,不然Cache的存在就失去了意义。同样,如果将数据全部放到全国的CDN上,必然导致Cache分散,而Cache分散又会导致访问请求命中同一个Cache的可能性降低,那么命中率就成为一个问题
3、发布更新问题。如果一个业务系统每周都有日常业务需要发布,那么发布系统必须足够简洁高效,而且你还要考虑有问题时快速回滚和排查问题的简便性
从前面的分析来看,将商品详情系统放到全国的所有CDN节点上是不太现实的,因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢?答案是“可以”,但是这样的节点需要满足几个条件:
1、靠近访问量比较集中的地区
2、离主站相对较远
3、节点到主站间的网络比较好,而且稳定
4、节点容量比较大,不会占用其他CDN太多的资源
5、节点不要太多
基于上面几个因素,选择CDN的二级Cache比较合适,因为二级Cache数量偏少,容量也更大,让用户的请求先回源的CDN的二级Cache中,如果没命中再回源站获取数据,部署方式如下图所示:
使用CDN的二级Cache作为缓存,可以达到和当前服务端静态化Cache类似的命中率,因为节点数不多,Cache不是很分散,访问量也比较集中,这样也就解决了命中率问题,同时能够给用户最好的访问体验,是当前比较理想的一种CDN化方案
除此之外,CDN化部署方案还有以下几个特点:
1、把整个页面缓存在用户浏览器中
2、如果强制刷新整个页面,也会请求CDN
3、实际有效请求,只是用户对“刷新抢宝”按钮的点击
这样就把90%的静态数据缓存在了用户端或者CDN上,当真正秒杀时,用户只需要点击特殊的“刷新抢宝”按钮,而不需要刷新整个页面。这样一来,系统只是向服务端请求很少的有效数据,而不需要重复请求大量的静态数据
秒杀的动态数据和普通详情页面的动态数据相比更少,性能也提升了3倍以上。所以“抢宝”这种设计思路,让我们不用刷新页面就能够很好地请求到服务端最新的动态数据
其他注意点
实现动静分离的几种架构方案,选择不同会引入不同的问题
1、比如我们把缓存数据从CDN上移到用户的浏览器里,针对秒杀这个场景是没问题的,但针对一般的商品可否也这样做呢?存储在浏览器或CDN上,有多大区别?
区别很大!因为在CDN上,我们可以做主动失效,而在用户的浏览器里就更不可控,如果用户不主动刷新的话,你很难主动地把消息推送给用户的浏览器
2、另外,在什么地方把静态数据和动态数据合并并渲染出一个完整的页面也很关键
假如在用户的浏览器里合并,那么服务端可以减少渲染整个页面的CPU消耗
如果在服务端合并的话,就要考虑缓存的数据是否进行Gzip压缩了: 如果缓存Gzip压缩后的静态数据可以减少缓存的数据量,但是进行页面合并渲染时就要先解压,然后再压缩完整的页面数据输出给用户;如果缓存未压缩的静态数据,这样不用解压静态数据,但是会增加缓存容量。虽然这些都是细节问题,需要在设计架构方案时考虑清楚
热点数据
我们一定要关注热点,因为热点会对系统产生一系列的影响:
1、热点请求会大量占用服务器处理资源,虽然这个热点可能只占请求总量的亿分之一,然而却可能抢占90%的服务器资源,如果这个热点请求还是没有价值的无效请求,那么对系统资源来说完全是浪费
2、即使这些热点是有效的请求,我们也要识别出来做针对性的优化,从而用更低的代价来支撑这些热点请求
热点分为热点操作和热点数据:
- 热点操作: 例如大量的刷新页面、大量的添加购物车、双十一零点大量的下单等都属于此类操作
- 对系统来说,这些操作可以抽象为“读请求”和“写请求”,这两种热点请求的处理方式大相径庭,读请求的优化空间要大一些,而写请求的瓶颈一般都在存储层,优化的思路就是根据CAP理论做平衡
- 热点数据: 就是用户的热点请求对应的数据
- 静态热点数据: 就是能够提前预测的热点数据。例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点
- 动态热点数据: 就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买
发现热点数据
1、发现静态热点数据
静态热点数据可以通过商业手段,例如强制让卖家通过报名参加的方式提前把热点商品筛选出来,实现方式是通过一个运营系统,把参加活动的商品数据进行打标,然后通过一个后台系统对这些热点商品进行预处理,如提前进行缓存。但是这种通过报名提前筛选的方式也会带来新的问题,即增加卖家的使用成本,而且实时性较差,也不太灵活
不过,除了提前报名筛选这种方式,你还可以通过技术手段提前预测,例如对买家每天访问的商品进行大数据计算,然后统计出TOP N的商品,我们可以认为这些TOP N的商品就是热点商品
2、发现动态热点数据
能够动态地实时发现热点不仅对秒杀商品,对其他热卖商品也同样有价值,所以我们需要想办法实现热点的动态发现功能。
- 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点Key,如Nginx、缓存、RPC服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)
- 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上Nginx模块统计的热点URL
- 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护
其中用户访问商品时经过的路径有很多,我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中 我们通过部署在每台机器上的Agent把日志汇总到聚合和分析集群中,然后把符合一定规则的热点数据,通过订阅分发系统再推送到相应的系统中。你可以是把热点数据填充到Cache中,或者直接推送到应用服务器的内存中,还可以对这些数据进行拦截,总之下游系统可以订阅这些数据,然后根据自己的需求决定如何处理这些数据
打造热点发现系统时,有几点注意事项:
1、这个热点服务后台抓取热点数据日志最好采用异步方式,因为“异步”一方面便于保证通用性,另一方面又不影响业务系统和中间件产品的主流程
2、热点服务发现和中间件自身的热点保护模块并存,每个中间件和应用还需要保护自己。热点服务台提供热点数据的收集和订阅服务,便于把各个系统的热点数据透明出来
3、热点发现要做到接近实时(3s内完成热点数据的发现),因为只有做到接近实时,动态发现才有意义,才能实时地对下游系统提供保护
处理热点数据
处理热点数据通常有几种思路:一是优化,二是限制,三是隔离
优化
优化热点数据最有效的办法就是缓存热点数据,如果热点数据做了动静分离,那么可以长期缓存静态数据。但是,缓存热点数据更多的是“临时”缓存,即不管是静态数据还是动态数据,都用一个队列短暂地缓存数秒钟,由于队列长度有限,可以采用LRU淘汰算法替换
限制
限制更多的是一种保护机制,限制的办法也有很多,例如对被访问商品的ID做一致性Hash,然后根据Hash做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源
隔离
秒杀系统设计的第一个原则就是将这种热点数据隔离出来,不要让1%的请求影响到另外的99%,隔离出来后也更方便对这1%的请求做针对性的优化。具体到“秒杀”业务,我们可以在以下几个层次实现隔离:
- 业务隔离。把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热
- 系统隔离。系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外99%分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中
- 数据隔离。秒杀所调用的数据大部分都是热点数据,比如会启用单独的Cache集群或者MySQL数据库来放热点数据,目的也是不想0.01%的数据有机会影响99.99%数据
实现隔离有很多种办法。比如,你可以按照用户来区分,给不同的用户分配不同的Cookie,在接入层,路由到不同的服务接口中; 再比如,你还可以在接入层针对URL中的不同Path来设置限流策略。服务层调用不同的服务接口,以及数据层通过给数据打标来区分等等这些措施,其目的都是把已经识别出来的热点请求和普通的请求区分开
参考:
如何设计一个秒杀系统