特征与难点

QPS分析

  • 写强一致性:抢购商品的时候不能出现超卖现象
  • 读弱一致性:可能读到有库存,但是实际下单会失败
  • 抢购人数远多于库存,读写并发巨大
  • 库存少导致有效写少(真正形成订单的比较少)

难点在于极致性能的实现以及高可用的保证。

  • 高并发下,某个小依赖可能直接造成雪崩
  • 流量预估难精确,过高也造成雪崩
  • 分布式集群,机器多,出故障的概率高

秒杀原理

  • 合理利用 CDN:例如订单详情页,有效减少回源流量
  • Nginx 限流:过载保护
  • 异步队列:高并发的流量 -> 均匀的流量
  • Nginx 负载均衡:接入层分摊流量到无状态 service 层提供服务

CDN原理

cdn.png

  1. 用户输入访问的域名,操作系统向 LocalDns 查询域名的ip地址.
  2. LocalDns向 ROOT DNS 查询域名的授权服务器(这里假设LocalDns缓存过期)
  3. ROOT DNS将域名授权dns记录回应给 LocalDns
  4. LocalDns得到域名的授权dns记录后,继续向域名授权dns查询域名的ip地址
  5. 域名授权dns查询域名记录后(一般是CNAME),回应给 LocalDns
  6. LocalDns得到域名记录后,向智能调度DNS查询域名的ip地址
  7. 智能调度DNS根据一定的算法和策略(比如静态拓扑,容量等),将最适合的CDN节点ip地址回应给 LocalDns
  8. LocalDns 将得到的域名ip地址,回应给用户端
  9. 用户得到域名ip地址后,访问站点服务器
  10. 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的节点,达到加速效果。

限流算法

令牌桶算法

典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。(因此,如果桶中有足够多的令牌,就可以以峰值流量进行处理,但是长期来看流量速率是恒定的)从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。

rate_limit_token_bucket.png

Google 的 Guava 包中的 RateLimiter 类就是令牌桶算法的实现。

漏桶算法

这种算法可以强制限制数据的传输速率,而令牌桶算法除了能够限制平均传输速率,还允许一定程度的突发传输

漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
如图所示,把请求比作是水,水来了都先放进桶里,并以限定的速度出水,当水来得过猛而出水不够快时就会导致水直接溢出,即拒绝服务。

rate_limit_leak_bucket.png

可以看出,漏桶算法可以很好的控制流量的访问速度,一旦超过该速度就拒绝服务。

Nginx按请求速率限速模块使用的是漏桶算法,即能够强行保证请求的实时处理速度不会超过设置的阈值。

需要注意的是,在某些情况下,漏桶算法不能够有效地使用网络资源,因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。

应用层面可以使用计数器限流

nginx限流配置

CDN加速的原理

CDN加速原理

常用负载均衡算法

例如nginx作为接口接入层主要有以下的几种算法

轮询

带权轮询

这种算法用得比较多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ServerWeight = {
A: 1, // A服务器权重1
B: 3 // B服务器权重3
};

let wA = wB = 0;

function weightRoundRobin() {
wA += ServerWeight.A;
wB += ServerWeight.B;

let target = null;
if (wA > wB) {
target = 'A';
wA -= ServerWeight.A;
} else {
target = 'B';
wB -= ServerWeight.B;
}
return target;
}

以上的代码可以用爬楼梯来形象理解:有些人一次能走3步,但是有些人一次只能走一步,每次取走在前面的同学,走得最快的人被取到的次数越多。

IP哈希

一个非常大的缺陷:当一个IP作为一个学校的公网出口的时候,命中到后台的服务器的流量可能非常大,并不是一种均匀的策略,比较少使用。

架构设计原则

  • 减少第三方依赖,同时自身服务部署也要做到隔离
  • 压测、降级、限流方案、确保核心服务可用
  • 需要健康度检查机制(分布式环境中及时摘除有问题的节点),整个链路避免单点
  • 缩短请求访问路径、减少 IO
  • 减少接口数、降低吞吐数据量、减少请求次数

秒杀服务核心实现

  • 满足基本需求,做到单服务的极致性能
  • 请求链路优化,从客户端到服务器每层的优化(流量是一个逐层衰减的过程)

扣库存的方案

扣库存的方案

方案一:其中创建订单、扣库存在一个事务中,一个订单肯定会减一个库存,不会超卖,但是可能存在恶意下单但是不支付,这样商品就卖不出去了
方案二:支付和扣库存放在一个事务中,可以有效解决方案一中的订单超卖问题,但是可能存在订单超卖的问题,有一部分订单必然是支付不了的
方案三:这种方案并发量更大,因为扣库存相对来讲是一个比较简单耗时少的操作,可以尽早拦截无效请求。设置一个订单超时,如果不支付的话订单自动取消

扣库存的2种实现

去IO之后的减库存直接修改的是内存,并且推送到MQ中的数据只是扣库存成功的用户。性能非常高,IO非常低,并且可以解耦创建订单和减库存。

单机器如果扛不住请求(即使是内存也有QPS上限)可以把库存分摊到多台服务器。

库存分摊到多个节点

入上图所示,假设有1000个库存,可以10台机器各分配100个库存,通过接入层进行负载均衡。即使客户端来了100W的请求,均摊到每个减库存的服务每个处理10W的量,最后流入MQ中的QPS最多为1000。大部分流量被拦截了。这种方式肯定不会超卖,但是可能存在减库存的时候挂了导致少卖。我们可以采用如下的方式:

预留buff统一减库存

统一库存如果为1000,有10个节点的本地库存,这10个节点库存总数必须大于1000,最理想的状态是,某个节点挂掉后,其它9个节点的库存加起来还能等于1000,如果没有节点挂点,所有的请求在统一库存那里也会被合理拦截,最终控制在1000,本地库存主要是为了减轻库存查询压力,大于等于实际的库存即可,但是不能超过太多。

对于这样的恶意用户可以记录下来,让其在以后的秒杀中丧失资格。

  1. 初始化库存到本地
  2. 本地减库存,成功则进行统一减库存(相当于做了一个汇总,防止少卖和超卖),失败则返回
  3. 统一减库存(边界情况下超卖可以用redis lua脚本解决)成功才写入MQ,异步创建订单
  4. 告知用户抢购成功

抢购进度查询的实现

假设成功创建订单的有1000个用户。维护一个容量为1000的数组记为A和一个容量为1000的哈希表记为B。

  1. 数组A存储排队中,待创建订单的用户,哈希表B存储uid对应的在A中的索引
  2. 每次从数组A中依次消费数据,并记录最近的消费索引值X
  3. 当用户来查排队进度的时候从哈希表B中取出该uid对应的存储索引值Y
  4. Y - X即为排队进度

参考链接