当前位置: 首页 > 科技观察

实战!如何从零搭建10万级别QPS高流量、高并发的优惠券系统

时间:2023-03-17 14:03:34 科技观察

作者|吴海涛需求背景春节活动期间,多个业务方有发放优惠券的需求,QPS明确发行优惠券需求的水平。所有的优惠券发放、核销、查询都需要一个新的系统来承载。因此,我们需要设计开发一个能够支持10万级QPS的优惠券系统,并维护优惠券的完整生命周期。需求拆解与技术选型需求拆解需要配置优惠券,会涉及到优惠券批次(优惠券模板)的创建、优惠券模板的有效期、优惠券库存信息。发放优惠券,会涉及到优惠券记录的创建和管理(过期时间,状态),所以我们可以简单的把需求拆解成两部分:同时无论是优惠券模板还是优惠券记录,一个open需要查询接口支持查询优惠券模板/优惠券记录。系统选型和中间件决定了基本需求,我们根据需求进一步分析可能使用的中间件和系统的整体组织。存储由于优惠券模板和优惠券记录是需要持久化的数据,还需要支持条件查询,所以我们选择通用的结构化存储MySQL作为存储中间件。缓存因为发放优惠券时需要优惠券模板信息,所以在大流量的情况下不可能每次都从MySQL中获取优惠券模板信息。因此,缓存的引入也是一样的。优惠券库存管理,或者说库存扣减,也是一个高频、实时的操作,所以我们也考虑放在缓存中。主流的缓存Redis可以满足我们的需求,所以我们选择Redis作为缓存中间件。消息队列由于优惠券模板/优惠券记录需要显示过期状态,并根据不同状态进行业务逻辑处理,因此需要引入延迟消息队列来处理优惠??券模板/优惠券状态。RocketMQ支持延迟消息,所以我们选择RocketMQ作为消息队列。系统框架作为下游服务,优惠券发放系统需要被上游服务调用。公司内部服务之间使用RPC服务调用,系统开发语言使用golang,所以我们使用golang服务的RPC框架kitex进行代码编写。我们使用kitex+MySQL+Redis+RocketMQ实现优惠券发放系统,RPC服务部署在公司的docker容器中。系统开发与实践系统设计与整体系统架构的实现从需求拆解部分,我们对要开发的系统有了一个大概的了解。下面给出整体系统架构,包括一些具体的功能。数据结构ER图对应系统架构,我们需要创建对应的MySQL数据存储表。核心逻辑实现优惠券发放:优惠券发放流程分为三部分:参数验证、幂等验证、库存抵扣。通过幂等操作,保证当发券请求错误时,业务方通过重试补偿的方式再次请求,最终只能发一张券,防止资金流失。优惠券过期:优惠券过期是一个状态递增的过程,这里我们使用RocketMQ来实现。由于RocketMQ支持的延迟消息是有最大限制的,而优惠券的有效期不是固定的,可能会超过限制,所以我们循环处理优惠券过期消息,直到优惠券过期。高流量高并发场景下的问题及解决方案在实现了系统的基本功能后,我们来讨论一下系统在高流量高并发场景下可能遇到的一些问题及解决方案。存储瓶颈和解决方案瓶颈:在系统架构中,我们使用MySQL和Redis作为存储组件。我们知道单台服务器的I/O能力终究是有限的。在实际测试过程中,可以得到如下数据:单个MySQL的写入速率约为4000QPS/s。如果超过这个数字,MySQL的I/O延迟将急剧增长。如果MySQL单表记录达到千万条,查询效率会大大降低。如果超过1亿,数据查询就会成为问题。单个Redisshard的写瓶颈在2w左右,读瓶颈在10w左右。解决方案:读写分离。在查询优惠券模板、查询优惠券记录等场景,我们可以将MySQL读写分离,让这部分查询流量通过MySQL读库,从而减轻MySQL写库的查询压力。分割。在软件设计中,有一种分而治之的思想。对于存储瓶颈问题,业界普遍的解决方案是分而治之:流量分散和存储分散,即:分库分表。发放优惠券,说到底就是持久化保存用户的优惠券领取记录。针对MySQL本身的I/O瓶颈,我们可以将MySQL的不同分片部署在不同的服务器上,横向扩展MySQL。这样写请求就会分布在不同的MySQL主机上,可以大大提高MySQL的整体吞吐量。如果向用户发放优惠券,则用户必须检查他或她已获得的优惠券。基于这个逻辑,我们将user_id的后四位作为shardingkey,对用户收到的记录表进行水平拆分,以支持用户维度的优惠券记录查询。每种类型的优惠券都有相应的数量。在给用户发放优惠券的过程中,我们会在Redis中记录发放优惠券的数量。在大流量的情况下,我们还需要对Redis进行横向扩展,以减轻Redis单机的压力。.容量预估:基于以上思路,我们预估存储资源满足12wQPS发券需求。A。MySQL资源在实际测试中,单次发券存在非事务性写入MySQL,单台MySQL服务器写入瓶颈为4000,据此计算出我们需要的MySQL主库资源:120000/4000=30b。Redis资源假设发卡券的12wQPS是同一个卡券模板,单个分片的写入瓶颈为2w,则最少需要的Redis分片为:120000/20000=6个大流量的热点库存问题和解决方案问题在发卡场景,如果我们只使用一个卡券模板,那么每次扣库存,访问的Redis都必须是特定的分片。所以这个shard的写瓶颈是肯定会达到的,越严重,可能会导致整个Redis集群不可用。针对库存过热的问题,业界有一个通用的解决方案:即扣除的库存key不要集中在某个shard上。如何保证这个优惠券模板的key不集中在某个shard上?我们只需要删除密钥(删除库存)。如图:在业务逻辑中,我们在构建优惠券模板的时候,将这个热销优惠券模板的库存进行拆分,然后在扣库存的时候扣掉对应的子库存。bondbuild的库存扣减还存在一个问题,就是每次扣分库存从1开始,Redis对应分片的压力并没有得到缓解。因此,我们需要做的是:请求次数,随机不重复轮询子库存。以下是本项目采用的具体思路:Redis子股的key最后一位为分片编号,如:xxx_stock_key1,xxx_stock_key2...在扣除子股时,我们先生成一个与分片总数对应的随机数。重复数组,比如第一次是[1,2,3],第二次是[3,1,2],这样每次扣减库存的请求都会分发到不同的Redis分片,同时缓解Redis单分片的压力,也可以支持更高QPS的扣费请求。这种思路的一个问题是,当我们的库存接近耗尽时,轮询很多子库存就会变得没有意义,所以我们可以在每次请求时记录子库存的剩余量,当子库存某张优惠券模板用完后,随机不重复轮询操作直接跳过该子库存段,可以优化系统在库存即将用完时的响应速度。对于Redis热点key的处理,除了划分key外,还有一个key备份的思路:即通过一定的策略,将同一个key备份到不同的Redis分片中,从而打散热点。这种思路适合读多写少的场景,不适合发优惠券这种处理大写量的场景。当面对特定的业务场景时,我们需要根据业务需求选择合适的方案来解决问题。优惠券模板获取失败问题及解决方案问题在高QPS、高并发场景下,即使我们能将接口成功率提高0.01%,实际性能还是很可观的。现在回头看一下发放优惠券的整个流程:查询优惠券模板(Redis)-->查询-->幂等性(MySQL)-->发放优惠券(MySQL)。在查看优惠券模板信息时,我们会请求Redis,它是一个强依赖。在实际观察中,我们会发现Redis超时的概率大约是万分之二到三。所以,这部分的优惠券请求必然会失败。解决方案为了提高这部分请求的成功率,我们有两种解决方案。一种是在从Redis获取优惠券模板失败时在内部重试;另一种是在实例本地内存中缓存优惠券模板信息,即引入二级缓存。内部重试可以提高部分请求的成功率,但不能从根本上解决Redis的超时问题,重试次数也与接口的响应时间成正比。二级缓存的引入,可以从根本上避免Redis超时导致的优惠券发放请求失败。因此,我们选择二级缓存方案:当然,引入本地缓存后,我们还需要在每个服务实例中启动一个定时任务,将最新的优惠券模板信息刷入本地缓存和Redis,并刷新模板信息写入Redis中,需要加分布式锁,防止多个实例同时写入Redis对Redis造成不必要的压力。服务治理系统开发完成后,需要进行一系列的操作来保证系统的可靠运行。超时设置。优惠券系统是一个RPC服务,所以我们需要设置一个合理的RPC超时时间,保证不会因为上游系统的故障而拖垮系统。比如优惠券发放接口,我们内部执行时间不超过100ms,那么我们可以设置接口超时为500ms。如果有异常请求,会在500ms后拒绝,以保证我们服务的稳定运行。监控报警。对于一些核心接口、稳定性、重要数据以及系统CPU、内存等的监控,我们会在Grafana上制作相应的可视化图表。春节期间,我们会实时观察Grafana仪表盘,确保最快观察到系统异常。同时,对于一些异常情况,我们也有完善的告警机制,让我们第一时间感知到系统的异常。限制。优惠券系统是一个底层服务,在实际业务场景中会被多个上游服务调用。因此,对这些上游服务进行合理的限流,也是保证优惠券系统自身稳定性的一个必不可少的环节。资源隔离。因为我们的服务都是部署在docker集群中,为了保证服务的高可用,服务部署的集群资源尽量分布在不同的物理区域,避免集群导致服务不可用。系统压测和实际性能完成了以上这一系列的工作之后,就到了测试我们服务在生产环境中的性能的时候了。当然,一项新业务上线前,首先要对业务进行压力测试。下面总结一下压测过程中可能需要注意的一些问题以及压测得出的结论。注意事项1.首先是压测的思路,因为一开始我们无法确定docker的瓶颈和存储组件的瓶颈。所以我们压测的思路一般是:找单实例瓶颈,找MySQLmaster写瓶颈,找读瓶颈,找Redis单分片写瓶颈,读瓶颈。得到以上数据后,我们可以大致估算出需要的资源数量。对服务进行整体压力测试。2、压测资源也很重要。只有提前申请足够的压力测试资源,才能制定合理的压力测试方案。3、压测过程中,注意服务和资源的监控,对不符合预期的部分进行深入思考,优化代码。4、及时记录测压数据,更好恢复。5、实际使用的资源一般是压测数据的1.5倍。我们需要保证线上有一些资源是冗余的,以应对突然的流量增长。结论在13wQPS优惠券发放请求下,系统请求成功率超过99.9%,系统监控正常。春节红包雨期间,优惠券系统承载了两次红包雨的所有流量,期间无任何异常,圆满完成了发放优惠券的任务。系统化的业务思考目前系统仅支持高并发的优惠券发放功能,不足以满足优惠券的业务探索。后续需要结合业务,尝试批量发放优惠券(优惠券包),批量验证等功能。综上所述,从头搭建一个大流量、高并发的优惠券系统,首先要充分了解业务需求,然后进行需求拆解,并根据拆解后的需求合理选择各种中间件;本文主要旨在搭建一个优惠券系统,因此,利用各种存储组件和消息队列来完成优惠券的存储、查询、过期等操作;在系统开发和实现过程中,描述了核心的优惠券发放和优惠券过期实现流程,并针对大型应用中可能遇到的存储瓶颈、热库存、优惠券模板缓存获取超时等问题提出了相应的解决方案。流量和高并发场景。其中,我们采用分而治之的思想,横向扩展存储中间件,解决存储瓶颈;采用库存拆分、分库存的思路解决热库存问题;引入本地缓存,解决从Redis获取优惠券模板超时问题。最终保证优惠券系统在大流量、高并发的情况下稳定可用;除了服务本身,我们还从服务超时设置、监控告警、限流、资源隔离等方面对服务进行管理,保证服务的高可用;压力测试是新服务不可避免的一部分。通过压测,我们可以清楚的了解服务的整体情况,压测过程中暴露出来的问题,线上也会遇到。通过压力测试,我们如果能够了解新服务的整体情况,您会对服务的正式上线更有信心。