大家好,我是隐墨星辰,前几天在渠道路由章节中提到过自动化渠道开关,今天聊聊支付系统中全自动化渠道开关的设计与实现。主要讲清楚在什么情况下需要考虑建设自动化渠道开关,以及如何设计并实现一个平衡灵敏度和噪音的自动化渠道开关。
从设计到落地,需要一定的技术实力和研发资源消耗,当对接的渠道量大且经常不稳定时,才能发挥出最大价值,比如跨境支付场景,国外的渠道大部分不稳定;如果只对接了微信支付或支付宝,必要性就不太大。
1. 前言
如果你做的是支付系统,恰好又负责外部渠道的运维,那多少会碰到半夜三更收到告警要起床处理渠道挂了的情况,睡眼朦胧中你大概率会想,如果系统能自动识别渠道挂了,然后自动关闭,最后在渠道恢复服务后自动打开,那就爽歪歪了。
在支付系统中,渠道的稳定性不仅直接影响到支付的成功率和用户体验,还特别影响研发和运维同学的幸福感。所以,当对接的渠道量大且不稳定时,建立一套全自动化的渠道开关系统就是一个非常不错的选择,不但能够有效提高系统的稳定性和支付成功率,同时降低维护成本和人工干预频率。
2. 自动化渠道开关的核心功能
自动化渠道开关需要满足以下几个核心功能:
- 实时监控渠道状态:一般通过监听支付引擎或渠道网关的支付结果,实时统计渠道成功率数据。
- 自动关闭故障渠道:当检测到成功率下降到一定阈值或连续失败数达到一定的阈值,自动关闭该渠道。
- 自动探测渠道状态:在关闭渠道后,能自动发起探测渠道状态的服务。
- 自动灰度恢复渠道:当渠道恢复正常后,自动灰度开启该渠道。
- 人工打标与验证:前期判断通过人工打标进行验证,确保系统的自动化判断是准确无误的。
- 一键降级能力:在大促期间,为避免因流量变化大可能引起误判,可以一键关闭自动化开关。
3. 核心模块设计
自动化渠道开关系统主要包括以下几个核心模块:
- 交易数据采集模块:负责实时监听支付交易结果,并保存到时序数据库。
- 决策模块:根据交易数据和预设的策略,决定渠道的开关状态。
- 执行模块:判断当前请求是否被关闭。和决策模块的区别联系:决策模块只是用于决策当前灰度应该打开多少,至于是否生效,由执行模块来决定。
- 探测模块:在渠道关闭后,捞取最近一笔成功的交易,向渠道发起查询,查询成功后,渠道开关进入灰度打开状态。
- 人工打标模块:在前期阶段通过人工打标验证系统判断的准确性。
- 手动控制模块:在大促期间,一键降级自动化开关,手动控制渠道状态。
- 告警通知模块:当渠道异动时,发出通知。
3.1. 交易数据采集模块
采集交易结果数据保存到时序数据库,决策模块将根据这些数据进行渠道是否关闭的判断。
说明:
- 支付结果数据的采集,只需要保留三种数据:初始化,成功,失败。
- 时序数据库创建三个表:全量数据(只保存初始化状态的数据),成功数据(只保存成功的数据),连续失败数据(只保存连续失败的数据,如果指定渠道有成功数据进来,就清除)。
- 三个表用于计算:成功率=成功数据/全量数据,连续失败数据用于在成功率还没有下降到指定阀值,但是连续失败次数达到阀值,也需要关闭渠道。
3.2. 决策模块
用于决策当前渠道是否需要关闭,以及是否需要打开。
说明:
- 渠道初始为完全打开。
- 当指定时间内成功率低于阀值或指定时间内连续失败次数大于阀值,就关闭渠道。
- 关闭渠道后,捞取最近成功的一笔发起查询探测,如果查询失败,仍然关闭。
- 如果查询成功,说明和渠道的通路是通的,且渠道能提供基本的服务,打开灰度25%。
- 如果灰度25%情况下,成功率不达标,仍然关闭。
- 如果灰度25%情况下,成功率达标,继续加大灰度比例,直到100%。
注:上述灰度打开算法还可以优化为:N*2算法,其中N初始为1。举个例子:先打开1%,符合要求后,依次打开:2%,4%,8%,16%,32%,64%,100%。通过7次操作后,100%打开。这个算法适合一些体量大的渠道,直接开25%如果仍然失败会影响很大批量的用户。对于小流量渠道,灰度1%可能很久也没有量进来,不如直接25%见效快。
3.3. 执行模块
决策模块只是用于决策,至于是否生效,由执行模块来决定。在自动开关降级期间,决策模块仍然在工作,只是不会真实关闭渠道。
说明:
- 先判断人工关闭和定时关闭,如果命中,就直接关闭。
- 如果没有启用自动开关,直接返回有效。
- 否则读取决策模块缓存的结果,这里可能是灰度打开50%,所以还需要做灰度计算。
- 返回渠道状态:有效或关闭。
3.4. 探测模块
渠道被自动关闭后,我们不知道渠道什么时候恢复正常,这个时候可以先启动查询探测服务,也就是捞起最近支付成功的一笔单据,向渠道发起查询,如果查询成功,说明渠道能提供基本的服务,可以打开灰度开关。
说明:
- 定时器调度,如果开着比例为0%,说明是关闭,就调用支付引擎查询接口进行查询。
- 如果查询成功,就打开灰度25%。(也可以是N*2算法,初始打开1%,详见决策模块的说明)
3.5. 人工打标与数据分析模块
主要考虑前期算法的精确度需要调优,所以先只计算,不真正执行。计算出来的结果,先由人工进行打标,如果一段时间内(比如1个月)判断的结果都被人工打标是正确的,那就可以正式开启自动化开关。
3.6. 一键降级模块
主要考虑大促或异常场景下,担心自动化的结果可能存在问题,就一键降级掉。
4. 决策模块核心算法与实现
考虑到不同的渠道的流量有高有低,同一渠道在不同时间段的流量也是有高有低,如果所有的渠道设置统一的触发阀值,必须面临有些渠道的灵敏度不够(应该关而没有关),有些渠道的噪音又太大(不应该关却被关了)。
一个常用的解决方案就是应用滑动时间窗口算法。
4.1. 滑动时间窗口算法原理
滑动时间窗口算法(Sliding Window Algorithm)是一种常用于限流、统计和监控的算法,能够在给定的时间范围内统计事件的发生次数。相比于固定时间窗口算法,滑动时间窗口算法能够更灵活地应对突发流量和变化的负载情况。
滑动时间窗口算法通过在时间轴上划分多个小的时间窗口(也称为桶),并在这些窗口中统计事件的发生次数。每当一个新的事件到来时,算法会根据事件的时间戳确定它属于哪个时间窗口,并将其计入该窗口内。然后,根据所有窗口的统计数据计算出当前时间窗口内事件的总次数。
滑动时间窗口算法的应用场景主要有:
- 限流:在API网关等场景下,滑动时间窗口算法可以用来限制单位时间内的请求次数,防止系统过载。
- 统计和监控:在数据统计和监控场景下,滑动时间窗口算法可以实时统计一定时间范围内的事件数量,如监控支付系统的成功率。
- 流量控制:在网络流量控制场景下,滑动时间窗口算法可以用来限制单位时间内的数据流量,确保网络稳定。
我们本次就是用到统计的功能。
4.2. 时序数据库应用
时序数据库(Time Series Database,TSDB)非常适合处理时间序列数据。它能够高效地存储和查询大量的时间序列数据,特别适用于滑动时间窗口的计算。
以下是几个知名的时序数据库,建议根据实际情况选用:
- InfluxDB:InfluxDB 是一个开源的时序数据库,具有高性能的写入和查询能力,支持丰富的数据查询语言(InfluxQL)和强大的数据聚合功能。
- Prometheus:Prometheus 是一个开源的监控系统和时序数据库,提供多维度数据模型和强大的查询语言(PromQL),常用于监控和告警。
- OpenTSDB:OpenTSDB 是一个基于 HBase 构建的分布式时序数据库,具有高可扩展性和高性能,支持大规模的时间序列数据存储和查询。
- Graphite:Graphite 是一个开源的企业级监控工具,支持收集和存储时间序列数据,并提供灵活的图形化展示和查询功能。
4.3. 综合判断渠道是否异常
在渠道刚开始发生异常时,因为前面还有成功的数据,成功率不会立即下降到指定阀值,但是渠道实际上已经挂了。基于此,还需要综合考虑连续失败数是否达到指定阀值,如果达到,也需要关闭。
前面"交易数据采集模块"有提到,数据库一共分三个表:
- 全量表:每创建一笔支付单,就插入一条记录。
- 成功表:每成功一笔,就插入一条记录。
- 连续失败表:每失败一笔,就插入一条记录,每成功一笔,就清空对应渠道的连续失败表数据(这个很重要)。
判断关闭逻辑很简单:
ini
public int calculateRate(channel) {
// 动态计算出当前渠道的窗口大小
int timeWindowSize = fetchTimeWindowSize(channel);
// 动态计算出当前渠道的连续失败窗口大小
int failTimeWindowSize = fetchFailTimeWindowSize(channel);
// 获取时间窗口内成功数
int successCount = loadSuccessCount(channel, timeWindowSize);
// 获取时间窗口内所有请求数
int allCount = loadAllCount(channel, timeWindowSize);
// 获取连续失败时间窗口的失败数
int failCount = loadFailCount(channel, failTimeWindowSize);
// 因为成功率不需要太精确,所以使用整数就行
int successRate = successCount / allCount;
if (successRate < thresholdSuccessRate || failCount > thresholdFailCount) {
setRateToCache(channel, 0);
// 返回灰度比例为0,说明被关闭
return 0;
}
// 从缓存中获取当前渠道的开关比例
int rate = fetchRateFromCache(channel);
if (rate = 100) {
// 全开就直接返回
return rate;
}
// 上一次更新时间
int lastChangeTime = fetchLastChangeTime(channel);
int now = getNow();
int changeWindowSize = fetchChangeWindowSize(channel);
// 仍在灰度中,如果距离上一次调整阀值时间超过指定的窗口大小,就上调灰度比例
if (now - lastChangeTime > changeWindowSize) {
rate += 25;
setRateToCache(channel, rate);
}
return rate;
}
4.4. 参数调优
前面有提到,不同渠道的流量大小不一样,同一渠道在不同时间段的流量也不一样,不同渠道的日常成功率也不一样,如果全部靠人工设置参数,将是一个非常繁杂的工作,而且误差还可能比较大。
下面介绍如何更准确地设置这些参数。
4.4.1. 渠道流量分类
建议按高、低分成两种类型的渠道,每种流量设置一定的阀值,只要符合这个阀值,就归属于对应的渠道分类中。下面是一个示例(实际应用时需要根据公司的业务量自行调整):
高:最近X分钟大于Y笔。
低:最近X分钟小于等于Y笔。
在实际应用中,先根据渠道流量分类参数,去查询时序数据库里里当前各渠道的流量,把渠道进行分类(高、低),然后根据当前渠道所在分类里,去计算成功率和连续失败次数,进而判断当前渠道是否需要关闭。
4.4.2. 成功率选取
每个渠道的日常成功率是不一样的,推荐一个算法:按过去7天的成功率除以2。比如一个渠道日常成功率是95%,如果当前成功率低于47%,虽然还没有跌零,但大概率会挂。如果不放心,也可以选择除以3或4。
4.4.3. 连续失败次数设置
前面也有提到,在渠道刚开始发生异常时,因为前面还有成功的数据,成功率不会立即下降到指定阀值,但是渠道实际上已经挂了。基于此,还需要综合考虑连续失败数是否达到指定阀值,如果达到,也需要关闭。
那连续失败数应该设置多大?建议根据渠道的流量大小设置2个初始值(高、低),然后再去调优。比如:50,20,10。
4.4.4. 时间窗口大小设置
同样的,时间窗口大于取决于流量的大小,流量越大,窗口越小,灵敏度越高,流量越小,窗口越大,噪音越小。不同公司的业务量不一样,建议根据公司业务量来设置。
也同样建议根据渠道的流量大小设置2个初始值(高、低),然后再去调优,比如X分钟,Y分钟。
5. 最佳实践指南
- 充分利用历史数据:分析历史数据,设置合理的成功率阀值,确保渠道开关策略的有效性。
- 灵活调整参数:根据实际业务需求,灵活调整滑动窗口大小、成功率阀值、连续失败次数阀值等参数。文中只区分了高、低流量,如果要更准确,可以分为高、中、低三种类型,甚至四种类型。
- 人工打标与验证:前期通过人工打标验证系统判断的准确性,确保自动化策略的可靠性。
- 灰度打开机制:在渠道恢复时,逐步增加流量,确保渠道真正恢复正常。
- 探测服务:使用最近成功的单据去查询,判断渠道是否恢复,减少对用户影响。
- 综合判断:综合考虑渠道的成功率和连续失败次数,避免单一因素导致误判。
- 定期回顾与优化:定期回顾自动化渠道开关的策略和参数,结合最新的业务需求和历史数据,不断优化和提升系统的稳定性和性能。
6. 结束语
国内基本已经被微信、支付宝、银联、网联等覆盖,这些超大渠道的质量都非常过硬,所以自动化的渠道开关用处不太大。
但是如果做的是跨境交易系统,国外基本都是直连渠道,且这些渠道中很多都是质量比较差,自动化渠道开关就能发挥很大的效能,能够显著提升支付系统的稳定性和支付成功率,降低维护成本和人工干预频率。
希望本文能为大家在实际项目中设计和实现自动化渠道开关提供一些有益的参考。
这是《百图解码支付系统设计与实现》专栏系列文章中的第(26)篇。欢迎和我一起深入解码支付系统的方方面面。
公众号:隐墨星辰