查看原文
其他

化繁为简 - 腾讯计费高一致TDXA的实践之路

ApachePulsar 2021-10-18

The following article is from 腾讯技术工程 Author gerryyang

🎙️ 阅读本文需 15 分钟


导语:腾讯计费是孵化于支撑腾讯内部业务千亿级营收的互联网计费平台,在如此庞大的业务体量下,腾讯计费要支撑业务的快速增长,同时还要保证每笔交易不错账。采用最终一致性或离线补偿的方案往往会带来较多的处理风险或投诉。因此,我们提出了一种通用的基于应用层的长事务解决方案,将复杂的分布式一致性问题化繁为简。


>>> 引言 <<<


英国计算机专家 Hoare 所言,软件设计构建有两种方法:一是使其尽可能简单,从而一目了然确定其中不存在缺陷;另一种方法则是使其极为复杂,以至于看不出什么明显的缺陷。然而,实现第一种方法要困难的多。


腾讯计费是孵化于支撑腾讯内部业务千亿级营收的互联网计费平台,目前汇集国内外主流支付渠道,提供账户管理、精准营销、安全风控、稽核分账、计费分析等多维度服务。平台承载了公司每天数亿收入大盘,为上百个国家(地区)、万级业务代码、 百万级结算商户提供服务,托管账户总量 300 多亿,是一个全方位的一站式计费平台。


在如此庞大的业务体量下,腾讯计费要支撑业务的快速增长,同时还要保证每笔交易不错账。然而在分布式场景下要满足 ACID,保证所有操作均执行成功才提交结果,随着分布式规模的扩大要达成一致性的时间周期越长。例如,Bitcoin 的 Scalability 问题,为了满足一致性一笔交易的平均确认时间需要 10 分钟。


因此,为了适应复杂的业务场景出现了 Base 理论,使用最终一致性来代替强一致性。在计费场景下,通常的一些最佳实践是:


  • 先长款后短款原则。由平台方掌握主动权,即使出现异常情况下也可以通过补偿的方式保证一致性。例如,先支付后发货,若发货失败通过重试机制达到最终一致。

  • 临界资源访问尽量使用乐观锁。例如,在读多写少场景使用乐观锁实现互斥,在保证一致性的前提下提高并发处理能力。

  • 远程服务调用保证可重入。例如,已经支付成功的订单,如果再次提交也不允许重复支付。

  • 非关键服务异步化。例如,对于支付旁路的操作,通常借助消息中间件进行解耦处理,并保证最终执行完成。

  • 服务全链路实时监控。关键操作实时上报,监控每一步的成功率和转化率,做到异常的实时发现。

通过上述等机制,在异常情况下可做到读修复,写修复,异步修复等,以保证事前和事中的交易高一致。但考虑到问题的闭环,我们也建立了一套完善的针对实时订单,异步订单,离线订单的三级对账机制,以做到事后的保证。


然而,对于日均过亿的交易量,采用最终一致性或补偿性的方案往往会带来较多的处理风险及投诉,需要我们做到实时的一致性。同时,计费系统的逻辑多且复杂,目前涵盖了近百个特点迥异的支付渠道,异常处理强依赖开发者的经验,需要由平台统一处理异常问题来提高服务的容错性。随着业务系统的发展,系统会有不断的新功能迭代,加强逻辑的可管理性从而降低开发门槛,也是我们需要考虑的。


化繁为简是我们的目标,为了应对上述挑战,结合我们在计费领域的多年工程实践经验,决定着力打造一套通用的基于应用层的分布式高一致解决方案 TDXA (Titan Distribute eXtended Architecture),将复杂的分布式一致性问题交给引擎平台处理使业务开发更加聚焦,主要实现以下目标。


  • 分布式事务的原子性,保证交易实时高一致。

  • 自动的异常处理,提高容错性。

  • 基于状态机的流程管理,加强流程的可管理性。

TDXA 通过在内部业务的试点,基于 TDXA 实现的计费服务,上线后通过对账统计发现异常的交易订单数量明显减少,整体服务质量提高。同时,由于 TDXA 屏蔽了复杂的异常处理,业务开发效率也随之提高,相同需求的开发工作周期较之前减少了近 50%。




>>> 设计思路 <<<


>> 分布式事务


在工程上,作为完整的计费服务,一致性不仅需要考虑数据层的数据一致性,同时也要考虑应用层的逻辑一致性。比如,转账这个行为,数据层可以保证每次数据的更新一致性,而应用层如果只减不加,用户看到的账户数据也是不一致的。交易的一致性,简单讲就是收对钱,发对货。看似简单的描述,但在实现中却面临了很多约束,导致容易出现一致性问题。比如:


  • 计费功能模块多,业务逻辑复杂

  • 支付交易链路长,一笔流程多达几十次 rpc

  • 业务请求峰值高,分布式服务节点多

  • 请求响应时间低,需要保证用户体验

  • 系统部署环境差,网络超时错误多

在 OLTP 场景下,用户的一次购买请求通常会涉及多个后端服务的操作,如果其中某一个服务调用出现了二义性错误(网络超时),此时应该返回失败还是成功,之前调用过的服务是否需要回滚?倘若请求量小的情况下,可以借助人工来处理部分调用异常,但是当请求量大了以后,人工介入的成本就非常高。



计费服务的整体质量要求至少达到四个 9,同时不允许交易出现坏账,对外提供事务一致性保障。从一致性的本质来看,要么保证在一个业务逻辑中包含的服务都成功,要么都失败。在计费场景下,后端服务可能涉及接口,数据库,分布式缓存,消息队列等,我们希望做到针对不同后端资源的长事务一致性。



我们的做法是,通过引入分布式事务管理器(Transaction Manager),将全局事务分解为多个子事务,并交给不同的资源管理器(Resource Manager)处理。同时,针对不同的资源类型抽象为不同的事务处理模型,帮助业务实现自动的事务提交或回滚。


面对复杂的长事务流程,业务可以自定义组合不同的事务模型,并根据业务自身特点指定不同的异常处理策略(Error Strategies),让业务开发像处理单机事务一样来保证整体分布式事务的一致性。通过这样的方法,可以有效地帮助业务开发减少异常处理,更专心地实现业务核心逻辑。下面分别对不同的事务模型说明如下。


🔧TCC 事务模型

分别代表 Try,Confirm,Cancel。此事务模型类似两阶段提交协议(2PC),用于保证分布式事务的一致性。与 2PC 不同的是,对资源的加锁是业务资源级别的隔离,减小了资源锁的粒度,因此在事务没有完成提交或回滚时,可以允许执行其他事务。TM 根据所有RM的反馈来决定提交或中止事务,如果所有的 RM 全部执行成功则提交,但只要有一个失败则回滚事务。此模式需要后端服务提供相应的资源操作接口,并保证幂等。调用关系如下图所示。




🔧DB 事务模型

DB 事务模型相比 TCC 事务模型,主要针对原生的数据库操作,减少了对业务的侵入性,由引擎根据用户提交的 SQL 实现跨数据库实例的操作。因为基于传统的关系型数据库本地事务特性,DB 事务模型不允许跨连接,而 TCC 事务模型没有这个限制。针对这个问题,对于包含异步处理的事务流程,一种做法是,引擎会先把已经执行的 SQL 进行 COMMIT,并根据后续接收到的异步通知结果来决定继续正常执行,还是执行 SQL 反事务。另外可以结合数据库的外部 XA 特性,通过两阶段提交的方式,对于 Prepare 成功的操作实现数据库事务的跨连接,根据异步执行的结果来决定是 Commit 还是 Rollback。调用关系如下图所示。




🔧TRY_BEST 事务模型

此事务模型表示尽最大努力执行,主要用在逻辑上可以保证一定执行成功的 RPC 调用。例如,用于一些通知类接口。在整个事务执行流中,建议放在最后执行,以保证整个事务的执行成功。调用关系如下图所示。



除了以上常见的几种事务处理模型,我们也在不断完善新的事务模型。其中每种事务模型都定义了相应的回调接口由业务实现(如下表所示),同时在使用上也有一些约束准则要求。


  1. TRY_BEST 和 TCC 两种模式调用的 RPC 接口需要保证幂等。

  2. 三种模式支持混合使用,但是需要保证调用顺序,最佳实践是优先执行可以回滚的操作。例如,TRY_BEST 和 TCC 两种模式混合使用,应该先执行 TCC 的子事务,然后再执行 TRY_BEST 子事务,这样如果执行 TCC 子事务失败可以回滚,如果 TRY_BEST 子事务执行失败可以通过补偿最终保证成功。


最后,以用户转账为例看如何实现混合类型的事务流程。首先在第一阶段,调用订单服务创建一笔订单(try),然后提交转账 sql1 和 sql2。第一阶段执行成功后并对用户发起一条消息通知(do)。若第一阶段执行成功,在第二阶段,框架会自动完成第一阶段的确认过程(confirm 和 commit);若第一阶段出现异常,框架也会自动完成回滚过程(cancel 和 rollback)。业务开发需要保证业务逻辑上的合理性,将可以回滚的操作先执行,而由框架保证整体事务的一致性。



具体交互过程如下图所示




>> 自动的异常处理

根据墨菲定律,事情如果有变坏的可能,不管这种可能性有多小,它总会发生。我们的系统服务也是,虽然大多数情况都是可以正常对外提供服务的,但是在少数情况下也会出现服务不可用,网络超时等问题。因此,当服务调用出现异常时,如何保证事务可以继续自动执行完成?一种可行的方案是,将事务状态保存和业务事务通过数据库本地事务打包成一个原子事务,当异常出现时,可以通过一个消息重放服务将事务继续执行完成。


此方案的特点是,存在一定的业务侵入性,并且依赖单机 DB 的处理性能。为了解决这样限制,提出了另一种基于消息中间件的方案,首先消息中间件本身需要保证数据的可靠性,同时,在客户端通过引入一个生产代理 Proxy,来解决在无法及时写入的情况下,使用本地存储充当一个缓冲区。通过结合本地队列和远程分布式队列构成一个可用性更高,延迟更低的分布式消息队列方案。



因此,我们需要消息中间件满足以下要求:


  • 一致性:计费场景要求数据一条不能丢,这是最基本的诉求。

  • 高可用:需具容灾能力,在异常情况下能够自动修复。

  • 海量存储:在移动互联网时代,产生大量的交易数据,需要具备海量堆积能力。

  • 快速响应:在亿级支付场景下,要求能提供平滑的响应时间,尽可能控制在 10ms 内。

下面是我们基于 Pulsar 实现的一套高可靠的消息中间件解决方案,整体架构如下图所示。



关于消息中间件的实践经验,我们团队在 Apache Pulsar Meetup 北京站也做过一次分享,可以参考另外这篇文章 Apache Pulsar 在腾讯计费场景下的应用 



>> 状态机的流程管理

由于计费流程的复杂性,通常一笔完整的支付请求需要至少几十次的 rpc 调用。我们希望加强对流程的约束管理以减少可能的逻辑错误。我们的主要解决思路是,通过状态图定义服务调用流程,基于事件进行驱动来完成整个流程。其中,每个节点表示一个 rpc 操作,每个 rpc 操作分为三个独立过程(Pre/RPC/Post);每条边上可以定义多个算子,通过算子的返回值实现服务路由。



同时,通过引入有限状态机白名单的方式,保证业务状态的流转是合理的,可规避一些不必要的业务逻辑错误,减少异常错误的发生。例如,不会出现已经支付的订单被再次提交支付,即 Double Pay 的问题。



业务开发只需要关心业务节点注册的 Pre 和 Post 回调接口的具体实现,以及每个业务节点操作结果的返回算子,其余工作由框架完成整个流程的驱动,以及分布式事务的提交或回滚。同时,我们支持将当前的事务状态通过 Json 描述出来,并转换成流程图,让复杂的流程更加直观。




>>> 系统架构 <<<


腾讯计费高一致 TDXA,旨在帮助业务解决长事务的一致性问题,通过使用状态机来控制事务的执行路由,基于插件化注册机制,提供多种子事务处理模型,实现对不同类型资源的分布式事务处理,完成自动化的提交或回滚。并提供规范的二进制对接协议,以及 Java 等语言二次开发能力。整体架构如下图所示。



对每个模块的功能简要说明如下:


  • TM: 主要实现流程的注册,分布式事务控制,以及异常策略处理。

  • CM: TM 的配置管理,包括流程注册配置信息,节点的属性信息,事务类型。

  • Producer: MQ 的生产代理,事务的持久化,及异常情况的事务重试。

  • Consumer: MQ 的消费代理,执行业务指定的事务补偿策略。

  • RM: 实现外部接口协议转换,及提供具体的业务资源接口,由 TM 调用。

  • TDMQ: 保证消息完整性和低延迟的分布式消息队列。

TDXA 的内部工作流程,主要分为三个部分:

一、初始化流程

  • 初始化时,TDXA 将从配置服务获取要加载的所有注册 so 或 xml 流程配置,并依次加载。

  • 加载后通过回调协议接口取得要注册的流程名及对应的注册函数。

  • 通过调用注册函数完成流程注册。


二、正常执行流程

  • 通过解析业务请求中的事务流程名,创建需要执行的流程,并将请求数据添加到事务的数据池中。

  • 初始化事务状态为 begin,并通过转移条件决定下一个要执行的状态。

  • 执行当前状态的预处理和对应服务的执行调用。

  • 根据执行结果判断服务路由,若没有找到满足的路由,则根据业务指定的错误策略完成事务恢复。

  • 重复执行 c~d,直至达到状态 end,或发生异常情况。

  • 最后根据事务的执行结果,设定事务返回信息。

三、事务恢复流程

  • 反序列化分布式消息队列通知的事务信息。

  • 对未完成的事务断点执行。

为了方便业务快速接入,我们实现了一套前端自助化流程配置和功能验证系统。希望能够帮助业务在接入时做到尽量能所见即所得。前端每生成一个节点, 就会生成一份对应的后台代码, 支持分 Tab 页代码编解功能。为方便重度开发人员,页面上也可以下载整体的含框架代码,开发者可以选择使用其他的 IDE 进行开发。




>>> 系统可靠性 <<<


>> 逻辑可靠性

TDXA 内部提供了一些必要的逻辑异常检测机制,帮助业务规避不必要的错误,包括但不限于:


  • 注册接口参数检测。

  • DAG(Directed Acyclic Graph)合理性检测,需要满足,仅有一个连通分量,不能回环(但允许自环),每个节点都能达到终止节点保证闭环。

  • 运行时异常流程错误告警等。


TDXA 对于业务异常的流程处理原则分为两类。对于业务类错误,引擎会把控制权交给开发者,由开发者根据返回值信息决定事务流程如何流转;对于系统类错误,引擎会返回 E_PROCESSING 错误码给前端,然后内部借助消息中间件进行异步重试处理,其中重试次数和时间间隔均可由业务决定。


>> 服务可靠性

在服务可靠性方面,TDXA 中的事务管理器 TM 是与业务服务集成在一起的,并通过消息中间件实现事务的持久化。考虑可能出现的以下几种组件异常情况,以及如何实现故障迁移。


情况一在 TM 进行事务持久化前,TM 出现故障。此时,可以通过原始的事务请求进行重放。



情况二在 TM 完成事务持久化后,TM 出现故障。此时,未完成的事务可以由集群中其他的 TM 继续完成处理。



情况三在 TM 进行事务持久化时,TDMQ 出现故障。此时,通过本地的 TDMQ Proxy 异步重试。



情况四在 TM 进行事务恢复时,TDMQ 出现故障。此时,会选取新的可用的 Broker 进行消费,可能出现多次消费,需要业务保证幂等处理。




>>> 系统性能优化 <<<


事务的核心是锁,而事务和性能是两个相悖的特性,因此在满足分布式事务的前提下,也需要考虑对系统性能的调优,通常的优化方法包括:


  • 尽可能减少锁的覆盖范围。

  • MVCC 多版本并发控制协议,读写不冲突。

  • 选择正确的锁类型。悲观锁适合并发争抢比较严重的场景,而乐观锁适合并发争抢不太严重的场景。TDXA 在性能调优方面做了以下考虑。


优化 1通常引入事务协调者会增加网络调用次数,因此会减少系统的并发处理能力。为了尽量减少不必要的网络调用,我们将事务协调者与业务服务集成在一起实现,提供了插件和配置的方式注册业务事务。


优化 2允许不相关的事务可以并行执行,即 pipeline 操作。例如,b0~b3 与 c0~c3 是两个独立的操作流程允许并行执行。



优化 3对于 TCC 事务模型,操作相同资源的不同事务是独立的,不需要等待执行。例如,事务 T1 在没有 confirm 或 cancel 时,事务 T2 可以继续执行 try 而不需要等待事务 T1 完成。



优化 4针对数据库外部 XA 的性能优化。在 XA 模型下,如果由于不稳定的网络通信导致事务没有提交,会影响所有其他事务都在等待。我们针对数据库的网络模型增加了线程池的处理模式提高了 I/O 的处理能力,较原生的半同步方式提高了近 5 倍的性能。具体可以参考计费平台部高一致性存储层架构变迁之路---分布式 MySQL 数据库(TDSQL)架构分析




>>> 下一步方向 <<<


腾讯计费作为承载每年千亿级业务营收的计费平台,保证支付交易高一致,每笔收支不错账是我们的核心能力。分布式服务相比单体服务,为我们提供了更好的扩展性和灵活性,但也带来了复杂的一致性问题。


腾讯计费高一致 TDXA 在此背景下应运而生,旨在实现长事务的一致性,支持多种资源的混合分布式事务,在异常出现时可以实现零人工介入处理。让复杂的问题在这里变得简单,化繁为简是我们的目标。最后,考虑到极端情况,我们还会借助三级对账,通过实时订单,异步订单,离线订单的三级策略,来保障异常情况下的交易一致性。


腾讯计费通过多年的实践探索,我们构建了一套完整的金融级计费解决方案, TDXA 作为其中的一个重要组成部分,其他的还包括 TDSQL,TSM,TBC 等。目前,TDXA 已经在腾讯计费内部业务广泛应用,包括,大额的企业支付,腾讯云计费,腾讯计费开放版等,在实际场景中得到了验证,减少了不必要的人力成本,业务服务质量得到显著提高。


最后,我们在解决了一些问题的同时,也遇到一些新的问题。

1. 兼顾易用性和性能。

目前 TCC 的模式,将一次操作分成两个独立的子事务,不会阻塞其他的事务执行,可以保证较高的并发处理能力。
但是依赖业务的接口实现资源的互斥隔离,对业务的实现存在一定侵入性;而 DB 的模式,目前是通过数据库的外部 XA 事务实现的,可以支持原生 SQL 的分布式事务,减少了对业务的侵入性,但是由于资源是通过数据库行锁进行隔离的,只有提交或回滚的事务释放资源后才能继续处理其他的事务。易用性和性能的平衡是我们继续优化方向。

2. 支持更多的开发者。

考虑到执行效率,TDXA 的核心是通过 C++ 实现的,并提供了 C 的接口给业务使用。考虑到可以推广到更多的开发者,我们基于 C 的接口使用 JNI 封装了 Java 的接口,从而也可以支持 Java 技术栈的开发。除此之外,越来越多新的高级开发语言出现,比如 GoLang。如何将我们的能力普适给更多的开发者,也是我们后续需要考虑的。

3. 事务隔离性要求。

TDXA 保证了长事务的一致性,但是却牺牲了一定的隔离性。
两个事务并行执行的时候,其中一个事务可能读取了另一个未提交事务的数据,即脏读。对于大多数业务场景可能问题不大,可以通过事务补偿来保证一致性,但是在某些业务场景下可能是不允许的。

4. 支持更多的资源类型事务。

TDXA 的目标是希望业务可以像处理单机事务一样处理分布式事务,目前已经支持 RPC,DB 等常见的接口。
不难发现,对资源的加锁都是后端接口提供的能力,为了支持更多的资源类型,在不依赖后端的前提下我们应该如何支持。



: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存