抗住双11的秒杀系统如何设计?
秒杀大家都不陌生。自 2011 年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见。
图片来自 Pexels
简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。
从架构视角来看,秒杀系统本质上是一个高性能、高一致、高可用的三高系统。而打造并维护一个超大流量的秒杀系统需要进行哪些关注,就是本文讨论的话题。
整体思考
首先从高维度出发,整体思考问题。秒杀无外乎解决两个核心问题,一是并发读,一是并发写,对应到架构设计,就是高可用、一致性和高性能的要求。
关于秒杀系统的设计思考,本文即基于此 3 层依次推进,简述如下:
高性能。秒杀涉及高读和高写的支持,如何支撑高并发,如何抵抗高 IOPS?
核心优化理念其实是类似的:高读就是尽量“少读”或"读少",高写就是数据拆分。本文将从动静分离、热点优化以及服务端性能优化 3 个方面展开。
一致性。秒杀的核心关注是商品库存,有限的商品在同一时间被多个请求同时扣减,而且要保证准确性,显而易见是一个难题。
如何做到既不多又不少?本文将从业界通用的几种减库存方案切入,讨论一致性设计的核心逻辑。
高可用。大型分布式系统在实际运行过程中面对的工况是非常复杂的,业务流量的突增、依赖服务的不稳定、应用自身的瓶颈、物理资源的损坏等方方面面都会对系统的运行带来大大小小的的冲击。
如何保障应用在复杂工况环境下还能高效稳定运行,如何预防和面对突发问题,系统设计时应该从哪些方面着手?本文将从架构落地的全景视角进行关注思考。
高性能
动静分离
大家可能会注意到,秒杀过程中你是不需要刷新整个页面的,只有时间在不停跳动。
这是因为一般都会对大流量的秒杀系统做系统的静态化改造,即数据意义上的动静分离。
动静分离三步走:
数据拆分
静态缓存
数据整合
①数据拆分
动静分离的首要目的是将动态页面改造成适合缓存的静态页面。因此第一步就是分离出动态数据,主要从以下 2 个方面进行:
用户。用户身份信息包括登录状态以及登录画像等,相关要素可以单独拆分出来,通过动态请求进行获取;与之相关的广平推荐,如用户偏好、地域偏好等,同样可以通过异步方式进行加载。
时间。秒杀时间是由服务端统一管控的,可以通过动态请求进行获取。
②静态缓存
怎么缓存
哪里缓存
而作为缓存键,URL 唯一化是必不可少的,只是对于商品系统,URL 天然是可以基于商品 ID 来进行唯一标识的,比如淘宝的:
https://item.taobao.com/item.htm?id=xxxx
浏览器
CDN
服务端
失效问题。任何一个缓存都应该是有时效的,尤其对于一个秒杀场景。所以,系统需要保证全国各地的 CDN 在秒级时间内失效掉缓存信息,这实际对 CDN 的失效系统要求是很高的。
命中率问题。高命中是缓存系统最为核心的性能要求,不然缓存就失去了意义。如果将数据放到全国各地的 CDN ,势必会导致请求命中同一个缓存的可能性降低,那么命中率就成为一个问题。
临近访问量集中的地区
距离主站较远的地区
节点与主站间网络质量良好的地区
部署方式如下图所示:
③数据整合
ESI 方案:Web 代理服务器上请求动态数据,并将动态数据插入到静态页面中,用户看到页面时已经是一个完整的页面。这种方式对服务端性能要求高,但用户体验较好。
CSI 方案:Web 代理服务器上只返回静态页面,前端单独发起一个异步 JS 请求动态数据。这种方式对服务端性能友好,但用户体验稍差。
小结:动静分离对于性能的提升,抽象起来只有两点,一是数据要尽量少,以便减少没必要的请求;二是路径要尽量短,以便提高单次请求的效率。具体方法其实就是基于这个大方向进行的。
热点优化
①热点操作
②热点数据
热点识别:热点数据分为静态热点和动态热点。
具体如下:
静态热点:能够提前预测的热点数据。大促前夕,可以根据大促的行业特点、活动商家等纬度信息分析出热点商品,或者通过卖家报名的方式提前筛选。
另外,还可以通过技术手段提前预测,例如对买家每天访问的商品进行大数据计算,然后统计出 TOP N 的商品,即可视为热点商品
动态热点:无法提前预测的热点数据。冷热数据往往是随实际业务场景发生交替变化的,尤其是如今直播卖货模式的兴起——带货商临时做一个广告,就有可能导致一件商品在短时间内被大量购买。
由于此类商品日常访问较少,即使在缓存系统中一段时间后也会被逐出或过期掉,甚至在 DB 中也是冷数据。瞬时流量的涌入,往往导致缓存被击穿,请求直接到达 DB,引发 D B压力过大。
异步采集交易链路各个环节的热点 Key 信息,如 Nginx 采集访问 URL 或 Agent 采集热点日志(一些中间件本身已具备热点发现能力),提前识别潜在的热点数据。
聚合分析热点数据,达到一定规则的热点数据,通过订阅分发推送到链路系统,各系统根据自身需求决定如何处理热点数据,或限流或缓存,从而实现热点保护。
热点数据采集最好采用异步方式,一方面不会影响业务的核心交易链路,一方面可以保证采集方式的通用性。
热点发现最好做到秒级实时,这样动态发现才有意义,实际上也是对核心节点的数据采集和分析能力提出了较高的要求。
热点隔离:热点数据识别出来之后,第一原则就是将热点数据隔离出来,不要让 1% 影响到另外的 99%。
可以基于以下几个层次实现热点隔离:
业务隔离。秒杀作为一种营销活动,卖家需要单独报名,从技术上来说,系统可以提前对已知热点做缓存预热。
系统隔离。系统隔离是运行时隔离,通过分组部署和另外 99% 进行分离,另外秒杀也可以申请单独的域名,入口层就让请求落到不同的集群中。
数据隔离。秒杀数据作为热点数据,可以启用单独的缓存集群或者 DB 服务组,从而更好的实现横向或纵向能力扩展。
热点优化:热点数据隔离之后,也就方便对这 1% 的请求做针对性的优化。
方式无外乎两种:
缓存:热点缓存是最为有效的办法。如果热点数据做了动静分离,那么可以长期缓存静态数据。
限流:流量限制更多是一种保护机制。需要注意的是,各服务要时刻关注请求是否触发限流并及时进行 Review。
小结:数据的热点优化与动静分离是不一样的,热点优化是基于二八原则对数据进行了纵向拆分,以便进行针对性地处理。
系统优化
总结一下
一致性
减库存的方式
下单减库存。买家下单后,扣减商品库存。下单减库存是最简单的减库存方式,也是控制最为精确的一种。
付款减库存。买家下单后,并不立即扣减库存,而是等到付款后才真正扣减库存。但因为付款时才减库存,如果并发比较高,可能出现买家下单后付不了款的情况,因为商品已经被其他人买走了。
预扣库存。这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 15 分钟),超过这段时间,库存自动释放,释放后其他买家可以购买。
减库存的问题
①下单减库存
②付款减库存
③预扣库存
小结:减库存的问题主要体现在用户体验和商业诉求两方面,其本质原因在于购物过程存在两步甚至多步操作,在不同阶段减库存,容易存在被恶意利用的漏洞。
实际如何减库存
卖的出去:恶意下单的解决方案主要还是结合安全和反作弊措施来制止。比如,识别频繁下单不付款的买家并进行打标,这样可以在打标买家下单时不减库存。
再比如为大促商品设置单人最大购买件数,一人最多只能买 N 件商品;又或者对重复下单不付款的行为进行次数限制阻断等。
避免超卖:库存超卖的情况实际分为两种。对于普通商品,秒杀只是一种大促手段,即使库存超卖,商家也可以通过补货来解决。
而对于一些商品,秒杀作为一种营销手段,完全不允许库存为负,也就是在数据一致性上,需要保证大并发请求时数据库中的库存字段值不能为负。
一般有多种方案:
一是通过事务来判断,即保证减后库存不能为负,否则就回滚。
二是直接设置数据库字段类型为无符号整数,这样一旦库存为负就会在执行 SQL 时报错。
三是使用 CASE WHEN 判断语句。
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
一致性性能的优化
①高并发读
②高并发写
应用层排队。通过缓存加入集群分布式锁,从而控制集群对数据库同一行记录进行操作的并发度,同时也能控制单个商品占用数据库连接的数量,防止热点商品占用过多的数据库连接。
数据层排队。应用层排队是有损性能的,数据层排队是最为理想的。业界中,阿里的数据库团队开发了针对 InnoDB 层上的补丁程序(patch),可以基于 DB 层对单行记录做并发排队,从而实现秒杀场景下的定制优化。
注意,排队和锁竞争是有区别的,如果熟悉 MySQL 的话,就会知道 InnoDB 内部的死锁检测,以及 MySQL Server 和 InnoDB 的切换都是比较消耗性能的。
另外阿里的数据库团队还做了很多其他方面的优化,如 COMMIT_ON_SUCCESS 和 ROLLBACK_ON_FAIL 的补丁程序,通过在 SQL 里加入提示(hint),实现事务不需要等待实时提交,而是在数据执行完最后一条 SQL 后,直接根据 TARGET_AFFECT_ROW 的结果进行提交或回滚,减少网络等待的时间(毫秒级)。
目前阿里已将包含这些补丁程序的 MySQL 开源:AliSQL
https://github.com/alibaba/AliSQL?spm=a2c4e.10696291.0.0.34ba19a415ghm4
小结:高读和高写的两种处理方式大相径庭。读请求的优化空间要大一些,而写请求的瓶颈一般都在存储层,优化思路的本质还是基于 CAP 理论做平衡。
总结一下
高可用
流量削峰
①答题
防止作弊。早期秒杀器比较猖獗,存在恶意买家或竞争对手使用秒杀器扫货的情况,商家没有达到营销的目的,所以增加答题来进行限制。
延缓请求。零点流量的起效时间是毫秒级的,答题可以人为拉长峰值下单的时长,由之前的 <1s 延长到 <10s。
这个时间对于服务端非常重要,会大大减轻高峰期并发压力;另外,由于请求具有先后顺序,答题后置的请求到来时可能已经没有库存了,因此根本无法下单,此阶段落到数据层真正的写也就非常有限了。
②排队
线程池加锁等待
本地内存蓄洪等待
本地文件序列化写,再顺序读
请求积压。流量高峰如果长时间持续,达到了队列的水位上限,队列同样会被压垮,这样虽然保护了下游系统,但是和请求直接丢弃也没多大区别。
用户体验。异步推送的实时性和有序性自然是比不上同步调用的,由此可能出现请求先发后至的情况,影响部分敏感用户的购物体验。
③过滤
读限流:对读请求做限流保护,将超出系统承载能力的请求过滤掉。
读缓存:对读请求做数据缓存,将重复的请求过滤掉。
写限流:对写请求做限流保护,将超出系统承载能力的请求过滤掉。
写校验:对写请求做一致性校验,只保留最终的有效数据。
小结:系统可以通过入口层的答题、业务层的排队、数据层的过滤达到流量削峰的目的,本质是在寻求商业诉求与架构性能之间的平衡。
Plan B
架构阶段:考虑系统的可扩展性和容错性,避免出现单点问题。例如多地单元化部署,即使某个 IDC 甚至地市出现故障,仍不会影响系统运转。
编码阶段:保证代码的健壮性,例如 RPC 调用时,设置合理的超时退出机制,防止被其他系统拖垮,同时也要对无法预料的返回错误进行默认的处理。
测试阶段:保证 CI 的覆盖度以及 Sonar 的容错率,对基础质量进行二次校验,并定期产出整体质量的趋势报告。
发布阶段:系统部署最容易暴露错误,因此要有前置的 Checklist 模版、中置的上下游周知机制以及后置的回滚机制。
运行阶段:系统多数时间处于运行态,最重要的是运行时的实时监控,及时发现问题、准确报警并能提供详细数据,以便排查问题。
故障发生:首要目标是及时止损,防止影响面扩大,然后定位原因、解决问题,最后恢复服务。
预防:建立常态压测体系,定期对服务进行单点压测以及全链路压测,摸排水位。
管控:做好线上运行的降级、限流和熔断保护。需要注意的是,无论是限流、降级还是熔断,对业务都是有损的,所以在进行操作前,一定要和上下游业务确认好再进行。
就拿限流来说,哪些业务可以限、什么情况下限、限流时间多长、什么情况下进行恢复,都要和业务方反复确认。
监控:建立性能基线,记录性能的变化趋势;建立报警体系,发现问题及时预警。
恢复:遇到故障能够及时止损,并提供快速的数据订正工具,不一定要好,但一定要有。
总结一下
个人总结
作者:阿哲
编辑:陶家龙、孙淑娟
出处:https://segmentfault.com/a/1190000020970562
精彩文章推荐: