跳至主要內容

《极客时间教程 - 秒杀系统》笔记

钝悟...大约 16 分钟笔记设计设计架构秒杀系统超卖

《极客时间教程 - 秒杀系统》笔记

开篇词丨秒杀系统架构设计都有哪些关键点?

秒杀的整体架构可以概括为“稳、准、快”几个关键字

  • 稳-高可用 - 服务需要考虑各种容错场景,保证服务可用
  • 准-一致性 - 高并发下的库存数量增减不能出错,避免超卖
  • 快-高性能 - 支持高并发的读写

设计秒杀系统时应该注意的 5 个架构原则

秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。

架构原则:“4 要 1 不要”

  • 数据尽量少
    • 请求及响应的数据量越小,则传输数据量越小,可以显著减少 CPU 和带宽;
    • 依赖数据库的数据越少,数据库压力越小,I/O 耗时越少
  • 请求数尽量少 - 合并 css+js,减少静态资源的请求数
  • 路径尽量短
    • 路径,是指用户发出请求、收到响应的整个过程中,数据经过的节点数。
    • 路径越短,则 I/O 传输耗时越少,也更加可靠。
  • 依赖尽量少 - 依赖,是指要完成一次用户请求必须依赖的系统或者服务。
  • 避免单点
  • 对于应用服务,应设计为无状态,然后以集群模式提供整体服务,以此提高可用性
  • 对于数据库,应通过副本机制+故障转移,来保证可用性。

不同场景下的不同架构案例

(1)请求量级 10w QPS 的架构

架构要点:

  1. 把秒杀系统独立出来单独打造一个系统,这样可以有针对性地做优化
  2. 在系统部署上也独立做一个机器集群,这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载;
  3. 将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能”;
  4. 增加秒杀答题,防止有秒杀器抢单。

(1)请求量级 100w QPS 的架构

  1. 对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,而只需要点击抢宝按钮,借此把页面刷新的数据降到最少;
  2. 在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。
  3. 增加系统限流保护,防止最坏情况发生。

小结:架构之道,在于权衡取舍。要取得极致的性能,往往要在通用性、易用性、成本等方面有所牺牲,反之亦然。

如何才能做好动静分离?有哪些方案可选?

何为动静数据

“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据

所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。更通俗的来说,是不是每个人看到的页面是相同的。

怎样对静态数据做缓存呢?

  • 第一,你应该把静态数据缓存到离用户最近的地方。常见技术:CDN、Cookie、服务器缓存
  • 第二,静态化改造就是要直接缓存 HTTP 连接。例如:Nginx 静态缓存
  • 第三,让谁来缓存静态数据也很重要。Web 服务器(如 Nginx、Apache、Varnish)更擅长处理大并发的静态文件请求。

如何做动静分离的改造

  1. URL 唯一化。商品详情系统天然地就可以做到 URL 唯一化,比如每个商品都由 ID 来标识,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作为唯一的 URL 标识。为啥要 URL 唯一呢?前面说了我们是要缓存整个 HTTP 连接,那么以什么作为 Key 呢?就以 URL 作为缓存的 Key,例如以 id=xxx 这个格式进行区分。
  2. 分离浏览者相关的因素。浏览者相关的因素包括身份、认证信息等。这部分少量数据可以通过动态请求来获取。
  3. 分离时间因素。服务端输出的时间也通过动态请求获取。
  4. 异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。
  5. 去掉 Cookie。服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意,这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了,而是说,在缓存的静态数据中不含有 Cookie。

分离出动态内容之后,如何组织这些内容页就变得非常关键了。动态内容的处理通常有两种方案:

  1. ESI 方案(或者 SSI):即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
  2. CSI 方案。即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。

动静分离的几种架构方案

方案 1:实体机单机部署

这种方案是将虚拟机改为实体机,以增大 Cache 的容量,并且采用了一致性 Hash 分组的方式来提升命中率。这里将 Cache 分成若干组,是希望能达到命中率和访问热点的平衡。Hash 分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致 Cache 被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题。

实体机单机部署有以下几个优点:

  1. 没有网络瓶颈,而且能使用大内存;
  2. 既能提升命中率,又能减少 Gzip 压缩;
  3. 减少 Cache 失效压力,因为采用定时失效方式,例如只缓存 3 秒钟,过期即自动失效。

缺点:

  • 一定程度上也造成了 CPU 的浪费,因为单个的 Java 进程很难用完整个实体机的 CPU。
  • 一个实体机上部署了 Java 应用又作为 Cache 来使用,这造成了运维上的高复杂度。

方案 2:统一 Cache 层

所谓统一 Cache 层,就是将单机的 Cache 统一分离出来,形成一个单独的 Cache 集群。

优点:

  1. 应用无需单独维护 Cache
  2. 运维简单
  3. 可以共享内存,最大化利用内存

缺点:

  1. Cache 层内部交换网络成为瓶颈;
  2. 缓存服务器的网卡也会是瓶颈;
  3. 机器少风险较大,挂掉一台就会影响很大一部分缓存数据。

方案 3:CDN

动静分离后,缓存如果前置到 CDN,由于离用户更近,因此访问更快。

CDN 方案有以下问题:

  1. 失效问题。需要考虑如果让 CDN 分布在全国各地的 Cache 在秒级时间内失效。
  2. 命中率问题。如果将数据全部放到全国的 CDN 上,必然导致 Cache 分散,而 Cache 分散又会导致访问请求命中同一个 Cache 的可能性降低,那么命中率就成为一个问题。
  3. 发布更新问题。若业务迭代快速,则发布系统必须足够简洁高效

将商品详情系统放到全国的所有 CDN 节点上是不太现实的,因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢?答案是“可以”,但是这样的节点需要满足几个条件:

  1. 靠近访问量比较集中的地区;
  2. 离主站相对较远;
  3. 节点到主站间的网络比较好,而且稳定;
  4. 节点容量比较大,不会占用其他 CDN 太多的资源。

最后,还有一点也很重要,那就是:节点不要太多。

基于上面几个因素,选择 CDN 的二级 Cache 比较合适,因为二级 Cache 数量偏少,容量也更大,让用户的请求先回源的 CDN 的二级 Cache 中,如果没命中再回源站获取数据,部署方式如下图所示:

二八原则:有针对性地处理好系统的“热点数据”

所谓“静态热点数据”,就是能够提前预测的热点数据。例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。

所谓“动态热点数据”,就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。

发现热点数据

动态热点发现系统的具体实现。

  1. 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。
  2. 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx 模块统计的热点 URL。
  3. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。

这里我给出了一个图,其中用户访问商品时经过的路径有很多,我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中。

处理热点数据

处理热点数据通常有几种思路:一是优化,二是限制,三是隔离

具体到“秒杀”业务,我们可以在以下几个层次实现隔离。

  1. 业务隔离。把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热。
  2. 系统隔离。系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外 99%分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。
  3. 数据隔离。秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想 0.01%的数据有机会影响 99.99%数据。

流量削峰这事应该怎么做?

流量削峰的思路:排队、答题、分层过滤

排队 - 使用 MQ 削峰、解耦

适用于内部上下游系统之间调用请求不平缓的场景,由于内部系统的服务质量要求不能随意丢弃请求,所以使用消息队列能起到很好的削峰和缓冲作用。

答题 - 延缓请求、限制秒杀器

适用于秒杀或者营销活动等应用场景,在请求发起端就控制发起请求的速度,因为越到后面无效请求也会越多,所以配合后面介绍的分层拦截的方式,可以更进一步减少无效请求对系统资源的消耗。

分层过滤 - 请求分别经过 CDN、前台读系统(如商品详情系统)、后台系统(如交易系统)和数据库这几层分层过滤。

分层过滤非常适合交易性的写请求,比如减库存或者拼车这种场景,在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的,所以读的数据是否一定要非常准确呢?其实不一定,你可以放一些请求过去,然后在真正减的时候再做强一致性保证,这样既过滤一些请求又解决了强一致性读的瓶颈。

分层校验的基本原则是:

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

影响性能的因素有哪些?又该如何提高系统的性能?

  • 影响性能的因素:响应时间、线程数
  • 如何发现瓶颈
    • 瓶颈点: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:如何设计兜底方案

高可用系统建设:

  1. 设计阶段:考虑系统的可扩展性和容错性。避免单点问题,采用多活方案(多机房部署)。
  2. 编码阶段:保证代码的健壮性。识别边界,捕获、处理异常;设置合适的超时机制。
  3. 测试阶段:测试用例覆盖度尽量全面。
  4. 发布阶段:自动化发布,支持灰度发布、回滚。
  5. 运行阶段:健全监控机制:日志、指标、链路监控
  6. 故障发生:容错处理、故障恢复、故障演练

降级

“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。

限流

限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。

拒绝服务

过载保护 - 当系统负载达到一定阈值时,例如 CPU 使用率达到 90%或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。

拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。

高可用建设需要长期规划并进行体系化建设,要在预防(建立常态的压力体系,例如上线前的单机压测到上线后的全链路压测)、管控(做好线上运行时的降级、限流和兜底保护)、监控(建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系,发现问题及时预警)和恢复体系(遇到故障要及时止损,并提供快速的数据订正工具等)等这些地方加强建设。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.7