前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >亿级流量下的高可用实践:携程门票秒杀架构如何设计?

亿级流量下的高可用实践:携程门票秒杀架构如何设计?

作者头像
TakinTalks稳定性社区
发布2024-11-14 16:34:15
发布2024-11-14 16:34:15
2280
举报

# 一分钟精华速览 #

随着旅游业的后疫情时代复苏,携程门票预订交易系统迎来复苏浪潮。面对亿级流量冲击,系统以"稳、准、快"为目标,应对高并发秒杀活动。通过缓存热点识别、大Key优化、数据库负载减轻、供应商稳定性提升、精细化流量控制及数据一致性保障等策略,携程成功提升了系统性能,例如,Redis查询性能从300μs优化至100μs,系统能支撑数十万单/分钟的交易流量,确保了在高负载下的稳定运行和持续高可用性。详细的解决策略和方法,请参阅文章正文。

作者介绍

携程高级后端开发专家——涂宏亮

TakinTalks稳定性社区专家团成员,携程高级后端开发专家。拥有超过10年的技术架构和管理经验。专注系统性能、稳定性、承载能力和交易质量,主导过多次大的系统重构以及技术升级,对于大流量、高并发系统有着丰富的实战经验。

温馨提醒:本文约7000字,预计花费10分钟阅读。

背景

大家都曾经参与过的那些令人难忘的大流量场景,比如:双11的购物狂欢,疫情期间的紧张抢菜,节假日的抢票,门票和演唱会抢购等等。

从系统的角度来看,这些秒杀活动大致可以分为两类:

一类是那些流量巨大但弱一致性的场景,比如双11购物节、抢菜,我们买到了商品,但在最终履约时,有可能会因为缺货而不得不退货。另一类则是强一致性的场景,比如火车票、演唱会门票等,一旦抢到了票,系统就必须保证我们能够顺利出票,这样的场景对系统的要求是更高的。

秒杀活动的业务场景特征是:

  • 大流量,并发量也很高
  • 往往是在准点开始的,这就带来了热点问题
  • 下单后,我们还需要确保能够顺利履约

整个流程不仅包括了预订前的准备,还包括了预订后的保障。

接下来,先回顾一下携程最近两到三年的一些营销活动案例。

图 - 携程历史大型营销活动

如果将系统在正常水平时的流量设为1,那么在秒杀活动过程中,我们系统经历了5倍到70多倍的流量增长。这远远超出了系统设计的10倍流量承载能力。

图 - 营销活动&节假日与日常峰值流量的倍数

面对这样的挑战,我们需要确保两件事情:第一,必须能够支撑得住这样的大流量场景;第二,如果系统负载继续增加,比如现在是73倍,未来可能是100倍,我们的系统如何保持高可用性?

一、大流量活动给系统带来了哪些问题?

在大流量的场景下,我们通常会将挑战归纳为两个阶段:订购前和订购后。在这两个阶段中,我们可能会遇到一系列挑战。

在订购前阶段,用户从首页浏览到营销页面,再到详情页,最后到填写页,直至完成下单的整个流程中,可能会遇到页面打开缓慢、卡顿,甚至服务器端的宕机问题。而在订购后阶段,也就是用户完成付款之后,可能会遇到订单无法立即确认,甚至付款后被退款的情况。这些都是我们在大流量场景下常见的问题。

从研发的角度来看,页面打开慢或服务器宕机可能是由于服务端的负载过高,比如Redis、DB超负载。也可能供应商系统不稳定,因为有些系统是直接与供应商相连的。另外,如果是在完成下单后不能付款,可能是由于库存问题,比如库存超卖或少卖,或者是海外访问速度慢等问题。

二、如何设计架构,以应对大流量对交易系统的冲击?

在大流量下的应对策略。系统设计目标是“稳、准、快”。

首先,稳定性是要确保系统稳定运行,确保售卖过程流畅无阻。

其次,准确性是要数据必须一致,确保用户在下单后能够保障履约,这是我们对每一位用户的承诺。

最后,快速响应是不能出现卡顿现象,整个购买流程必须顺畅。

那么,我们如何设计系统架构,来满足以上设计目标?

2.1 如何保障秒杀系统稳定?

用户在参与秒杀时可能会看到网络异常,页面打开慢,这可能会导致他们不得不进行重试。具体的原因可能有很多,这里我举三个常见原因来分析——Redis超负载/缓存热点、DB超负载、供应商系统不稳定。

问题一:Redis超负载与缓存热点

2.1.1 缓存热点问题
问题背景:

针对Redis这一场景,我们最常见的解决方案是进行水平扩容。这种方法可以让系统承载更多的流量,通过让更多的服务器加入到集群中,从而让每一台机器都能分摊一部分流量。

2.1.1 图1 - 常规手段-水平扩容

2.1.1 图2 - Redis 各实例 CPU Util

在秒杀场景下,我们可能会遇到一个特定的挑战。那就是,某一个热门商品可能会路由到单一的一台服务器上,这就产生了热点问题。即使我们从20个实例扩展到100个实例,所有的请求可能仍然会集中在处理那个热门商品的单一实例上从而出现热点问题。

原因分析:

在正常情况下,服务器可能处理2万次每秒的连接请求(QPS),而CPU的使用率只有40%,这是一个相对轻松的负载。然而,当请求量增加到4万次每秒时,CPU的使用率可能会飙升到80%,这时候就有可能把整个服务器拖垮。这种情况就需要我们特别注意,以避免服务器因过载而崩溃。

2.1.1 图3 - 缓存热Key的原因/危害

应对策略:

针对这个问题,我们采取了主动发现热Key场景的策略。在访问Redis的SDK组件中,我们引入了一种机制来识别热Key。具体来说,我们使用有限的LRU队列,在一个设定的时间窗口内,对访问某个特定key的请求进行计数。如果这个Key的访问频次过高,我们就会将其识别出来,将其转移到本地缓存中。这样,我们就可以通过本地缓存来减轻Redis的压力。同时,如果我们知道某个key是热点key,我们可以直接将配置并加入本地缓存,也可以减少对Redis的访问。

2.1.1 图4 - 技术策略

2.1.1 图5 - 采用了两级缓存的机制

优化效果:

多级缓存上线之后,平均耗时降低了约25%。同时,对Redis的访问量也明显地减少了,而本地缓存的访问量则有相应增加。

2.1.1 图6 - 多级缓存实施效果

2.1.2 缓存大Key问题
问题背景:

另一个常见的场景是大Key问题。比如,我们系统中某个key的大小达到了5M,这样就会导致网络请求阻塞,内存占用过大,或者是由于网络传输的报文太大,导致传输速度变慢。

针对这个场景我们做了一个压测,压测结果显示,当报文大小超过200KB时,它的耗时大约是10KB大小报文的3倍。而当大小达到1MB时,请求处理耗时可能会增加到15.7倍。对应的请求承载能力也相应下降。

2.1.2 图1 - 多场景压测结果对比

应对策略:

针对这一场景,我们对一些无用的字段进行了清理,甚至在清理后对字段进行了压缩,以此来减少key的大小,如果优化后的缓存对象无法达到预期效果,也可以对大key进行拆分,将一个大key拆分成多个小key(注意:拆分过细会导致IO压力,具体拆分粒度需要压测后看效果)。

2.1.2 图1 - 缓存大Key 监控界面

除此之外,我们对整个Redis集群进行了深入分析,拉取了数据并识别出了对应的key,分阶段进行治理,比如首先从500KB大小的key开始,因为500KB的key的性能耗时大约是10KB的7倍。

我们计划每周定期使用工具扫描并治理这些大key。这样,每位开发者都能清楚地看到自己负责的Redis集群中的大key,并能够制定相应的计划去处理它们。

经过一段时间的处理,整个大key的数量有了明显的下降,我们逐步清理了这些大key。清理完成后,在后续的压力测试中,由于性能问题导致的测试不通过的情况也大大减少了。

优化效果:

在大Key优化后,Redis查询性能有较为明显的提升(缓存查询耗时从300μs优化至100μs)。

问题二:DB 超负载

问题背景:

接下来讨论的是另一个典型场景,即DB超负载。在秒杀场景下,可能会遇到一些缓存架构的问题,比如,有些场景下,数据变更时会先删除缓存,删除缓存后,系统会从DB中重新获取数据,并更新缓存。这个操作在正常情况下可能看起来没什么问题,但在秒杀场景下,这种缓存架构可能会带来一些严重的挑战。

在高流量冲击下,这种架构可能会导致缓存频繁地被击穿,从而使得流量直接打到缓存里,进而增加数据库的负载。这不仅会导致响应速度变慢,甚至可能使我们的整个业务变得不可用。

过去,主要的问题在于删除缓存时需要访问数据库。如果商品变更的消息量很大,那么在删除缓存后,大量的请求可能会再次访问数据库,从而导致数据库负载过高。

2.1.3 图1 - 商品库的波动导致全业务不可用

应对策略:

针对这个问题,我们采取了两个措施。

首先,我们将删除缓存的方式改为直接更新缓存,不再进行删除操作,而是通过set命令重新更新缓存。

其次,对于消息量过大的场景,比如1秒钟内可能有十几万次变更,并且这些变更可能是针对同一个商品,我们实施了消息聚合策略。这样,整体上我们可能每秒只更新一次缓存,而不是每次变更都去读取数据库。

2.1.3 图2 - 商品变更新数据覆盖更新

问题三:供应商系统不稳定

问题背景:

我们依赖的供应商系统,在某些情况下可能会出现异常。如下图所示,是供应、售卖、履约的整个流程。

2.1.4 图1 - 供应商系统对接的整个环节

在供应阶段,也就是秒杀开始前,我们需要与供应商系统对接;在预定阶段,也就是真正的秒杀环节;下单后,有些订单需要在供应商系统再次下单,因为有些系统是直连的。如果一瞬间的订单量过大,而供应商系统没有提前准备好资源或无法进行扩容,就可能会出现限流或系统不稳定的情况,导致订单无法成功下单。

应对策略:

针对这个场景,我们进行了两项优化。

2.1.4 图1 - 技术策略 -削峰填谷/缓冲池、禁售策略

1)削峰填谷/缓冲池:

过去,当我们向供应商系统提交订单时,可能会遇到供应商系统对下单频率进行限制,比如1秒钟或1分钟只能提交30单。在极端情况下,供应商系统甚至可能完全不可用,这会导致下单流程被阻断。

优化后,我们设定了自己的下单频率,比如假设是1分钟100单。如果提交到供应商系统时遇到限流,我们会先将订单放入自己的缓冲池中,然后通过后台任务以均匀的速度提交给供应商。

2)禁售策略

如果供应商系统出现大面积的长时间不可用,我们会采取禁售策略。比如,我们可能会以1分钟为单位,如果连续几次提交都不成功,我们可能会将该供应商下线。但同时,我们会持续重试,一旦供应商系统恢复,我们会将对应的商品重新上线。

优化效果:

总的来说,上线后的效果是非常明显的。之前,我们从收单到下单,再到供应商处理订单的整个流程可能会受到限制。现在,我们自己下单时不再受到这些限制,之后我们可以通过类似于离线同步的方式将订单同步给供应商。

2.2 如何确保流量在安全水位?

问题背景:

上文的三个例子,都对系统的可用性产生了影响。除了系统稳定性之外,我们还需要减少不必要的资源投入。例如,如果10万人抢5000张票,这里有两个不确定因素:预约的人数是否准确,以及我们需要准备的资源是否足够。可能预计是十万人,但实际上来了20万或30万人。

我们的目标是,即使系统流量超出负载,系统也必须保持稳定。同时,如果只有少量票,比如1000张,我们也不一定需要扩展大量资源来保证售卖。我们需要对系统做一些限制,以确保资源投入的最小成本,并保证系统在超负载情况下也在安全线内。

下图是我们对大型秒杀场景的正常交易流程。从左到右,流量从高到低,承载能力也是从大到小。针对上文提到的例子,10万人抢5000张票,我们需要考虑如何限流以保障系统的稳定性。

应对策略:

我们的限流方案有两种:SOA限流、自定义限流(按活动商品限流)

服务端如果实施了SOA限流,可能会影响整体业务的表现。因为秒杀场景只是系统中的一部分,虽然时间很短,但它的交易量可能只占整个业务量的1%左右。如果出现限流,不能让它影响到其他99%的正常业务场景。

因此,我们为这个场景定制了一种限流策略——按活动商品限流。将系统能力分为两部分,一部分30%用于秒杀业务,剩下的70%用于99%的正常非秒杀业务,包括正常的预订和交易。限流策略仅针对秒杀商品实施。

2.2 图1 - SOA限流 VS 自定义限流 (按活动商品限流)

在这个限流的场景下,我们需要进行更细致的划分。之前我们可能是按照秒级的商品来限流,但在秒杀的场景下,可能在最初的10毫秒内,就会有成千上万的订单涌入,产生数千的QPS,这会导致大量的并发问题。

例如,尽管机器可能已经提前准备好了,但可能会在数据库查询或更新时遇到并发问题,这可能会在那一刻给我们带来一些较大的挑战。

2.2 图2 - 技术策略 :自定义限流 (按活动商品限流)

针对这个问题,在秒杀场景下,我们实施了一个滑动窗口的策略。将1秒钟分成10个100毫秒的窗口,每个窗口可能需要处理的QPS假设是100,那么在整个10个窗口中,每一次都会限流。这样,用户的感觉可能是平滑的,因为在每一个时间段都会有正常的交易进来。这主要是为了防止高并发对下游带来压力。

优化效果:

让我们来看一下优化后的效果。从左侧的图中可以看到,过去在流量突增超过系统负载时,会影响整个系统的稳定性,可能导致一些正常的交易也无法下单。

2.2 图3 - 商品级限流效果

但在我们对系统进行升级之后,情况有了明显改善。现在,我们有两套不同的处理池。如前所述,在秒杀场景下,我们会根据商品维度进行限流。当流量超过限制时,用户可能会遇到排队或收到“活动太火爆”的提示。这个等待时间很短,一旦流量高峰过去,系统就会恢复正常。这时,正常的商品交易,也就是非秒杀商品,并不会受到影响。

2.3 如何保障数据准确?

接下来,让我们谈谈数据准确性的问题,即在用户下单后,我们如何确保订单的履约。例如,扣减库存不一致的问题,如何避免超卖或者少卖。

问题背景:

下单过程中可能包含以下几个关键步骤:

2.3 图1 -性能瓶颈 – MySQL热点行扣减库存(行级锁)

这里我们重点关注的是扣库存环节。在客户下单时,因为是针对单个商品进行库存扣减,这时会遇到一个问题——行级锁,大量并发请求在同一时刻到来,并且都要更新数据库中同一行的记录,特别是对这一行记录的某个字段进行更新,这时就会产生行级锁。行级锁的存在会限制提交订单的处理能力。

应对策略:

针对这个问题,我们的处理方式主要是对库存操作进行了异步化,以此来消除数据库行级锁的问题。这个过程大致可以分为三个步骤:

2.3 图2 - 技术策略 -扣减库存异步化,消除DB行级锁

第一步,在秒杀场景中,后台会预先设置好秒杀活动的场次,并将相应的库存数据同步到Redis中。

第二步,当用户真正下单时,系统会从Redis中扣减库存,并同时发送一个消息。在消费这个消息时,会去更新数据库中实际扣减库存的事务。这个过程是异步的,非常平滑。如果库存扣完了,下单流程也就相应结束了。

第三步,对于未支付订单的取消场景,处理流程是反过来的。首先需要取消数据库中的库存,然后在库存成功返还后,最后再通过原子操作更新Redis中的库存。

优化效果:

优化上线后的效果,我们之前做了对比,发现有明显的提升。如果之前纯粹依靠数据库来扣减库存,可能在并发量上,同时更新同一行的并发量大概在50左右,超过这个数字TPS就可能达到400左右,这时可能会遇到一些瓶颈。

在优化完成后,我们可以轻松支撑每分钟数十万的订单量。当然,这也是在非极限压测情况下的表现。

2.4 如何确保响应快速、体验丝滑?

这里结合携程海外Trip.com的场景,我们针对海外用户访问国内服务时速度比较慢的情况,聊聊如何去优化海外用户的性能。

问题背景:

对于海外用户来说,他们之前是通过直接访问上海的国内专线来使用Trip.com这个APP。这种访问方式涉及到一次回源,相当于是跨洋的数据访问,这对性能的影响是比较大的,速度会比较慢。整体的延时可能在200到300毫秒之间。

2.4 图1 - 全球分布及延时情况

应对策略:

方案一:减少跨Region调用 提升系统响应速度

我们之前考虑了两种方案。一种是将所有海外应用的全量数据在海外部署一份,这样的话,成本会比较高,因为我们有上千个应用需要部署。

2.4 图2 - 海外跨Region应用数据单元化

方案二:入口服务聚合 + Redis构建 轻量化上云(减少回源)

另一种方案是进行一些轻量级的改造。对于一部分与信用安全或用户信息无关的数据,可以通过在海外服务器上提前构建缓存来优化。这样,海外用户在访问时可以直接使用这些缓存,如果需要被动访问,我们再异步地从国内的服务更新数据。这种方法成本相对较低,是一种轻量级且可以快速验证并提升性能的方式。

2.4 图3 - 入口服务聚合 + Redis构建 轻量化上云(减少回源)

优化效果:

在海外场景下,我们的整体效果有显著提升。以前海外用户访问可能会比较慢,有时甚至需要切换回上海来进行秒杀。但现在,在海外流量突增的场景下,尤其是秒杀场景,由于缓存利用率的提高,性能反而得到了增强。访问量越大,性能反而越快。

三、如何让高可用具备“可持续性”?

除了上述内容,我们实际上还针对如何持续实现高可用性,进行了架构健康度治理的相关工作。

3.1 架构健康度治理

我们定义的目标是架构健康度治理,分成三个部分。

第一块是系统运行时的健康度。这涉及到运维阶段的性能监控,比如API性能、服务的垃圾回收(GC)情况、限流阈值设置,以及数据库的慢查询、Redis的大key和命中率等。

第二部分是设计健康度。比如,我们的应用规模可能非常大,代码行数可能达到几十万行,这会导致代码复杂度增加。此外,应用间的重复调用和循环依赖可能会导致调用量放大,以及链路层级过深,这些都可能在流量大时导致性能损耗。

第三部分是工程化健康度,比如包的大小,编译时长等。有时候编译一个应用可能需要几分钟,甚至一个小时。在紧急问题修复和上线时,这个过程漫长而痛苦。

这三部分的治理顺序,通常是从工程化健康度开始,再到设计层面,最后是系统运行层面。但携程在治理过程中,我们重点关注的是生产环境的健康度,然后反过来指导我们在设计和工程阶段应该采取的措施。

3.1 图1 - 基于『架构健康度』,分步实现应用架构治理维度、标准和方法的统一

针对这一部分,我们会为每个应用计算一个健康度分数,每项指标都会得到一个分数,从而得出一个总分。这样,我们就可以对应用的健康度有一个量化的评估。最后,每个应用都会得到一个总分。我们之前的优化可能是针对单个应用,但现在我们转向了系统性的优化。这可能涉及到整个部门、多个业务线、甚至上千个应用的统一标准治理。

这样做的好处是,我们可以提前识别出问题和风险点,将安全隐患发现并解决在开发的早期阶段,而不是等到生产环境中出现问题再去救火。

3.2 完善的稳定性保障体系

我们引入了大型活动节假日保障体系。在日常开发过程中,我们的工作重点是优化。而在节假日期间,我们会按照节假日的稳定保障计划来执行工作。这样就形成了日常开发+大型活动节假日保障的闭环。

四、总结展望

整个设计可以总结为两个主要问题。

1)小问题会被放大,关注读/写瓶颈,要求极高的一致性、实时性与性能。首先是我们日常开发中的小问题,比如缓存穿透、多线程问题,或者在平常场景下不会出现的一致性问题。在秒杀场景下,如果遇到超时后没有进行降级处理,没有处理好一致性问题,这些问题就会被放大。例如,一个场景下的依赖超时可能只影响一单,但在秒杀场景下可能瞬间影响数万单。另外是读瓶颈,如Redis、数据库的瓶颈或热点问题,以及写操作中的热点记录更新问题。另外,对于秒杀场景,我们有极高的一致性要求,超时后的不一致性问题在这种场景下会被放大。

2)重视持续性优化与稳定性保障:日常架构健康度治理 + 大型活动节假日保障。我们强调如果要实现稳定性,不能仅从一个视角出发,而是要从研发视角重视日常的架构治理。我们前文提到的不健康的指标,虽然看似细碎,但如果其中某个问题变得严重,就可能导致大问题。最后,我们重视节假日的大型活动保障,每次节假日活动前都会进行持续性的压力测试。(全文完)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 TakinTalks稳定性社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题一:Redis超负载与缓存热点
    • 2.1.1 缓存热点问题
    • 问题背景:
    • 原因分析:
    • 应对策略:
    • 优化效果:
    • 2.1.2 缓存大Key问题
    • 问题背景:
    • 应对策略:
    • 优化效果:
  • 问题二:DB 超负载
    • 问题背景:
    • 应对策略:
  • 问题三:供应商系统不稳定
    • 问题背景:
    • 应对策略:
    • 优化效果:
    • 问题背景:
    • 应对策略:
    • 优化效果:
    • 问题背景:
    • 应对策略:
    • 优化效果:
    • 问题背景:
    • 应对策略:
    • 优化效果:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档