电商网站秒杀活动
秒杀活动通常是这样定义:活动方在有限的时间段内(通常是M分钟到H小时不等的时间)给出指定数量O个P商品的大减价抢购名额。
这类秒杀活动一般都会出现如下情况↓↓
第一、在某一时间内QPS超过系统负载;
第二、架构不合理导致系统的其它与秒杀活动不相关的模块变得异常缓慢;
第三、少数用户重复抢到名额;
第四、最终抢到的名额数量超过库存数量;
第五、服务器宕机后恢复迟缓导致大量用户流入竞争对手的网站;
第六、机器人流量占用了网站访问导致真实用户访问迟缓。
解决方案都是人想出来的,只是时间问题罢了。
解决方案背景:LNMP技术栈
第一、六个问题:
-
舍即是得:既然指定时间内,秒杀活动的QPS达到峰值Peak1,那么在秒杀活动并发测试的时候我们应该首先得到这个值得平均范围,然后取其中的极小值(min),这样就可以通过nginx的ngx_http_limit_req_module和ngx_http_limit_conn_module两个模块来限制,nginx的配置如下:
http {
#geot和map两段用于处理限速白名单,map段映射名单到$limit,处于geo内的IP将被映射为空值,否则为其IP地址。
#limit_conn_zone和limit_req_zone指令对于键为空值的将会被忽略,从而实现对于列出来的IP不做限制
geo $whiteiplist {
default 1;
127.0.0.1 0;
121.199.16.249 0;
}
map $whiteiplist $limit {
1 $binary_remote_addr;
0 "";
}
#limit_conn_zone定义每个IP的并发连接数量
#设置一个缓存区保存不同key的状态,大小10m。使用$limit来作为key,以此限制每个源IP的链接数
limit_conn_zone $limit zone=perip:10m;
#limit_req_zone定义每个IP的每秒请求数量
#设置一个缓存区reqps保存不同key的状态,大小10m。这里的状态是指当前的过量请求数。
#$limit为空值则不限速,否则对应的IP进行限制每秒5个连接请求。
limit_req_zone $limit zone=reqps:10m rate=5r/s;
server {
listen 80;
server_name http://www.yoururl.com;
#只对PHP的秒杀页面的请求进行限速
location ~ [^/]miaosha\.php(/|$)
{
#对应limit_conn_zone块
#限制每IP的PHP页面请求并发数量为5个
limit_conn perip 5;
#对应limit_req_zone块
#限制每IP的每秒的PHP页面请求次数为上面定义的rate的值:每秒5个请求,不延迟
limit_req zone=reqps nodelay;
}
}
}
上面的这段nginx配置其实是对单个IP进行限制,效果是有的,但不够明显。
2.过滤无效请求:
前端生成签名字符串,例如通过crypto.js,对当前unix时间戳time,产生随机字符串nonce,还有一个key必须是用户填写好验证码后主机返回给浏览器客户端一个token名称的cookie字段值(有一个过期时间)结合混淆算法生成的,最后然后经过自定义的签名算法在前端生成签名字符串signature,最后在发送抢购表单时带上以上4个字段信息,当请求到达nginx之后,我们使用nginx的lua模块编写lua脚本验证signature的正确性,并且限定以上token的过期时间为30秒,且客户端返回过来的time参数必须跟服务器的unix时间戳相差不超过5秒钟,否则直接在nginx的lua层面上直接屏蔽掉该请求,这里面就不得不说Openresty技术了,感兴趣的小伙伴可以去深入研究一下。
3.概率性丢弃超负载的请求:
既然我们已经在前期并发测试的时候获得了一个峰值参数PeakMin,我们应该尽量保证所有的有效秒杀请求不大于这个值,首先我们得获得当前nginx的总连接数CurrentConnectionCount,当QPS达到PeakMin的时候,我们测算出来的连接数是PeakMinConnectionCount,那么我们使用nginx的lua模块获取这个值,在系统负载达到0.8*PeakMinConnectionCount的时候,我们就对超出的部分90%的丢弃率,返回一个未能秒杀中的提示,并把用户对此次活动的秒杀结果写入memcached缓存进行记录,当系统负载达到PeakMinConnectionCount时,我们直接100%丢弃请求,前端根据状态码是5XX来给出用户未能秒杀中的消息提示,当然我想说的是这里必须保证用户的体验是正常的。
第二个问题:
-
分功能模块设计系统:
一个成熟的电商系统,一般会分成很多相对独立的模块,比如产品中心,会员中心,订单中心,物流中心,配置中心,搜索中心等大模块,这些大模块之间的库表数据通常是低耦合的,因此还可以把这些大模块分割成很多子功能模块,这样就可以让整个电商系统的模块彼此的影响最大化缩小,其中的分布式服务端架构包含了很多架构实践,在这里就不细讲了。
第三个问题:
1.缓存key原子验证:
同一个用户重复抢到名额这个问题比较简单,最有可能是用户(机器人程序)在非常短的时间内(假设是0.01秒)提交了2次以上的并发请求,以ProductId+ActivityId+UserId命名的key写入用户成功秒杀的记录值,利用memcached的add原子性来写入信息,如果add出错则证明已经add过一次,那就返回。
第四个问题:
1.乐观锁:
memcached有一个很不错的CAS检查机制,就是二话不说我先抢到一个一个名额,到真的要保存的数据的时候我再看看CAS值是否跟一开始的时候一样,如果不一样就不操作返回没有秒杀到的消息提示,否则就减掉一个有效秒杀名额,直到保存秒杀库存的key为0即止。
第五个问题:
1.冷热多备份:
不管是应用服务器,缓存服务器,数据库服务器,消息队列服务器等,都应该有自己的多备份,尤其是数据库服务器与缓存服务器更是直接影响了系统数据层面的东西,有条件的还需要做好异地多活,多数据中心等架构设施。
2.自动化运维:
多使用批量管理与配置工具,例如ansible,docker等技术,这里面包含的学问比较多,本人对这一块的技术实践也掌握不够,需要不断磨练啊。