1 背景介绍
服务稳定性和高可用性在现代业务中扮演着至关重要的角色。服务稳定性指的是系统能够持续地提供可靠、无故障的服务,而高可用性则强调系统在遇到故障或异常情况时依然能够保持正常运作。这两个方面的重要性在于它们直接影响到用户体验、业务连续性和企业声誉。当服务不稳定或不可用时,用户可能会面临访问中断、数据丢失或延迟等问题,从而降低用户满意度并可能导致客户流失。另外,对于广告投放和计费业务,高可用性尤为重要。广告是互联网企业最常见的盈利手段,即使短暂的中断也可能导致巨大的财务损失。因此,投资和优化服务稳定性与高可用性是必须考虑的关键因素。
本次主要介绍广告计费系统在稳定性和可用性方面所做的优化改进和升级。
1.1 广告计费方式介绍
互联网广告大家应该都不陌生,日常 pc 和 app 应该见过很多弹窗广告,视频广告等,那么这些广告是怎样计费的,先来了解下几种常见的计费模式。
图片
广告投放主要是为了提高曝光和转化,使用得比较多的为 CPT、CPM、CPC、 CPA 和 CPS 几种;不同的计费方式会对广告主和平台的收益产生不同的影响:
- CPT(Cost Per Time):按时间计费的商业产品,比如频道页的顶部展位,用于店铺快速引流;
- CPM(Cost Per Mille):按曝光计费的商业产品,比如开屏广告、视频广告、朋友圈广告,用于品牌推广或app拉新促活;
- CPC(Cost Per Click):按点击计费的商业产品,电商行业常用于站内广告资源位推广商品;
- CPA(Cost Per Action):按行为计费的商业产品,如打电话、关注、收藏;
- CPS(Cost Per Sale):按成交计费的商业产品,多用于站外引流,如:淘宝联盟、京东联盟、多多进宝;
图片
1.2 广告计费系统功能介绍
图片
从图中可以看出,广告计费系统主要功能包括两大部分
- 广告检索阶段: 根据广告信息生成唯一的扣费凭证,作为扣费阶段的扣费依据。能否正常生成扣费凭证直接决定了后续能否正常扣费,因此,提高系统的可用性能够降低对收入的影响。
- 广告计费阶段: 我们对扣费请求进行了一系列的处理,包括反作弊措施、扣费操作以及事后处理等逻辑。这一阶段的核心任务是确保扣费不遗漏,并保障扣费的实时性。扣费的实时性不仅对上游超时量产生影响,还关乎下游业务的及时性。多种下游业务场景,如推广活动、广告主通知等,都依赖于准确的计费结果。例如,当预算不足时,系统需要自动下线推广活动;当余额不足时,系统应向广告主发送提醒通知。
2 升级背景
2.1 初始流程
图片
在广告计费阶段,我们采用异步线程处理扣费请求,以提升系统的并发性能和响应速度。当扣费请求失败时,我们将相关信息存储到 Redis 中,以便后续定时任务进行失败数据处理。通过定时任务的方式,我们能够处理失败的扣费请求,避免遗漏,确保扣费过程的完整性和准确性。
- 优点: 异步处理扣费逻辑,可以提高服务吞吐量和响应性能,减少上游等待时间
- 缺点:
2.1 升级契机
2.1.1 问题发现
起初,我们收到了线上的告警通知:扣费服务的线程池任务队列大小远远超出了报警设定的阈值,而且队列大小随着时间推移还在持续变大。详细告警内容如下:
图片
图片
相应的,广告业务指标:点击数、收入等也出现了非常明显的下滑,几乎同时发出了业务告警通知。其中,点击数指标对应的曲线表现如下:
图片
2.1.2 原因分析
通过线程池的报警通知,可以清楚地发现有线程受到了阻塞,导致需要不断创建新线程来处理请求。通过生成线程快照,我们发现用于扣费业务的线程池中的所有线程都处于等待状态,并且全部卡在了 countDownLatch.await()
方法处,这表明它们正在等待计数器变为0后释放共享锁。
具体导致线程阻塞的原因:
1. 数据库中的数据抽取任务存在慢查询,导致数据库出现阻塞现象,进而使得创建扣费订单的耗时增加。
2. 由于创建扣费订单的耗时增加,导致扣费任务的整体执行时间变长。此外,扣费服务中存在线程池使用不当的情况,父子任务使用了同一个线程池。当线程池中的所有线程都在执行父任务,且所有父任务都存在未执行完的子任务时,就发生了死锁现象。
通过下面一张图再来直观地看下死锁的情况:
一次扣费行为被视为父任务,其中包含多个子任务,这些子任务用于执行反作弊策略。假设线程池的核心线程数为2,目前正在执行扣费父任务1和2。此外,反作弊子任务1和3已完成执行,而反作弊子任务2和4仍在任务队列中等待调度。
由于反作弊子任务2和4尚未执行完毕,导致扣费父任务1和2也无法完成执行,从而发生了死锁现象。这种情况下,核心线程永远无法释放,最终导致任务队列不断积压,直至程序遭遇 OOM 崩溃。
图片
2.1.3 解决方案
1. 通过重启服务,可以快速恢复业务正常运行。
2. 通过规范线程池的使用,为父子任务分配独立的线程池,可以避免死锁情况的发生,确保任务的顺利执行。
2.1.4 问题反思
虽然隔离父子任务的线程池可以有效地解决死锁问题,但这次线上事故也暴露了计费系统补偿机制的不完善。尽管我们设置了定时任务来处理失败的扣费,但这种方式存在明显的缺点。
1. 无法及时执行:由于定时任务的执行是按照预设的时间间隔进行的,因此对于一些突发性的扣费失败情况,可能无法做到及时处理。
2. 对于此次的问题,由于失败的扣费数据还未被加入到 Redis 中,系统就重启了,这给线上的收入造成了不良影响。
针对上述问题,我们对扣费系统的高可用方案进行了升级。
3 升级版
3.1 改进后流程
图片
首先整个扣费流程仍然是异步化处理,当收到实时扣费请求后,系统先将扣费信息打印日志用于灾难恢复,然后发送 MQ 消息,这两步完成后扣费动作就算结束了。
3.2 改进点1
使用了 MQ 代替异步线程池来处理扣费请求,MQ 相比较异步线程池的主要优点是解耦和可靠性。MQ 提供了一种基于消息的分布式通信机制,可以将数据以消息的形式发送到队列中,由消费者进行异步处理。这种解耦能力使得生产者与消费者之间的耦合度降低,提高了系统的可伸缩性和灵活性。更重要的是,MQ 还具有高度可靠性,在消息传递过程中能够确保消息的可靠投递和持久化存储,即使在消费者不可用或重启时也能保证消息不丢失。
这样做的好处是利用 MQ 的可靠性投递和重试机制不仅确保了机器的流量均衡同时还保证了整个扣费流程的最终一致性。
图片
图片
3.3 改进点2
针对 MQ 不可用的情况采用了降级方案。当 MQ 不可用时,使用异步线程处理作为降级方案;这样可以使系统保持可用性并继续运行。异步线程处理允许将数据直接提交给线程池进行处理,而不依赖于MQ的消息传递机制。这样可以在MQ不可用的情况下,临时绕过MQ,通过异步线程池来处理扣费请求,以确保系统的稳定性和扣费功能的可用性。
虽然这种降级方案可能会导致一些延迟或性能损失,但它可以有效地应对 MQ 故障或不可用的情况,并避免完全的系统停机。当 MQ 恢复正常后,系统可以再次切换回 MQ 作为主要的消息传递机制,从而实现正常的异步处理流程。
图片
3.4 改进点3
在获取扣费凭证时,扣费信息是存储在 Redis 中的。当 Redis 不可用时,借助了 TiKV 来确保系统的持续可用性。
召回阶段获取扣费凭证时 Redis 和 TiKV 同时存储扣费信息。主存储同步写入 Redis,调用方等待结果;从存储异步写入 TiKV,在不影响系统性能的前提下,使用TiKV作为备份。
图片
图片
3.5 改进点4
任务补偿除了 MQ 的重试机制,还新增了 Spark 任务用于恢复大批量扣费失败的情况;Spark 任务可以从相关日志文件中提取关键信息,并对需要进行扣费操作的数据进行筛选和分析。一旦扣费失败的数据被确认,Spark 任务会重新发送扣费 MQ,系统重新进行消费,做到跟系统无缝衔接。
通过使用Spark进行日志采集和数据处理,可以有效地自动化和加速扣费失败数据的确认过程。Spark 的并行计算和分布式架构使得它能够处理大规模的日志数据,并通过灵活的数据转换和筛选功能提取所需的信息。这种离线方式可以提供更高的可靠性和效率,同时减少对人工干预的依赖,从而提升整体的数据处理效能和系统稳定性。
图片
4 结语
广告计费系统的稳定性是确保广告商正常结算和交易的基础,对于建立信任与合作关系至关重要;持续优化广告计费系统的稳定性和可用性是非常必要的,以适应变化的业务需求,同时最大化系统效率和资源利用。有助于为广告行业持续创造价值,并保持竞争优势。
关于作者
张蓉,转转商业高级开发工程师,目前负责广告检索、计费以及特征工程等系统。