秒杀系统的设计与实现
特征与难点
- 写强一致性:抢购商品的时候不能出现超卖现象
- 读弱一致性:可能读到有库存,但是实际下单会失败
- 抢购人数远多于库存,读写并发巨大
- 库存少导致有效写少(真正形成订单的比较少)
难点在于极致性能的实现以及高可用的保证。
- 高并发下,某个小依赖可能直接造成雪崩
- 流量预估难精确,过高也造成雪崩
- 分布式集群,机器多,出故障的概率高
秒杀原理
- 合理利用 CDN:例如订单详情页,有效减少回源流量
- Nginx 限流:过载保护
- 异步队列:高并发的流量 -> 均匀的流量
- Nginx 负载均衡:接入层分摊流量到无状态 service 层提供服务
CDN原理
- 用户输入访问的域名,操作系统向 LocalDns 查询域名的ip地址.
- LocalDns向 ROOT DNS 查询域名的授权服务器(这里假设LocalDns缓存过期)
- ROOT DNS将域名授权dns记录回应给 LocalDns
- LocalDns得到域名的授权dns记录后,继续向域名授权dns查询域名的ip地址
- 域名授权dns查询域名记录后(一般是CNAME),回应给 LocalDns
- LocalDns得到域名记录后,向智能调度DNS查询域名的ip地址
- 智能调度DNS根据一定的算法和策略(比如静态拓扑,容量等),将最适合的CDN节点ip地址回应给 LocalDns
- LocalDns 将得到的域名ip地址,回应给用户端
- 用户得到域名ip地址后,访问站点服务器
- CDN节点服务器应答请求,将内容返回给客户端.(缓存服务器一方面在本地进行保存,以备以后使用,二方面把获取的数据返回给客户端,完成数据服务过程)
为了实现对普通用户透明(使用缓存后用户客户端无需进行任何设置)访问,需要使用DNS(域名解析)来引导用户来访问Cache服务器,以实现透明的加速服务. 由于用户访问网站的第一步就是域名解析,所以通过修改dns来引导用户访问是最简单有效的方式.
对于普通的Internet用户,每个CDN节点就相当于一个放置在它周围的网站服务器. 通过对dns的接管,用户的请求被透明地指向离他最近的节点,节点中CDN服务器会像网站的原始服务器一样,响应用户的请求. 由于它离用户更近,因而响应时间必然更快.从上面图中虚线圈起来的那块,就是CDN层,这层是位于客户端和源站之间.
智能调度DNS(例如F5中的3DNS)是CDN服务中的关键系统.当用户访问加入CDN服务的网站时,域名解析请求将最终由 “智能调度DNS”负责处理。它通过一组预先定义好的策略,将当时最接近用户的节点地址提供给用户,使用户可以得到快速的服务。同时它需要与分布在各地的CDN节点保持通信,跟踪各节点的健康状态、容量等信息,确保将用户的请求分配到就近可用的节点上。
CNAME即别名(Canonical Name)。可以用来把一个域名解析到另一个域名,当 DNS 系统在查询 CNAME 左面的名称的时候,都会转向 CNAME 右面的名称再进行查询,一直追踪到最后的 PTR 或 A 名称,成功查询后才会做出回应,否则失败。例如,你有一台服务器上存放了很多资料,你使用docs.example.com去访问这些资源,但又希望通过documents.example.com也能访问到这些资源,那么你就可以在您的DNS解析服务商添加一条CNAME记录,将documents.example.com指向docs.example.com,添加该条CNAME记录后,所有访问documents.example.com的请求都会被转到docs.example.com,获得相同的内容。接入CDN时,在CDN提供商控制台添加完加速域名后,您会得到一个CDN给您分配的CNAME域名, 您需要在您的DNS解析服务商添加CNAME记录,将自己的加速域名指向这个CNAME域名,这样该域名所有的请求才会都将转向CDN的节点,达到加速效果。
限流算法
令牌桶算法
典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。(因此,如果桶中有足够多的令牌,就可以以峰值流量进行处理,但是长期来看流量速率是恒定的)从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。
Google 的 Guava 包中的 RateLimiter 类就是令牌桶算法的实现。
漏桶算法
这种算法可以强制限制数据的传输速率,而令牌桶算法除了能够限制平均传输速率,还允许一定程度的突发传输。
漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
如图所示,把请求比作是水,水来了都先放进桶里,并以限定的速度出水,当水来得过猛而出水不够快时就会导致水直接溢出,即拒绝服务。
可以看出,漏桶算法可以很好的控制流量的访问速度,一旦超过该速度就拒绝服务。
Nginx按请求速率限速模块使用的是漏桶算法,即能够强行保证请求的实时处理速度不会超过设置的阈值。
需要注意的是,在某些情况下,漏桶算法不能够有效地使用网络资源,因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。
应用层面可以使用计数器限流
CDN加速的原理
常用负载均衡算法
例如nginx作为接口接入层主要有以下的几种算法
轮询
带权轮询
这种算法用得比较多。
1 | const ServerWeight = { |
以上的代码可以用爬楼梯来形象理解:有些人一次能走3步,但是有些人一次只能走一步,每次取走在前面的同学,走得最快的人被取到的次数越多。
IP哈希
一个非常大的缺陷:当一个IP作为一个学校的公网出口的时候,命中到后台的服务器的流量可能非常大,并不是一种均匀的策略,比较少使用。
架构设计原则
- 减少第三方依赖,同时自身服务部署也要做到隔离
- 压测、降级、限流方案、确保核心服务可用
- 需要健康度检查机制(分布式环境中及时摘除有问题的节点),整个链路避免单点
- 缩短请求访问路径、减少 IO
- 减少接口数、降低吞吐数据量、减少请求次数
秒杀服务核心实现
- 满足基本需求,做到单服务的极致性能
- 请求链路优化,从客户端到服务器每层的优化(流量是一个逐层衰减的过程)
扣库存的方案
方案一:其中创建订单、扣库存在一个事务中,一个订单肯定会减一个库存,不会超卖,但是可能存在恶意下单但是不支付,这样商品就卖不出去了
方案二:支付和扣库存放在一个事务中,可以有效解决方案一中的订单超卖问题,但是可能存在订单超卖的问题,有一部分订单必然是支付不了的
方案三:这种方案并发量更大,因为扣库存相对来讲是一个比较简单耗时少的操作,可以尽早拦截无效请求。设置一个订单超时,如果不支付的话订单自动取消
去IO之后的减库存直接修改的是内存,并且推送到MQ中的数据只是扣库存成功的用户。性能非常高,IO非常低,并且可以解耦创建订单和减库存。
单机器如果扛不住请求(即使是内存也有QPS上限)可以把库存分摊到多台服务器。
入上图所示,假设有1000个库存,可以10台机器各分配100个库存,通过接入层进行负载均衡。即使客户端来了100W的请求,均摊到每个减库存的服务每个处理10W的量,最后流入MQ中的QPS最多为1000。大部分流量被拦截了。这种方式肯定不会超卖,但是可能存在减库存的时候挂了导致少卖。我们可以采用如下的方式:
统一库存如果为1000,有10个节点的本地库存,这10个节点库存总数必须大于1000,最理想的状态是,某个节点挂掉后,其它9个节点的库存加起来还能等于1000,如果没有节点挂点,所有的请求在统一库存那里也会被合理拦截,最终控制在1000,本地库存主要是为了减轻库存查询压力,大于等于实际的库存即可,但是不能超过太多。
对于这样的恶意用户可以记录下来,让其在以后的秒杀中丧失资格。
- 初始化库存到本地
- 本地减库存,成功则进行统一减库存(相当于做了一个汇总,防止少卖和超卖),失败则返回
- 统一减库存(边界情况下超卖可以用redis lua脚本解决)成功才写入MQ,异步创建订单
- 告知用户抢购成功
抢购进度查询的实现
假设成功创建订单的有1000个用户。维护一个容量为1000的数组记为A和一个容量为1000的哈希表记为B。
- 数组A存储排队中,待创建订单的用户,哈希表B存储uid对应的在A中的索引
- 每次从数组A中依次消费数据,并记录最近的消费索引值X
- 当用户来查排队进度的时候从哈希表B中取出该uid对应的存储索引值Y
- Y - X即为排队进度