可靠消息模式

近期项目的事一直很忙,发现要学习只能自己挤时间。最近大致看了下淘宝曾宪杰写的《大型网站系统与Java中间件实践》,感觉还是受益匪浅。这里结合自己的体会把可靠消息这块总结一下。

可靠消息,顾名思义就是在前置交易成功后系统能保证一定把消息发送出去,另一方面,如果前置交易失败,消息一定不能发送。可靠消息和事务交易还是有区别的,事务消息一般是通过事务管理器来保证消息系统和应用服务的数据库系统的一致性。由于消息系统和数据库系统一般是两个系统,所以事务消息一般采用两阶段提交来做,而两阶段提交在异常情况下问题很多,实际系统一般很少采用。可靠消息是事务消息的简化版,它提供了最终的一致性,同时有避免了厚重的事务管理器。

注: 本文介绍的可靠消息专指发送端一致性的保证,即保证消息一定能发送到消息中间件或接收端。对于接收端的消息一致性,即接收端一定能收到并处理消息不做阐述。接收端消息一致性的实现方式有很多,很多消息中间件的文档中都会提到,包括但不限于如下两种(1)接收端消息处理成功才认为消息接收成功,接收端进行幂等处理(进行判重或业务逻辑支持重复);(2)接收端采用拉取方式从中间件主动获取消息。

下面简单介绍一下可靠消息的实现方式:

1、本地事务方式

这种是最简单的方式,即在同一个本地事务中将待发送的消息放在单独的表中,并更新(或新增)前置交易的状态。详细处理过程如下:

联机交易处理过程:

  1. 启动事务 1.1 记录“待发送消息表”,状态为“待发送” 1.2 完成业务处理,更新(或新增)前置交易的状态,例如记录业务流水等
  2. 提交事务
  3. 进行异步消息发送功能。在异步发送消息确认成功后,将“待发送消息表”状态改为“发送成功”。

异常情况处理:

  • 启动自动任务定时扫描“待发送消息表”中的长期“待发送”的消息,然后进行消息发送。
  • 对于多次重试发送消息均失败的情况(可能消息系统故障,或应用程序问题导致消息格式不正确等),将“待 发送消息表”状态置为“发送失败”,后续由人工处理。

上述这种本地事务方式的优点是:

  1. 由本地事务保证发送消息和记录交易状态的一致性,而无需应用提供单独的查询交易确定交易的状态;
  2. 相比后面处理方案,无需考虑应用本身阻塞的问题,可保证消息永远不会漏发;
  3. 由自动任务保证消息最终一定会发送成功,从而保证了最终一致性。

缺点是:

  1. 需要保证消息待发送表和业务流水信息表在一个数据库中,在分布式环境下,相当于每个数据库上都需要有这样一套表;
  2. 需要应用本身支持事务,且需要应用明确记录待发送表,或通过框架隐式将业务更新和消息记录进行事务耦合;
  3. 需单独的自动任务对待发送消息表进行轮训、更新,增加了数据库的开销。

2、预发送及确认机制

这是曾宪杰书中提到的方式,即在应用记录前置交易状态之前、之后反别发送预发送消息、确认发送消息。详细处理过程如下:

联机交易处理: 1. 发送预发送消息:接收方一般为消息中间件或其他单独消息服务,此系统需保证持久化后再返回成功。消息中需包含应用交易标示信息,以便能在后面确认应用是否处理成功。 2. 完成业务处理,更新(或新增)前置交易的状态,例如记录业务流水等。 3. 发送确认消息:接收方收到消息后进行消息发送,并更新本地的消息状态为发送成功。此步骤可以异步来做,以减少联机交易等待时间。

异常情况处理:

  • 消息中间件或消息服务对于长时间处于待发送的消息,需要调应用提供的查询交易确认实际交易结果,如实际交易成功则发送消息,如失败则取消发送。
  • 需要注意应用查询交易返回“处理中”或“无此流水”的情况,这时由于应用本身可能存在阻塞,故不能盲目将消息置为失败。建议置入异常消息,进行人工处理,或设置很长的超时时间,超时后置为失败。
  • 对于消息服务实际发送消息时的失败,需要重试进行发送,对于长时间发送均失败的情况,需要置入异常队列,由人工处理。

上述处理方式的优点是:

  1. 只需要应用增加一个预发送的消息,即可保证消息可靠性
  2. 由单独的消息中间件来保证消息发送,避免了与应用过多耦合
  3. 可以严格保证消息的最终一致性,而无需引入事务的开销
  4. 可扩展性好,各组件功能切分明确,对应用侵入性小

缺点是:

  1. 当应用阻塞时无法断定交易即为失败,超时时间的设定有经验的因。为防止事务挂起,异常情况只能人工确认。系统无法做到消息发送和业务功能的强一致。
  2. 应用需额外开发查询交易,来确认交易的结果。此交易需要保证幂等性。

3、接收方定期校对方式

当发送方无法或不愿意进行改动以保证消息一致性时,可以通过接收方的定期校对来保证消息的最终一致性。大体实现逻辑如下:

联机交易处理:

  1. 完成业务处理,更新(或新增)前置交易的状态,例如记录账务流水等。
  2. 发送消息。(发送消息可能会丢失,发送方不进行任何保证)。接受方收到消息进行相应处理(异步)。

异常情况处理:

  • 当接收方没有收到消息时或收到消息但业务处理异常时,需要根据定时策略,定时调用发送方的查询交易进行查询并校对,对遗漏的消息进行补偿处理。

上述处理方式的优点:

  1. 对发送方无约束,其可以不保证发送消息一定发送成功;
  2. 可以适用于消息发送渠道不可靠或即使收到消息也无法保证消息被处理的情况。比如移动手机端的消息推送;
  3. 无需消息中间件支持。

缺点是:

  1. 定期校对功能的实现开销,比对哪些消息没处理当交易量大时并不是一个简单的工作。
  2. 需要发送方提供业务查询或下载的功能,以便确认漏处理的消息。
  3. 接收方需要进行消息幂等处理(其实不算缺点了,上面两个方案也需要进行此保证)

上面简单介绍了可靠消息的设计思路,大家可结合自身应用的特点进行选择。可靠消息本身是有额外开销的,我们能做的只能尽量减少对应用的影响。实际中消息中间件的实现还有其他考虑的内容,比如消息存储、性能、应用如何处理中间件故障等等,大家可以参考曾宪杰书中的内容。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

账务系统热点帐号问题

账务系统的热点帐号是一个很常见的问题,特别是随着互联网支付的发展,热点帐号的问题更加突显。很多支付机构由于业务限制往往在一个银行只有一个对公帐号,这一个帐号往往就是热点帐号。

对于不同帐号的并发处理相对比较简单,比如按客户或帐号拆表、拆库,这之间比较复杂的是分布式事务问题。目前处理分布式事务一般的解决方案包括:

  1. 可靠消息+补偿模式;
  2. TCC分布式事务模式;
  3. 两阶段提交;
  4. 拆分为多个局部事务。

这些方案往往通过自动任务来处理异常的场景从而保证最终一致性。

如果说去IOE最困难的问题应该就数热点帐号问题了(或者说分布式事务问题),因为单个帐号的扣减余额等肯定要通过事务来保护,防止出现余额和流水不符的情况。但事务必然会导致并发度下降,即使对于功能很强大的主机核心系统,并发度也会显得捉襟见肘。为了防止自己处理能力超限,主机往往会进行流控以保护自己,但这样的用户体验会很差。如果这件事在开放系统中做,由于每个服务器性能有限,并发度会更低。下面介绍下自己对热点帐号问题的理解,因我本人并不负责核心系统,有不当之处欢迎指正。大体解决方案分为如下几种:

1. 汇总记账方式

汇总记账分为两种,收单和付款。其中收单采用异步入账的方式比较好,可以将钱先放在内部过渡户中(过渡户可以是一个或多个),然后异步将钱整批汇总后入收方帐号。对于付款会复杂些,主要涉及到透支的问题,如果业务上允许透支也可以采用先银行垫付一段时间,然后整批一笔入付方帐号;如果业务不允许透支就比较复杂了,一种可行的方案是将每笔账务先预先记录流水然后返回“处理中”,后台采用批量的形式以批量为单位更新付方帐号的余额。但这样带来的一个问题是响应不够及时。

2. 异步缓冲记账

参考资料1中提到的削峰填谷方式,思路比较简单,对于不超过并发数的交易同步响应,对于超过并发的交易放在缓存队列慢慢处理,可以返回处理中或成功(返回成功有透支风险)。这种方式本质并没有提高系统的并发量,只能算是一种简单体验改进。

3. 同步批量缓冲记账

在参考资料2中提到了一种可行的解决方式,但并没有介绍如何实现,结合参考资料3,可行的解决方案如下。当接到客户的转账请求后,如果为热点帐号,则在执行完所有的校验后,将交易放在一个队列(或多个队列)中。针对每个队列会有单一的线程进行处理,它一次或获取多条待处理记录然后统一记录数据库流水,对于热点帐号的余额扣减只扣减一次。对于这种方法如何确定队列的大小以及同步等待的时间是一个需要分析的问题。这种方法可以成倍减少数据库的压力,类似与数据库事务管理器的提交机制,但它也带来一个问题就是增加了单笔交易的延时。为了使这种方法收益最大化,在框架设计层面需要把同一个热点帐号的交易转到特定的服务器来处理。

4. 建立多个影子帐号

上述“同步批量缓冲入账”方式的限制是单台应用服务器的处理上限,单台服务器的处理上限。为了解决此问题,一种可行的解决方案是将一个热点帐号拆分为多个影子帐号,每个影子帐号有自己的余额和流水信息。所有影子帐号的余额之和是整个帐号的余额。拆分后的处理逻辑和单个帐号的处理方式相同,可以采用不同的处理方式,如异步缓冲记账或是同步批量缓冲记账等。这个方案需要特殊考虑的点包括:

  1. 帐号余额增加时如何分配给其他的影子帐号:为避免分布式事务,建议采用异步方式处理。当然由于这个交易量很少,采用分布式事务也是可行的。
  2. 当单个影子帐号余额不足但整体帐号余额充足时的处理方式:可以采用“直接报错“”或是“返回处理中”改为异步处理。对于客户的特殊提取交易不应直接报错,因为报错则无法支持客户转出所有余额。对于异步处理的方案在返回处理中时可将此笔账务交易转移到其他影子帐号处理,如果所有影子帐号均余额不足但整体余额充足时,此时需要进行影子帐号间调账,将此笔交易放入特殊队列,将此影子帐号置为停用,在处理时将此影子帐号的钱转给其他影子帐号然后继续处理。直到所有的帐号均不支持则进行报错。可以看出当开始停用影子帐号时系统整体的并发度会显著降低,所以采用影子账号应让客户知晓,并提前做好客户的工作,例如帐号至少要保留特定余额、指定特殊的销户流程、与客户约定好各种异常的处理策略等等。
  3. 多个影子帐号之间需要保证负载均衡问题:另外需支持动态增加和减少影子帐号的个数。

另外提一下,内存数据库并不是一个好的解决方案,首先如何保证内存数据库和关系数据库的数据一致性是一个困难的问题;另一方面内存数据库本身也是一个单点,会存在单点问题;再一方面关系数据库本身对于热点数据也会做内存级别缓存,所以内存数据库意义并不是很大。

上面介绍了很多方案,可以看出并没有完美的方案,热点帐号方案更多的考虑是一种权衡,我们需要根据不同的情况来指定不同的策略。后续我会试着对于方案三、四进行实现,看是否有其他问题。后面如发现更好的解决方案也会更新此文章,也欢迎大家告知其他解决方案。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

《面向生产环境的SOA系统设计》PPT解读

很长一段时间一直在考虑支付系统的可靠性、一致性问题,生产上随着交易量的增大或是其他程序原因发生过很多问题,例如:

  1. 服务器JVM crash导致客户流水状态不一致、批量自动任务未执行完成
  2. 数据库处理阻塞导致记录/更新流水失败,导致数据状态不一致
  3. 应用服务器阻塞导致交易处理缓慢,客户迟迟查不到结果或是查到的结果不正确
  4. 前端组件阻塞导致客户交易发到应用系统时已超时,客户查无流水但最终交易成功
  5. 由于后端核心系统问题流控或其他异常导致客户交易异常失败

这些问题都不太好解决,但又是作为支付系统必须面对的,将这些问题留给客户处理实在是不可取。最近在网上发现一个PPT《准备好发射了吗?–面向生产环境的SOA系统设计》,看了很有启发,和大家分享一下。这个PPT的作者是程立,支付宝的首席架构师,这里我就不多做介绍了,大家可以在网上搜一下。

下面逐个对其中介绍的内容进行介绍。有写的不对的地方欢迎指正。

1. 典型的SOA应用

典型的SOA应用可分为几层:

  1. 接入层:界面展现服务、客户接入服务(集成服务),主要负责渠道接入功能;
  2. 产品服务层:包括各个产品应用,比如收付款、代收付、理财等产品应用,主要完成产品的业务处理,一般作为主项目对整个产品负责。
  3. 公共服务:提供基础的公共服务如客户信息、机构员工信息、收费处理、安全检查等等,这些一般不会直接作为产品,但通过这些划分可以大大简化产品开发,提高一致的企业级要求。
  4. 基础服务:这些是多个公共服务中的公共功能,和上面的公共服务的区别比较模糊。
  5. 集成服务:这里指的是调用外部系统的网关,对于支付宝来说一般是负责接入各家银行系统,从设计上集成服务一般仅负责接口适配、路由转发(在目前我们系统中是这样的)。

2. 服务的内部

服务作为基础业务单元,本身具有高内聚的特性。它需要保证对外提供的服务具有如下指标:质量约束(包括功能、非功能指标)、服务位置、功能描述(包括功能描述,还包括其他特性描述,如是否满足幂等性、可查性、TCC等等)、交互模式(包括同步联机交易、异步消息等)、通信协议、消息格式。服务对外表现为独立的业务单元,不仅要考虑自身的功能还需要在设计时充分考虑调用方的需求、业务场景以及业务扩展,另外尽量简化调用方的使用。

3. SOA技术基础设置

这些基础设施是为各个产品服务、基础服务来使用的,从后面的PPT可以看出是根据各个服务对功能、性能以及监控的要求而独立出来的。大家从各个服务角度来看待这些基础设施会好理解很多。(再想想,我们系统现在就是缺少这些基础设施,|-_- )

4. 一个典型的电子支付应用

这里拿一个典型的电子支付做例子,电子支付本身很复杂,涉及到很多的业务规则,也涉及到很多其他的服务。这里也可以看出,所有的汇聚点是在“订单处理”,也就是说订单处理需要负责整个全流程的控制、处理,它也就是我上面说的产品服务。

5. 响应时间分析

这里可以看出整个处理时间依赖与各个服务的处理时间,这也可以看出SOA并不能减少交易时延,甚至还会增加时延。这里说的合理评估务必要通过技术手段来统计各个服务的响应时间,这也就是公共基础设施“服务监控”的作用。

6. 响应时间优化

除了交易自身处理逻辑本身的优化外,另两个常用的(有效的)优化措施包括:(1)改为异步调用;(2)通过future并发调用来减少时延,当然并发调用本身也会带来额外的时延(文中为10ms)。(3)通过缓存服务结果来提高服务本身的响应时间。这个会后面文里提到的。

7. 关于性能的基础设施支持

上面优化方案都需要基础设置的支持,这些基础设置包括企业服务总线、服务监控、服务代理等等。有了这些服务支持才能使性能优化过程大大简化,更易于实现,也更科学。

8. 吞吐量分析

每个新业务上线会对底层服务带来大量的吞吐量。如果超过了底层服务的吞吐量就需要对新业务能支持的吞吐量进行调整。 注:这页PPT可以通过放映模式观看,会更易懂。

9. 关键服务的吞吐量优化

每个服务都需要给出自己的吞吐量公式,指出其优化的方向。对于无状态的服务只要增加服务器即可,但对于有状态的服务(例如记账、结算)就没那么简单了,可能需要拆库、拆表设计改造,另外也要保证服务事务的一致性。从后面程立对支付宝的重构的讲解中可以看出他对高可用、水平扩展很重视,也就是说这些关键服务必须要保证其水平扩展,这个决定是需要强大的技术支持、也需要魄力的。

10. 非关键服务的吞吐量优化

非关键服务可以通过optional在超量时进行短路降级,以保证整体系统的可用性。这也是需要基础设施支持的。

11. 资源使用分析

文中指出通过sql执行次数来进进行分析,这个数据也是依赖于服务监控的。可以看出性能要通过监控数据优化,通过监控能迅速找到性能瓶颈,减少分析成本。监控的常见手段包括日志分析,交易链路分析,服务器性能指标分析等。

12. 资源使用优化

文中指出了如何通过服务代理缓存服务结果,以减少对资源的直接访问。其实通过缓存手段对于服务本身进行改造也是一个手段,这样也更加可控。缓存的key值选取需要根据应用而已,且务必保证结果正确性。

13. 关于容量的基础设施支持

文中介绍了几个基础设施,如果没有这些基础设施或是基础设施不易用、功能不完善,会导致各个产品功能优化开发很难开展,问题排查也很痛苦。

14. 单个服务的故障条件

单个服务的故障条件有很多种,对于资源(主要是数据库,也可能是文件系统)来说,包括资源不可用、资源响应超时;对于外部服务来说,存在通信中断、服务不可用、服务响应超时、服务违背功能契约(外部服务自身问题);对于服务使用者来说会有并发请求、重复请求、超量请求、请求积压;对于服务自身来说会有BUG、处理中断、处理超时等。正如PPT中所说,唯一确定的是不确定。如何在这些不确定中保证系统的正确性是一个值得探讨的问题。

15. 应对方式

虽然这些问题多种多样,但总有应对之道。ppt中指出了应对的思路,但也指出需要从下面几个方面进行控制:避免发生、降低概率、控制影响、快速恢复。

故障条件 应对方式 类型
超量请求 配额控制 避免发生
重复请求 幂等控制 控制影响
并发请求 并发控制 降低概率或控制影响
请求积压 请求丢弃 控制影响
服务/资源响应超时 时间控制 控制影响
可恢复通信故障 合理重试 控制影响、快速恢复
处理中断 事务/分布事务 控制影响、快速恢复
BUG 自检 降低概率

16. 局部配额控制

通过令牌控制超量请求,在服务发起之前申请令牌,服务完成后归还令牌。当然为了防止令牌未归还的情况,令牌需要设置超时时间。此令牌服务也是一种基础设置。

17. 幂等服务、幂等控制

这里说的幂等服务包括两类,一类是服务本身支持幂等,另一类是通过唯一ID来进行判重,以便保证不会重发。这种幂等控制一般采用数据库的唯一索引来做,这样可以做到严格的唯一性控制且性能很好。例如在我们系统中设计了判重辅助表,通过插入操作判断是否为重复交易。PPT中指出通过“操作日志服务”来进行唯一性判断,具体的实现方式文中没说,应该是类似的数据库的实现。

18. 基于资源的并发

文中详细介绍了悲观和乐观的方案,两者的区别是事务的长短,会影响系统的伸缩性。两者的另一个不同是悲观方案可以保证在并发不大时对客户不报错,乐观锁如果要做到这一点需要做更多的工作例如应用级的重试。在系统设计时需要充分考虑利弊,尽量做到在系统层面支持并发。另外这两种方案都不适用于热点资源,针对热点资源的并发是很复杂的问题,例如春运火车票,很大程度需要根据业务情况进行设计解决。文中还提到了通过分布式锁的方式,其本质是将悲观或是乐观方案中的锁采用单独的服务来提供,以达到控制并发的目的。

19. 请求丢弃、时间控制

交易请求中需包括请求发出时间+客户端超时设置(或是约定好的固定超时时间),业务活动能否继续取决于整体的时延,在系统的关键位置(例如外呼、预计和更新流水信息等)中需要提供对交易是否超时的判断,如超时将直接报错。这样可以避免我最开始列的交易阻塞的问题。这里我们采用了时间,而时间在分布式系统中是不一致的,需要考虑时间的补偿(一般采用直接扣减差异的方式)。

注:前面提到的请求发起时间的设置一般是在前置机或是接入系统中进行设置,如果采用客户的时间会带来太大偏差,而使用整个系统内部的时间误差是可控的。另外在考虑超时时间时还需要考虑http的超时时间,例如客户端超时时间是30s,但接入系统的超时时间不能是30s,应该减去接入系统http的超时时间例如15s,则整个交易的超时时间保守设置应该是15s。此请求时间和超时时间应保证做到全交易线传递。

20. 领域自检

领域自检即对数据库资源内容进行简单的核验,保证其数据的正确性,自检的时机为领域对象读取后、更新前。采用这种方式可以一定程度上避免程序的bug。自检的内容包括不变式如取值范围等的校验、状态变迁,即状态变化可选范围的控制。

21. 分布式事务(TCC模式)

通过事务可以避免程序处理中断(如jvm crash等)导致的数据库状态不一致。例如两次资金记账操作,为了保证一致性,可以采用此模式。此TCC模式说起来简单,但实现起来可不简单,其本质相当于使用应用服务来作为数据库的资源管理器的角色,例如负责实现隔离性、原子性,通过统一的调度来实现最终一致性。

22. 分布事务(补偿模式)

这种事务的难点是如何实现补偿,以及在无法实现补偿时如何处理(或是通过业务处理)。这种事务不提供隔离性支持。

23. 总结

可以看出企业级的SOA设计不是单独一个项目组这么简单,很多东西需要是企业级,需要大量的基础设施的支持。目前比较火热的微服务、DevOps不仅仅是一个概念,它们体现了一种架构思想,也体现了一种企业文化,甚至也体现了一种企业组织架构。这个PPT已经发布很久,但现在来看还是很有借鉴意义,相信大家在阅读时结合自己的经历会更有体会。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》