高并发业务下的无损技术方案设计

0 前言

秒杀,既有需求真实且迫切的用户,也有试图牟利的黄牛。系统挑战,就是相较于以往千倍万倍的用户规模,可能是真人可能是机器人,在同一瞬间对系统发起冲击,需要海量的计算资源才能支撑。

秒杀系统的设计套路往往适用于其他高并发场景,具有较高的借鉴价值。 同时,其特殊的挑战和需求,需要架构师在设计中权衡考量,这也有助于培养个人在权衡取舍方面的能力。

1 无损技术方案

好比治水:

  • 分流,让支流分摊压力,隔离风险。软件设计就是系统隔离,分割流量

  • 建水库存储洪水,再缓慢排出,削峰填谷。软件设计就是无损消峰

  • 拓宽河道,清除淤沙提高水流流速。软件设计就是性能优化,如多级缓存,特定场景的高性能库存扣减

一个请求打到服务器的基本链路:

DNS->网关->前端/后端

流量峰值也应该逐层减少:

1.1 系统隔离

秒杀活动因其高峰值特性,一般把它隔离出来成一个独立的秒杀系统(常规服务都是按领域特性做纵切,但这里按品类做横切,带有秒杀活动标的商品将会分流到独立的秒杀系统集群)。

考虑到交易系统体量很大,若为秒杀品类把整个交易系统复制一份,成本过大。所以把隔离区分为物理隔离和逻辑隔离:

  • 需定制化逻辑的能力和有特殊非功能性要求的能力剥离出来做物理隔离
  • 标准化且没有特殊非功能要求的能力采用逻辑隔离
部署架构图

先采用独立的秒杀域名和nginx集群(物理隔离)。这样:

  • 可以隔离流量大头 ,防止峰值冲击交易系统**(非功能性需求)**

  • 灵活扩展 ,针对不同时段的流量预估扩展nginx及后边服务的规模**(非功能性需求)**

  • 灵活增减私有的防控逻辑 ,而不影响原交易系统**(定制化逻辑)**

接着,将商详页和下单页独立部署(前端+BFF,物理隔离) 。基于秒杀活动的玩法特征,海量用户在活动快开始时会反复刷新商详页,在活动开始时又会瞬时并发到访问下单页,所以这俩页面都是承受流量冲击的大头,需隔离开**(非功能性需求)** 。同时因秒杀活动特性,商品属极端供不应求的场景,卖家占优势,所以可做服务降级,以降低计算资源消耗、提高性能(定制化逻辑)。如:

  • 商详页可将履约时效拿掉,不再计算预计多久能到货。还可拿掉评价信息,不再展示评价
  • 下单页可不再计算优惠金额分摊,秒杀商品不参与任何叠加优惠活动。仅保留必要信息,如商品信息,商品主图,购买按钮,下单按钮等
  • 结算页、收银台,则看情况,若流量压力不大,可不用物理隔离:
    • 支付扣减库存下单的压力会直接传递到结算
    • 但下单扣减就不需要并发支付,仅抢到的用户需结算,压力就很小
    • 为降低流量影响面积,这里假设下单扣减,毕竟秒杀商品也不怕客户下单后不支付

秒杀商品不怕客户不支付的原因

  • 商品吸引力:秒杀商品往往是极具吸引力的折扣商品,抢购用户通常都会完成支付,未支付的比例较低
  • 多次释放机制:即使有用户不支付,库存会在短时间内释放出来,其他用户可以继续抢购,这个过程在秒杀活动的激烈程度下通常很快完成。

最后,商品购买成功还需要依赖,订单系统创建订单,库存系统扣减商品库存,结算系统确认支付等等步骤。到达这里,流量相对已较平稳,且逻辑上没啥定制化诉求(压力小,没必要围绕性能做定制化),所以采用逻辑隔离复用原交易系统的集群。逻辑隔离两种实现思路:

  • 依赖限流框架,如在订单系统设置来源是秒杀系统BFF的创建订单请求,TPS不超100,并发连接数不能超过20
  • 依赖RPC框架 ,RPC框架可设置分组,只把订单系统集群里面部分服务节点设置成"秒杀组",再把秒杀服务BFF的客户端也设置为"秒杀组",那么秒杀系统的流量就只会打到订单系统集群里面属于"秒杀组"的节点上。这种隔离方式分割了集群,集群节点少了,出现故障发生过载的可能就提高了,可能会导致秒杀系统不可用

Q:为啥集群节点少了,出现故障发生过载的可能就提高?

A:好比公里原本4条道能并行4辆车,现在给按车辆类型分成了机动车和公交车专用,机动车道2条。如果其中1条机动车道发生车祸,原本分散在2条道上的车流就要汇聚在1条道,原本顺畅的通行可能立马就开始堵车了。

1.2 多级缓存

在系统的多个层级进行数据缓存,以提高响应效率,高并发架构中最常用方案之一。

1.2.1 DNS层

一般将静态资源挂到CDN上,借助CDN来分流和提高响应效率。以秒杀系统为例,就是将秒杀前端系统的商详页和下单页缓存到CDN网络上。

借助CDN的用户请求链路:

如果用户终端有页面缓存就走终端本地缓存,没有就请求远端CDN的域名(静态资源走CDN域名),请求来到DNS调度的节点,调度一个最近的CDN节点,如果该CDN节点有页面缓存则返回,没有则向缘站发起溯源,请求就会走普通链路过秒杀系统ng到秒杀系统前端。

1.2.2 网关层

网关这个有多种组合情况,最简单的就是一个接入层网关加一个应用层网关,比如:ISV(四层)-> Nginx(七层)。以这个为例,这里的缓存优化主要看接入层的负载均衡算法和应用层的本地缓存和集中内存缓存。

缓存还要提LB算法,是因为节点的本地缓存的有效性和LB算法强绑定。常用LB算法有轮询(也叫取模)和一致性哈希:

  • 轮询可以让请求分发更均衡 ,但同个缓存key的请求不一定会路由到同个应用层Nginx上,Nginx的本地缓存命中率低
  • 一致性哈希可让同个缓存key路由到同个应用层Nginx上,Nginx的本地缓存命中率高,但其请求分发不均衡容易出现单机热点问题。有种做法是设置一个阈值,当单节点请求超过阈值时改为轮询,可以算是自适应性负载均衡的变种。但这种基于阈值判断的做法在应对真正的高并发时效果并不理想。

所以想运用本地缓存强依赖业务运营,需对每个热点商品key有较为准确的流量预估,并人为的组合这些商品key,进而控制流量均匀的落到每个应用层Nginx上(其实就是数据分片,然后每片数据流量一致)。这非常困难,所以笔者认为,还是采用轮询加集中内存缓存比较简单有效。

从接入层开始带有本地缓存和集中内存缓存的请求链路:

1.2.3 服务层
应用层ngnix->秒杀系统BFF->订单服务

其实两两组合和网关层是一样的场景。应用层ngnix基于ngnix的负载均衡转发请求到秒杀系统BFF,秒杀系统BFF基于RPC框架的负载均衡转发请求到订单服务。都面临着负载均衡策略选择和是否启用本地缓存的问题。不一样的点只是缓存的粒度和启用缓存的技术栈选择。

1.2.4 多级缓存失效

因为缓存分散到多层,很难用单一技术栈应对缓存失效问题,但都等到缓存过期,这种更新时延较长又不一定能被业务接受。

有个做法是基于DB的binlog监听,各层监听自己相关的binlog信息,在发生缓存被变更的情况时,及时让集成内存的缓存失效。

本地缓存在这里还有个缺陷,就是缓存失效时需要广播到所有节点,让每个节点都失效,对于频繁变更的热key就可能产生消息风暴。

1.3 无损消峰

为流量峰值准备对应的服务集群,首先成本太高,接着单纯的水平扩展也不一定能做到(分布式架构存在量变引起质变的问题,资源扩展到一定量级,原先的技术方案整个就不适用了。

如当集群节点太多,服务注册发现可能会有消息风暴;出入口的带宽出现瓶颈,需要在部署上分流)。更别说这个峰值也不受控制,想要高枕无忧就会有很高的冗余浪费。

所以一般采用消峰:

  • 直接断头,把超出负荷的流量直接都丢弃掉 ,也就是我们常见的限流,也称为有损消峰(如果这是大促的订单,砍掉的可能都是钱,这个有损是真的资损)
  • 分流,也叫消峰填谷, 通过技术或者业务手段将请求错开,铺到更长的时间线上,从而降低峰值,常见的有MQ异步消费和验证码问答题。先谈无损消峰
1.3.1 MQ异步消费

MQ依赖三个特性可以做到平滑的最终一致,分别是:

  • 有消息堆积才能起到蓄水池的效果,在出水口流速恒定的情况下能接住入水口瞬时的大流量
  • 有匀速消费才能让下游集群的流量压力恒定,不会被冲击
  • 有至少成功一次,才能保证事物最终一致

以秒杀系统BFF下单操作向订单服务创建订单为例。如果没有消息队列(MQ),同时有100W个创建请求,订单系统就必须承担100W个并行连接的压力。但是,如果使用了MQ,那么100W个创建请求的压力将全部转移到MQ服务端,订单系统只需要维持64个并行连接,以稳定地消费MQ服务端的消息。

这样一来,订单系统的集群规模就可以大大减小 ,而且更重要的是,系统的稳定性得到了保障 。由于并行连接数的减少,资源竞争也会降低,整体响应效率也会提高, 就像在食堂排队打饭一样,有序排队比乱抢效率更高。但是,**用户体验可能会受到影响,**因为点击抢购后可能会收到排队提示(其实就是友好提示),需要延迟几十秒甚至几分钟才能收到抢购结果。

1.3.2 验证码问答题

两层好处:

  • 消峰,用户0.5秒内并发的下单事件,因为个人的手速差异,被平滑的分散到几秒甚至几十秒中
  • 防刷,提高机器作弊成本
验证码

基本实现步骤:

  • 请求到来时生成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应用开发

  • 区块链应用开发

  • 大数据开发挖掘经验

  • 推荐系统项目

    目前主攻市级软件项目设计、构建服务全社会的应用系统。 本文由博客一文多发平台 OpenWrite 发布!

相关推荐
徐*红21 分钟前
java 线程池
java·开发语言
尚学教辅学习资料21 分钟前
基于SSM的养老院管理系统+LW示例参考
java·开发语言·java毕设·养老院
2401_8576363921 分钟前
计算机课程管理平台:Spring Boot与工程认证的结合
java·spring boot·后端
1 9 J23 分钟前
Java 上机实践4(类与对象)
java·开发语言·算法
Code apprenticeship24 分钟前
Java面试题(2)
java·开发语言
憨子周1 小时前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
霖雨3 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404193 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言