秒杀,既有需求真实且迫切的用户,也有试图牟利的黄牛。系统挑战,就是相较于以往千倍万倍的用户规模,可能是真人可能是机器人,在同一瞬间对系统发起冲击,需要海量的计算资源才能支撑。
**秒杀系统的设计套路往往适用于其他高并发场景,具有较高的借鉴价值。**同时,其特殊的挑战和需求,需要架构师在设计中权衡考量,这也有助于培养个人在权衡取舍方面的能力。
好比治水:
分流,让支流分摊压力,隔离风险。软件设计就是系统隔离,分割流量
建水库存储洪水,再缓慢排出,削峰填谷。软件设计就是无损消峰
拓宽河道,清除淤沙提高水流流速。软件设计就是性能优化,如多级缓存,特定场景的高性能库存扣减
一个请求打到服务器的基本链路:
DNS->网关->前端/后端
流量峰值也应该逐层减少:
秒杀活动因其高峰值特性,一般把它隔离出来成一个独立的秒杀系统(常规服务都是按领域特性做纵切,但这里按品类做横切,带有秒杀活动标的商品将会分流到独立的秒杀系统集群)。
考虑到交易系统体量很大,若为秒杀品类把整个交易系统复制一份,成本过大。所以把隔离区分为物理隔离和逻辑隔离:
先采用独立的秒杀域名和nginx集群(物理隔离)。这样:
可以隔离流量大头,防止峰值冲击交易系统**(非功能性需求)**
能灵活扩展,针对不同时段的流量预估扩展nginx及后边服务的规模**(非功能性需求)**
能灵活增减私有的防控逻辑,而不影响原交易系统**(定制化逻辑)**
接着,将商详页和下单页独立部署(前端+BFF,物理隔离)。基于秒杀活动的玩法特征,海量用户在活动快开始时会反复刷新商详页,在活动开始时又会瞬时并发到访问下单页,所以这俩页面都是承受流量冲击的大头,需隔离开**(非功能性需求)。同时因秒杀活动特性,商品属极端供不应求的场景,卖家占优势,所以可做服务降级,以降低计算资源消耗、提高性能(定制化逻辑)**。如:
秒杀商品不怕客户不支付的原因:
最后,商品购买成功还需要依赖,订单系统创建订单,库存系统扣减商品库存,结算系统确认支付等等步骤。到达这里,流量相对已较平稳,且逻辑上没啥定制化诉求(压力小,没必要围绕性能做定制化),所以采用逻辑隔离复用原交易系统的集群。逻辑隔离两种实现思路:
Q:为啥集群节点少了,出现故障发生过载的可能就提高?
A:好比公里原本4条道能并行4辆车,现在给按车辆类型分成了机动车和公交车专用,机动车道2条。如果其中1条机动车道发生车祸,原本分散在2条道上的车流就要汇聚在1条道,原本顺畅的通行可能立马就开始堵车了。
在系统的多个层级进行数据缓存,以提高响应效率,高并发架构中最常用方案之一。
一般将静态资源挂到CDN上,借助CDN来分流和提高响应效率。以秒杀系统为例,就是将秒杀前端系统的商详页和下单页缓存到CDN网络上。
借助CDN的用户请求链路:
如果用户终端有页面缓存就走终端本地缓存,没有就请求远端CDN的域名(静态资源走CDN域名),请求来到DNS调度的节点,调度一个最近的CDN节点,如果该CDN节点有页面缓存则返回,没有则向缘站发起溯源,请求就会走普通链路过秒杀系统ng到秒杀系统前端。
网关这个有多种组合情况,最简单的就是一个接入层网关加一个应用层网关,比如:ISV(四层)-> Nginx(七层)。以这个为例,这里的缓存优化主要看接入层的负载均衡算法和应用层的本地缓存和集中内存缓存。
缓存还要提LB算法,是因为节点的本地缓存的有效性和LB算法强绑定。常用LB算法有轮询(也叫取模)和一致性哈希:
所以想运用本地缓存强依赖业务运营,需对每个热点商品key有较为准确的流量预估,并人为的组合这些商品key,进而控制流量均匀的落到每个应用层Nginx上(其实就是数据分片,然后每片数据流量一致)。这非常困难,所以笔者认为,还是采用轮询加集中内存缓存比较简单有效。
从接入层开始带有本地缓存和集中内存缓存的请求链路:
应用层ngnix->秒杀系统BFF->订单服务
其实两两组合和网关层是一样的场景。应用层ngnix基于ngnix的负载均衡转发请求到秒杀系统BFF,秒杀系统BFF基于RPC框架的负载均衡转发请求到订单服务。都面临着负载均衡策略选择和是否启用本地缓存的问题。不一样的点只是缓存的粒度和启用缓存的技术栈选择。
因为缓存分散到多层,很难用单一技术栈应对缓存失效问题,但都等到缓存过期,这种更新时延较长又不一定能被业务接受。
有个做法是基于DB的binlog监听,各层监听自己相关的binlog信息,在发生缓存被变更的情况时,及时让集成内存的缓存失效。
本地缓存在这里还有个缺陷,就是缓存失效时需要广播到所有节点,让每个节点都失效,对于频繁变更的热key就可能产生消息风暴。
为流量峰值准备对应的服务集群,首先成本太高,接着单纯的水平扩展也不一定能做到(分布式架构存在量变引起质变的问题,资源扩展到一定量级,原先的技术方案整个就不适用了。
如当集群节点太多,服务注册发现可能会有消息风暴;出入口的带宽出现瓶颈,需要在部署上分流)。更别说这个峰值也不受控制,想要高枕无忧就会有很高的冗余浪费。
所以一般采用消峰:
MQ依赖三个特性可以做到平滑的最终一致,分别是:
以秒杀系统BFF下单操作向订单服务创建订单为例。如果没有消息队列(MQ),同时有100W个创建请求,订单系统就必须承担100W个并行连接的压力。但是,如果使用了MQ,那么100W个创建请求的压力将全部转移到MQ服务端,订单系统只需要维持64个并行连接,以稳定地消费MQ服务端的消息。
这样一来,订单系统的集群规模就可以大大减小,而且更重要的是,系统的稳定性得到了保障。由于并行连接数的减少,资源竞争也会降低,**整体响应效率也会提高,**就像在食堂排队打饭一样,有序排队比乱抢效率更高。但是,**用户体验可能会受到影响,**因为点击抢购后可能会收到排队提示(其实就是友好提示),需要延迟几十秒甚至几分钟才能收到抢购结果。
两层好处:
基本实现步骤:
请求到来时生成1串6位随机字符串 verification_code
用特定前缀拼接用户ID作为key,verification_code做为value存redis,超时5s
生成一个图片,将 verification_code 写到图片上,返回给用户
用户输入图片中字符串
从redis里面取出 verification_code 做比对,如果一致,执行下单操作
但这样其实是可以用暴力破解的,比如,用机器仿照一个用户发起10W个请求携带不同的6位随机字符。所以校验验证码时可以使用 GETDEL ,让验证码校验无论对错都让验证码失效。
基本实现思路和验证码几乎一样。差别在于,问答题的题库要提前生成,请求到来时从题库中拿到一组问题和答案。然后把答案存redis,问题塞到图片里返回给用户。
验证码和问答题具有很好的消峰效果。特别是问答题,想要提高消峰效果只要提高问题难度就行,例如,笔者曾经在12306上连续错了十几次问答题。但是这也是用户体验有损的,例如,虽然笔者当初未能成功抢到票而感到沮丧,但这魔性的题库依然把笔者成功逗笑。
无损消峰,无损了流量,但损失了用户体验。现如今技术水平在不断进步,解决方法在增多,这些有损用户体验的技术方案可能都会慢慢退出历史舞台,就像淘宝取消618预售。
关注我,紧跟本系列专栏文章,咱们下篇再续!
作者简介:魔都架构师,多家大厂后端一线研发经验,在分布式系统设计、数据平台架构和AI应用开发等领域都有丰富实践经验。
各大技术社区头部专家博主。具有丰富的引领团队经验,深厚业务架构和解决方案的积累。
负责:
- 中央/分销预订系统性能优化
- 活动&券等营销中台建设
- 交易平台及数据中台等架构和开发设计
- 车联网核心平台-物联网连接平台、大数据平台架构设计及优化
- LLM Agent应用开发
- 区块链应用开发
- 大数据开发挖掘经验
- 推荐系统项目
目前主攻市级软件项目设计、构建服务全社会的应用系统。