作者:Tom哥
公众号:微观技术
博客:https://offercome.cn
人生理念:知道的越多,不知道的越多,努力去学
秒杀系统要如何架构,在做技术方案时要注意哪些问题,搞了个秒杀专辑,专门收集秒杀系列文章。
当你去一家公司面试时,很多面试官都会问你如何设计一个高性能秒杀系统。秒杀涉及的技术域从客户端、浏览器、网络、负载均衡、应用服务器、CDN、静态化、库存超卖、流量排队、流控、各种缓存组合、数据库存储等,非常之多,整个后端领域知识基本都会用到。比起普通的业务系统 ,秒杀对技术要求无论在深度还是广度都非常之高,很容易全面考察候选人的技术水平。
当然不同公司、不同业务场景,在系统设计灵活性、技术框架选型可能也会有不同,如何用最少的成本满足业务需求,才是最靓的技术方案,所以也能考察候选人的思维应变能力。
1、活动一般都是整点开始,一瞬间会有大量的用户流量涌入,流量可能是平时的几十倍,系统QPS非常高,因此对系统的性能要求非常高
2、虽然流量非常高,但是与常规业务不同,不是每个用户请求都是要对其负责并处理。简单来说,可以响应标准化错误文案。
3、持续时间非常短,往往只有几秒钟,长的话也可能只有几分钟
4、活动商品一般都会有库存限制,一定要控制好并发,不能因为高并发引发了库存超卖
秒杀活动属于营销玩法,带动网站氛围。具有时间短、并发高的特点。对网站正常业务可能会有影响。我们一般会将秒杀系统单独部署,采用独立域名,从物理资源层面做到风险隔离。
一般会引入缓存机制,商品详情页面静态化处理,放入CDN,用户可以从最近的CDN节点拉取内容。动态的内容,比如库存,采用ajax异步化形式从中心服务器获取,当然后面应用服务器也会有缓存机制。
为了避免用户直接访问下单页面URL,需要将URL动态化,每次打开详情页时动态生成一个随机数,用于后端校验请求的合法性。
计算商品页面的大小,然后可以算出需要的网络带宽,(网络带宽= 单个页面大小*QPS),如果带宽不足需要及时购买。同时大部分的带宽都是图片资源,我们可以考虑将图片、js、css等信息缓存到CDN。
1、数据要尽量少。请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)。因为首先这些数据在网络上传输需要时间,其次不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗 CPU,所以减少传输的数据量可以显著减少 CPU 的使用。例如,我们可以简化秒杀页面的大小,去掉不必要的页面装修效果,等等。
2、请求数要尽量少。用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,比如说,这个页面依赖的 CSS/JavaScript、图片,以及 Ajax 请求等等都定义为“额外请求”,这些额外请求应该尽量少。因为浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久。所以你要记住的是,减少请求数可以显著减少以上这些因素导致的资源消耗。
例如,减少请求数最常用的一个实践就是合并 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、路径要尽量短。所谓“路径”,就是用户发出请求到返回数据这个过程中,需要经过的中间的节点数。
所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。
4、依赖要尽量少。举个例子,比如说你要展示秒杀页面,而这个页面必须强依赖商品信息、用户信息,还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉。
5、系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”。避免将服务的状态和机器绑定,即把服务无状态化,这样服务就可以在机器中随意移动。
接下来,从产品-->前端-->后端--->数据存储,庖丁解牛讲解各个模块的设计思路,以及要注意的问题。
秒杀系统为秒杀而设计,不同于一般的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,而不是商品详情等用户体验细节,因此秒杀系统的页面设计应尽可能简单。
商品页面中的购买按钮只有在秒杀活动开始的时候才变亮,在此之前及秒杀商品卖出后,该按钮都是灰色的,不可以点击。
下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许在订单提交后再修改;只有第一个提交的订单发送给网站的订单子系统,其余用户提交订单后只能看到秒杀结束页面。
1、html页面一般比较大,即使做了压缩,http头和内容的大小也可能高达数十K,加上其他的css, js,图片等资源,如果同时有几千万人参与一个商品的抢购,一般机房带宽也就只有1G10G,网络带宽就极有可能成为瓶颈,所以这个页面上各类静态资源首先应分开存放,然后放到cdn节点上分散压力,由于CDN节点遍布全国各地,能缓冲掉绝大部分的压力,而且还比机房带宽便宜
2、秒杀倒计时。这个时间可以从后端实时获取,避免本地时间不准(另外本地的系统时间可以随意更改)。如果活动时间未到,下单按钮置灰,不允许下单购买。
3、用户点击“查询”或者“购票”后,按钮置灰,等待结果返回,禁止用户重复提交请求。JS层面,限制用户在x秒之内只能提交一次请求;
前端层的请求拦截,只能拦住小白用户(不过这是99%的用户),高端的程序员根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?
同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面
同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面
如此限流,又有99%的流量会被拦截在站点层。
前两层只能拦住普通程序员,高级黑客,假设他控制了10w台肉鸡(并且假设买票不需要实名认证),这下uid的限制不行了吧?怎么整?
用户请求分发模块:使用Nginx或Apache将用户的请求分发到不同的机器上。
用户请求预处理模块:判断商品是不是还有剩余来决定是不是要处理该请求。
用户请求处理模块:把通过预处理的请求封装成事务提交给数据库,并返回是否成功。
数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。
分片解决的是“数据量太大”的问题,也就是通常说的“水平切分”。一旦引入分片,势必有“数据路由”的概念,哪个数据访问哪个库。路由规则通常有3种方法:
1、范围:range
优点:简单,容易扩展
缺点:各库压力不均(新号段更活跃)
2、哈希:hash 【大部分互联网公司采用的方案二:哈希分库,哈希路由】
优点:简单,数据均衡,负载均匀
缺点:迁移麻烦(2库扩3库数据要迁移)
3、路由服务:router-config-server
优点:灵活性强,业务与路由算法解耦
缺点:每次访问数据库前多一次查询
秒杀的并发非常高,库存只有一条记录,如果控制不当,很容易产生超卖现象,解决思路无非就是加锁,分为两种:
1、悲观锁
悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。虽然能解决安全问题,但是系统的吞吐量不高。
2、乐观锁
乐观锁采用“version”版本号控制更新,预设所有请求都有资格可以修改,并获得当前的版本号,如果版本号一致才能更新成功。
重启与过载保护。如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。
秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。
1、识别一些”秒杀器”、“刷票软件”的请求,并能有效拦截,杜绝资损,保证活动效果
2、一个账号,一次性发出多个请求,破坏了秒杀和抢购的公平性。后台服务有防刷机制,需要全局性的访问计数器控制,比如说一秒钟只允许一次请求。
3、多个账号,一次性发送多个请求。很多公司的账号注册功能,在发展早期几乎是没有限制的,很容易就可以注册很多个账号。因此,也导致了出现了一些特殊的工作室,通过编写自动注册脚本,积累了一大批“僵尸账号”,数量庞大,几万甚至几十万的账号不等,专门做各种刷的行为(这就是微博中的“僵尸粉“的来源)。解决方案:可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很高,可以给它弹出一个验证码或者直接禁止它的请求,弹出验证码,可以分辨出真实用户。当然市面也有专门的打码服务,通过机器学习自动识别验证码,所以验证码的防识别难度也在不断加大。比如:有些验证码采用随机答题库,防止秒杀器抢单。
4、多个账号,不同IP发送不同请求。目前市面上有专门卖代理服务的公司,提供代理IP,尽量模拟真实用户请求,这个时候,通常只能通过设置业务门槛高来限制这种请求了,或者通过账号行为的”数据挖掘“来提前清理掉它们。
僵尸账号也还是有一些共同特征的,例如账号很可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务手段,也是可以过滤掉一些僵尸号。
面对如此高的流量请求,如果是按常规模式,先让业务方评估秒杀规模,然后估算峰值流量,进而估算需要加多少台服务器,然后准备线上紧急扩容。活动结束后,还要将机器回收。劳民伤财,有没有更优的解决方案?
异步化处理可以对流量进行削峰,满足高性能需求,同时不需要扩容太多的机器。
设计方案中,我们在前端和下单系统之间,增加一个排队系统。我们简单看下详细的处理过程:
1、用户访问商品详情页,然后提交订单请求,订单请求首先是进入请求队列,同时返回一个排队号码
2、前端会跳转到一个中间态的等待页,这个页面会根据排队号,定时地查询排队系统,排队系统会返回预订单在队列中的位置信息,包括它前面还有多少未处理的预订单,以及后台系统大概还要多久会处理这个预订单,这样用户就不会焦虑;
3、在排队系统的处理区,有很多消费者,它们依次从排队区的队列里获取预订单,然后调用后台下单系统生成实际的订单;
4、随着预订单变成正式的订单,队列里的预订单会逐渐变少,如果当前的预订单已经从队列里被移除了,用户的等待页就会检测到这个情况,页面自动跳转到订单完成页,这就和常规的购物流程一样了,用户进行最后的支付,最终完成整个前台下单过程。
上图是滴滴叫车的排队等待页,司机数代表系统的并发处理能力。如果,这片区域司机较少,叫车乘客较多的话,就会出现排队现象。
关于队列的技术选型,我们采用Redis 的List结构,更轻量级且性能更好。除了和 MQ 一样支持消息的先进先出以外,我们还可以获取队列的长度,以及通过排队号获取消息在队列中的位置,这样我们就可以给前端反馈预订单的处理进度。
另外,秒杀系统一般都是直接下单,不会采用购物车形式,一个订单往往只有一件商品。所以我们可以为每个商品提供一个单独的队列,将数据分片,多个队列可以提供更好的性能。
关于队列长度,为了保证用户能够买到商品,我们并不是把所有前台的下单请求都会放到队列里,而是根据参与活动的秒杀商品库存数量,按照 1:1 的比例,设置队列初始长度,这样就保证了进入队列的请求最终都能生成订单。这个可用队列长度会随着预订单进入队列,不断地减少,当数值变为 0 时,排队系统会拒绝接受新请求进入队列,直接反馈用户下单失败。当然,如果后台订单生成异常或用户取消订单后,可用队列长度会增加,前台会重新开放预订单进入队列。
任何事情都有两面性,有优势自然有不足。该方案适合瞬间有高并发流量,比如秒杀场景。如果该高峰期持续时间较长,同时注重用户体验,需要实时看到下单结果,则不适合采用异步下单方案。比如”外卖业务“,高峰期集中在中午11点到下午1点,从下单到送达一般在半个小时左右,对实时性要求很高。此时需要通过水平扩容,提升系统的处理能力。当然如果部署在阿里云,可以考虑弹性扩容、缩容,节省成本。
1、降级。所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。
2、限流。如果说降级是牺牲了一部分次要的功能和用户的体验效果,那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
3、拒绝服务。当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求。这种方式是最暴力但也最有效的系统保护方式。比如:在最上层的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码。拒绝服务可以说是一种不得已的兜底方案,防止最坏情况发生。防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复。