《极客时间教程 - 秒杀系统》笔记
《极客时间教程 - 秒杀系统》笔记
开篇词丨秒杀系统架构设计都有哪些关键点?
秒杀的整体架构可以概括为“稳、准、快”几个关键字
- 稳-高可用 - 服务需要考虑各种容错场景,保证服务可用
- 准-一致性 - 高并发下的库存数量增减不能出错,避免超卖
- 快-高性能 - 支持高并发的读写
设计秒杀系统时应该注意的 5 个架构原则
秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。
架构原则:“4 要 1 不要”
- 数据尽量少
- 请求及响应的数据量越小,则传输数据量越小,可以显著减少 CPU 和带宽;
- 依赖数据库的数据越少,数据库压力越小,I/O 耗时越少
- 请求数尽量少 - 合并 css+js,减少静态资源的请求数
- 路径尽量短
- 路径,是指用户发出请求、收到响应的整个过程中,数据经过的节点数。
- 路径越短,则 I/O 传输耗时越少,也更加可靠。
- 依赖尽量少 - 依赖,是指要完成一次用户请求必须依赖的系统或者服务。
- 避免单点
- 对于应用服务,应设计为无状态,然后以集群模式提供整体服务,以此提高可用性
- 对于数据库,应通过副本机制+故障转移,来保证可用性。
不同场景下的不同架构案例
(1)请求量级 10w QPS 的架构
架构要点:
- 把秒杀系统独立出来单独打造一个系统,这样可以有针对性地做优化
- 在系统部署上也独立做一个机器集群,这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载;
- 将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能”;
- 增加秒杀答题,防止有秒杀器抢单。
(1)请求量级 100w QPS 的架构
- 对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,而只需要点击抢宝按钮,借此把页面刷新的数据降到最少;
- 在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。
- 增加系统限流保护,防止最坏情况发生。
小结:架构之道,在于权衡取舍。要取得极致的性能,往往要在通用性、易用性、成本等方面有所牺牲,反之亦然。
如何才能做好动静分离?有哪些方案可选?
何为动静数据
“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。
所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。更通俗的来说,是不是每个人看到的页面是相同的。
怎样对静态数据做缓存呢?
- 第一,你应该把静态数据缓存到离用户最近的地方。常见技术:CDN、Cookie、服务器缓存
- 第二,静态化改造就是要直接缓存 HTTP 连接。例如:Nginx 静态缓存
- 第三,让谁来缓存静态数据也很重要。Web 服务器(如 Nginx、Apache、Varnish)更擅长处理大并发的静态文件请求。
如何做动静分离的改造
- URL 唯一化。商品详情系统天然地就可以做到 URL 唯一化,比如每个商品都由 ID 来标识,那么
http://item.xxx.com/item.htm?id=xxxx
就可以作为唯一的 URL 标识。为啥要 URL 唯一呢?前面说了我们是要缓存整个 HTTP 连接,那么以什么作为 Key 呢?就以 URL 作为缓存的 Key,例如以 id=xxx 这个格式进行区分。 - 分离浏览者相关的因素。浏览者相关的因素包括身份、认证信息等。这部分少量数据可以通过动态请求来获取。
- 分离时间因素。服务端输出的时间也通过动态请求获取。
- 异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。
- 去掉 Cookie。服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意,这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了,而是说,在缓存的静态数据中不含有 Cookie。
分离出动态内容之后,如何组织这些内容页就变得非常关键了。动态内容的处理通常有两种方案:
- ESI 方案(或者 SSI):即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
- CSI 方案。即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。
动静分离的几种架构方案
方案 1:实体机单机部署
这种方案是将虚拟机改为实体机,以增大 Cache 的容量,并且采用了一致性 Hash 分组的方式来提升命中率。这里将 Cache 分成若干组,是希望能达到命中率和访问热点的平衡。Hash 分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致 Cache 被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题。
实体机单机部署有以下几个优点:
- 没有网络瓶颈,而且能使用大内存;
- 既能提升命中率,又能减少 Gzip 压缩;
- 减少 Cache 失效压力,因为采用定时失效方式,例如只缓存 3 秒钟,过期即自动失效。
缺点:
- 一定程度上也造成了 CPU 的浪费,因为单个的 Java 进程很难用完整个实体机的 CPU。
- 一个实体机上部署了 Java 应用又作为 Cache 来使用,这造成了运维上的高复杂度。
方案 2:统一 Cache 层
所谓统一 Cache 层,就是将单机的 Cache 统一分离出来,形成一个单独的 Cache 集群。
优点:
- 应用无需单独维护 Cache
- 运维简单
- 可以共享内存,最大化利用内存
缺点:
- Cache 层内部交换网络成为瓶颈;
- 缓存服务器的网卡也会是瓶颈;
- 机器少风险较大,挂掉一台就会影响很大一部分缓存数据。
方案 3:CDN
动静分离后,缓存如果前置到 CDN,由于离用户更近,因此访问更快。
CDN 方案有以下问题:
- 失效问题。需要考虑如果让 CDN 分布在全国各地的 Cache 在秒级时间内失效。
- 命中率问题。如果将数据全部放到全国的 CDN 上,必然导致 Cache 分散,而 Cache 分散又会导致访问请求命中同一个 Cache 的可能性降低,那么命中率就成为一个问题。
- 发布更新问题。若业务迭代快速,则发布系统必须足够简洁高效
将商品详情系统放到全国的所有 CDN 节点上是不太现实的,因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢?答案是“可以”,但是这样的节点需要满足几个条件:
- 靠近访问量比较集中的地区;
- 离主站相对较远;
- 节点到主站间的网络比较好,而且稳定;
- 节点容量比较大,不会占用其他 CDN 太多的资源。
最后,还有一点也很重要,那就是:节点不要太多。
基于上面几个因素,选择 CDN 的二级 Cache 比较合适,因为二级 Cache 数量偏少,容量也更大,让用户的请求先回源的 CDN 的二级 Cache 中,如果没命中再回源站获取数据,部署方式如下图所示:
二八原则:有针对性地处理好系统的“热点数据”
所谓“静态热点数据”,就是能够提前预测的热点数据。例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。
所谓“动态热点数据”,就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。
发现热点数据
动态热点发现系统的具体实现。
- 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。
- 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx 模块统计的热点 URL。
- 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。
这里我给出了一个图,其中用户访问商品时经过的路径有很多,我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中。
处理热点数据
处理热点数据通常有几种思路:一是优化,二是限制,三是隔离。
具体到“秒杀”业务,我们可以在以下几个层次实现隔离。
- 业务隔离。把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热。
- 系统隔离。系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外 99%分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。
- 数据隔离。秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想 0.01%的数据有机会影响 99.99%数据。
流量削峰这事应该怎么做?
流量削峰的思路:排队、答题、分层过滤
排队 - 使用 MQ 削峰、解耦
适用于内部上下游系统之间调用请求不平缓的场景,由于内部系统的服务质量要求不能随意丢弃请求,所以使用消息队列能起到很好的削峰和缓冲作用。
答题 - 延缓请求、限制秒杀器
适用于秒杀或者营销活动等应用场景,在请求发起端就控制发起请求的速度,因为越到后面无效请求也会越多,所以配合后面介绍的分层拦截的方式,可以更进一步减少无效请求对系统资源的消耗。
分层过滤 - 请求分别经过 CDN、前台读系统(如商品详情系统)、后台系统(如交易系统)和数据库这几层分层过滤。
分层过滤非常适合交易性的写请求,比如减库存或者拼车这种场景,在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的,所以读的数据是否一定要非常准确呢?其实不一定,你可以放一些请求过去,然后在真正减的时候再做强一致性保证,这样既过滤一些请求又解决了强一致性读的瓶颈。
分层校验的基本原则是:
- 将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读;
- 对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
- 对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
- 对写请求做限流保护,将超出系统承载能力的请求过滤掉;
- 对写数据进行强一致性校验,只保留最后有效的数据。
影响性能的因素有哪些?又该如何提高系统的性能?
- 影响性能的因素:响应时间、线程数
- 如何发现瓶颈:
- 瓶颈点:CPU、内存、磁盘、带宽
- 针对 CPU 而言,可以使用 CPU 相关工具:JProfile、Yourkit、jstack,此外,还可以使用链路追踪进行链路分析
- 如何优化系统:编码、序列化、压缩、传输方式(NIO)、并发
秒杀系统“减库存”设计的核心逻辑
减库存的一般方式:
- 下单减库存:不会出现超卖;不能应对下单不付款的情况
- 付款减库存:高并发下,可能出现超卖——下单后无法付款的情况(库存已经清空)
- 预扣库存:买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。
针对秒杀场景,一般“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用“下单减库存”更加合理。另外,理论上,“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。
“下单减库存”在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般我们有多种解决方案:一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错;再有一种就是使用 CASE WHEN 判断语句,例如这样的 SQL 语句:
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
准备 Plan B:如何设计兜底方案
高可用系统建设:
- 设计阶段:考虑系统的可扩展性和容错性。避免单点问题,采用多活方案(多机房部署)。
- 编码阶段:保证代码的健壮性。识别边界,捕获、处理异常;设置合适的超时机制。
- 测试阶段:测试用例覆盖度尽量全面。
- 发布阶段:自动化发布,支持灰度发布、回滚。
- 运行阶段:健全监控机制:日志、指标、链路监控
- 故障发生:容错处理、故障恢复、故障演练
降级
“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。
限流
限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。
拒绝服务
过载保护 - 当系统负载达到一定阈值时,例如 CPU 使用率达到 90%或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。
拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。
高可用建设需要长期规划并进行体系化建设,要在预防(建立常态的压力体系,例如上线前的单机压测到上线后的全链路压测)、管控(做好线上运行时的降级、限流和兜底保护)、监控(建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系,发现问题及时预警)和恢复体系(遇到故障要及时止损,并提供快速的数据订正工具等)等这些地方加强建设。