大家好,我是Leo
之前我们介绍了秒杀系统的五大原则,动静分离方案,二八原则,冷热处理的一些理论方案。今天我们接着上一篇,继续介绍一下大并发流量打过来之后,我们如何做一些削峰处理以及服务端的一些优化技巧。
推荐阅读
3万字聊聊什么是Redis(完结篇)
3万字聊聊什么是MySQL(初篇)
2万字聊聊什么是秒杀系统(上)
首先我们说一下为什么要削峰,不削峰会有什么影响呢?
类似于公交车一样,早高峰,晚高峰是上车人最多的时候,除了这两个时间段白天运营的时候人是比较少的。这就导致了资源浪费。
我见过这么一种情况,每周五中学放假的时候,我们县城的中巴车在某一个站点拉不过来了,就会调度其他站点的中巴车去支援爆满的这个站点。如果不调度的话就会影响用户体验和中巴车超载出现意外风险。
系统也是一样的,如果按照最大量配置的话,服务器的资源就是一笔非常大的开销。而且削峰的话也是对服务器处理更加平稳。下面我们主要从排队,答题,分层过滤这几种方案梳理一下
降级,限流和拒绝服务也算是削峰的一种,在下面的高可用中会介绍的。
利用排队的思想,我们最先想到的肯定还是消息队列了。与其把流量都堆积在服务端,不如把流量都打到消息队列,根据服务端的处理能力不断的消费消息队列中的数据。达到处理并发的能力。
利用消息队列的确可以缓解很多流量,但是以双十一那种超大并发流量,积压达到了存储空间的上限,我想消息队列也有宕机的情况。
除了消息队列,类似的方法还有很多
其实很多时候,处理方案大差不差,就类似于MySQL的binlog机制,就是把一步操作变成两步操作。增加的那一步操作就起到了缓存的作用。
答题我们可以理解成验证码,一般秒杀活动主要针对于稀缺物品。比如原价1000元的,现在只买100元。只出售100件。如果是正常用户还好,就是怕那种秒杀器恶意抢票或者抢东西之类的。
采用答题机制,主要解决了两类问题
原本 1秒下单购买就够了,现在把请求延迟到2-3秒这也是流量削峰的一种手段。
这样一来,请求峰值基于时间分片了。这个时间的分片对服务端处理并发非常重要,会大大减轻压力。而且,由于请求具有先后顺序,靠后的请求到来时自然也就没有库存了,因此根本到不了最后的下单步骤,所以真正的并发写就非常有限了。这种设计思路目前用得非常普遍,如当年支付宝的“咻一咻”、微信的“摇一摇”都是类似的方式
分层过滤就好比漏斗一样,我们应一层一层筛选,CDN => 前端 => 后端 => DB
过滤了之后,最终到DB之后的量是比原来小很多的。
分层过滤的核心思想是:在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请求。而要达到这种效果,我们就必须对数据做分层的校验。
分层的校验的基本原则:
将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读;
对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
对写请求做限流保护,将超出系统承载能力的请求过滤掉;
对写数据进行强一致性校验,只保留最后有效的数据。
分层校验的目的是: 在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。
聊到服务端优化,我们可以先从影响服务端的性能入手
我们一般用 QPS
和 RT
来衡量服务端性能
QPS:每秒请求数
RT:响应时间
正常情况下 RT
越短,QPS
自然也就会越多,这在单线程处理的情况下看起来是线性的关系,即我们只要把每个请求的响应时间降到最低,那么性能就会最高。
提到了单线程肯定要聊多线程,通过多线程,来处理请求。这样理论上就变成了“总 QPS =(1000ms / 响应时间)× 线程数量”,这样性能就和两个因素相关了,一个是一次响应的服务端耗时,一个是处理请求的线程数。下面分别介绍一下两者的关系。
首先,我们先来看看响应时间和 QPS 有啥关系。
对于大部分的 Web 系统而言,响应时间一般都是由 CPU 执行时间和线程等待时间(比如 RPC、IO 等待、Sleep、Wait 等)组成,即服务器在处理一个请求时,一部分是 CPU 本身在做运算,还有一部分是在各种等待。
真正对性能有影响的是 CPU 的执行时间。 因为 CPU 的执行真正消耗了服务器的资源。经过实际的测试,如果减少 CPU 一半的执行时间,就可以增加一倍的 QPS。也就是说,我们应该致力于减少 CPU 的执行时间。
其次,我们再来看看线程数对 QPS 的影响。
单看总QPS计算公式,你会觉得线程数越多 QPS 也就会越高。其实不是这样的,线程不是越多越好,线程越多,CPU的调度成本就会越高,而且每个线程也都会耗费一定内存。
总 QPS =(1000ms / 响应时间)× 线程数量
那么到底设计多少个线程合适呢?可以参考下列计算公式。
默认配置:线程数 = 2 * CPU 核数 + 1
实践后:线程数 = [(线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间] × CPU 数量
最好的办法是通过性能测试来发现最佳的线程数。要提升性能我们就要减少 CPU 的执行时间,另外就是要设置一个合理的并发线程数,通过这两方面来显著提升服务器的性能。
了解了如何快速提升性能,下面我们聊一下应该怎么发现系统那里最消耗CPU资源呢?
对服务器来说,CPU,内存,磁盘,网络都有可能影响瓶颈。我们大方向是秒杀,它的瓶颈更多地发生在CPU上。
知道了哪些瓶颈下面就开始优化了
对 Java 系统来说,可以优化的地方很多,这里我重点说一下比较有效的几种手段,供你参考,它们是:减少编码、减少序列化、Java 极致优化、并发读优化。接下来,我们分别来看一下。
第三点直接返回的好处是 减少数据的序列化与反序列化
第四点我们把库存放入缓存,就会涉及到缓存,数据库数据不一致情况导致超卖。这里就要用到分层校验原则了,读可以读但是写的时候就必须限制啦。
电商减库存场景最严重的问题就是超卖啦。第一次接触秒杀的话可能觉得只要判断为0就停止不就好了嘛,其实没那么简单。
下单和付款这是两个阶段。产生的影响是 下单扣库存还是付款扣库存呢?
如果下单就扣,万一不付款呢?
如果付款再扣,那客户已经下单成功了,付款失败了体验就变差了!
如果下单减库存 ,正常用户还好。如果不是正常用户的话就会占着茅坑不拉屎。秒杀黄金期间过后再申请退款。这样的活动就没啥意义了。
如果付款减库存 ,假如100件商品,可能出现300件购买成功,因为下单时不会减库存,所以也就可能出现下单成功数远远超过真正库存数的情况,这尤其会发生在做活动的热门商品上。这样一来,就会导致很多买家下单成功但是付不了款,买家的购物体验自然比较差。
如果采用预扣库存 ,这也是当下比较流行的一种方式,我们可以给他设置一个过期时间,一旦到期后不付款就会自动恢复库存。这种方案确实可以在一定程度上缓解上面的问题。但是存在另外一种情况。心怀不轨的人下单10分钟后,再次下单,这样非法人员就会一直占用库存名额。
针对预扣库存的问题,我们可以给经常下单不付款的买家进行识别打标(可以在被打标的买家下单时不减库存)、给某些类目设置最大购买件数(例如,参加活动的商品一人最多只能买 3 件),以及对重复下单不付款的操作进行次数限制等。
针对“库存超卖”这种情况,在 10 分钟时间内下单的数量仍然有可能超过库存数量,遇到这种情况我们只能区别对待:对普通的商品下单数量超过库存数量的情况,可以通过补货来解决;但是有些卖家完全不允许库存为负数的情况,那只能在买家付款时提示库存不足。
在大并发场景中,下单减库存比预扣库存,付款减库存更优一些,主要是逻辑更简单,速度更快。
“下单减库存” 在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般我们有多种解决方案:一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错;再有一种就是使用 CASE WHEN 判断语句,例如这样的 SQL 语句:
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
大并发中库存是个关键数据,也是热点数据,可以通过把库存数据放到缓存中大大提升读性能。
如果秒杀商品的减库存逻辑非常单一,比如没有复杂的 SKU 库存和总库存这种联动关系的话,完全可以利用Redis作为存储库存数据。
如果减库存比较复杂还是老老实实的用MySQL吧,毕竟数据的一致性才是最重要的。
放到MySQL中就会引发另一个问题。同一数据在数据库里肯定是一行存储(MySQL),因此会有大量线程来竞争 InnoDB 行锁,而并发度越高时等待线程会越多,TPS(Transaction Per Second,即每秒处理的消息数)会下降,响应时间(RT)会上升,数据库的吞吐量就会严重受影响。
这就可能引发一个问题,就是单个热点商品会影响整个数据库的性能, 导致 0.01% 的商品影响 99.99% 的商品的售卖。
解决思路: 我们可以采用前面的隔离思想,把这个热点商品单独用一个数据库中。但是这个也有个缺点就是在维护上比较麻烦。比如数据迁移等。
咳咳。迁移到单独的数据库但是还是没有解决并发锁问题呀!要解决并发锁有两种办法:
数据更新问题除了前面介绍的热点隔离和排队处理之外,还有些场景(如对商品的 lastmodifytime 字段的)更新会非常频繁,在某些场景下这些多条 SQL 是可以合并的,一定时间内只要执行最后一条 SQL 就行了,以便减少对数据库的更新操作。
高可用架构主要分两个阶段
前期: 系统架构
中后期: 降级,限流,拒绝服务
下面我们分别介绍一下
做各种防护手段,不如把自己的系统架构设计的优秀一些。我们可以从这几个方面入手
所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。
执行降级无疑是在系统性能和用户体验之间选择了前者,降级后肯定会影响一部分用户的体验,例如在双 11 零点时,如果优惠券系统扛不住,可能会临时降级商品详情的优惠信息展示,把有限的系统资源用在保障交易系统正确展示优惠信息上,即保障用户真正下单时的价格是正确的。所以降级的核心目标是牺牲次要的功能和用户体验来保证核心业务流程的稳定,是一个不得已而为之的举措。
如果说降级是牺牲了一部分次要的功能和用户的体验效果,那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
总体来说,限流既可以是在客户端限流,也可以是在服务端限流。下面我们分别聊一下两种优缺点
在限流的实现手段上来讲,基于 QPS 和线程数的限流应用最多,最大 QPS 很容易通过压测提前获取,例如我们的系统最高支持 1w QPS 时,可以设置 8000 来进行限流保护。线程数限流在客户端比较有效,例如在远程调用时我们设置连接池的线程数,超出这个并发线程请求,就将线程进行排队或者直接超时丢弃。
限流无疑会影响用户的正常请求,所以必然会导致一部分用户请求失败,因此在系统处理这种异常时一定要设置超时时间,防止因被限流的请求不能 fast fail(快速失败)而拖垮系统。
如果限流还不能解决问题,最后一招就是直接拒绝服务了。
当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。例如秒杀系统,我们在如下几个环节设计过载保护:
在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护。
拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。
大概总结了
首先感谢许令波老师的秒杀课 经过了一周的学习,对秒杀系统的理念,思想也有了初步的认识。大概的概念就是这些了,秒杀系列的《2万聊聊什么是秒杀系统(下)》 预计会在半个月后发出。第三篇打算梳理一些代码的优化成果。
电商系统的效果可以通过【公众号】=>【项目经验】=>【跨境电商】查看
非常欢迎大家关注公众号【欢少的成长之路】有关后端方面的问题我们在群内一起讨论! 我们下期再见!