iBatis锁机制导致的交易阻塞

前段时间生产上出现了数据库锁太多的告警,其中等待的锁短时间内达到了6000个,好在十分钟后锁很快解除,否则可能会有数据库宕机的风险。此问题的具体表现如下:

  1. 其中一台应用服务器在凌晨2点钟出现大量交易变慢,导致weblogic线程超时阻塞,进而导致服务重启1
  2. 其他服务器在2点钟某一个账务交易出现大量长交易,但其他账务交易、查询交易均正常;
  3. 数据库某个限额统计表在2点多出现大量的行锁,触发数据库告警。
  4. 数据库在2点10分恢复正常,行锁也跟着消失

根据数据库锁的信息,结合那段时间的交易流水,可以确定是由于某个大客户的同一帐号的转账导致的。进一步梳理了代码,没有发现死锁的问题;另外这个客户日间交易量更大,理论上不应该出现这种问题。

进一步分析出问题的应用服务器怀疑是由于iBatis的锁导致的(原来生产上也出现过由于IBatis导致的锁定问题),但在什么情况下会出现长时间的行锁呢?目前iBatis有三个相关参数:maxRequests、maxSessions、maxTransactions,目前生产上分别设置的是100,60, 30。但根据iBatis的文档看maxRequests的值建议是maxTransactions的是10倍。为了验证是否是由于此参数的问题,我们在非功能环境进行了测试。

1. maxSessions=90,数据库连接数60,只做查询:

为了看出问题我们去掉了索引使查询交易耗时边长。此测试发现当查询并发小于等于60时没问题,但大于60时会出现连接不够的错误。

2. maxSessions=90,数据库连接数100,只做查询:

发现查询并发数增大(超过数据库连接)并不会导致交易失败,但查询时长会边长。这说明ibatis自身有锁机制来进行等待。

3. maxSessions=90,数据库连接数100,同时做付款与查询:

转账交易10个并发,在转账交易稳定后开始发送查询交易,查询交易85个并发。发现在启动查询交易一段时间后转账交易开始出现超时和失败,但查询交易无失败和超时,但交易时间有增加。

转账交易10个并发,在转账交易稳定后开始发送查询交易,查询交易75个并发。发现在启动查询交易一段时间后转账交易正常无失败和超时,查询交易亦无失败和超时。

这个测试我们为了看出对账务交易的影响对账务交易代码进行了修改,伪码如下:

Enquire agreementInfo and other info; (no transaction)
Start transaction #spring逻辑
	Get session  #ibatis处理逻辑
		Get Request #ibatis处理逻辑
			Do update account limit
		Return Request #ibatis处理逻辑
	Return session #ibatis处理逻辑
	Sleep 1s #增加的代码,为了便于看出效果
	Get session #ibatis处理逻辑;在这里可能出现拿不到session而阻塞,但这时其实线程是有数据库连接的,因为transaction未断开,不应该出现阻塞
	    Get Request #ibatis处理逻辑
			Insert account transaction information  #预计账务流水
		Return Request #ibatis处理逻辑
	Return session #ibatis处理逻辑
End Transaction #spring逻辑

我们可以看出由于iBatis在每次数据库操作开始都会请求session,而在每次数据库执行完成都会释放session。如果拿不到锁就会阻塞等待。这在仅仅是查询时没问题,但是在更新等事务中就会有问题,会导致数据库行锁,而且会影响到这条记录相关的所有交易。

后来我们仔细阅读了iBatis的代码,包括spring对于iBatis处理封装的逻辑,发现其实:maxRequests、maxSessions、maxTransactions这三个参数只有maxSession是有用的,因为maxRequest都是在session中进行获取,所以其取值不会比session大,而transaction是spring负责管理的,所以此参数也无效。这三个参数在iBatis的2.3.1版本中已经去掉了,可能也是因为这个问题。

基于上述分析我们决定使用数据库连接作为限制条件(通过使maxSession值大于数据库连接数),放弃使用session来进行控制。这个事也让我明白了在事务中尽量不要使用锁(特别是无等待时间的锁),不管是显示的还是隐式的(框架的)。关于jdbc的超时设置可以见参考资料2 3 4

参考资料:

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

Ubuntu 16.04虚拟机安装小记

虚拟机的下载安装在此就不谈了,在安装完成后建议安装“虚拟机工具包”,以便可以方便地在宿主主机和虚拟间进行复制粘贴,共享文件、自由调节分辨率等操作。下面主要记录一下我安装虚拟后做的一些常见设置。

1. 将个人用户文件夹变为英文

因为我一般喜欢在终端中进行操作,这时文件夹是中文很难切换。具体操作方法如下:

  1. 打开终端,先修改为英文环境:export LANG=en_US
  2. 更新文件夹为英文:xdg-user-dirs-gtk-update,会弹出对话框,选择确认
  3. 恢复中文环境: export LANG=zh_CN

重启后会有提示文件夹与当前语种不一致,此时选择不变更即可。

2. 禁止Super快捷键

默认按下Super键时会弹出Dash页面(注: 左上角的图标),整个屏幕都会被占据,体验很差。禁止方法如下:

  1. 在“Ubuntu 软件”中搜索compizconfig-settings-manager,然后下载安装
  2. 输入快捷键Alt+F2,进入“显示运行命令提示符”;输入about:config并回车确认,打开Unity configuration
  3. Launcher选项卡中将Super快捷键禁止即可

3. 安装oh-my-zsh

使你的终端从此不一样,用过的人都知道,哈哈。

  1. 安装zsh:sudo apt-get install zsh
  2. 安装oh-my-zsh

    sh -c “$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)”

  3. chsh -s `which zsh`
  4. 重启虚拟机

更详细的安装方法见:在Ubuntu上安装zsh

4. 安装其他命令行工具

见我另一个文章常用软件推荐

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

Java测试那点事

最近公司在整顿大家的单元测试,会定期出coverage报告。我个人还是挺支持这事的,一方面能培养大家的开发习惯,也能提升代码质量(如果不考核就更好了,哈哈)。现在公司使用的是Spring框架,大家现在所说的单元测试其实更应该算是集成测试,因为很多人会测试交易报文的正确性、整个交易的执行正确性、数据库操作的正确性。

在测试过程中发现很多我以前没在意的问题,也让我对代码测试有了更深的理解。下面简要介绍一下:

1. Spring Bean中使用static变量:

使用Spring的初衷是使用它的IOC特性,能够装配出想要的bean,这些类只需要是普通的POJO对象就可以了,但是有些人却使用static类型来保存数据,更可气的是不支持多次加载(会判断如果static变量中有值时就报错),这样就把Spring的优点消失殆尽了。其实这样只是为了可以直接使用静态函数访问这个类。

这样做的缺点是显而易见的,在连续执行多个测试案例(suite)时,如果Spring需要重新加载(见我上一篇文章),则会报错,因为不支持多次加载。解决方法目前想到的只能使用ant通过fork出一个新的jvm的方式来进行测试。或者使用Powermock工具通过单独的classloader加载,但这样会导致Spring context不能复用、找不到某些类如log4j等、代码覆盖度报告无显示等很多其他问题。

2. 使用final、static等不可测试的方法:

普通的Mock方式一般是针对接口,或是通过继承来修改一个类的方法,如果遇到这些方法,想要进行单元测试,一般的Mock工具都不好使。但如果为了测试而修改代码风险又很大,好在现在发现一个好用的工具Jmockit,它可以解决上述不可测代码的问题。它是通过java.lang.instrument包结合ASM字节码工具来实现的,具体可以参考此博文。我原来是使用Powermock来做的,它是通过自己的Classloader在类加载时进行修改来实现的,相比之下Jmockit要更直接一些,功能也更加强大。

[注]: Jmockit官网的教程是最新的,如果要看历史版本的文档可以在这里下载,里面包括源码和文档。

3.测试覆盖度报告工具使用

目前有很多测试覆盖度工具,包括coberturajacocoJmockit code coverage。其中eclispe的代码覆盖度工具EclEmma使用的就是jacoco。每个代码覆盖度工具提供的覆盖度报告是不同的,包括行覆盖度、分支覆盖度等等,具体大家可以参考它们的文档。下面说下使用不同工具应该关注的点:

  1. 是否支持maven、ant工具,是否可以支持多个工程报告合并
  2. 你使用的mock方法和这些代码覆盖度工具是否兼容
  3. 能提供的报告内容是否满足你的需要,你是否认可它的指标
  4. 生成覆盖度报告的样式你是否满意

测试覆盖度工具、Mock工具建议大家了解一下它们的原理,可以在网上搜以下。这样大家在使用时可以更得心应手。

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

Spring Test的一些小技巧

目前项目在使用Spring Test进行单元测试(其实算是集成测试了),有两个小知识想和大家分享下。

1.对于同时存在注解、xml配置的bean会以xml中的配置为准

例如spring配置文件如下:

    <!-- application.xml -->
    <context:component-scan base-package="com.itechlib.com.service" />
    
    <!-- 在测试目录下定义stub类 -->
    <bean id="userDao" class="com.itechlib.com.test.stub.UserDaoStubServiceImpl" />

Java代码如下:

@component("userService")
class UserServiceImpl implements IUserService {

@Resource(name="userDao")
private IUserDao userDao;

  public void doUpdate(){
    //...
    userDao.update();
    //...
  }
}

这样在测试时UserServiceImpl调用的就是stub包下定义的userDao的模拟类了。

2.同时运行多个测试类的时候,可以有不同的ContextConfiguration配置

例如使用Junit TestSuite来一次执行多个测试类,这时如果不同测试类的ContextConfiguration配置不一样会怎么样呢?其实这些测试类都能够正确运行,因为spring会针对不同的configuration创建不同的ApplicationContext,而且这些ApplicationContext都会进行缓存,只会加载一次。

下面这些key不同就会生成不同的ApplicationContext,具体见spring reference 文档 Context caching

  • locations (from @ContextConfiguration)
  • classes (from @ContextConfiguration)
  • contextInitializerClasses (from @ContextConfiguration)
  • contextLoader (from @ContextConfiguration)
  • activeProfiles (from @ActiveProfiles)
  • resourceBasePath (from @WebAppConfiguration)

如果测试中破坏了ApplicationContext,可以对这个测试类声明@DirtiesContext

例如下面的写法会生成两个独立的ApplicationContext。

@ContextConfiguration(loader = SpringBeanTestContextLoader.class, locations = {
    "classpath:test-resources/application/application.xml"})
class UserServiceTests extends AbstractJunit4SpringContextTests{
    @Test
    public void testUpdate(){
    //...    
    }
}

@ContextConfiguration(loader = SpringBeanTestContextLoader.class, locations = {
    "classpath:test-resources/application/application-custom.xml"})
class OrderServiceTests extends AbstractJunit4SpringContextTests{
    @Test
    public void testAdd(){
    //...    
    }
}

了解这些你可以更好的规划你的测试配置,更好地测试各种场景和分支。

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