尖峰场景的特征
秒杀场景是电商网站定期举办的活动。这个活动有明确的起止时间,参与互动的产品都是提前定义好的。参与秒杀的产品数量也有限。同时会提供一个秒杀入口,供用户通过这个入口抢购。
总结峰值场景的特征:
定时开始,
秒杀时大量用户会在同一时间,抢购同一商品,网站瞬时流量激增。
库存有限,
秒杀下单数量远远大于库存数量,只有少部分用户能够秒杀成功。
操作可靠,
秒杀业务流程比较简单,一般就是下订单减库存。库存就是用户争夺的“资源”,实际被消费的“资源”不能超过计划要售出的“资源”,也就是不能被“超卖”。
系统隔离的设计思想
在分析了秒杀的特点后,我们发现秒杀活动是有计划性的,短时间内会爆发大量的请求。为了不影响现有业务系统的正常运行,我们需要将其与现有系统隔离开来。
即使秒杀活动出了问题,也不会影响现有系统。隔离的设计思路可以从三个维度来考虑。
业务隔离
技术隔离
数据库隔离
业务隔离
既然秒杀是一项活动,那就一定不同于常规业务。我们可以把它看作一个独立的项目。活动开始前,最好设计一个“热场”。
“热场”的形式多种多样,比如分享活动领券、领秒杀等等。“热场”的形式不重要,重要的是通过它获得一些备考信息。
比如:有可能参与的用户数量,他们的地域分布,他们感兴趣的商品。为以下技术架构提供数据支持。
技术隔离
技术隔离架构图
随着准备工作的进行,需要从技术上考虑以下几个方面:
客户端,
前端秒杀页面使用专门的页面,这些页面包括静态的 HTML 和动态的 JS,他们都需要在 CDN 上缓存。
接入层,
加入过滤器专门处理秒杀请求,即使我们扩展再多的应用,使用再多的应用服务器,部署再多的负载均衡器,都会遇到支撑不住海量请求的时候。所以,在这一层我们要考虑的是如何做好限流,当超过系统承受范围的时候,需要果断阻止请求的涌入。
应用层,
瞬时的海量请求好比请求的“高峰”,我们架构系统的目的就是“削峰”。需要使用服务集群和水平扩展,让“高峰”请求分流到不同的服务器进行处理。同时,还会利用缓存和队列技术减轻应用处理的压力,通过异步请求的方式做到最终一致性。由于是多线程操作,而且商品的额度有限,为了解决超卖的问题,需要考虑进程锁的问题。
数据库隔离
尖峰活动持续时间短,瞬时数据量大。为了不影响现有数据库的正常业务,可以建立新的数据库或表进行处理。
秒杀结束后,需要将这部分数据同步到主业务系统或者查表。如果数据量极其巨大,达到几千万甚至上亿,建议使用子表或者子数据库。
客户设计
在上面提到的三个隔离维度中,我们最关注的是技术维度。如果浏览器/客户端是用户接触“秒杀系统”的入口,就需要在这一层提供缓存数据。
在设计之初,我们会为秒杀产品生成专门的产品页面和订单页面。这些页面主要是静态HTML,包含尽可能少的动态信息。
从业务角度来说,这些产品的信息早就被用户熟知,他们关心的是如何在秒杀的时候快速下单。
由于产品详情页面和订单页面都是静态生成的,因此有必要定义一个URL,并在高峰开始前打开该URL供用户访问。
为了防止“程序员或者内部人员”作弊,这里的地址可以通过时间戳和哈希算法生成,也就是说这个地址只有系统知道,只有在快速秒杀之前才会被系统发出。
有人说如果浏览器/客户端存储的都是静态页面,那么“控制开始订购”和发送“订购请求”这两个按钮是静态的吗?
答案是否定的,其实静态页面方便客户端缓存,点餐的动作和点餐时间的控制还是在服务器端。
只是以JS文件的方式发送给客户端,这部分JS会在它即将杀出来之前下载到客户端。
因为它的业务逻辑很少,基本上只包括时间、用户信息、商品信息等等。所以它对网络的要求并不高。
同时在网络设计上,我们会在CDN上同时缓存JS和HTML,让用户可以从离自己最近的CDN服务器上获取这些信息。
为了防止秒杀程序参与秒杀,会在客户端设计一些问答或者滑块功能,减轻这类机器人对服务器的压力。
道钉系统前端设计图
代理层设计
说完秒杀系统的前端设计,要求自然就到了代理层。由于用户的大量请求,我们需要使用负载均衡和服务器集群来面对空之前的压力。
代理层三大功能示意图
在这一层中,可以进行缓存、过滤和电流限制:
缓存,
以 Nginx 为例,它可以缓存用户的信息。假设用户信息的修改没有那么频繁,即使有类似的修改也可以通过更新服务来刷新。总比从服务器上获取效率要高得多。
过滤,
既然缓存了用户信息,这里就可以过滤掉一些不满足条件的用户。注意,这里的用户信息的过滤和缓存只是一个例子。主要想表达的意思是,可以将一些变化不频繁的数据,提到代理层来缓存,提高响应的效率。同时,还可以根据风控系统返回的信息,过滤一些疑似机器人或者恶意请求。例如:从固定 IP 过来的,频率过高的请求。最重要的就是在这一层,可以识别来自秒杀系统的请求。如果是带有秒杀系统的参数,就要把请求路由到秒杀系统的服务器集群。这样才能和正常的业务系统分割开来。
限流,
每个服务器集群能够承受的压力都是有限的。代理层可以根据服务器集群能够承受的最大压力,设置流量的阀值。阀值的设置可以是动态调整的。例如:集群服务器中有 10 个服务器,其中一台由于压力过大挂掉了。此时就需要调整代理层的流量阀值,将能够处理的请求流量减少,保护后端的应用服务器。当服务器恢复以后,又可以将阀值调回原位。可以通过 Nginx+Lua 合作完成,Lua 从服务注册中心读取服务健康状态,动态调整流量。
应用层设计
「秒杀系统」秒杀什么?它只是一种商品。对于系统来说,就是商品的库存。一旦购买的商品超过库存,就不能再次出售。
超卖预防
超过库存就可以卖给用户,这就是“超卖”,在制度设计上要避免。为了承担繁重的流量,我们使用横向扩展的服务,但是只有一个资源“库存”供它们消费。
为了提高效率,这些库存信息将放在缓存中。以流行的Redis为例。当它用来存储库存信息,被多个线程访问时,就会出现资源竞争。也就是说,分布式程序竞争唯一的资源。为了解决这个问题,我们需要实现分布式锁。
假设有多个应用程序响应用户的订单请求,它们将同时访问Redis中存储的库存信息。他们每接受一次用户的请求,就会从Redis的库存中减去一个商品库存。
当任何一个进程访问Redis中的库存资源时,其他进程都无法访问,所以这里需要考虑锁的情况(乐观、悲观)。
Redis缓存主机清单变量
如果长时间不释放锁,就需要考虑锁的到期时间,设置两个超时时间:
资源本身的超时时间,
一旦资源被使用一段时间还没有被释放,Redis 会自动释放掉该资源给其他服务使用。
服务获取资源的超时时间,
一旦一个服务获取资源一段时间后,不管该服务是否处理完这个资源,都需要释放该资源给其他服务使用。
订单处理流程
这里的“扣款服务”完成了最简单的存货扣款,没有处理过其他项目服务,更不用说访问数据库了。
订单流程图
下面的过程就比较复杂了。我们先看图,按图解释一下:
首先,扣减服务作为下单流程的入口,会先对商品的库存做扣减。同样它会检查商品是否还有库存?由于订单对应的操作步骤比较多,为了让流量变得平滑,这里使用队列存放每个订单请求,等待订单处理服务完成具体业务。订单处理服务实现多线程,或者水平扩展的服务阵列,它们不断**队列中的消息。一旦发现有新订单请求,就取出订单进行后续处理。注意,这里可以加入类似 ZooKeeper 这样的服务调度来帮助,协调服务调度和任务分配。订单处理服务,处理完订单以后会把结果写到数据库。写数据库是 IO 操作,耗时长。所以,在写数据库的同时,会把结果先写入缓存中,这样用户是可以第一时间查询自己是否下单成功了。结果写入数据库,这个操作有可能成功也有可能失败。为了保证数据的最终一致性,我们用订单结果同步的服务不断的对比,缓存和数据库中的订单结果信息。一旦发现不一致,会去做重试操作。如果重试依旧不成功,会重写信息到缓存,让用户知道失败原因。用户下单以后,焦虑地刷新页面查看下单的结果,实际上是读到的缓存上的下单结果信息。虽然,这个信息和最终结果有偏差,但是在秒杀的场景,要求高性能是前提,结果的一致性,可以后期补偿。
数据库设计
说完了秒杀的过程,再来说说数据库设计中应该注意的点。
数据估计
如前所述,尖峰场景需要隔离,这里的隔离包括“业务隔离”。也就是说,在我们杀之前,需要借助商业手段,比如热点实地活动、问卷调查、历史数据分析等。他们估计可能需要存储在这个尖峰中的数据量。
这里有两部分数据需要考虑:
业务数据
日志数据
不言而喻,前者是针对业务系统的。后者用于分析和跟进问题订单,也可用于秒杀结束后的还价。
子表和子库
这些数据的存储需要根据具体情况进行讨论。比如MySQL单表的推荐存储容量是500W记录(经验数字)。
如果估算超过这个数据,建议做一个子表。如果服务的连接数较多,建议划分数据库。
数据隔离
因为大量的数据操作都是插入,所以有少量的修改操作。如果使用关系型数据进行存储,建议使用专门的表进行存储,不建议使用业务系统正在使用的表。
正如开头提到的,数据隔离是必要的。一旦秒杀系统挂机,不会影响正常的业务系统。你要有这个风险意识。除了ID,在表的设计中最好不要设置其他主键,这样才能保证快速插入。
数据合并
因为它存储在一个特殊的表中,所以在spike活动之后,它需要与现有的数据合并。其实交易已经完成了,合并的目的是为了查询。
这个合并需要具体情况具体分析。对于那些“只读”的数据,对于做了读写分离的公司来说,可以导入到阅读专用的数据库或者NoSQL数据库中。
压力测试
如果建立秒杀系统,肯定会上线,所以上线前压力测试必不可少。
我们压力测试的目的是检查系统崩溃的边缘在哪里?制度的极限在哪里?
这样就可以合理设置流量上限。为了保证系统的稳定性,需要丢弃多余的流量。
压力测试方法
合理的测试方法可以帮助我们深入了解系统。这里有两种压力测试方法:
正压力测试
负压力测试
正压测试。每个秒杀活动都会有计划,要用多少服务器资源,要承载多少请求。
你可以继续对这个请求施压,直到系统接近崩溃或者真的崩溃。简单来说,就是补充。
正压测试示意图
负压测试。在系统正常运行时,逐渐减少支持系统的资源(服务器),看系统何时无法支持正常的业务请求。
比如在系统正常运行的情况下,逐步减少服务器或者微服务的数量,观察业务请求。说白了就是减法。
负压测试示意图
压力测试步骤
测试步骤
有了测试方法的加持,我们来看看需要遵循哪些测试步骤。以下操作是常规,在其他系统的压力测试中也可以这么做,供参考。
首先,确定测试目标。与性能测试不同,压力测试的目标是当系统接近崩溃时。比如需要支持500W的访问量。
二、确定关键功能。压力测试其实是有重点的。根据2/8原则,系统中20%的功能使用最多。我们可以对这些核心功能进行压力测试。比如:下单,股票抵扣。
专注于核心服务
第三,确定载荷。这与关键服务的理念是一致的。不是每个服务都有高负载。我们的测试实际上侧重于那些负载较大的服务,或者系统中某些服务的负载在一段时间内有波动。这些都是测试目标。
第四,选择环境。建议构建一个与生产环境相同的环境进行测试。
第五,确定监控点,实际上就是监控关心的参数,比如CPU负载、内存利用率、系统吞吐量等等。
第六,生成负载。这里我们需要从生产环境中获取一些真实的数据作为负载数据源。这部分数据源是根据目标系统的承载需求由脚本驱动的,会对系统造成冲击。
建议使用之前道钉系统的数据或者实际生产系统的数据进行测试。
第七,执行测试。这里主要是带负载测试目标系统和关键部件,返回监控点的数据。
建议团队可以制定一个测试的计划,模拟不同的网络环境和硬件条件,定期进行测试。
第八,分析数据。以测试为目的,分析关键服务的压力测试数据,找出服务的承载极限在哪里。
分析一段时间内有波动或负载较重的业务的数据,得出业务转型的方向。
摘要
Spike系统的特点是大并发,资源有限,操作相对简单,可以访问热数据。因此,我们需要将它与业务、技术和数据隔离开来,以确保它不会影响现有的系统。
所以架构设计需要分几层考虑,从客户请求到数据库存储,再到最后上线前的压力测试。
每个人的简单思维导图。
思维顺序如下:客户端→代理层→应用层→数据库→压力测试:
客户端 90% 静态 HTML+10% 动态 JS;配合 CDN 做好缓存工作。接入层专注于过滤和限流。应用层利用缓存+队列+分布式处理好订单。做好数据的预估,隔离,合并。上线之前记得进行压力测试。
本文来自心已碎♂无心醉投稿,不代表舒华文档立场,如若转载,请注明出处:https://www.chinashuhua.cn/24/508833.html