01
前言
政府人才房系统是贝壳CTO线广州研发中心与某互联网大厂合作研发,针对政府发放的人才房进行全线上化管理、运营与房源分配的系统。该系统体量较大,涉及员工积分、房源管理、分房规则匹配、在线选房、入住后管理等一系列流程节点,行业上未有类似产品可供参考。项目研发过程中,广研的小伙伴们攻克了许多业务和技术上的困难,尤其是在开源产品的使用上,对开源社区也有一定的贡献。
02
背景
相信各位研发的小伙伴都曾遇到过,当我们把系统微服务化,大幅提高应用的可用性、复用性、扩展性同时,也带来了一些“水土不服”的症状,其中“分布式事务”的需求就首当其冲。
政府人才房系统在需求迭代的过程中,引入了越来越多批量数据处理的场景。比如所有员工选房完成后,进入下一个流程节点时需要批量操作员工、房源、轮候库等信息。受限于业务需求,我们无法使用消息队列的最终一致性解决方案,只能跨服务实时操作数据。
03
选型
经过一系列需求评估,广研技术团队决定在项目中引入分布式事务框架来解决问题。
LCN | Seata AT | Seata TCC | 消息事务+最终一致性 | |
SQL支持 | / | 少 | / | / |
隔离性支持 | / | 脏读 | / | / |
社区活跃度 | 不活跃 | 活跃 | / | / |
编码难度 | 高 | 低 | 高 | 高 |
代码侵入性 | 高 | 低 | 高 | 高 |
性能 | 低 | 低 | 高 | 高 |
资源占用 | 高 | 很高 | 高 | 高 |
通过对几种流行的分布式事务解决方案,以及框架的对比,我们最终决定采用由阿里巴巴开源的Seata,它的AT模式是最契合人才房系统的需求的。
04
存在问题
接入了社区版 Seata 来管理全局事务之后,意料之中的性能问题开始凸显。比如对单表插入30000条数据,在无全局事务的情况下,其用时约为3.5s,但是加上了 Seata 全局事务之后,其用时约为280s。大批量操作本身的一个性能消耗再加上 Seata 对于全局事务控制的开销,其性能损耗基本上是不可接受的。
注:基于Seata 0.9.0
05
排查 & 优化
基于存在问题的评估,我们对社区版Seata做了相应排查和优化,以便其既满足人才房系统的实时一致性需求,又能最大化的降低性能的损耗。下面就是广研针对“批量操作数据”的场景对社区版Seata的优化,并且将我们的成果提交给了官方。本文撰写时,我们针对Seata批量操作数据的性能优化均已被Seata官方采纳(如下详述),并入社区版Seata主线代码。
5.1
查询锁问题
Seata 在查询前置镜像的时候,使用的是 pk = #{pk1} or pk = #{pk2} ... 这种形式,根据测试,在使用主键查询的时候,in的性能是比or要高的,这一点在MySQL的官方说明中也可以得到验证。
除了in和or的写法的性能上的差异,在以下代码中,rowsIncludingPK.pkRows()是一个时间复杂度为O(MN)级别的方法(其中M为字段数,N为影响的行数),因此在循环里面使用rowsIncludingPK.pkRows().size()这种写法会使得循环的时间复杂度为O(MN²);经过优化之后,可以避免在循环里面调用rowsIncludingPK.pkRows().size(),从而使得时间复杂度可以优化成O(MN)级别的,在数据量大的时候,整体的性能影响还是很大的。
对此,我们进行了第一次的针对性的优化,用相同的插入30000条数据进行测试,用时大约是16s,虽然花销也是原来的几倍,但是相对于原本的280s已经可以算得上质的飞跃了,同时根据这个优化的原理可以大概猜测,进行分批也可以在一定程度上缓解这个性能问题。
MySQL官网连接参数说明: 【https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html#function_in】
Seata社区pr地址: 【https://github.com/seata/seata/pull/1938】
5.2
Rollback Info 压缩
在 Seata 的全局事务中,Seata 通过DML的执行前镜像,执行后镜像,去打包成一个大的字段保存。对于一个非常大批量的请求的话,那这个字段就会非常的大,对于网络IO和数据库的磁盘IO的压力都会比较大。
经过测试,压缩后的Rollback Info的大小大概是原来的10%左右,单个 30000 条的数据插入的性能可以提升17%左右,整个全局事务的用时约为7.9s。
以下为进行压缩的实验数据,表格中的单位为ms
原始rollbackinfo长度:1150524
common | zip | gzip | lz4 | bzip2 | 7z | |
compress usage | 0 | 17 | 16 | 3 | 425 | 486 |
insert usage | 193 | 22 | 28 | 95 | 32 | 33 |
sum | 193 | 39 | 45 | 98 | 457 | 519 |
原始rollbackinfo长度:11500524
common | zip | gzip | lz4 | |
compress usage | 0 | 165 | 166 | 29 |
insert usage | 1153 | 168 | 172 | 641 |
sum | 1153 | 334 | 338 | 670 |
压缩率
100时原始长度为:115524
1000时原始长度为:1150524
10000时原始长度为:11500524
zip | gzip | |
100 | 10.9% | 10.9% |
1000 | 10.5% | 10.5% |
10000 | 10.4% | 10.4% |
Seata社区pr地址:
【https://github.com/seata/seata/pull/3172】
5.3
JDBC 参数优化
在DB模式下,Seata 需要把全局锁信息插入到一张lock_table的表里面。这里的批量插入的策略采取的是addBatch->executeBatch的形式。
根据MySQL官网的说明:MySQL Performance Extensions Doc,连接参数中的rewriteBatchedStatements为true时,在执行executeBatch,并且操作类型为insert时,jdbc驱动会把对应的SQL优化成insert into () values (), ()的形式来提升批量插入的性能。
经过测试,在单个 30000 条的数据插入的性能可以再次提升19%左右,来到6.4s左右。
Seata社区pr地址:
【https://github.com/seata/seata/pull/3291】
5.4
多线程、分批处理
对于 Seata 而言,判断是否为同一个全局事务范围内的方法是根据其启动全局事务管理的时候生成的全局事务id,那么在异步线程里面,如果可以绑定调用线程的全局事务xid到ThreadLocal变量里面,那么很自然的,这个异步线程就被纳入到全局事务的管控内了。
同时,借助SpringBoot对于切面的支持,在全局事务退出的时候,做一次等待全部分支事务都执行完成再进行全局提交,这样的话,就可以达到 Seata 和多线程的完美结合了。同时,考虑到单个请求传输数据量以及请求超时的问题,我们也将一个大批量的请求拆分成多个小的请求进行处理,再结合多线程进行请求,这样的话的总的时间也可以再进一步的降低,并且经过不断的测试,单个批次的数据量为5000的时候的性能是比较高的,在单个批次的数据量为5000的时候,进行一次30000条的数据插入,最终用时约为3.4秒,这个时候使用Seata跟没有使用Seata用时已经是大体相当了,并且经过测试,当数据量提升的时候,比如说到达100000条,性能提升会比较显著。
(该优化暂未提交)
06
后话
经过上述的一系列优化,30000条数据的批量处理时间已经从一开始280s降低至6.4s,执行时间减少了98%,整体性能达到预期。
经过人才房系统的线上考验,现在Seata已经广泛应用在广研的系统中。我们也在持续跟进和优化,使其更好的支撑业务的迭代。希望本文针对“批量数据处理”的思路,能给大家带来一定的实战帮助。