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

抖音支付10万级TPS流量券发放实战

时间:2023-03-20 10:44:31 科技观察

作者|戚建潮背景近年来春节期间,抖音会为用户带来丰富多彩的春节活动,每年都有数亿用户参与其中。2022年春节,抖音支付也参与春节活动,向大量用户发放抖音支付券,帮助用户在抖音春节活动期间获得更好的体验。对于抖音支付来说,这是一个很大的挑战,因为抖音支付团队之前并没有真正体验过春节大事件的应用场景,这对抖音支付营销系统测试是一个很大的挑战.抖音支付营销体系简介目前的营销体系分层结构如下:营销团队业务主要分为三个方向,营销投放、营销活动、营销资产。营销投放主要负责达成营销权益,将营销收益暴露给用户。营销活动主要负责营销手段的构建和权益的分配。营销资产主要负责用户营销资产的管理,如优惠券、即时折扣等的发放和使用。等待。春节期间的优惠券发放,链接是将营销活动链接到春节主会场,调用营销资产接口向用户发放支付券。春节挑战期间会有大量的同期活动,同时参与领券的人数也会非常多。高峰期对系统的影响会非常大。春节活动面向全国用户,受众非常广泛。用户体验非常重要,所以发放优惠券的耗时动作需要尽可能低。春节活动的参与人数非常多,春节期间预计发放的支付券数量也会非常多,需要保证资金安全。解决方案性能保证优惠券异步发放——提高接口响应速度考虑到支付券多用于抖音电商场景,春节期间用户网购流量较小,用户立即使用优惠券的概率收到它们后很低。保证从收到优惠券到实际感知优惠券(查看优惠券或使用优惠券)的时延可控,不影响用户体验。因此,我们采用了异步优惠券发放模式。marketing收到上游优惠券发放请求后,下单后立即返回,并通知上游接受成功。异步发放优惠券带来的一个问题是,用户收到优惠券后,感知到优惠券已经领取成功,但实际的优惠券最终并没有发放给用户,大部分是因为库存不足,风险控制拦截等原因。针对这个问题,我们和运营同学进行了讨论。春节期间营销活动库存尽量配置,风控拦截率降到最低。除异常刷单用户外,不做任何拦截,尽量减少异步发券失败。可能性。双层本地队列——提高处理能力,通畅流量流量异步化后,由于活跃流量多为心电图结构,波峰波谷明显。我们借鉴了生产者-消费者模型,引入了队列来疏通流量。一开始我们考虑的是RocketMQ,但是一旦在发券这个核心环节引入了额外的中间件,就产生了依赖,需要额外考虑它的可用性和容灾方案,而且RocketMq是一个远程队列,生产者和消费者之间的延迟也很难控制,所以我们设计了本地队列模型来避免上述问题。本地队列模型如上。队列消费者逻辑会先从分布式限流器中获取token,获取成功后从队列中获取数据,创建新的goroutine处理事件奖励逻辑,然后重复这个过程。虽然队列本身具有调峰功能,但仍然不能精确控制消费率。当上行流量过大时,队列消费端的消费率也会增加,存在系统崩溃的风险。精准限流,但是只在接口维度限流的粒度太粗,所以在业务逻辑层再引入一层业务限流。这里的分布式限流器使用的自研分布式限流组件会先在本地获取token,当本地token不足时,会从远程批量拉取token到本地。此外,我们在这个模型中使用了双层队列。一级队列用于保护营销活动层,根据营销活动处理能力设置流量限制;活动决策通过后,将请求放入二级队列,二级队列的限流是根据系统对营销资产的承载能力来设置的。通过双层队列,可以避免营销活动和营销资产之间的容量差异,最大化双方的系统吞吐量。库存抵扣优化-减少热点,减轻压力出于性能考虑,营销资产的优惠券批量库存放在Redis中。目前营销资产清点操作的逻辑是:收到用户发券请求->读取Redis,查看已发券批次存量是否充足->写入Redis,扣除批次券存量。为了让Redis集群的流量均匀,不同批次的优惠券的库存数据分散到不同的Redis分片,但是当某一批次的优惠券在一段时间内集中发放时,流量还是会大量转移到一个shard,导致Redis数据热点问题。如果我们想办法将一批优惠券中的多个扣减库存操作组合在一起,那么数据热点问题就可以大大缓解。合并优惠券发放逻辑如上图所示。营销活动尝试以非阻塞方式从二层队列中获取N条优惠券发放请求数据。如果可以获取,将打包发送到营销资产。如果数据条数小于N,则表示此时队列中没有数据,会尽快发放优惠券;如果获取不到数据,它会休眠一小段时间,重新尝试获取。如果经过有限次数的重试数据仍无法获取,则循环结束。营销资产收到合并优惠券的请求后,会尝试将同一批优惠券的发放请求合并到请求中,在扣除库存时进行集中扣除。例如,同一张优惠券批次A,之前发给了N个不同的用户。库存每减少1,需要对Redis进行N次写操作;合并发放优惠券后,只需要对Redis进行一次写操作,将库存扣除N。另外,扣除库存前的校验逻辑其实不需要每次都去访问Redis。这个验证本身只是一个预验证。最终扣减成功与否取决于后续扣减操作的执行结果。校验最大的作用是拦截Redis库存不足后不必要的扣款动作,不需要很精确。因此,我们考虑在应用的本地内存中维护一个优惠券批量库存信息,并定时更新Redis库存信息同步到本地。发放优惠券时,只需简单查看本地内存中的库存信息,无需访问远程Redis。优雅退出——提高系统健壮性使用本地队列进行数据处理的缺点之一是内存波动会导致数据无法持久化存储。当应用重新发布或升级时,本地队列中的优惠券数据可能会丢失。如果缺失,将无法正常处理用户的优惠券发放请求。为了不丢失内存中的数据,我们需要能够感知到应用程序退出的信号,并在应用程序退出前处理内存中的数据。因此,我们调查了字节云应用实例的生命周期。当实例终止时,当前应用程序实例将首先从服务注册表中删除。该操作执行后,表示当前实例将不再接收新的外部流量;SIGINT退出业务流程的信号。应用收到SIGINT信号后,不再消费队列中剩余的优惠券请求数据,而是将数据发送到远程队列,其他还活着的应用实例消费数据。底层补偿——保证最终一致性虽然应用已经优雅退出,但在极端情况下,如应用退出导致的panic、oom、物理机宕机等异常情况,应用无法接收到SIGINT信号,无法执行业务优雅退出的逻辑。因此,我们加入了自下而上的补偿机制,通过定时任务扫描表的方式,将长时间卡在中间状态的数据重新投递到本地队列进行处理。既然有了定时任务作为后备补偿,还需要优雅的退出逻辑吗?其实还是很有必要的。当应用频繁启动时,会出现频繁的应用重启。这个时候本地队列中很可能有大量的请求没有处理完。如果只依赖定时任务进行自下而上的处理,那么用户领取凭证会从优惠券到实际领取。延迟可能会非常大,这可能会导致用户体验不佳。因此,优雅退出和自下而上的补偿是相辅相成的关系。优雅退出最大程度保证用户体验,自底向上补偿保证数据的最终一致性。绿色通道——提升用户体验异步发放优惠券的假设是用户收到优惠券到真正感知到优惠券存在有一段缓冲期。但用户可以在领取优惠券后直接进入春节钱包查看。如果此时异步优惠券发放还没有完成,可能会引起客户投诉。针对这种情况,我们已经和上游春节主端做了一个约定。当用户收到优惠券后短时间内进入春节钱包查看优惠券时,上游会再次调用优惠券发放接口,并添加绿色通道标识。我们收到此标识后,将异步发放优惠券改为同步发放,当前用户优先发放优惠券,保证用户体验。资金防控除了业绩保障,资金安全也需重点关注。在本次春节优惠券发放活动中,我们主要采取了以下防控措施。幂等验证为每个优惠券发行动作生成一个全球唯一的序列号。发放优惠券时,序列号会作为唯一索引落入数据库。序列号因唯一索引冲突无法成功下线,避免重复发放优惠券造成资金损失。用户维度接收限制序列号的幂等验证可以解决一些问题,但是对于一些专业的黄牛来说,可能会通过伪造序列号来绕过幂等验证来绕过这个限制。在这种情况下,我们维护用户优惠券批次的集合数据。发放优惠券时,我们会核实每个用户是否已达到领取优惠券的上限。如未达到上限,则正常发放优惠券。否则,优惠券发放动作终止。优惠券批次分组互斥,用户维度领取限制主要是为了防止同一用户多次领取同一批次的可能。但是,在整个春节活动期间,运营同学可能会发放多个不同用途的优惠券批次,但发放的群体有可能出现重叠甚至同一批次。如果多次向用户发放不同批次的优惠券,可能会增加营销成本。基于以上原因,我们提炼出couponbatchgroup的概念。同组的优惠券批次的营销目的基本一致,比如都是为了吸引新人,促进激活或者留存。当用户收到该批次的优惠券群中的某一种优惠券后,该用户将无法再收到该群中其他批次的优惠券,即该群中的批次优惠券具有互斥关系,这样可以避免营销费用重复补贴。库存防超卖前面提到,营销凭证批次的库存数据存储在Redis中。每次在Redis上进行扣库存时,可能会出现网络超时、故障等异常情况,导致扣库存结果处于未知状态。当出现这种情况时,我们选择“容忍”,认为发券失败,不回滚,直接结束发券逻辑;库存扣除成功后,我们会实际发放优惠券给用户。可以尝试回滚Redis库存,因为已经确认请求成功扣除库存,但是回滚失败,不会再做额外的重试处理。上述方案可能会导致库存卖空,但这种较为保守的策略可以有效防止库存超卖的可能性,可以看作是数据一致性和可用性之间的权衡和平衡。风控平台接入了发券环节,我们也接入了字节内部风控平台。风控平台将收集分析用户、设备等信息,通过风险评估识别黄牛和恶意用户,拦截发送Bond的动作,避免潜在的资产损失。数据监控与验证除了上述资金防控措施外,我们还对优惠券的发放活动做了很多监控,包括批量发放优惠券的数量、批量发放优惠券的率、本地队列的累积情况等,本地排队消费者率等。当监控数据同比或环比异常时,会及时报警,人工介入调查。另外,在批次发放优惠券时,我们会再次检查数据的一致性,包括对比用户发放的优惠券数量与消耗的库存量是否一致,检查用户是否超过领取上限分批优惠券,等等。综上所述,经过以上方案的优化,我们成功支撑了今年主会场的春节发券活动,取得了不错的效果:系统整体营销可以承担10万TPS的发券吞吐量。业务方面,春节期间发放千万张抖音支付和DOU分期券,支持抖音支付和DOU分期两大核心业务的活动。99%的优惠券可以在0.5s内发放到用户账户。异步发放优惠券的实际延迟很低,用户体验更好,符合业务预期。后续规划经过今年春节活动流程的考验,营销积累了很多经验和系统能力,但仍有需要不断迭代和完善的地方:异步发券能力的标准化。我们初步尝试了异步发券,并应用到春节活动中,效果不错。可以预见,在618、双十一等重大促销节日期间,仍然会有很多适合异步发券的场景。优惠券发放接口标准化,将异步发放优惠券作为可选能力,与接入方和场景关联,实现优惠券发放方式的灵活选择和配置。推广本地队列模式。本次设计实现的两层本地队列很好地完成了优惠券发放的任务处理。与远程队列相比,任务执行延迟更低。队列分层、限流、优雅退出、补偿等辅助功能对系统具有很强的鲁棒性。性能也有很好的保证。未来我们会将这个模块抽象成一个通用的小框架,使其能够支持更多适合异步处理的业务场景。