导读:最初的秒杀系统的原型是淘宝详情上的定时上架功能,因为有些卖家为了吸引眼球,把价格压得非常低。但这给的详情系统带来了非常大压力,为了将这样的突发流量隔离,才设计了秒杀系统。文章主要介绍大秒系统以及这样的典型读数据的热点问题的解决思路和实践经验。

一些数据

大家还记得2013年的小米秒杀吗?三款小米手机各11万台开卖,走的都是大秒系统。3分钟后成为双十一第一家也是最快破亿的旗舰店。

经过日志统计,前端系统双11峰值有效请求约60w以上的QPS 。而后端cache的集群峰值近2000w/s、单机也近30w/s,但到真正的写时流量要小非常多了,当时最高下单减库存tps是红米创造。达到1500/s。

热点隔离

秒杀系统设计的第一个原则就是将这样的热点数据隔离出来,不要让1%的请求影响到另外的99%。隔离出来后也更方便对这1%的请求做针对性优化。

针对秒杀我们做了多个层次的隔离:

  • 业务隔离。把秒杀做成一种营销活动,卖家要參加秒杀这样的营销活动须要单独报名,从技术上来说,卖家报名后对我们来说就是已知热点。当真正開始时我们能够提前做好预热。

  • 系统隔离。系统隔离很多其它是执行时的隔离,能够通过分组部署的方式和另外99%分开。秒杀还申请了单独的域名,目的也是让请求落到不同的集群中。

  • 数据隔离。

    秒杀所调用的数据大部分都是热数据,比方会启用单独cache集群或MySQL数据库来放热点数据,眼下也是不想0.01%的数据影响另外99.99%。

当然实现隔离非常有多办法,如能够依照用户来区分。给不同用户分配不同cookie。在接入层路由到不同服务接口中。还有在接入层能够对URL的不同Path来设置限流策略等。服务层通过调用不同的服务接口;数据层能够给数据打上特殊的标来区分。

目的都是把已经识别出来的热点和普通请求区分开来。

动静分离

前面介绍在系统层面上的原则是要做隔离,接下去就是要把热点数据进行动静分离,这也是解决大流量系统的一个重要原则。

怎样给系统做动静分离的静态化改造我曾经写过一篇《高訪问量系统的静态化架构设计》具体介绍了淘宝商品系统的静态化设计思路。感兴趣的能够在《程序猿》杂志上找一下。我们的大秒系统是从商品详情系统发展而来,所以本身已经实现了动静分离。如图1。

图片描写叙述

图1 大秒系统动静分离

除此之外还有例如以下特点:

  • 把整个页面Cache在用户浏览器

  • 假设强制刷新整个页面,也会请求到CDN

  • 实际有效请求仅仅是“刷新抢宝”button

这样把90%的静态数据缓存在用户端或者CDN上,当真正秒杀时用户仅仅须要点击特殊的button“刷新抢宝”就可以,而不须要刷新整个页面,这样仅仅向服务端请求非常少的有效数据,而不须要反复请求大量静态数据。秒杀的动态数据和普通的详情页面的动态数据相比更少,性能也比普通的详情提升3倍以上。

所以“刷新抢宝”这样的设计思路非常好地攻克了不刷新页面就能请求到服务端最新的动态数据。

基于时间分片削峰

熟悉淘宝秒杀的都知道。第一版的秒杀系统本身并没有答题功能,后面才添加了秒杀答题,当然秒杀答题一个非常重要的目的是为了防止秒杀器,2011年秒杀非常火的时候,秒杀器也比較猖獗,而没有达到全民參与和营销的目的。所以添加的答题来限制秒杀器。添加答题后。下单的时间基本控制在2s后,秒杀器的下单比例也下降到5%下面。新的答题页面如图2。

图片描写叙述

图2 秒答题页面

事实上添加答题另一个重要的功能。就是把峰值的下单请求给拉长了。从曾经的1s之内延长到2~10s左右,请求峰值基于时间分片了,这个时间的分片对服务端处理并发很重要,会减轻很大压力,另外因为请求的先后,靠后的请求自然也没有库存了,也根本到不了最后的下单步骤,所以真正的并发写就很有限了。事实上这样的设计思路眼下也很普遍,如支付宝的“咻一咻”已及微信的摇一摇。

除了在前端通过答题在用户端进行流量削峰外。在服务端一般通过锁或者队列来控制瞬间请求。

数据分层校验

图片描写叙述

图3 分层校验

对大流量系统的数据做分层校验也是最重要的设计原则。所谓分层校验就是对大量的请求做成“漏斗”式设计。如图3所看到的:在不同层次尽可能把无效的请求过滤,“漏斗”的最末端才是有效的请求,要达到这个效果必须对数据做分层的校验,以下是一些原则:

  • 先做数据的动静分离

  • 将90%的数据缓存在client浏览器

  • 将动态请求的读数据Cache在Web端

  • 对读数据不做强一致性校验

  • 对写数据进行基于时间的合理分片

  • 对写请求做限流保护

  • 对写数据进行强一致性校验

秒杀系统正是依照这个原则设计的系统架构。如图4所看到的。

图片描写叙述

图4 秒杀系统分层架构

把大量静态不须要检验的数据放在离用户近期的地方。在前端读系统中检验一些基本信息,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束等。在写数据系统中再校验一些如是否是非法请求,营销等价物是否充足(淘金币等),写的数据一致性如检查库存是否还有等;最后在数据库层保证数据终于准确性。如库存不能减为负数。

实时热点发现

事实上秒杀系统本质是还是一个数据读的热点问题,并且是最简单一种,由于在文提到通过业务隔离,我们已能提前识别出这些热点数据,我们能够提前做一些保护,提前识别的热点数据处理起来还相对简单,比方分析历史成交记录发现哪些商品比較热门,分析用户的购物车记录也能够发现那些商品可能会比較好卖。这些都是能够提前分析出来的热点。比較困难的是那种我们提前发现不了突然成为热点的商品成为热点。这样的就要通过实时热点数据分析了。眼下我们设计能够在3s内发现交易链路上的实时热点数据。然后依据实时发现的热点数据每一个系统做实时保护。

详细实现例如以下:

  • 构建一个异步的能够收集交易链路上各个中间件产品如Tengine、Tair缓存、HSF等本身的统计的热点key(Tengine和Tair缓存等中间件产品本身已经有热点统计模块)。

  • 建立一个热点上报和可以依照需求订阅的热点服务的下发规范。主要目的是通过交易链路上各个系统(详情、购物车、交易、优惠、库存、物流)訪问的时间差,把上游已经发现的热点可以透传给下游系统,提前做好保护。

    比方大促高峰期详情系统是最早知道的,在统计接入层上Tengine模块统计的热点URL。

  • 将上游的系统收集到热点数据发送到热点服务台上,然后下游系统如交易系统就会知道哪些商品被频繁调用。然后做热点保护。如图5所看到的。

图片描写叙述

图5 实时热点数据后台

重要的几个:当中关键部分包含:

  • 这个热点服务后台抓取热点数据日志最好是异步的,一方面便于做到通用性。还有一方面不影响业务系统和中间件产品的主流程。

  • 热点服务后台、现有各个中间件和应用在做的没有代替关系,每一个中间件和应用还须要保护自己,热点服务后台提供一个收集热点数据提供热点订阅服务的统一规范和工具,便于把各个系统热点数据透明出来。

  • 热点发现要做到实时(3s内)。

关键技术优化点

前面介绍了一些怎样设计大流量读系统中用到的原则。可是当这些手段都用了,还是有大流量涌入该怎样处理呢?秒杀系统要解决几个关键问题。

Java处理大并发动态请求优化

事实上Java和通用的Webserver相比(Nginx或Apache)在处理大并发HTTP请求时要弱一点,所以一般我们都会对大流量的Web系统做静态化改造,让大部分请求和数据直接在Nginxserver或者Web代理server(Varnish、Squid等)上直接返回(能够降低数据的序列化与反序列化)。不要将请求落到Java层上。让Java层仅仅处理非常少数据量的动态请求,当然针对这些请求也有一些优化手段能够使用:

  • 直接使用Servlet处理请求。避免使用传统的MVC框架或许能绕过一大堆复杂且用处不大的处理逻辑,节省个1ms时间。当然这个取决于你对MVC框架的依赖程度。

  • 直接输出流数据。使用resp.getOutputStream()而不是resp.getWriter()能够省掉一些不变字符数据编码。也能提升性能;还有数据输出时也推荐使用JSON而不是模板引擎(一般都是解释运行)输出页面。

同一商品大并发读问题

你会说这个问题非常easy解决,无非放到Tair缓存里面即可,集中式Tair缓存为了保证命中率,一般都会採用一致性Hash,所以同一个key会落到一台机器上,尽管我们的Tair缓存机器单台也能支撑30w/s的请求,可是像大秒这样的级别的热点商品还远不够。那怎样彻底解决这样的单点瓶颈?答案是採用应用层的Localcache。即在秒杀系统的单机上缓存商品相关的数据,怎样cache数据?也分动态和静态:

  • 像商品中的标题和描写叙述这些本身不变的会在秒杀開始之前全量推送到秒杀机器上并一直缓存直到秒杀结束。

  • 像库存这样的动态数据会採用被动失效的方式缓存一定时间(通常是数秒)。失效后再去Tair缓存拉取最新的数据。

你可能会有疑问。像库存这样的频繁更新数据一旦数据不一致会不会导致超卖?事实上这就要用到我们前面介绍的读数据分层校验原则了,读的场景能够同意一定的脏数据,由于这里的误判仅仅会导致少量一些原本已经没有库存的下单请求误觉得还有库存而已。等到真正写数据时再保证终于的一致性。这样在数据的高可用性和一致性做平衡来解决这样的高并发的数据读取问题。

同一数据大并发更新问题

解决大并发读问题採用Localcache和数据的分层校验的方式,可是不管怎样像减库存这样的大并发写还是避免不了。这也是秒杀这个场景下最核心的技术难题。

同一数据在数据库里肯定是一行存储(MySQL),所以会有大量的线程来竞争InnoDB行锁,当并发度越高时等待的线程也会越多。TPS会下降RT会上升,数据库的吞吐量会严重受到影响。

讲到这里会出现一个问题,就是单个热点商品会影响整个数据库的性能。就会出现我们不愿意看到的0.01%商品影响99.99%的商品。所以一个思路也是要遵循前面介绍第一个原则进行隔离。把热点商品放到单独的热点库中。可是无疑也会带来维护的麻烦(要做热点数据的动态迁移以及单独的数据库等)。

分离热点商品到单独的数据库还是没有解决并发锁的问题。要解决并发锁有两层办法。

  • 应用层做排队。

    依照商品维度设置队列顺序运行,这样能降低同一台机器对数据库同一行记录操作的并发度。同一时候也能控制单个商品占用数据库连接的数量,防止热点商品占用太多数据库连接。

  • 数据库层做排队。应用层仅仅能做到单机排队。但应用机器数本身非常多,这样的排队方式控制并发仍然有限,所以假设能在数据库层做全局排队是最理想的,淘宝的数据库团队开发了针对这样的MySQL的InnoDB层上的patch。能够做到数据库层上对单行记录做到并发排队,如图6所看到的。

图片描写叙述

图6 数据库层对单行记录并发排队

你可能会问排队和锁竞争不要等待吗?有啥差别?假设熟悉MySQL会知道,InnoDB内部的死锁检測以及MySQL Server和InnoDB的切换会比較耗性能,淘宝的MySQL核心团队还做了非常多其它方面的优化。如COMMIT_ON_SUCCESS和ROLLBACK_ON_FAIL的patch,配合在SQL里面加hint。在事务里不须要等待应用层提交COMMIT而在数据运行完最后一条SQL后直接依据TARGET_AFFECT_ROW结果提交或回滚,能够降低网络的等待时间(平均约0.7ms)。

据我所知,眼下阿里MySQL团队已将这些patch及提交给MySQL官方评审。

大促热点问题思考

以秒杀这个典型系统为代表的热点问题依据多年经验我总结了些通用原则:隔离、动态分离、分层校验,必须从整个全链路来考虑和优化每一个环节。除了优化系统提升性能。做好限流和保护也是必备的功课。

除去前面介绍的这些热点问题外,淘系还有多种其它数据热点问题:

  • 数据訪问热点,比方Detail中对某些热点商品的訪问度非常高。即使是Tair缓存这样的Cache本身也有瓶颈问题,一旦请求量达到单机极限也会存在热点保护问题。有时看起来好像非常easy解决。比方说做好限流即可。但你想想一旦某个热点触发了一台机器的限流阀值,那么这台机器Cache的数据都将无效,进而间接导致Cache被击穿。请求落地应用层数据库出现雪崩现象。

    这类问题须要与详细Cache产品结合才干有比較好的解决方式。这里提供一个通用的解决思路,就是在Cache的client端做本地Localcache,当发现热点数据时直接Cache在client里。而不要请求到Cache的Server。

  • 数据更新热点,更新问题除了前面介绍的热点隔离和排队处理之外,还有些场景,如对商品的lastmodifytime字段更新会很频繁,在某些场景下这些多条SQL是能够合并的。一定时间内仅仅运行最后一条SQL即可了,能够降低对数据库的update操作。另外热点商品的自己主动迁移,理论上也能够在数据路由层来完毕,利用前面介绍的热点实时发现自己主动将热点从普通库里迁移出来放到单独的热点库中。

依照某种维度建的索引产生热点数据,比方实时搜索中依照商品维度关联评价数据。有些热点商品的评价许多,导致搜索系统依照商品ID建评价数据的索引时内存已经放不下,交易维度关联订单信息也相同有这些问题。这类热点数据须要做数据散列,再添加一个维度,把数据又一次组织。

作者简单介绍:许令波,2009年增加淘宝,眼下负责商品详情业务和稳定性相关工作,长期关注性能优化领域。參与了淘宝高訪问量Web系统基本的优化项目,著有《深入分析Java Web技术内幕》一书。个人站点http://xulingbo.net

posted on 2017-07-04 18:53  lxjshuju  阅读(209)  评论(0编辑  收藏  举报