架构原则
架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都将是空谈。
- 请求数据要尽量少
数据在网络上传输需要时间,服务器接受跟返回数据都需要时间,消耗CPU
系统依赖数据尽量少,减少数据序列化和反序列化,缩短数据传递链路(减少后台服务跟数据库打交道)
- 请求数要尽量少
如:页面CSS/JavaScript、图片,Ajax请求尽量少,请求多建立Http请求时间长,域名DNS解析
减少请求数可以合并CSS和JavaScript文件,URL中逗号隔开请求服务端,将文件合并返回。
- 路径要尽量短
减少RPC: 多个相互强依赖的应用合并部署,将RPC变成JVM内部之间方法调用
- 依赖要尽量少(一次用户请求必须强依赖的系统或者服务)
-
阶段一(10w/s)
秒杀系统独立,集群部署
热点数据(库存数据)单独放到缓存系统以提高读性能,cache里库存不需要100%和数据库一致
增加秒杀答题,防止有秒杀器抢单,减少用户请求频率阶段二(100w/s)
对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,而只需要点击抢宝按钮,借此把页面刷新的数据降到最少;
- 在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。
-
动静分离方案
“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。
阶段一(静态数据缓存)
静态数据缓存到离用户最近的地方
静态数据就是相对不会变化的数据,因此可以缓存起来。存在: 用户浏览器里、CDN上或者在服务端的 Cache 中。
- 静态化改造(直接缓存 HTTP 连接)
静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据。Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。
URL唯一化可以使URL作为缓存key,缓存整个http连接
- 分离浏览者相关的因素
浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取。
- 分离时间因素
服务端输出时间数据需动态请求
- 异步化地域因素
与地域相关的因素做成异步方式获取活着动态请求
- 去掉Cookie
服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意,这里说的去掉Cookie 并不是用户端收到的页面就不含 Cookie 了,而是说,在缓存的静态数据中不含有 Cookie。
动态内容处理方案
- ESI(Edge Side Includes)方案
在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
- CSI(Client Side Include)方案
即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。
动静分离架构方案
- 实体机单机部署
虚拟机改为实体机,以增大Cache的容量,并且采用一致性Hash分组的方式来提升命中率。
将 Cache 分成若干组,是希望能达到命中率和访问热点的平衡。Hash 分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致 Cache 被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题。
优点:
- 没有网络瓶颈,而且能使用大内存
- 既能提升命中率,又能减少 Gzip 压缩
- 减少 Cache 失效压力,采用定时失效方式,例如只缓存 3 秒钟,过期即自动失效
缺点:
- 增加单机的内存容量,但是一定程度上也造成了 CPU 的浪费,因为单个Java程序很难用完整个实体机的 CPU
- 部署了 Java 应用又作为 Cache 来使用,造成了运维上的高复杂度
- 统一Cache 层
单机的 Cache 统一分离出来,形成一个单独的 Cache 集群。 统一 Cache 层是更理想的可推广方案。
优点
- 单独一个 Cache 层,可以减少多个应用接入时使用 Cache 的成本。这样接入的应用只要维护自己的 Java 系统就好,不需要单独维护 Cache,而只关心如何使用即可。
- 统一 Cache 的方案更易于维护,如后面加强监控、配置的自动化,只需要一套解决方案就行,统一起来维护升级也比较方便。
- 可以共享内存,最大化利用内存,不同系统之间的内存可以动态切换,从而能够有效应对各种攻击。
缺点
- Cache 层内部交换网络成为瓶颈
- 缓存服务器的网卡也会是瓶颈
- 机器少风险较大,挂掉一台就会影响很大一部分缓存数据
要解决上面这些问题,可以再对 Cache 做 Hash 分组,即一组 Cache 缓存的内容相同,这样能够避免热点数据过度集中导致新的瓶颈产生。
- 上CDN
存在问题
- 时效问题
- 命中率问题
- 发布更新问题
满足以下条件可以采用该方案
1. 靠近访问量比较集中的地区;
2. 离主站相对较远;
3. 节点到主站间的网络比较好,而且稳定;
4. 节点容量比较大,不会占用其他 CDN 太多的资源。
流量削峰
一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。针对秒杀这一场景,
削峰从本质上来说就是更多地延缓用户请求的发出,以便减少和过滤掉一些无效请求,它遵
从“请求数要尽量少”的原则。
- 排队
消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。
还可以使用下列方式
1. 利用线程池加锁等待也是一种常用的排队方式
2. 先进先出、先进后出等常用的内存排队算法的实现方式
3. 把请求序列化到文件中,然后再顺序地读文件(例如基于 MySQL binlog 的同步机制)
来恢复请求等方式
虽然增加了访问请求的路径需要跟增加缓存步骤,减少系统崩溃相互取舍
- 答题(增加购买的复杂度)
- 防止部分买家使用秒杀器在参加秒杀时作弊
- 延缓请求,起到对请求流量进行削峰的作用,从而让系统能够更好地支持瞬时的流量高峰
- 分层过滤
分层校验的基本原则
- 将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读;
- 对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
- 对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
- 对写请求做限流保护,将超出系统承载能力的请求过滤掉;
- 对写数据进行强一致性校验,只保留最后有效的数据。
分层校验的目的是:在读系统中,尽量减少由于一致性校验带来的系统瓶颈,但是尽量将不
影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正
确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;在写数据系统中,主要对
写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库
存”不能减为负数)。
扣减库存
扣减库存方式
- 下单减库存
问题:买家下单不付款(恶意刷单),导致商品销售异常
保证大并发请求时库存数据不能为负数
- 应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚
- 数据库的字段数据为无符号整数
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
- 付款减库存
问题:超卖,付款提示库存不足,用户体验差
- 预扣库存
秒杀减库存的极致优化
解决大并发读问题,可以采用 LocalCache(即在秒杀系统的单机上缓存商品相关的数据)和对数据进行分层过滤的方式。分离热点商品到单独的数据库解决并发锁的问题
应用层做排队
按照商品维度设置队列顺序执行,这样能减少同一台机器对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用太多的数据库连接。
数据库层做排队
应用层只能做到单机的排队,但是应用机器数本身很多,这种排队方式控制并发的能力仍然有限,所以如果能在数据库层做全局排队是最理想的。阿里的数据库团队开发了针对这种 MySQL 的 InnoDB 层上的补丁程序(patch),可以在数据库层上对单行记录做到并发排队。
系统的高可用建设涉及架构阶段、编码阶段、测试阶段、发布阶段、运行阶段,
以及故障发生时。接下来,我们分别看一下。
- 架构阶段:架构阶段主要考虑系统的可扩展性和容错性,要避免系统出现单点问题。例
如多机房单元化部署,即使某个城市的某个机房出现整体故障,仍然不会影响整体网站
的运转。 - 编码阶段:编码最重要的是保证代码的健壮性,例如涉及远程调用问题时,要设置合理
的超时退出机制,防止被其他系统拖垮,也要对调用的返回结果集有预期,防止返回的
结果超出程序处理范围,最常见的做法就是对错误异常进行捕获,对无法预料的错误要
有默认处理结果。 - 测试阶段:测试主要是保证测试用例的覆盖度,保证最坏情况发生时,我们也有相应的
处理流程。 - 发布阶段:发布时也有一些地方需要注意,因为发布时最容易出现错误,因此要有紧急
的回滚机制。 - 运行阶段:运行时是系统的常态,系统大部分时间都会处于运行态,运行态最重要的是
对系统的监控要准确及时,发现问题能够准确报警并且报警数据要准确详细,以便于排
查问题。 - 故障发生:故障发生时首先最重要的就是及时止损,例如由于程序问题导致商品价格错
误,那就要及时下架商品或者关闭购买链接,防止造成重大资产损失。然后就是要能够
及时恢复服务,并定位原因解决问题。保障系统的稳定运行(运行阶段处理)
降级
当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。
降级方案可以这样设计:当秒杀流量达到 5w/s 时,把成交记录的获取从展示 20 条降级到
只展示 5 条。“从 20 改到 5”这个操作由一个开关来实现,也就是设置一个能够从开关系
统动态获取的系统参数。
这里,我给出开关系统的示意图。它分为两部分,一部分是开关控制台,它保存了开关的具
体配置信息,以及具体执行开关所对应的机器列表;另一部分是执行下发开关数据的
Agent,主要任务就是保证开关被正确执行,即使系统重启后也会生效。限流
限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
总体来说,限流既可以是在客户端限流,也可以是在服务端限流。此外,限流的实现方式既要支持 URL 以及方法级别的限流,也要支持基于QPS 和线程的限流。
客户端限流
好处可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。
缺点就是当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果设的太大,则起不到限制的作用。
服务端限流
好处是可以根据服务端的性能设置合理的阈值
缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。拒绝服务
拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。
在最前端的 Nginx 上设置过载保护,当机器负载达到某个值时直接拒绝。HTTP 请求并返回 503 错误码,在 Java 层同样也可以设计过载保护。
异步请求如何返回结果多种方案
一页面中采用轮询的方式定时主动去服务端查询结果
例如每秒请求一次服务端看看有没有处理结果(现在很多支付页面都采用了这种策略),这种方式的缺点是服务端的请求数会增加不少。
二采用主动 push 方式
这种就要求服务端和客户端保持连接了,服务端处理完请求主动 push 给客户端,这种方式的缺点是服务端的连接数会比较多。
异步的请求失败了,怎么办?
对秒杀来说,我觉得如果失败了直接丢弃就好了,最坏的结果就是这个人没有抢到而已。但是你非要纠结的话,就要做异步消息的持久化以及重试机制了,要保证异步请求的最终正确处理一般都要借助消息系统,即消息的最终可达。
例如阿里的消息中间件是能承诺只要客户端消息发送成功,那么消息系统一定会保证消息最终被送到目的地,即消息不会丢。因为客户端只要成功发送一条消息,下游消费方就一定会消费这条消息,所以也就不存在消息发送失败的问题了。
单独秒杀库,崩了不影响其他业务
秒杀url动态化,防止链接提前暴露https://blog.csdn.net/canot/article/details/53966987
Redis集群,主从同步、读写分离,我们还搞点哨兵,开启持久化直接无敌高可用!延迟:监控程序https://blog.csdn.net/fd2025/article/details/80076832
Nginx
cdn
按钮控制
前段限流:按钮可以点击之后也得给他置灰几秒
后端限流:产品卖光了,return了一个false,前端直接秒杀结束
mq:削峰填谷
限流,顶不住就挡一部分出去但是不能说不行,降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就独立的