秒杀系统架构设计-终章

服务器

浏览数:8

2020-6-29

AD:资源代下载服务

流量削峰怎么做

秒杀请求在时间上高度集中于某一特定的时间点。这样一来,就会导致一个特别高的流量峰值,它对资源的消耗是瞬时的。

但是秒杀场景,最终能抢到商品的人数是固定的,也就是说100和10000人发起请求结果是一样的,并发度越高,无效请求也越多。

但是从业务上来说,秒杀活动是希望更多的人来参与的,也就是开始之前希望有更多的人来刷页面,开始下单时,可以设计一些规则,让并发的请求更多地延缓,过滤掉一些无效请求。

为什么要削峰

服务器的处理资源是恒定的,出现峰值的话,很容易处理不过来,闲的时候却又没有什么要处理。但是为了保证服务质量,很多处理资源只能按照最忙的时候来预估,而这会导致资源的一个浪费。

针对秒杀这一场景,削峰从本质上来说就是更多地延缓用户请求的发出,遵从“请求数要尽量少”的原则。

削峰的一些操作思路:熔断算法、答题、分层过滤。

分层过滤的核心思想是:在不同的层次尽可能的过滤掉无效请求,让“漏斗”最末端的才是有效请求。而要达到这种效果,我们就必须对数据做分层的校验。分层校验的基本原则是:

  1. 将动态请求的读数据缓存在WEB端,过滤掉无效的数据读
  2. 对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题
  3. 对写数据进行基于时间的合理分片,过滤掉过期的失效请求
  4. 对写请求做限流保护,将超出系统承载能力的请求过滤掉
  5. 对写数据进行强一致性校验,只保留最后有效的数据

分层校验的目的是:在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性。

影响性能的因素

服务端性能一般用QPS(Query Per Second,每秒请求数)来衡量,还有一个影响和QPS也息息相关,那就是响应时间(Response Time ,RT),它可以理解为服务器处理响应的耗时。

正常情况下响应时间(RT)越短,一秒钟处理的请求数(QPS)自然也就会越多

总QPS = (1000ms /响应时间)* 线程数量,这样性能就和两个因素相关,一个是一次响应服务端耗时,一个是处理请求的线程数

对于大部分的Web系统而言,响应时间一般都是由CPU执行时间和线程等待时间(比如RPC、IO等待、Sleep、Wait等)组成,即服务器在处理一个请求时,一部分是CPU本身在做运算,还有一部分是在各种等待。

线程数不是越多越好,因为线程本身也消耗资源。例如线程越多系统的线程切换成本就会越高,而且每个线程也都会耗费一定内存。

一般默认配置,即“线程数=2*CPU核数+1”,一个根据最佳实践出来的公式:

线程数=【(线程等待时间+线程CPU时间)/线程CPU时间】*CPU数量

如何发现瓶颈

就服务器而言,会出现瓶颈的地方很多,例如CPU、内存、磁盘以及网络等都可能会导致瓶颈。此外,不同的系统对瓶颈的关注度也不一样,例如对缓存系统而言,制约它的是内存,而对存储型系统来说I/O更容易是瓶颈。我们定位的场景是秒杀,它的瓶颈更多地发生在CPU上

CPU诊断工具可以发现CPU的消耗,最常用的就是JProfiler和Yourkit这两工具,它们可以列出整个请求中每个函数的CPU执行时间,可以发现那个函数消耗的CPU时间最多,以便你有针对性地做优化。还可以通过jstack定时地打印调用栈,如果某些函数调用频繁或者耗时较多,那么那些函数就会多次出现在系统调用栈里,这样相当于采样的方式也能够发现耗时较多的函数。

简单判断CPU是不是瓶颈?一个办法就是看当QPS达到极限时,你的服务器的CPU使用率是不是超过了95%,如果没有超过,那么表示CPU还有提升的空间,要么是有锁限制,要么是有过多的本地I/O等待发生。

如何优化系统

  1. 减少编码
  2. 减少序列化
  3. java极致优化

    1. 直接使用Servlet处理请求。避免使用传统的MVC框架,这样可以绕过一大堆复杂且用户不大的处理逻辑,节省1ms时间
    2. 直接输出流数据。使用resp.getOutputStream()而不是resp.getWriter()函数,可以省掉一些不变字符数据的编码,从而提升性能;数据输出时推荐使用JSON而不是模板引擎来输出页面
  4. 并发读优化

    1. 采用应用层LocalCache,即在秒杀系统的单机上缓存商品相关的数据。需要将数据划分成动态数据和静态数据分别进行处理:

      1. 像商品中“标题”和“描述”这些本身不变的数据,会在秒杀开始之前全量推送到秒杀机器上,并一直缓存到秒杀结束;
      2. 像库存这类动态数据,会采用“被动失效”的方式缓存一定时间(一般数秒),失效后再去缓存拉去最新的数据。

库存这种频繁更新的数据,一旦数据不一致,会不会导致超卖?

这就要用到前面介绍的读数据的分层校验原则,读的场景可以允许一定的脏数据,因为这里的误判只会导致少量原本无库存的下单请求被误认为有库存,可以等到真正写数据时再保证最终一致性,通过在数据的高可用性和一致性之间平衡,来解决高并发的数据读取问题。

总结:首先是发现短板,其次是减少数据,两个地方特别影响性能,一是服务端在处理数据时不可避免地存在字符到字节的相互转化,二是HTTP请求时要做Gzip压缩,还有网络传输的耗时。再次,就是数据分级,也就是要保证首屏为先、重要信息为先,次要信息则异步加载,以这种方式提升用户获取数据的体验。最后就是减少中间环节,减少字符到字节的转换,增加预处理(提前做字符到字节的转换)去掉不需要的操作。此外,要做好优化,你还需啊做好应用基线,比如性能基线(何时性能突然下降)、成本基线(活动用了多少台机器)、链路基线(我们的系统发生了那些变化),你可以通过这些基线持续关注系统的性能,做到在代码上提升编码质量,在业务上改掉不合理的调用,在架构和调用链路上不断的改进。

秒杀系统架构设计-减库存

减库存有哪几种方式

用户实际购买流程一般分为两步:下单和支付。我们选择在哪个阶段减少库存是个问题

减库存操作一般有如下几个方式:

  • 下单减库存
  • 付款减库存
  • 预扣库存

这3种方式都会有一些问题:

  1. 下单减库存:竞争对手通过恶意下单的方式将改卖家的商品全部下单,那么这款商品就不能正常售卖了。
  2. 付款减库存:可能会出现买家成功下单,但是支付的时候提示库存不足,影响用户体验
  3. 预扣库存:下单以后预扣库存,设置超时时间,在超时后,竞争对手还是可以再次下单

针对这种情况,解决办法还是要结合安全和反作弊的措施来制止。例如,给经常下单不付款的卖家进行识别打标、给某些类目设置最大购买件数、以及对重复下单不付款的操作进行次数限制。

秒杀库存的极致优化:

秒杀商品和普通商品的减库存还是有些差异的,例如商品数量比较少,交易时间段也比较短,我们可以大胆的用缓存来保存库存,进行操作。

如果必须使用数据库减少库存,由于mysql在处理同一个数据的时候会有大量线程来竞争InnoDB行锁,而并发度越高时等待线程会越多,TPS(Transaction Per Second,每秒处理的消息数)会下降,RT(响应时间)会上升,数据库的吞吐量就会严重受影响。

这时候单个热点商品会影响整个数据库的性能,导致0.01%的商品影响99.99%的商品的售卖,这是我们不愿意看到的。一个思路是进行隔离,将热点商品放到单独的热点库中。但是维护上会比较麻烦,比如要做热点数据的动态迁移以及单独的数据库等。

而分离热点数据,仍然没有解决并发锁的问题,要解决并发锁的问题,我们可以

  • 应用层排队。按照商品维度设置队列顺序执行,这样能减少同一台机器对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用太多的数据库连接
  • 数据库层排队。应用层只能做到单机的排队,但是应用机器数本身很多,这种排队方式控制并发的能力仍然有限,所以在数据库层做全局排队最理想。可以使用patch,在数据库层对单行记录做到并发排队。

排队和锁竞争都是要等待,但是有区别。mysql的innodb内部的死锁检测,以及mysql server和innodb的切换会比较消耗性能。

准备plan b,兜底方案

高可用建设需要考虑到系统建设的各个阶段,也就是说它其实贯穿了系统建设的整个生命周期。

  1. 架构阶段:架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题。
  2. 编码阶段:编码最重要的是保证代码的健壮性,例如涉及远程调用问题时,要设置合理的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有逾期,防止返回的结果超出程序处理范围,最常见的做法就是对错误异常进行捕获,对无法预料的错误要有默认处理结果。
  3. 测试阶段:测试主要是保证测试用例的覆盖度,保证最坏情况发生时,我们也有响应的处理流程。
  4. 发布阶段:发布出现错误,要有紧急的回滚机制
  5. 运行阶段:运行时是系统的常态,系统大部分时间都会处于运行态,运行态最重要的是对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排查问题。
  6. 故障发生:故障发生时首先最重要的就是及时止损,例如由于程序问题导致商品价格错误,那就要及时下架商品或者关闭购买链接,防止造成重大资产损失。然后要能够及时恢复服务,并定位原因解决问题。

针对秒杀系统,我们在遇到大流量时,应该从哪些方面来保障系统的稳定运行,更多的是看针对运行阶段进行处理。

  • 降级,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。
  • 限流,就是限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
  • 拒绝服务,当系统负载达到一定阈值时,例如CPU使用率达到90%或者系统load值达到2*CPU核数时,系统直接拒绝所有请求。可以在nginx上设置过载保护,这是最后的兜底方案。

作者:JlDang