1,秒杀场景核心是要保证三点:稳,准,快;对应的技术落地便是高可用,一致性,高性能;
2,架构设计五原则
2.1 数据要尽量少:网络传输耗时,服务器压缩以及字符编码,序列化与反序列化消耗cpu
2.2 请求数要尽量少:页面依赖的 CSS/JavaScript、图片,以及 Ajax,建立连接要做三次握手
2.3 路径要尽量短:缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)
2.4 依赖要尽量少: 完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖,可以给系统进行分级。
2.5 不要有单点:单点意味着没有备份,风险不可控。避免将服务的状态和机器绑定,即把服务无状态化,把和机器相关的配置动态化;存储服务一般要通过冗余多个备份的方式来解决单点问题。
优化的点:秒杀系统单独独立出来;独立做一个机器集群;将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能“ ;增加秒杀答题,防止有秒杀器抢单;页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面;在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台获取;增加系统限流保护,防止最坏情况发生。
3,如何做好动静分离,有哪些方案。
3.1 概念:“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有Cookie 等私密数据。
3.2 怎样对静态数据做缓存:了解了动静数据的概念后,我们很容易想到对静态数据做缓存用来提高系统性能。
1,把静态数据缓存到离用户最近的地方,缓存到用户浏览器里、CDN 上或者在服务端的 Cache 中
2,静态化改造就是要直接缓存 HTTP 连接。
3,让谁来缓存静态数据也很重要,缓存可以放在Web 服务器(如 Nginx、Apache、Varnish)更擅长处理大并发的静态文件请求。
3.3 方案:1,实体机单机部署;2,统一cache层;3,上CDN
4,热点数据的处理:二八原则
4.1 概念:热点分为热点操作和热点数据。所谓“热点操作”,例如大量的刷新页面、大量的添加购物车;热点数据”比较好理解,那就是用户的热点请求对应的数据。而热点数据又分为“静态热点数据”和“动态热点数据”。
4.2 静态热点数据”,就是能够提前预测的热点数据。例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。
4.3 动态热点数据”,就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。
4.4 发现热点数据:
4.4.1 发现静态热点数据:强制卖家报名,活动商品打标;大数据计算统计top N 商品
4.4.2 发现动态热点数据:1,构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key;2,建立一个热点上报和可以按照需求订阅的热点服务的下发规范;3,将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。
4.5 处理热点数据:
4.5.1 优化:缓存起来,然后可以采用 LRU 淘汰算法替换。
4.5.2 限制:例如对被访问商品的 ID 做一致性 Hash,然后根据 Hash 做分桶,每个分桶设置一个处理队列,这样可以把热点商品限制在一个请求队列里,防止因某些热点商品占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。
4.5.3 隔离:
4.5.3.1 业务隔离:开卖前,卖家单独报名,对热点数据提前做预热
4.5.3.2 系统隔离:可以通过分组部署的方式和另外 99% 分开,杀可以申请单独的域名,目的也是让请求落到不同的集群中。
4.5.3.3 数据隔离:秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想0.01% 的数据有机会影响 99.99% 数据。
5,流量削峰这事应该怎么做?
5.1 为什么要削峰
服务器的处理资源是恒定的,为了用户体验,我们必须按照用户请求的最大峰值进行预分配机器吗,但这样会大大增加服务器成本;所以削峰一方面是为了降低成本,一方面是为了系统的稳定性。
5.2 有哪些手段
5.2.1 排队:通过使用消息中间件(作用:异步,解耦,削峰填谷)把请求平滑缓冲入队列中,可以承接瞬间过来的大流量,避免瞬时大流量请求导致系统崩溃。然后业务系统再去处理队列中的请求。如果瞬时流量过大,达到了消息队列处理的上限,这时候请求可能会被直接丢弃。我们可以采用把请求序列化到文件中来进行处理(类似于mysql bin log的方式),后期再从文件中读取请求进行进一步的处理。
5.2.2 答题: 1,可以防止秒杀器 2,可以把请求基于时间分片,降低瞬时请求并发率。
5.2.3 分层过滤:在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效的请求。总之就是读不做强一致性校验,写时需要做一致性检查。非常适合交易性的写请求,比如减库存或者拼车等,拼车时座位是一直在变化的,不一定要保证读到的一定是准确的,只需要在写数据时进行强一致性检验即可。
5.2.4 业务手段:除了采用技术手段解决,还可以采用业务手段。比如秒杀时,可以通过发放优惠券或者开启抽奖活动等吸引一部分买家到其他地方,也可以起到缓存流量的作用。
6,影响性能的因素有哪些,又该如何提高系统的性能
6.1 影响性能的因素:
这里主要讨论服务端性能。主要因素:响应时间、线程数
qps:每秒处理的请求数。
6.1.1 响应时间对qps的影响
响应时间 = cpu执行时间+线程等待时间(比如 RPC、IO 等待、Sleep、Wait)
真正对cpu有影响的是cpu的执行时间,我们应该致力于减少 CPU 的执行时间。
6.1.2 线程数对qps的影响
线程并非越多越好,线程本身也消耗资源,也受到其他因素的制约,比如线程切换成本高,每个线程也会耗费一定的内存。
最佳实践计算公式:线程数 = [(线程等待时间 + 线程 CPU 时间)/线程 CPU 时间] × CPU 数量,最好的办法是通过性能测试来发现最佳的线程数。
6.2 如何发现瓶颈
首先,秒杀场景的瓶颈更多的发生在cpu上。
6.2.1 通过cpu诊断工具:JProfiler、Yourkit
6.2.2 通过jstack定时打印调用栈,如果某些函数调用频繁或者耗时较多,这些函数就会出现在系统调用栈里,通过采样的方式定时发现耗时较多的函数
6.2.3 怎么判断cpu有没有达到瓶颈:当QPS达到极限时,判断cpu使用率有没有超过95%
6.3 如何优化
6.3.1 减少编码:编码查表非常耗费资源
6.3.2 减少序列化:序列化大部分发生在RPC调用当中,应该避免或减少RPC调用,可以将两个关联性比较强的应用合并部署到同一台机器,使用同一个TOMCAT,且不能走本机的SOCKET。
6.3.3 java极致优化 1,首先做静态化改造,让大部分的请求在NIGNX服务器或WEB服务器上直接返回;2,使用Servlet处理请求,避免使用mvc框架,可以绕过一些无用的处理逻辑(取决于项目对框架的依赖);3,直接输出流数据。响应时推荐使用JSON,而不是模板引擎(一般都是解释执行)。
6.3.4 并发读优化:集中式缓存为了提高命中率一般会采用一致性hash的策略。但还不足以处理大秒,可以采用应用层的localcache,在秒杀系统的单机上缓存商品相关的数据,所以还需要动静数据的分离。静态数据全量推送上去,动态数据采用被动失效的方式缓存一段时间,失效后再去主动缓存拉取最新的数据。
7,秒杀系统“减库存”设计的核心逻辑
我们使用电商平台购物一般都会涉及两个核心流程:下订单、付款,那么我们应该是在用户下订单的时候减库存还是在实际付款后再进行减库存操作呢?接下去我们分析一下各个方案的优缺点,以及存在的问题。
7.1 减库存的三种方式及各自存在的问题
7.1.1 下单减库存:这种方式一定不会出现超卖,但可能出现客户下完单,不付款的情况。如果存在恶意用户大量下单,却不付款,这种方式很快就会把库存减为0,导致商品不能正常售卖。
7.1.2 付款减库存:会产生大量客户下完单,付款时却不成功的情况,库存超卖,导致客户体验较差。
7.1.3 预扣库存: 用户下单后,库存为其保存一段时间(如10分钟),超时未付款后,库存自动释放,释放后其他买家可以继续购买。在买家付款前,系统会检验该订单的库存是否还有保留:1,如果保留成功,则完成付款则实际减去库存,2,如果没有保留,则再次尝试预扣;如果库存不足(就是预扣失败),则不允许付款。如果预扣成功,则完成付款并实际减去库存。
7.2 预扣库存仍存在问题
7.2.1 问题: 预扣库存的方式,虽然可以在一定程度上解决上面的问题,但无法彻底解决。针对恶意下单这种情况,虽然把有效的付款时间设置为 10 分钟,但是恶意买家完全可以在 10 分钟后再次下单,或者采用一次下单很多件把库存减完。
7.2.2 解决方案:解决办法还是要结合安全和反作弊的措施来制止。例如给经常下班不付款的用户打标(可以在被打标的用户下单时不减库存^-^),给某些类目设置最大购买件数(例如活动商品一次只能购买三件),以及对重复下单不付款的操作进行次数限制。
7.3 大型秒杀中怎样减库存
普通业务系统会采用预扣库存的方式,秒杀系统因为大部分人都是抱着“抢到就是赚到的心态”,很少会下单不付款。而且秒杀中商家一般不允许超卖,另外逻辑简单,在性能上也更有优势。下单减库存,在数据一致性上主要表现为保证大并发时库存不能为负数,一般采用事务控制,保证减后不为负数,其次可以采用库存字段设置为无符号整数,这样减为负数时会抛出sql错误;再有一种是使用case when语法,UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
7.4 秒杀减库存的极致优化
7.4.1 库存在交易系统中很明显是热点数据。大并发下单的读可以采用localcache(即在秒杀系统的单机上缓存商品相关的数据)和对数据进行分层过滤的方式,但是大并发写时无论如何都避免不了的。
7.4.2 如果业务逻辑没有复杂的SKU库存和总库存这种联动关系的话,完全可以直接放在带有持久化功能的缓存系统(如redis)中,如果有比较复杂的逻辑或者需要使用事务,还是放在数据库中操作比较好。
7.4.2 另外就是单个热点商品会影响整个数据库的性能,导致0.01%影响99.99%的商品的售卖,可以采用把热点商品单独放在热点库中,这种需要做热点数据的动态迁移以及单独的数据库。
7.4.3 要解决并发锁的问题有两种方案:1,应用层做排队,2,数据库层做排队
8,准备Plan B,如何设计兜底计划
8.1 高可用建设从哪里着手(6个阶段)
8.1.1 架构阶段:异步容灾,异步化,分组隔离,避免单点
8.1.2 编码阶段:超时处理,错误捕获,限流保护,异步线程
8.1.3 测试阶段:beta测试,自动化对比测试 ,保证测试用例的覆盖度,最坏情况下也有相应的处理流程。
8.1.4 发布阶段:分批发布,多版本发布,有紧急的回滚机制
8.1.5 运行阶段:实时监控报警,过载保护,自动降级,数据对账
8.1.6 故障发生:快速恢复,故障定位
8.2 秒杀系统高可用方案:
8.2.1 降级:配置一套系统化预案与开关系统(如临时下架优惠券系统)
8.2.2 限流:限制一部分流量保护系统。分为:客户端限流与服务端限流。限流的实现方式既要支持URL以及方法级别的限流又要支持基于QPS与线程的限流(通过压测获取系统最大QPS,比如10000,我们可以设置8000来进行限流保护)。限流时会导致用户请求失败一定要设置超时,防止因被限流的请求因不能fast fail快速失败而拖垮系统。
8.2.3 拒绝服务:当系统负载达到一定的阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到2*CPU 核数时,系统直接拒绝所有请求。在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝 HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护。这样设计在系统负载过高时不提供服务用来保护系统,当负载下降时又可以很容易恢复。
8.2.4 总结:网站的高可用建设是基础,可以说要深入到各个环节,更要长期规划,并进行体系化建设,要在预防(建立常态的压力体系,例如上线前的单机压测到上线后的全链路压测)、管控(做好线上运行时的降级、限流和兜底保护)、监控(建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系,发现问题及时预警)和恢复体系(遇到故障要及时止损,并提供快速的数据订正工具等)等这些地方加强建设,每一个环节可能都有很多事情要做。