- 最近在做性能测试,发现的一些问题和《Java 并发编程》里:”性能与可伸缩性“一章,所描述的场景相似,所以记录、分享出来。
问题描述
- QA 发起一笔5并发请求,请求完之后显示超时,测试的第一天立刻检查了日志,发现日志并没有打完,并没有太在意,于是就放到那里了。之后第二天又再次检查了一遍,发现这5笔交易的日志并不是没有打印完毕,而是约在15分钟之后(
开始:22:46 -- 结束:23:02
)打印出来了,全部都是显示事务超时。
问题分析
- 我检查了这个接口的超时耗时配置为(250s)。所以如果这里5笔并发请求会串行执行总耗时约为:20.8 分钟。猜测这里可能是这些接口耗时配置的太长,导致(数据库表锁)资源没有释放,请求全部阻塞等待,导致全部超时失败。
原因分析
-
猜想分析:统计了这5笔的请求总共的执行时间,果然是我和之前的猜想一致,5笔的交易按照串行的模式总共执行了20分钟。基本也验证了前面的猜想。
-
继续看打印完整的日志,发现所有的日志卡住在一个
交易流水表
的insert
的操作。先说一点前提:目前的应用中,每次接口的请求响应前,会将所有的请求信息登记到交易流水表
中,也就是说:表锁导致了应用的串行化执行。于此同时就意味着,数据库表的性能决定了应用的性能上限了。 -
解决方案:调低每笔交易的超时时长,避免每笔请求长时间的占用公共表锁资源,目前表锁定的原因暂时不明,还在排查中。
-
系统提供了
交易流水表
的多个分片处理,可以路由到不同分片中。从原本都要获取同一张表资源,现在可以实现多张表的资源,降低了系统资源竞争。
思考
- 从前面的问题我们很容易发现,系统这种
热点域
的设计,在并发请求的场景下,由于某些原因很容易导致系统变成串行、效率低下、甚至是不可用的情况。
HashTable 与 ConcurrentHashMap 优化
- 其实这样的问题很容易从前人的系统设计中找到类似的问题,对比经典的
HashTable
到ConcurrentHashMap
改进,JDK
做了从单一的synchronize-排它锁
改进为多个segment的分段锁
,这里将锁的粒度细粒度化,可以提供更高的并发,提供了更强大的横向扩展能力。
水平扩展
那么究竟怎样可以提供一个良好的水平扩展能力呢?《Java并发编程实战》给出以下的优化点
软件设计层面
- 发现在框架中隐藏的串行部分:串行的部分往往是性能瓶颈部分,它阻碍了系统的扩展性、并发度
代码层面
- 避免热点域(本文)
- 减少线程上下文的切换
- 减小锁的范围:同步代码块里的代码越少越好
- 减小锁的粒度:锁分段