[转]秒杀系统架构分析与实战


0 系列文件夹css

  • 秒杀系统架构
    • 秒杀系统架构分析与实战

1 秒杀业务分析

  1. 正常电子商务流程(1)查询商品。(2)建立订单。(3)扣减库存;(4)更新订单。(5)付款;(6)卖家发货
  2. 秒杀业务的特性(1)低便宜格;(2)大幅推广;(3)瞬时售空;(4)一般是定时上架;(5)时间短、瞬时并发量高;

2 秒杀技术挑战

假设某站点秒杀活动仅仅推出一件商品,估计会吸引1万人參加活动,也就说最大并发请求数是10000。秒杀系统需要面对的技术挑战有:html

  1. 对现有站点业务形成冲击秒杀活动仅仅是站点营销的一个附加活动,这个活动具备时间短,并发訪问量大的特色,假设和站点原有应用部署在一块儿。一定会对现有业务形成冲击,稍有不慎可能致使整个站点瘫痪。解决方式:将秒杀系统独立部署,甚至使用独立域名。使其与站点全然隔离

  2. 高并发下的应用、数据库负载用户在秒杀開始前,经过不停刷新浏览器页面以保证不会错过秒杀,这些请求假设依照通常的站点应用架构,訪问应用server、链接数据库,会相应用server和数据库server形成负载压力。解决方式:又一次设计秒杀商品页面,不使用站点原来的商品具体页面。页面内容静态化,用户请求不需要通过应用服务

  3. 忽然添加的网络及server带宽假设商品页面大小200K(主要是商品图片大小)。那么需要的网络和server带宽是2G(200K×10000),这些网络带宽是由于秒杀活动新增的,超过站点平时使用的带宽。

    解决方式:由于秒杀新增的网络带宽。必须和运营商又一次购买或者租借。前端

    为了减轻站点server的压力,需要将秒杀商品页面缓存在CDN,一样需要和CDN服务商暂时租借新增的出口带宽java

  4. 直接下单秒杀的游戏规则是到了秒杀才干開始对商品下单购买,在此时间点以前。仅仅能浏览商品信息,不能下单。

    而下单页面也是一个普通的URL,假设获得这个URL,不用等到秒杀開始就可以下单了。mysql

    解决方式:为了不用户直接訪问下单页面URL,需要将改URL动态化。即便秒杀系统的开发人员也没法在秒杀開始前訪问下单页面的URL。web

    办法是在下单页面URL添加由server端生成的随机数做为參数,在秒杀開始的时候才干获得redis

  5. 怎样控制秒杀商品页面购买button的点亮购买button仅仅有在秒杀開始的时候才干点亮。在此以前是灰色的。假设该页面是动态生成的,固然可以在server端构造响应页面输出,控制该button是灰色还 是点亮,但是为了减轻server端负载压力。更好地利用CDN、反向代理等性能优化手段。该页面被设计为静态页面,缓存在CDN、反向代理server上,甚至用户浏览器上。秒杀開始时。用户刷新页面,请求根本不会到达应用server。

    解决方式:使用JavaScript脚本控制,在秒杀商品静态页面中添加一个JavaScript文件引用,该JavaScript文件里包括 秒杀開始标志为否;当秒杀開始的时候生成一个新的JavaScript文件(文件名称保持不变。仅仅是内容不同),更新秒杀開始标志为是,添加下单页面的URL及随机数參数(这个随机数仅仅会产生一个,即所有人看到的URL都是同一个。server端可以用redis这样的分布式缓存server来保存随机数),并被用户浏览器载入,控制秒杀商品页面的展现。算法

    这个JavaScript文件的载入可以加上随机版本(好比xx.js?v=32353823)。这样就不会被浏览器、CDN和反向代理server缓存sql

    这个JavaScript文件很是小,即便每次浏览器刷新都訪问JavaScript文件server也不会对server集群和网络带宽形成太大压力。数据库

  6. 怎样仅仅赞成第一个提交的订单被发送到订单子系统由于终于可以成功秒杀到商品的用户仅仅有一个。所以需要在用户提交订单时。检查是否已经有订单提交。假设已经有订单提交成功,则需要更新 JavaScript文件,更新秒杀開始标志为否,购买button变灰。事实上,由于终于可以成功提交订单的用户仅仅有一个,为了减轻下单页面server的负载压力, 可以控制进入下单页面的入口,仅仅有少数用户能进入下单页面,其它用户直接进入秒杀结束页面。解决方式:假设下单server集群有10台server,每台server仅仅接受最多10个下单请求。

    在尚未人提交订单成功以前,假设一台server已经有十单了,而有的一单都没处理,可能出现的用户体验不佳的场景是用户第一次点击购买button进入已结束页面。再刷新一下页面。有可能被一单都没有处理的server处理,进入了填写订单的页面。可以考虑经过cookie的方式来应对,符合一致性原则。固然可以採用最少链接的负载均衡算法。出现上述状况的几率大大下降。

  7. 怎样进行下单前置检查
    • 下单server检查本机已处理的下单请求数目:

    假设超过10条,直接返回已结束页面给用户;

    假设未超过10条,则用户可进入填写订单及确认页面。

    • 检查全局已提交订单数目:

    已超过秒杀商品总数,返回已结束页面给用户。

    未超过秒杀商品总数,提交到子订单系统。

  8. 秒杀一般是定时上架该功能实现方式很是多。

    只是眼下比較好的方式是:提早设定好商品的上架时间,用户可以在前台看到该商品,但是没法点击“立刻购买”的button。

    但是需要考虑的是。有人可以绕过前端的限制,直接经过URL的方式发起购买,这就需要在前台商品页面,以及bug页面到后端的数据库,都要进行时钟同步。

    越在后端控制。安全性越高。定时秒杀的话。就要避免卖家在秒杀前对商品作编辑带来的不可预期的影响。这样的特殊的变动需要多方面评估。通常禁止编辑。如需变动,可以走数据订正多的流程。

  9. 减库存的操做有两种选择,一种是拍下减库存 第二种是付款减库存。眼下採用的“拍下减库存”的方式,拍下就是一瞬间的事。对用户体验会好些。
  10. 库存会带来“超卖”的问题:售出数量多于库存数量由于库存并发更新的问题,致使在实际库存已经不足的状况下。库存依旧在减,致使卖家的商品卖得件数超过秒杀的预期。

    方案:採用乐观锁

    1
    2
    3
    update auction_auctions set
    quantity = #inQuantity#
    where auction_id = #itemId# and quantity = #dbQuantity#
  11. 秒杀器的应对秒杀器通常下单个购买及其迅速。依据购买记录可以甄别出一部分。可以经过校验码达到必定的方法。这就要求校验码足够安全。不被破解,採用的方式有:秒杀专用验证码,电视发布验证码,秒杀答题

3 秒杀架构原则

  1. 尽可能将请求拦截在系统上游传统秒杀系统之因此挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,差点儿所有请求都超时,流量虽大,下单成功的有效流量甚小【一趟火车事实上仅仅有2000张票,200w我的来买,基本没有人能买成功。请求有效率为0】。
  2. 读多写少的常用多使用缓存这是一个典型的读多写少的应用场景【一趟火车事实上仅仅有2000张票,200w我的来买,最多2000我的下单成功,其它人都是查询库存。写比例仅仅有0.1%。读比例占99.9%】,很是适合使用缓存

4 秒杀架构设计

秒杀系统为秒杀而设计,不一样于通常的网购行为,參与秒杀活动的用户更关心的是怎样能快速刷新商品页面。在秒杀開始的时候抢先进入下单页面,而不是商品详情等用户体验细节,所以秒杀系统的页面设计应尽量简单。

商品页面中的购买button仅仅有在秒杀活动開始的时候才变亮,在此以前及秒杀商品卖出后,该button都是灰色的。不可以点击。

下单表单也尽量简单,购买数量仅仅能是一个且不可以改动,送货地址和付款方式都使用用户默认设置。没有默认也可以不填。赞成等订单提交后改动。仅仅有第一个提交的订单发送给站点的订单子系统,其他用户提交订单后仅仅能看到秒杀结束页面。

要作一个这样的秒杀系统,业务会分为两个阶段,第一个阶段是秒杀開始前某个时间到秒杀開始。 这个阶段可以称之为准备阶段,用户在准备阶段等待秒杀; 第二个阶段就是秒杀開始到所有參与秒杀的用户得到秒杀结果, 这个就称为秒杀阶段吧。

4.1 前端层设计

首先要有一个展现秒杀商品的页面, 在这个页面上作一个秒杀活动開始的倒计时。 在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面。这里需要考虑两个问题:

  1. 第一个是秒杀页面的展现咱们知道一个html页面仍是比較大的,即便作了压缩。http头和内容的大小也可能高达数十K,加上其它的css。 js,图片等资源,假设同一时候有几千万人參与一个商品的抢购,通常机房带宽也就仅仅有1G~10G。网络带宽就极有可能成为瓶颈。因此这个页面上各种静态资源首先应分开存放,而后放到cdn节点上分散压力,由于CDN节点遍及全国各地,能缓冲掉绝大部分的压力。并且还比机房带宽便宜~
  2. 第二个是倒计时出于性能缘由这个通常由js调用client本地时间。就有可能出现client时钟与server时钟不一致。另外server之间也是有可能出现时钟不一致。

    client与server时钟不一致可以採用client定时和server同步时间。这里考虑一下性能问题。用于同步时间的接口由于不涉及到后端逻辑。仅仅需要将当前webserver的时间发送给client就可以了,所以速度很是快。就我之前測试的结果来看。一台标准的webserver2W+QPS不会有问题,假设100W人同一时候刷,100W QPS也仅仅需要50台web。一台硬件LB就可以了~,并且webserver群是可以很是easy的横向扩展的(LB+DNS轮询),这个接口可以仅仅返回一小段json格式的数据。并且可以优化一降低低没必要要cookie和其它http头的信息。因此数据量不会很是大,通常来讲网络不会成为瓶颈,即便成为瓶颈也可以考虑多机房专线连通,加智能DNS的解决方式;webserver之间时间不一样步可以採用统一时间server的方式,比方每隔1分钟所有參与秒杀活动的webserver就与时间server作一次时间同步

  3. 浏览器层请求拦截(1)产品层面,用户点击“查询”或者“购票”后,button置灰,禁止用户反复提交请求;(2)JS层面,限制用户在x秒以内仅仅能提交一次请求;

4.2 站点层设计

前端层的请求拦截,仅仅能拦住小白用户(只是这是99%的用户哟),高端的程序猿根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?

(1)同一个uid。限制訪问频度,作页面缓存。x秒内到达站点层的请求,均返回同一页面

(2)同一个item的查询。好比手机车次,作页面缓存,x秒内到达站点层的请求。均返回同一页面

如此限流,又有99%的流量会被拦截在站点层。

4.3 服务层设计

站点层的请求拦截。仅仅能拦住普通程序猿,高级黑客。假设他控制了10w台肉鸡(并且假设买票不需要实名认证)。这下uid的限制不行了吧?怎么整?

(1)大哥。我是服务层,我清楚的知道小米仅仅有1万部手机。我清楚的知道一列火车仅仅有2000张车票。我透10w个请求去数据库有什么意义呢?对于写请求,作请求队列。每次仅仅透过有限的写请求去数据层,假设均成功再放下一批,假设库存不够则队列里的写请求所有返回“已售完”

(2)对于读请求,还用说么?cache来抗,不管是memcached仍是redis,单机抗个每秒10w应该都是没什么问题的;

如此限流。仅仅有很是少的写请求。和很是少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。

  1. 用户请求分发模块:使用Nginx或Apache将用户的请求分发到不一样的机器上。
  2. 用户请求预处理模块:推断商品是否是还有剩余来决定是否是要处理该请求。

  3. 用户请求处理模块:把经过预处理的请求封装成事务提交给数据库,并返回是否成功。
  4. 数据库接口模块:该模块是数据库的惟一接口,负责与数据库交互。提供RPC接口供查询是否秒杀结束、剩余数量等信息。
  • 用户请求预处理模块通过HTTPserver的分发后,单个server的负载相对低了一些,但总量依旧可能很是大,假设后台商品已经被秒杀完毕。那么直接给后来的请求返回秒杀失败就能够。没必要再进一步发送事务了。演示样例代码可以例如如下所看到的:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package seckill;
    import org.apache.http.HttpRequest;
    /**
    * 预处理阶段。把没必要要的请求直接驳回,必要的请求添加到队列中进入下一阶段.
    */
    public class PreProcessor {
       // 商品是否还有剩余
       private static boolean reminds = true ;
       private static void forbidden() {
           // Do something.
       }
       public static boolean checkReminds() {
           if (reminds) {
               // 远程检測是否还有剩余。该RPC接口应由数据库server提供。没必要全然严格检查.
               if (!RPC.checkReminds()) {
                   reminds = false ;
               }
           }
           return reminds;
       }
       /**
        * 每一个HTTP请求都要通过该预处理.
        */
       public static void preProcess(HttpRequest request) {
           if (checkReminds()) {
               // 一个并发的队列
               RequestQueue.queue.add(request);
           } else {
               // 假设已经没有商品了。则直接驳回请求就能够.
               forbidden();
           }
       }
    }
    • 并发队列的选择

    Java的并发包提供了三个常用的并发队列实现,各自是:ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue。

    ArrayBlockingQueue是初始容量固定的堵塞队列,咱们可以用来做为数据库模块成功竞拍的队列。比方有10个商品,那么咱们就设定一个10大小的数组队列。

    ConcurrentLinkedQueue使用的是CAS原语无锁队列实现,是一个异步队列。入队的速度很是快。出队进行了加锁。性能稍慢。

    LinkedBlockingQueue也是堵塞的队列。入队和出队都用了加锁,当队空的时候线程会暂时堵塞。

    由于咱们的系统入队需求要远大于出队需求,通常不会出现队空的状况,因此咱们可以选择ConcurrentLinkedQueue来做为咱们的请求队列实现:

    1
    2
    3
    4
    5
    6
    7
    package seckill;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.ConcurrentLinkedQueue;
    import org.apache.http.HttpRequest;
    public class RequestQueue {
       public static ConcurrentLinkedQueue<HttpRequest> queue = new ConcurrentLinkedQueue<HttpRequest>();
    }
  • 用户请求模块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package seckill;
    import org.apache.http.HttpRequest;
    public class Processor {
       /**
        * 发送秒杀事务到数据库队列.
        */
       public static void kill(BidInfo info) {
           DB.bids.add(info);
       }
       public static void process() {
           BidInfo info = new BidInfo(RequestQueue.queue.poll());
           if (info != null ) {
               kill(info);
           }
       }
    }
    class BidInfo {
       BidInfo(HttpRequest request) {
           // Do something.
       }
    }
  • 数据库模块数据库主要是使用一个ArrayBlockingQueue来暂存有可能成功的用户请求。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package seckill;
    import java.util.concurrent.ArrayBlockingQueue;
    /**
    * DB应该是数据库的惟一接口.
    */
    public class DB {
       public static int count = 10 ;
       public static ArrayBlockingQueue<BidInfo> bids = new ArrayBlockingQueue<BidInfo>( 10 );
       public static boolean checkReminds() {
           // TODO
           return true ;
       }
       // 单线程操做
       public static void bid() {
           BidInfo info = bids.poll();
           while (count-- > 0 ) {
               // insert into table Bids values(item_id, user_id, bid_date, other)
               // select count(id) from Bids where item_id = ?

               // 假设数据库商品数量大约总数,则标志秒杀已完毕,设置标志位reminds = false.
               info = bids.poll();
           }
       }
    }

4.4 数据库设计

4.4.1 基本概念

概念一“单库”

输入图片说明

概念二“分片”

输入图片说明

分片解决的是“数据量太大”的问题,也就是一般说的“水平切分”。一旦引入分片,势必有“数据路由”的概念,哪一个数据訪问哪一个库。路由规则一般有3种方法:

  1. 范围:range长处:简单。easy扩展缺点:各库压力不均(新号段更活跃)
  2. 哈希:hash 【大部分互联网公司採用的方案二:哈希分库。哈希路由】长处:简单,数据均衡,负载均匀缺点:迁移麻烦(2库扩3库数据要迁移)
  3. 路由服务:router-config-server长处:灵活性强,业务与路由算法解耦缺点:每次訪问数据库前多一次查询

概念三“分组”

输入图片说明

分组解决“可用性”问题,分组一般经过主从复制的方式实现。

互联网公司数据库实际软件架构是:又分片,又分组(例如如下图)

输入图片说明

4.4.2 设计思路

数据库软件架构师平时设计些什么东西呢?至少要考虑下面四点:

  1. 怎样保证数据可用性;
  2. 怎样提升数据库读性能(大部分应用读多写少,读会先成为瓶颈)。
  3. 怎样保证一致性;
  4. 怎样提升扩展性;
  • 1. 怎样保证数据的可用性?解决可用性问题的思路是=>冗余怎样保证站点的可用性?复制站点,冗余站点怎样保证服务的可用性?复制服务,冗余服务

    怎样保证数据的可用性?复制数据。冗余数据

    数据的冗余,会带来一个反作用=>引起一致性问题(先不说一致性问题。先说可用性)

  • 2. 怎样保证数据库“读”高可用?冗余读库输入图片说明冗余读库带来的反作用?读写有延时,可能不一致

    上面这个图是很是多互联网公司mysql的架构,写仍然是单点。不能保证写高可用。

  • 3. 怎样保证数据库“写”高可用?冗余写库输入图片说明採用双主互备的方式,可以冗余写库带来的反作用?双写同步,数据可能冲突(好比“自增id”同步冲突),怎样解决同步冲突,有两种常看法决方式:
    1. 两个写库使用不一样的初始值。一样的步长来添加id:1写库的id为0,2,4,6…;2写库的id为1,3,5,7…。
    2. 不使用数据的id。业务层本身生成惟一的id,保证数据不冲突;

实际中没有使用上述两种架构来作读写的“高可用”。採用的是“双主当主从用”的方式

输入图片说明

还是双主。但仅仅有一个主提供服务(读+写)。另外一个主是“shadow-master”。仅仅用来保证高可用。平时不提供服务。 master挂了,shadow-master顶上(vip漂移,对业务层透明。不需要人工介入)。这样的方式的长处:

  1. 读写没有延时;
  2. 读写高可用;

不足:

  1. 不能经过加从库的方式扩展读性能;
  2. 资源利用率为50%,一台冗余主没有提供服务;

那怎样提升读性能呢?进入第二个话题,怎样提供读性能。

  • 4. 怎样扩展读性能提升读性能的方式大体有三种。第一种是创建索引。这样的方式不展开,要提到的一点是,不一样的库可以创建不一样的索引输入图片说明写库不创建索引。

    线上读库创建线上訪问索引,好比uid;

    线下读库创建线下訪问索引,好比time;

    第二种扩充读性能的方式是。添加从库,这样的方法你们用的比較多,但是,存在两个缺点:

    1. 从库越多。同步越慢;
    2. 同步越慢,数据不一致窗体越大(不一致后面说,仍是先说读性能的提升)。

    实际中没有採用这样的方法提升数据库读性能(没有从库)。採用的是添加缓存

    常见的缓存架构例如如下:

    输入图片说明

    上游是业务应用,下游是主库,从库(读写分离)。缓存

    实际的玩法:服务+数据库+缓存一套

    输入图片说明

    业务层不直接面向db和cache。服务层屏蔽了底层db、cache的复杂性

    为何要引入服务层,今天不展开,採用了“服务+数据库+缓存一套”的方式提供数据訪问。用cache提升读性能

    不管採用主从的方式扩展读性能,仍是缓存的方式扩展读性能,数据都要复制多份(主+从。db+cache),必定会引起一致性问题

  • 5. 怎样保证一致性?主从数据库的一致性。一般有两种解决方式:1. 中间件输入图片说明

    假设某一个key有写操做,在不一致时间窗体内,中间件会将这个key的读操做也路由到主库上。这个方法的缺点是,数据库中间件的门槛较高(百度,腾讯,阿里,360等一些公司有)。

    2. 强制读主

    输入图片说明

    上面实际用的“双主当主从用”的架构,不存在主从不一致的问题

    第二类不一致,是db与缓存间的不一致

    输入图片说明

    常见的缓存架构如上,此时写操做的顺序是:

    (1)淘汰cache;

    (2)写数据库。

    读操做的顺序是:

    (1)读cache,假设cache hit则返回。

    (2)假设cache miss,则读从库;

    (3)读从库后,将数据放回cache。

    在一些异常时序状况下,有可能从【从库读到旧数据(同步尚未完毕),旧数据入cache后】,数据会长期不一致。解决的方法是“缓存双淘汰”。写操做时序升级为:

    (1)淘汰cache;

    (2)写数据库。

    (3)在经验“主从同步延时窗体时间”后,再次发起一个异步淘汰cache的请求;

    这样。即便有脏数据如cache。一个小的时间窗体以后,脏数据仍是会被淘汰。带来的代价是。多引入一次读miss(成本可以忽略)。

    除此以外,最佳实践之中的一个是:建议为所有cache中的item设置一个超时时间

  • 6. 怎样提升数据库的扩展性?原来用hash的方式路由,分为2个库。数据量仍是太大,要分为3个库,势必需要进行数据迁移。有一个很是帅气的“数据库秒级扩容”方案。怎样秒级扩容?首先,咱们不作2库变3库的扩容。咱们作2库变4库(库加倍)的扩容(将来4->8->16)

    输入图片说明

    服务+数据库是一套(省去了缓存),数据库採用“双主”的模式

    扩容步骤:

    第一步,将一个主库提高;

    第二步。改动配置,2库变4库(原来MOD2,现在配置改动后MOD4),扩容完毕。

    原MOD2为偶的部分,现在会MOD4余0或者2。原MOD2为奇的部分。现在会MOD4余1或者3;数据不需要迁移。同一时候,双主互一样步,一遍是余0,一边余2。两边数据同步也不会冲突。秒级完毕扩容!

    最后,要作一些收尾工做:

    1. 将旧的双主同步解除;
    2. 添加新的双主(双主是保证可用性的,shadow-master平时不提供服务);
    3. 删除多余的数据(余0的主,可以将余2的数据删除掉)。

    输入图片说明

    这样。秒级别内,咱们就完毕了2库变4库的扩展。

5 大并发带来的挑战

5.1 请求接口的合理设计

一个秒杀或者抢购页面,一般分为2个部分。一个是静态的HTML等内容,另外一个就是參与秒杀的Web后台请求接口

一般静态HTML等内容。是经过CDN的部署。通常压力不大。核心瓶颈实际上在后台请求接口上。这个后端接口,必须可以支持高并发请求。同一时候,很是重要的一点,必须尽量“快”,在最短的时间里返回用户的请求结果。为了实现尽量快这一点。接口的后端存储使用内存级别的操做会更好一点。仍然直接面向MySQL之类的存储是不合适的,假设有这样的复杂业务的需求,都建议採用异步写入

输入图片说明

固然,也有一些秒杀和抢购採用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才干够从页面中看到用户是否秒杀成功。但是,这样的属于“偷懒”行为。同一时候给用户的体验也很差,easy被用户以为是“暗箱操做”。

5.2 高并发的挑战:必定要“快”

咱们一般衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数)。解决每秒数万次的高并发场景。这个指标很是关键。举个样例,咱们假设处理一个业务请求平均响应时间为100ms,同一时候,系统内有20台Apache的Webserver。配置MaxClients为500个(表示Apache的最大链接数目)。

那么,咱们的Web系统的理论峰值QPS为(理想化的计算方式):

1
20*500/0.1 = 100000 (10万QPS)

咦?咱们的系统彷佛很是强大,1秒钟可以处理完10万的请求,5w/s的秒杀彷佛是“纸老虎”哈。

实际状况,固然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大添加

就Webserver而言,Apache打开了越多的链接进程,CPU需要处理的上下文切换也越多,额外添加了CPU的消耗,而后就直接致使平均响应时间添加

所以上述的MaxClient数目,要依据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以经过Apache自带的abench来測试一下。取一个合适的值。而后,咱们选择内存操做级别的存储的Redis,在高并发的状态下,存储的响应时间相当重要。网络带宽尽管也是一个因素,只是,这样的请求数据包通常比較小,通常很是少成为请求的瓶颈。负载均衡成为系统瓶颈的状况比較少,在这里不作讨论哈。

那么问题来了。假设咱们的系统。在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际状况。甚至不少其它):

1
20*500/0.25 = 40000 (4万QPS)

因而。咱们的系统剩下了4w的QPS。面对5w每秒的请求,中间相差了1w。

而后。这才是真正的恶梦開始。举个样例,快速路口。1秒钟来5部车,每秒经过5部车。快速路口运做正常。

忽然。这个路口1秒钟仅仅能经过4部车,车流量仍然依旧,结果一定出现大塞车。

(5条车道突然变成4条车道的感受)。

同理,某一个秒内。20*500个可用链接进程都在满负荷工做中。却仍然有1万个新来请求,没有链接进程可用,系统陷入到异常状态也是预期以内。

输入图片说明

事实上在正常的非高并发的业务场景中,也有相似的状况出现,某个业务请求接口出现故障,响应时间极慢,将整个Web请求响应时间拉得很是长,逐渐将Webserver的可用链接数占满。其它正常的业务请求,无链接进程可用。

更可怕的问题是。是用户的行为特色。系统越是不可用,用户的点击越频繁。恶性循环终于致使“雪崩”(当中一台Web机器挂了。致使流量分散到其它正常工做的机器上,再致使正常的机器也挂。而后恶性循环),将整个Web系统拖垮。

5.3 从新启动与过载保护

假设系统发生“雪崩”,贸然从新启动服务,是没法解决这个问题的。最多见的现象是,启动起来后,立马挂掉。这个时候,最好在入口层将流量拒绝,而后再将从新启动假设是redis/memcache这样的服务也挂了,从新启动的时候需要注意“预热”,并且很是可能需要比較长的时间

秒杀和抢购的场景,流量每每是超乎咱们系统的准备和想象的。这个时候,过载保护是必要的。假设检測到系统满负载状态。拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是。这样的作法是被用户“千夫所指”的行为。

更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回

6 做弊的手段:进攻与防守

秒杀和抢购收到了“海量”的请求。实际上里面的水分是很是大的。很多用户。为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽量多的请求到server。

另外一部分高级用户。制做强大的本身主动请求脚本。这样的作法的理由也很是简单,就是在參与秒杀和抢购的请求中。本身的请求数目占比越多,成功的几率越高

这些都是属于“做弊的手段”,只是,有“进攻”就有“防守”。这是一场没有硝烟的战斗哈。

6.1 同一个帐号,一次性发出多个请求

部分用户经过浏览器的插件或者其它工具,在秒杀開始的时间里,以本身的帐号,一次发送上百甚至不少其它的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。

这样的请求在某些没有作数据安全处理的系统里,也可能形成第二种破坏,致使某些推断条件被绕过。好比一个简单的领取逻辑。先推断用户是否有參与记录,假设没有则领取成功,最后写入到參与记录中。这是个很是简单的逻辑。但是。在高并发的场景下,存在深深的漏洞。多个并发请求经过负载均衡server,分配到内网的多台Webserver,它们首先向存储发送查询请求,而后,在某个请求成功写入參与记录的时间差内。其它的请求获查询到的结果都是“没有參与记录”。这里,就存在逻辑推断被绕过的风险。

输入图片说明

应对方案:

在程序入口处。一个帐号仅仅赞成接受1个请求,其它请求过滤。

不只攻克了同一个帐号。发送N个请求的问题,还保证了兴许的逻辑流程的安全。实现方案。可以经过Redis这样的内存缓存服务,写入一个标志位(仅仅赞成1个请求写成功,结合watch的乐观锁的特性),成功写入的则可以继续參加

输入图片说明

或者,本身实现一个服务。将同一个帐号的请求放入一个队列中,处理完一个,再处理下一个。

6.2 多个帐号,一次性发送多个请求

很是多公司的帐号注冊功能,在发展早期差点儿是没有限制的。很是easy就可以注冊很是多个帐号。

所以,也致使了出现了一些特殊的工做室,经过编写本身主动注冊脚本,积累了一大批“僵尸帐号”,数量庞大。几万甚至几十万的帐号不等,专门作各类刷的行为(这就是微博中的“僵尸粉“的来源)

举个样例。好比微博中有转发抽奖的活动,假设咱们使用几万个“僵尸号”去混进去转发,这样就可以大大提高咱们中奖的几率。

这样的帐号,使用在秒杀和抢购里。也是同一个道理。好比。iPhone官网的抢购,火车票黄牛党。

输入图片说明

应对方案:

这样的场景,可以经过检測指定机器IP请求频率就可以解决,假设发现某个IP请求频率很是高,可以给它弹出一个验证码或者直接禁止它的请求

  1. 弹出验证码。最核心的追求,就是分辨出真有用户。所以。你们可能常常发现。站点弹出的验证码,有些是“鬼神乱舞”的样子,有时让咱们根本没法看清。他们这样作的缘由,事实上也是为了让验证码的图片不被轻易识别,由于强大的“本身主动脚本”可以经过图片识别里面的字符。而后让脚本本身主动填写验证码。实际上。有一些很是创新的验证码,效果会比較好,好比给你一个简单问题让你回答,或者让你完毕某些简单操做(好比百度贴吧的验证码)。
  2. 直接禁止IP。其实是有些粗暴的。由于有些真有用户的网络场景刚好是同一出口IP的,可能会有“误伤“

    但是这一个作法简单高效,依据实际场景使用可以得到很是好的效果。

6.3 多个帐号。不一样IP发送不一样请求

所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工做室”。发现你对单机IP请求频率有控制以后。他们也针对这样的场景,想出了他们的“新进攻方案”,就是不断改变IP

输入图片说明

有同窗会好奇,这些随机IP服务怎么来的。有一些是某些机构本身占领一批独立IP,而后作成一个随机代理IP的服务,有偿提供给这些“工做室”使用

另外一些更为黑暗一点的。就是经过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运做。仅仅作一件事情,就是转发IP包,普通用户的电脑被变成了IP代理出口

经过这样的作法。黑客就拿到了大量的独立IP,而后搭建为随机IP服务。就是为了挣钱。

应对方案:

说实话,这样的场景下的请求,和真有用户的行为。已经基本一样了,想作分辨很是困难。

再作进一步的限制很是easy“误伤“真有用户,这个时候。一般仅仅能经过设置业务门槛高来限制这样的请求了,或者经过帐号行为的”数据挖掘“来提早清理掉它们

僵尸帐号也仍是有一些共同特征的,好比帐号很是可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等

依据这些特色。适当设置參与门槛,好比限制參与秒杀的帐号等级。经过这些业务手段,也是可以过滤掉一些僵尸号

7 高并发下的数据安全

咱们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同一时候执行同一段代码,假设每次执行结果和单线程执行的结果是同样的,结果和预期一样,就是线程安全的)。假设是MySQL数据库,可以使用它自带的锁机制很是好的解决这个问题,但是,在大规模并发的场景中。是不推荐使用MySQL的。秒杀和抢购的场景中,还有另一个问题,就是“超发”,假设在这方面控制不慎,会产生发送过多的状况。咱们也之前据说过。某些电商搞抢购活动,买家成功拍下后。商家却不认可订单有效。拒绝发货。

这里的问题,或许并不必定是商家奸诈,而是系统技术层面存在超发风险致使的。

7.1 超发的缘由

假设某个抢购场景中,咱们一共仅仅有100个商品,在最后一刻。咱们已经消耗了99个商品。仅剩最后一个。

这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,而后都经过了这一个余量推断。终于致使超发。

输入图片说明

在上面的这个图中。就致使了并发用户B也“抢购成功”。多让一我的得到了商品。这样的场景,在高并发的状况下很是easy出现。

7.2 悲观锁思路

解决线程安全的思路很是多,可以从“悲观锁”的方向開始讨论。

悲观锁,也就是在改动数据的时候。採用锁定状态,排斥外部请求的改动。遇到加锁的状态。就必须等待。

输入图片说明

尽管上述的方案的确攻克了线程安全的问题,但是。别忘记,咱们的场景是“高并发”。也就是说,会很是多这样的改动请求,每一个请求都需要等待“锁”。某些线程可能永远都没有机会抢到这个“锁”,这样的请求就会死在那里。同一时候,这样的请求会很是多。瞬间增大系统的平均响应时间,结果是可用链接数被耗尽,系统陷入异常

7.3 FIFO队列思路

那好。那么咱们略微改动一下上面的场景。咱们直接将请求放入队列中的,採用FIFO(First Input First Output。先进先出),这样的话,咱们就不会致使某些请求永远获取不到锁

看到这里,是否是有点强行将多线程变成单线程的感受哈。

输入图片说明

而后。咱们现在攻克了锁的问题。所有请求採用“先进先出”的队列方式来处理。

那么新的问题来了。高并发的场景下,由于请求很是多,很是可能一瞬间将队列内存“撑爆”。而后系统又陷入到了异常状态

或者设计一个极大的内存队列。也是一种方案,但是。系统处理完一个队列内请求的速度根本没法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,终于Web系统平均响应时候仍是会大幅降低。系统仍是陷入异常。

7.4 乐观锁思路

这个时候,咱们就可以讨论一下“乐观锁”的思路了。

乐观锁。是相对于“悲观锁”採用更为宽松的加锁机制,大都是採用带版本(Version)更新。

实现就是,这个数据所有请求都有资格去改动,但会得到一个该数据的版本,仅仅有版本符合的才干更新成功,其它的返回抢购失败

这样的话。咱们就不需要考虑队列的问题,只是,它会增大CPU的计算开销

但是,综合来讲。这是一个比較好的解决方式。

输入图片说明

有很是多软件和服务都“乐观锁”功能的支持。好比Redis中的watch就是当中之中的一个。经过这个实现,咱们保证了数据的安全。

8 总结

互联网正在快速发展,使用互联网服务的用户越多,高并发的场景也变得愈来愈多。

电商秒杀和抢购,是两个比較典型的互联网高并发场景。尽管咱们解决这个问题的具体技术方案可能千差万别,但是遇到的挑战倒是相似的,所以解决这个问题的思路也殊途同归



来源:http://blog.jobbole.com/96984/

相关文章
相关标签/搜索