在过去的工做中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程当中,整个Web系统遇到了不少的问题和挑战。若是Web系统不作针对性的优化,会垂手可得地陷入到异常状态。咱们如今一块儿来讨论下,优化的思路和方法哈。html
1. 请求接口的合理设计前端
一个秒杀或者抢购页面,一般分为2个部分,一个是静态的HTML等内容,另外一个就是参与秒杀的Web后台请求接口。redis
一般静态HTML等内容,是经过CDN的部署,通常压力不大,核心瓶颈实际上在后台请求接口上。这个后端接口,必须可以支持高并发请求,同时,很是 重要的一点,必须尽量“快”,在最短的时间里返回用户的请求结果。为了实现尽量快这一点,接口的后端存储使用内存级别的操做会更好一点。仍然直接面向 MySQL之类的存储是不合适的,若是有这种复杂业务的需求,都建议采用异步写入。数据库
固然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才能够从页面中看到用户是否秒杀成功。可是,这种属于“偷懒”行为,同时给用户的体验也很差,容易被用户认为是“暗箱操做”。后端
2. 高并发的挑战:必定要“快”浏览器
咱们一般衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标很是关键。举个例子,咱们假设处理一个业务请求平均响应时间为100ms,同时, 系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大链接数目)。缓存
那么,咱们的Web系统的理论峰值QPS为(理想化的计算方式):安全
20*500/0.1 = 100000 (10万QPS)服务器
咦?咱们的系统彷佛很强大,1秒钟能够处理完10万的请求,5w/s的秒杀彷佛是“纸老虎”哈。实际状况,固然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增长。网络
就Web服务器而言,Apache打开了越多的链接进程,CPU须要处理的上下文切换也越多,额外增长了CPU的消耗,而后就直接致使平均响应时间 增长。所以上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。能够经过Apache自带的abench来测试一 下,取一个合适的值。而后,咱们选择内存操做级别的存储的Redis,在高并发的状态下,存储的响应时间相当重要。网络带宽虽然也是一个因素,不过,这种 请求数据包通常比较小,通常不多成为请求的瓶颈。负载均衡成为系统瓶颈的状况比较少,在这里不作讨论哈。
那么问题来了,假设咱们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际状况,甚至更多):
20*500/0.25 = 40000 (4万QPS)
因而,咱们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。
而后,这才是真正的恶梦开始。举个例子,高速路口,1秒钟来5部车,每秒经过5部车,高速路口运做正常。忽然,这个路口1秒钟只能经过4部车,车流量仍然依旧,结果一定出现大塞车。(5条车道突然变成4条车道的感受)
同理,某一个秒内,20*500个可用链接进程都在满负荷工做中,却仍然有1万个新来请求,没有链接进程可用,系统陷入到异常状态也是预期以内。
其实在正常的非高并发的业务场景中,也有相似的状况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用链接数占满,其余正常的业务请求,无链接进程可用。
更可怕的问题是,是用户的行为特色,系统越是不可用,用户的点击越频繁,恶性循环最终致使“雪崩”(其中一台Web机器挂了,致使流量分散到其余正常工做的机器上,再致使正常的机器也挂,而后恶性循环),将整个Web系统拖垮。
3. 重启与过载保护
若是系统发生“雪崩”,贸然重启服务,是没法解决问题的。最多见的现象是,启动起来后,马上挂掉。这个时候,最好在入口层将流量拒绝,而后再将重启。若是是redis/memcache这种服务也挂了,重启的时候须要注意“预热”,而且极可能须要比较长的时间。
秒杀和抢购的场景,流量每每是超乎咱们系统的准备和想象的。这个时候,过载保护是必要的。若是检测到系统满负载状态,拒绝请求也是一种保护措施。在 前端设置过滤是最简单的方式,可是,这种作法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。
秒杀和抢购收到了“海量”的请求,实际上里面的水分是很大的。很多用户,为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽可 能多的请求到服务器。还有一部分高级用户,制做强大的自动请求脚本。这种作法的理由也很简单,就是在参与秒杀和抢购的请求中,本身的请求数目占比越多,成功的几率越高。
这些都是属于“做弊的手段”,不过,有“进攻”就有“防守”,这是一场没有硝烟的战斗哈。
1. 同一个帐号,一次性发出多个请求
部分用户经过浏览器的插件或者其余工具,在秒杀开始的时间里,以本身的帐号,一次发送上百甚至更多的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。
这种请求在某些没有作数据安全处理的系统里,也可能形成另一种破坏,致使某些判断条件被绕过。例如一个简单的领取逻辑,先判断用户是否有参与记 录,若是没有则领取成功,最后写入到参与记录中。这是个很是简单的逻辑,可是,在高并发的场景下,存在深深的漏洞。多个并发请求经过负载均衡服务器,分配 到内网的多台Web服务器,它们首先向存储发送查询请求,而后,在某个请求成功写入参与记录的时间差内,其余的请求获查询到的结果都是“没有参与记录”。 这里,就存在逻辑判断被绕过的风险。
应对方案:
在程序入口处,一个帐号只容许接受1个请求,其余请求过滤。不只解决了同一个帐号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现方案, 能够经过Redis这种内存缓存服务,写入一个标志位(只容许1个请求写成功,结合watch的乐观锁的特性),成功写入的则能够继续参加。
或者,本身实现一个服务,将同一个帐号的请求放入一个队列中,处理完一个,再处理下一个。
2. 多个帐号,一次性发送多个请求
不少公司的帐号注册功能,在发展早期几乎是没有限制的,很容易就能够注册不少个帐号。所以,也致使了出现了一些特殊的工做室,经过编写自动注册脚 本,积累了一大批“僵尸帐号”,数量庞大,几万甚至几十万的帐号不等,专门作各类刷的行为(这就是微博中的“僵尸粉“的来源)。举个例子,例如微博中有转 发抽奖的活动,若是咱们使用几万个“僵尸号”去混进去转发,这样就能够大大提高咱们中奖的几率。
这种帐号,使用在秒杀和抢购里,也是同一个道理。例如,iPhone官网的抢购,火车票黄牛党。
应对方案:
这种场景,能够经过检测指定机器IP请求频率就能够解决,若是发现某个IP请求频率很高,能够给它弹出一个验证码或者直接禁止它的请求:
3. 多个帐号,不一样IP发送不一样请求
所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工做室”,发现你对单机IP请求频率有控制以后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断改变IP。
有同窗会好奇,这些随机IP服务怎么来的。有一些是某些机构本身占据一批独立IP,而后作成一个随机代理IP的服务,有偿提供给这些“工做 室”使用。还有一些更为黑暗一点的,就是经过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运做,只作一件事情,就是转发IP包,普通用户的电 脑被变成了IP代理出口。经过这种作法,黑客就拿到了大量的独立IP,而后搭建为随机IP服务,就是为了挣钱。
应对方案:
说实话,这种场景下的请求,和真实用户的行为,已经基本相同了,想作分辨很困难。再作进一步的限制很容易“误伤“真实用户,这个时候,一般只能经过设置业务门槛高来限制这种请求了,或者经过帐号行为的”数据挖掘“来提早清理掉它们。
僵尸帐号也仍是有一些共同特征的,例如帐号极可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特色,适当设置参与门槛,例如限制参与秒杀的帐号等级。经过这些业务手段,也是能够过滤掉一些僵尸号。
4. 火车票的抢购
看到这里,同窗们是否明白你为何抢不到火车票?若是你只是老老实实地去抢票,真的很难。经过多帐号的方式,火车票的黄牛将不少车票的名额占据,部分强大的黄牛,在处理验证码方面,更是“技高一筹“。
高级的黄牛刷票时,在识别验证码的时候使用真实的人,中间搭建一个展现验证码图片的中转软件服务,真人浏览图片并填写下真实验证码,返回给中转软件。对于这种方式,验证码的保护限制做用被废除了,目前也没有很好的解决方案。
由于火车票是根据身份证明名制的,这里还有一个火车票的转让操做方式。大体的操做方式,是先用买家的身份证开启一个抢票工具,持续发送请 求,黄牛帐号选择退票,而后黄牛买家成功经过本身的身份证购票成功。当一列车箱没有票了的时候,是没有不少人盯着看的,何况黄牛们的抢票工具也很强大,即 使让咱们看见有退票,咱们也不必定能抢得过他们哈。
最终,黄牛顺利将火车票转移到买家的身份证下。
解决方案:
并无很好的解决方案,惟一能够动心思的也许是对帐号数据进行“数据挖掘”,这些黄牛帐号也是有一些共同特征的,例如常常抢票和退票,节假日异常活跃等等。将它们分析出来,再作进一步处理和甄别。
咱们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,若是每次运行结果和单线程运行的结果是一 样的,结果和预期相同,就是线程安全的)。若是是MySQL数据库,可使用它自带的锁机制很好的解决问题,可是,在大规模并发的场景中,是不推荐使用 MySQL的。秒杀和抢购的场景中,还有另一个问题,就是“超发”,若是在这方面控制不慎,会产生发送过多的状况。咱们也曾经据说过,某些电商搞抢购活 动,买家成功拍下后,商家却不认可订单有效,拒绝发货。这里的问题,也许并不必定是商家奸诈,而是系统技术层面存在超发风险致使的。
1. 超发的缘由
假设某个抢购场景中,咱们一共只有100个商品,在最后一刻,咱们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,而后都经过了这一个余量判断,最终致使超发。(同文章前面说的场景)
在上面的这个图中,就致使了并发用户B也“抢购成功”,多让一我的得到了商品。这种场景,在高并发的状况下很是容易出现。
2. 悲观锁思路
解决线程安全的思路不少,能够从“悲观锁”的方向开始讨论。
悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。
虽然上述的方案的确解决了线程安全的问题,可是,别忘记,咱们的场景是“高并发”。也就是说,会不少这样的修改请求,每一个请求都须要等待 “锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会不少,瞬间增大系统的平均响应时间,结果是可用链接数被耗 尽,系统陷入异常。
3. FIFO队列思路
那好,那么咱们稍微修改一下上面的场景,咱们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,咱们就不会致使某些请求永远获取不到锁。看到这里,是否是有点强行将多线程变成单线程的感受哈。
而后,咱们如今解决了锁的问题,所有请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,由于请求不少,极可能一瞬 间将队列内存“撑爆”,而后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,可是,系统处理完一个队列内请求的速度根本没法和疯狂涌 入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候仍是会大幅降低,系统仍是陷入异常。
4. 乐观锁思路
这个时候,咱们就能够讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号 (Version)更新。实现就是,这个数据全部请求都有资格去修改,但会得到一个该数据的版本号,只有版本号符合的才能更新成功,其余的返回抢购失败。 这样的话,咱们就不须要考虑队列的问题,不过,它会增大CPU的计算开销。可是,综合来讲,这是一个比较好的解决方案。
有不少软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。经过这个实现,咱们保证了数据的安全。
互联网正在高速发展,使用互联网服务的用户越多,高并发的场景也变得愈来愈多。电商秒杀和抢购,是两个比较典型的互联网高并发场景。虽然咱们解决问题的具体技术方案可能千差万别,可是遇到的挑战倒是类似的,所以解决问题的思路也殊途同归。