01 | 在设计秒杀系统时,应注意五个架构原则
说到秒杀,我想你一定很熟悉这两年,从双十一购物到春节抢红包,再到 12306 抢火车票,秒杀的场景随处可见。简单来说,秒杀就是同时要求大量购买同一件商品并完成交易的过程。用技术行话来说,是大量的并发读写。
无论哪种语言,并发都是程序员最头疼的部分。同样,软件也是如此。您可以快速添加、删除、更改和检查以制作第二次杀死系统,但支持高并发访问并不容易。例如,如何使系统面临数百万的要求流量无故障?如何确保数据在高并发条件下的一致性?于堆叠服务器吗?这显然不是最好的解决方案。
在我看来,秒杀系统本质上是一个满足大并发性、高性能和高可用性的分布式系统。今天,让我们来谈谈如何在满足良好结构的分布式系统的基础上,实现秒杀业务的终极性能改进。
结构原则:4 要 1 不要” 如果你是架构师,首先要勾勒出一个轮廓,思考如何构建一个超大流量、读写、高性能、高可用性的系统,需要考虑哪些要素。我把这些元素总结为4 要 1 不要”。
- 尽量少数据
所谓数据应该尽可能少,首先是指用户要求的数据可以尽可能少。请求的数据包括上传到系统的数据和系统返回给用户的数据(通常是网页)。
为什么数据应该尽可能少?因为首先,这些数据需要时间在网络上传输,其次,服务器需要处理请求数据和返回数据,而服务器通常需要在编写网络时制作压缩和字符编码,这是非常昂贵的 CPU,因此,可以显著减少传输的数据量 CPU 使用。例如,我们可以简化秒杀页面的大小,去除不必要的页面装饰效果,等等。
其次,数据应该尽可能少还要求系统依赖的数据少,包括系统需要读取和保存的数据来完成一些业务逻辑,通常处理背景服务和数据库。调用其他服务将涉及数据的序列化和反序列化 CPU 大杀手,也会增加延迟。而且,数据库本身很容易成为瓶颈,所以处理数据库越少越好,数据越简单,越小越好。
- 请求数要尽量少
在用户要求的页面返回后,浏览器渲染页面包含其他额外的请求,例如,该页面依赖于 CSS/JavaScript、图片,以及 Ajax 请求等都定义为额外请求,这些额外请求应尽可能少。因为浏览器发出的每个请求都会有一些消耗,比如三次握手建立连接,有时会有页面依赖或连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同要求的域名不同,也涉及到这些域名 DNS 分析可能需要更长的时间。因此,你应该记住,减少请求数量可以显著减少上述因素造成的资源消耗。
例如,减少请求数量最常用的实践之一是合并 CSS 和 JavaScript 多个文件 JavaScript 文件合并成文件 URL 用逗号隔开(https://g.xxx.com/tm/xx-b/4.0.94/mods/??module-preview/index.xtpl.js,module-jhs/index.xtpl.js,module-focus/index.xtpl.js)。这样,单个文件仍然存储在服务端,但服务端将有一个组件分析 URL,然后动态合并这些文件并返回。
- 尽量短路径
所谓路径,就是用户在发送请求返回数据的过程中的中间节点数。
通常,这些节点可以表示为一个系统或者一个新的 Socket 连接(例如,代理服务器只创建一个新的 Socket 连接来转发请求)。每经过一个节点,一般都会产生一个新的 Socket 连接。
然而,每次添加连接都会增加新的不确定性。在概率统计方面,如果一个请求通过了 5 每个节点的可用性是 99.9% 那么整个请求的可用性是:99.9% 的 5 次方,约等于 99.5%。
因此,缩短要求路径不仅可以提高可用性,还可以有效提高性能(减少中间节点可以减少数据的序列化和反序列化),减少延迟(减少网络传输的耗时)。
缩短访问路径的一种方法是合并和部署多个相互依赖的应用程序,并调用远程过程(RPC)变成 JVM 调用内部方法。在《大型网站技术架构演变与性能优化》一书中,我还介绍了该技术的详细实现。
- 尽量少依赖
所谓依赖,是指完成用户请求必须依赖的系统或服务,这里的依赖是指强依赖。
例如,如果你想显示第二次杀戮页面,这个页面必须强烈依赖商品信息、用户信息和其他信息,如优惠券、交易列表和其他信息(弱依赖),这些弱依赖可以在紧急情况下删除。
为了减少依赖性,我们可以对系统进行分级,例如 0 级系统、1 级系统、2 级系统、3 级系统,0 如果级系统是最重要的系统,那么 0 等等。
注意,0 尽量减少级系统的正确性 1 强烈依赖级别系统,防止重要系统被不重要系统拖垮。例如,支付系统是 0 等级系统,优惠券是 1 在极端情况下,优惠券可以降级,以防止支付系统被优惠券降级 1 拖垮级系统。
- 不要有单点
系统中的单点可以说是系统架构中的一大禁忌,因为单点意味着没有备份,风险无法控制。我们设计分布式系统最重要的原则是消除单点。
如何避免单点?我认为关键是避免将服务状态与机器绑定,即将服务无状态化,使服务能够在机器中随意移动。
如何解耦服务状态和机器?实现它的方法有很多。例如,动态配置与机器相关,这些参数可以通过配置中心动态推送,并在服务启动时动态拉下。我们在这些配置中心设置了一些规则,以便于改变这些映射关系。
无状态状态化是有效避免单点的一种方式,但存储服务本身很难无状态化,因为数据存储在磁盘上,与机器绑定,所以这种场景通常需要通过冗余多个备份来解决单点问题。
这些设计了一些这些设计原则,但是你有没有注意到我一直说的是尽量而不是绝对?
我想你一定会问你是否要求最少,我的答案是不一定。我们有一些 CSS 这样做可以减少对页面的依赖 CSS 要求加快了主页的渲染,但也增加了页面的大小,不符合数据尽可能少的原则。在这种情况下,为了提高主屏幕的渲染速度,我们只使用主屏幕 HTML 依赖的 CSS 内联进来,别的 CSS 依赖加载仍然放在文件中,尽量平衡第一个屏幕的打开速度和整个页面的加载性能。
因此,架构是一种平衡的艺术,一旦最好的架构脱离了它适应的场景,一切都将是空谈。我希望你记住的是,这里提到的几点只是一个方向。你应该努力朝这些方向工作,但也应该考虑其他因素的平衡。
不同场景下的不同架构案例 我之前说过一些架构原则,那么对于秒杀场景,什么是好的架构呢?接下来,我将以淘宝早期秒杀系统架构的演变为主线,帮助您梳理出我认为最好的秒杀系统架构。
如果你想快速建立一个简单的二次杀戮系统,你只需要在你的商品购买页面上添加一个定期货架功能,让用户二次杀戮开始时才能让用户看到购买按钮,当商品库存完成时。这是第一个版本的二次杀戮系统的实现。
但是随着请求量的增加(例如从 1w/s 到了 10w/s 这种简单的结构很快就遇到了瓶颈,因此需要进行结构改造来提高系统性能。这些结构改造包括:
独立打造秒杀系统,可以有针对性地优化。比如这个独立的系统降低了店铺装修的功能和页面的复杂性; 还在系统部署中独立建立机器集群,使秒杀的大流量不会影响正常商品购买集群的机器负载; 将热点数据(如库存数据)单独放入缓存系统中,以提高读取性能; 增加秒杀答案,防止有秒杀器抢单。 此时,系统架构如下图所示。最重要的是,秒杀细节已经成为一个独立的新系统,一些核心数据已经放入缓存中(Cache)其他相关系统也以独立集群的形式部署。
图 1 改造后的系统架构
然而,这种架构仍然不能超过 100w/s 为了进一步提高秒杀系统的性能,我们进一步升级了架构,如:
对页面进行彻底的动态和静态分离,使用户在第二次杀死时不需要刷新整个页面,而只需点击抓取按钮,以尽量减少页面刷新的数据; 在服务缓存服务端,无需调用依赖系统的后台服务获取数据,甚至无需到公共缓存集群查询数据,不仅可以减少系统呼叫,还可以避免压垮公共缓存集群。 增加系统限流保护,防止最坏情况。 经过这些优化,系统架构变成了下图。在这里,我们对页面进行了进一步的静态化。在第二次杀戮过程中,我们不需要刷新整个页面,而只需要向服务器要求很少的动态数据。此外,最关键的细节和交易系统还增加了本地缓存,以提前缓存第二次杀戮商品的信息,并独立部署热点数据库等。
图 2 进一步改造后的系统架构
从之前的升级来看,你需要定制的地方越多,也就是说,它就越不通用。例如,在每台机器的内存中缓存秒杀商品显然不适合同时杀死太多商品,因为单台机器的内存总是有限的。因此,为了实现最终的性能,我们必须牺牲其他地方(如通用性、易用性、成本等)。
总结 让我们回顾一下以下内容。我首先介绍了构建大并发、高性能、高可用系统的一些通用优化理念,并抽象总结为4 要 1 不要原则,也就是说,数据应该尽可能少,请求应该尽可能少,路径应该尽可能短,依赖应该尽可能少,没有单点。当然,你必须努力工作向,具体操作时还是要密切结合实际的场景和具体条件来进行。
然后,我给出了实际构建秒杀系统时,根据不同级别的流量,由简单到复杂打造的几种系统架构,希望能供你参考。当然,这里面我没有说具体的解决方案,比如缓存用什么、页面静态化用什么,因为这些对于架构来说并不重要,作为架构师,你应该时刻提醒自己主线是什么。
说了这么多,总体上我希望给你一个方向,就是想构建大并发、高性能、高可用的系统应该从哪几个方向上去努力,然后在不同性能要求的情况下系统架构应该从哪几个方面去做取舍。同时你也要明白,越追求极致性能,系统定制开发就会越多,同时系统的通用性也就会越差。
最后,欢迎你在评论区和我分享你在设计秒杀系统时的一些经验和思考,你的经验对我们这个专栏来说也很重要。