作者:邹建伟,北京开科唯识技术有限公司 技术专家。
一、互联网理财的兴起
在经济和科技飞速发展的趋势下,相比于以前传统的线下理财模式,互联网理财的模式,因其入围门槛相对较低,选择范围广,加上随时随地用电脑或者手机就能够进行理财,导致便捷性和灵活性的提升,从而让越来越的人们开始接受理财、乐于理财,理财的意识和投入的形态也越来越多。但随着监管制度的管控、用户规模、渠道规模、业务形态、高并发业务请求的不断增长和变化,传统的理财 IT 基础设施建设已经无法满足用户的使用体验,基于分布式系统建设新的业务系统必将破浪前行。
这次我们在中国某大型股份制银行—— G 行的互联网理财系统建设中,也是采用了分布式的数据库系统来取代传统 Oracle 数据库系统,在使用分布式数据库 TiDB 时,遇到了新技术适配的一些问题,通过迁移、开发改造和联调优化,积累了互联网理财场景中的一些分布式数据库 TiDB 的经验。本篇文章分享下在建设中遇到的问题和最终的解决方案,希望对所有准备建设和正在建设互联网理财系统的的用户有所帮助。
二、互联网理财业务简介
互联网理财最早于 2003 年就已经开展业务,主要承载基金代销、理财销售等线上业务,2018 年 4 月随着“资管新规”的发布,银行理财产品起售点由 5 万下调至 1 万,大大促进了银行资管理财业务的发展,更加激发了客户购买理财产品的热情,部分明星热销产品,更是吸引了大量客户集中抢购。G 行互联网理财系统主要涉及两大类业务场景:
- 联机交易场景 :处理开户申请请求,对单笔交易的响应时间有较高的要求;
- 批量任务场景 :处理理财批量业务,属于计算密集型的工作,对数据库大数据量下的吞吐能力要求较高。
2.1 业务功能拓扑
互联网理财系统主要包含文件传输、实时开户、交易确认、份额登记、收益结转、产品管理、产品运营等功能,拓扑图如下:
业务对数据库处理能力提出了明确的技术指标要求:
- 批量任务:要求数据库提供在 2h 内对 5000w 笔交易记录,能够跑完夜间批量任务的能力。
- 联机任务:要求数据库提供事务平均响应延时不超过 100ms 的能力。
2.2 数据存储拓扑
整套集群服务器使用接近 30 台物理服务器,每台物理服务器均配置 4 块 NVMe 盘、2 个万兆网卡、2 个千兆网卡、72vcore、512G 内存,预期可以保留 3 个月的在线数据。
三、选型之路
3.1 背景与依据
在传统理财业务系统使用集中式的 IOE 架构,原有传统数据库 Oracle 数据库遇到支撑的瓶颈,已无法适应互联网+理财业务模式的发展需求, 取而代之的是通过开源自主可控、分布式数据库的技术方案来支撑当下的互联网技术模式的系统建设,现有 Oracle 主要遇到亟待解决的的问题如下:
- 容量上限不能扩展 (存不下、查不出来,索引优化已没有用)。
- 分布式改造和适配如何取舍。
在当前分布式数据库解决方案领域内,可以聚焦的数据库选择范围就剩下分库分表方案 和 NewSQL 原生分布式方案(顺带的说明下,这里我们不争论分库分表究竟算不算分布式架构,我们相信技术是为场景存在的)。选型我们主要考虑三点:首先是数据库功能,例如括高可用性、SQL 兼容、横向扩展、扩容等等;其次是技术架构的前瞻性、发展潜力、社区活跃度;最后是大数据量下的数据库性能最为看重。前期我们与候选的数据库厂家的工程师合作模拟了 daemon 业务程序,来验证数据库的性能,主要两个部分内容:
- 数据初始化能力
来自互联网渠道的理财数据,预估每小时至少千万级别,在 1 个小时内我们有一系列的批量任务,留给数据库入库的时间最多 10 分钟。
- 业务性能测试
理财业务具有多任务并存的特点,大方向说包括联机交易任务和跑批任务两部分,跑批任务包括日间任务和夜间任务以及数仓 edw 任务。联机任务要求事务平均响应延时不超过 100ms,批量任务即使并存时也不可以超过总体执行时间。
经过多轮的测试、评审,分库分表对业务设计上的不解决够优雅,由于拆分字段的规则限制了业务实现的一些方式,不能灵活进行业务处理,并且在批量的复杂逻辑SQL 处理上性能无法满足业务要求,最终我们选择 TiDB 来进行分布式数据库的建设。验证下来,完全满足业务数据初始化效率、提升联机交易和批量处理响应能力、缩短批量处理时间周期,而且也无需限制业务场景的处理逻辑,根据压力需求灵活的进行在线扩展和在线收缩。上线前的非功能测试中,联机交易部分:在并发用户 30 下,数据库处理能力为 536 笔/s,响应时间为 0.0558s,满足预期 200TPS。批量入库:64 并发下,5kw 数据入库 9.7min,当然这并非极限值。
3.2 TiDB 的特性优势
水平弹性扩展
这里说的水平扩展包括两方面:计算能力和存储能力。TiDB Server 负责处理 SQL 请求,随着业务的增长,可以简单的添加 TiDB Server 节点,提高整体的处理能力,提供更高的吞吐。TiKV 负责存储数据,随着数据量的增长,可以部署更多的 TiKV Server 节点解决数据 Scale 的问题。
高可用
TiDB 集群核心三大组件:TiDB/TiKV/PD 都能容忍部分实例失效,不影响整个集群的可用性。TiDB 本身是无状态的,支持动态的增加删除。TiKV/PD 采用 Raft 协议,在大多数存活的前提集群便是可用的。
数据强一致
TiDB 的分布式算法使用 Raft,其事务可以跨分片、跨节点执行,并且强一致。
生态强大
这个特点应该毋庸置疑,说是“风口的猪”也不过分。TiDB 分布式数据库从 2015 年 4 月份开源以来,有来自社区的开发者、用户、布道师、设计师等不同角色的贡献者们的细心呵护,也有来自 PingCAP 持续不断的资源投入,截止到 2020 年 3 月,TiDB 社区项目已经聚集了来自全球的 760 多位 Contributor,100 多位核心用户,30000+ GitHub Stars 支持,TiDB 现已被近 1000 家不同行业的领先企业应用在实际生产环境。TiDB 详细的架构和原理可以参照官网(不再赘述)。
四、开发改造中的思考&问题
G 行采用的 TiDB 版本是 3.0 早期版本,仅提供乐观锁机制, 这个问题在 TiDB 3.0.8 后引入了悲观锁后得到了解决。作为一名“重度”悲观锁使用者,TiDB的乐观锁是我刚开始接触 TiDB,所认为的开发改造过程中最大“阻碍”。但实际开发过程中,这里的碰到的问题其实很少,更多是认知理解的过程。整个改造过程中主要遇到了以下几个方面的问题,也都通过 TiDB 提供的最佳开发实践获得了很好的处理效果。
问题 1:热点账户更新
select for update 语句是悲观锁机制下热点账户更新场景常被利用的机制,Oracle 数据库中,这个语句的作用是:
The FOR UPDATE clause lets you lock the selected rows so that other users cannot lock or update the rows until you end your transaction.
也就是说利用它可以实现数据行的锁定,阻塞其它事务对该行数据的 DML 操作。TiDB 也支持这个语句,不过在乐观锁机制下,这个语句不会在事务开启就锁住数据,而是其他事务在提交的时候进行冲突检查,如有冲突,会进行回滚。
问题 2:Rows Affected 不可信
悲观锁机制下,显式事务每条 DML 语句执行完成后数据库会返回影响行数:affect rows,业务会利用 affect rows 实际值不同进行不同的逻辑处理,这个是没问题的。不过 TiDB 乐观锁机制,显示事务每条 SQL 执行完成后返回的 affect rows 是不可信,究其根本原因,悲观锁是先加锁再更新,而乐观锁机制下,行冲突检测和数据上锁是放在事务提交时检测的,这里举一个例子:
对于事务 B 来说,update 语句执行后数据库返回 1 row affected,不过显而易见,这个语句提交时并没有发生更新,也即此时的 Affected rows 是不可信的。如果业务还希望使用 affected rows 来做程序执行逻辑的判断条件,可行的方式是:显示事务下,先判断事务成功的条件,然后再根据 affected rows 进行逻辑处理。而隐示事务下,可以直接使用 affected rows 进行逻辑处理。
问题 3:大事务
熟悉 MySQL 的同学会对事务大小设计比较熟悉,开发规范中会尽可能拆分大事务为多个小事务。TiDB 对大事务也有类似的要求,并给出了限定具体限定标准:
- 每个事务内的 SQL 数量不超过 5000 条(可配置)。
- 每个键值对不超过 6MB。
- 键值对的总大小不超过 100MB。
- 键值对的总数不超过 300,000。
开发使用过程中,体会最多的限制是键值对的总数不超过 300,000,老系统稍不注意就超出了这个限制,造成处理失败。这里要着重说明,键值对数与行数不等价。举例说明:一行数据是一个键值对,一行索引也是一个键值对,当一张表只有 9 个索引时,每 insert 一行数据会写入 10 个键值对。这里的原因涉及到 TiKV 使用的存储格式,感兴趣的同学可以搜索 TiDB 相关文章进一步阅读。
问题 4:WRITE CONFLICT
WRITE CONFLICT 是开发中碰到的频率最多的问题之一,报错示例信息:
Err:[kv:9007]Write conflict, txnStartTS=411771139330670593, conflictStartTS=411754809751764993, conflictCommitTS=0, key={tableID=698, handle=391317290} primary={tableID=698, indexID=5, indexValues={0, 391317246, }} [try again later], ErrCount:313, SnapshotVersion:411752587989614594
问题的成因这里不赘述,感兴趣的同学可以搜索相关文档查看。TiDB 默认针对这个错误不会进行事务重试,由参数 tidb_disable_txn_auto_retry 控制,3.0 版本默认值是 1,而在更早的 2.1 版本里没有这个参数,默认行为是会自动重试,默认值更改的原因是优化事务处理逻辑,适应更多场景。
五、总结
目前互联网理财业务系统稳定的运行在 TiDB 3.0 版本乐观锁下,TiDB 4.0 马上要 GA 了,我们当初能赶上基于 4.0 的改造该有多好啊,据说 4.0 版本有了更多的新特性
,让后续迁移到 TiDB 数据库的同学会更加的通用、易用。比如:
4.0 有了大事务
对于开发人员说,TiDB 4.0 以前事务的限制始终有点「达摩克利斯之剑」的意味,总是担心一个不留心就撞墙。4.0 版本开始,对事务的限制松绑很多,键值对的总大小允许 10GB 以内。
有了悲观锁
相比大事务带来的别扭,悲观锁个人觉得是最期待的特性。最早在 TiDB 3.0.8 版本引入,4.0 版本开始默认开启悲观锁。悲观事务模型是对于金融场景非常重要的一个特性,有了悲观锁我相信 TiDB 才会走的更高更远。
新特性很期待
TiDB 4.0 GA 版本即将发布,除了本文涉及的大事务、悲观锁等等还有很多新特性,例如 Sequence、Key Visualizer、Follower Read等等,有兴趣的同学可以官网查看相关文章。
更多 TiDB 实践案例阅读: 案例 | PingCAP关于 TiDB 使用上的问题可以登录可以在 TiDB 用户论坛 Asktug.com 交流探讨~