TiDB 在华泰证券的探索与实践

文章转载自公众号华泰证券数字科技。前端

原文连接:mp.weixin.qq.com/s/Hp-ZJLdvd…mysql

做者介绍 华泰证券数字科技分布式数据库项目组,主要负责华泰证券分布式数据库系统建设工做,项目组成员均拥有多年数据库从业经历,对数据库运做原理具备较深的研究,并积累了大量实战经验。sql

传统数据库存储能力有限、扩容成本高、服务器压力大、响应时间长等问题逐渐凸显,分布式数据库应运而生。2016年末,华泰证券就已经开始着手调研分布式数据库产品。近年来,国家不断提升对信息技术自主可控的战略要求,发展和支持国产数据库事业,不只能够提高自主掌控能力,还能够不断下降企业经营成本。通过多方比较,本文将从 TiDB 技术特色、开发注意事项以及 TiDB 在华泰证券的实践进展等方面进行介绍。数据库

1. TiDB 技术特色

1.1 TiDB 简介

TiDB 是一款开源分布式 NewSQL 数据库,结合了传统 RDBMS 和 NoSQL 的最佳特性,其设计灵感来源于 Google Spanner 和 F1。TiDB 的设计目标是覆盖 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析则经过 TiSpark 来完成。TiDB 屏蔽了分库分表等 Sharding 方案对业务的侵入性,开发人员再也不须要关注数据如何分片等细节问题,专一于业务开发,极大地提高研发的生产力。bash

1.2 总体架构

TiDB 采用 Shared-Nothing、计算存储分离的分布式集群架构,主要包括三个核心组件:TiDB Server、PD Server 和 TiKV Server。此外,还有用于解决用户复杂 OLAP 需求的 TiSpark 组件。总体架构以下图所示:服务器

  • TiDB Server

负责接收 SQL 请求,处理 SQL 相关的逻辑,并经过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。TiDB Server 是无状态的,其自己并不存储数据,只负责计算,能够无限水平扩展,能够经过负载均衡组件(如 LVS、HAProxy 或 F5)对外提供统一的接入地址。网络

  • PD(Placement Driver)Server

PD Server 是整个集群的管理模块,经过 Raft 协议实现多副本集群架构,保证数据的一致性和高可用。其主要工做有三个:一是存储集群的元数据信息(某个 Key 存储在哪一个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局惟一且递增的事务 ID。架构

  • TiKV Server

TiKV Server 负责存储数据,从外部看 TiKV 是一个支持事务的分布式 Key-Value 存储引擎。存储数据的基本单位是 Region,每一个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每一个 TiKV 节点会负责多个 Region。TiKV 使用 Raft 协议作复制,保持数据的一致性和高可用。副本以 Region 为单位进行管理,不一样节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 以 Region 为单位进行调度。并发

  • TiSpark

TiSpark 做为 TiDB 中解决用户复杂 OLAP 需求的主要组件,将 Spark SQL 直接运行在 TiDB 存储层 TiKV 上,同时融合 TiKV 分布式集群的优点,并融入大数据社区生态。至此,TiDB 能够经过一套系统,同时支持 OLTP 与 OLAP,免除用户数据同步的烦恼。负载均衡

1.3 核心特性

TiDB 具有以下核心特性:

  • 高度兼容 MySQL

对于没有事务冲突场景的业务系统,在大多数状况下无需修改代码便可从 MySQL 轻松迁移至 TiDB,分库分表后的 MySQL 集群亦可经过 TiDB 工具进行实时迁移。

  • 水平弹性扩展

这里说的水平扩展包括两方面:计算能力和存储能力。TiDB Server 负责处理 SQL 请求,随着业务的增加,能够简单的添加 TiDB Server 节点,提升总体的处理能力。TiKV 负责存储数据,随着数据量的增加,能够部署更多的 TiKV Server 节点解决数据容量的问题。PD 会在 TiKV 节点之间以 Region 为单位作调度,将部分数据迁移到新加的节点上。因此在业务的早期,能够只部署少许的服务实例,随着业务量的增加,按照需求添加 TiKV 或者 TiDB 实例。

  • 支持分布式事务

TiDB 支持标准的 ACID 事务,经过两阶段提交和乐观锁实现分布式事务。

  • 高可用

TiDB/TiKV/PD 这三个组件都能容忍部分实例失效,不影响整个集群的可用性。TiDB 是无状态的,能够部署多个实例,前端经过负载均衡组件对外提供服务。当单个实例失效时,会影响正在这个实例上进行的会话,从应用的角度看,会出现单次请求失败的状况,从新链接后便可继续得到服务。PD 是一个集群,经过 Raft 协议保持数据的一致性,单个实例失效时,若是这个实例不是 Raft 的 leader,那么服务彻底不受影响;若是这个实例是 Raft 的 leader,会从新选出新的 Raft leader,自动恢复服务。PD 在选举的过程当中没法对外提供服务,这个时间大约是 3 秒钟。TiKV 是一个集群,经过 Raft 协议保持数据的一致性,并经过 PD 作负载均衡调度。单个节点失效时,会影响这个节点上存储的全部 Region。对于 Region 中的 Leader 节点,会中断服务,等待从新选举;对于 Region 中的 Follower 节点,不会影响服务。当某个 TiKV 节点失效,而且在一段时间内没法恢复,PD 会将其上的数据迁移到其余的 TiKV 节点。

  • 一站式 HTAP 解决方案

TiDB 做为典型的 OLTP 行存数据库,同时兼具强大的 OLAP 性能,配合 TiSpark,可提供一站式 HTAP(Hybrid Transactional and Analytical Processing)解决方案,一份存储同时处理 OLTP & OLAP,无需传统繁琐的 ETL 过程。

  • 云原生 SQL 数据库

TiDB 是为云而设计的数据库,支持公有云、私有云和混合云。

2. TiDB 开发注意事项

做为一款新兴的 NewSQL 数据库,TiDB 在许多方面取得了使人瞩目的成绩。尤为在兼容性方面,TiDB 能够说兼容 MySQL 90% 以上的行为,这为业务系统平滑迁移奠基了良好的基础。但咱们依旧须要对剩下的 10% 的不兼容行为保持严谨的态度,避免给业务系统带来风险。

2.1 事务

(1)TiDB 的隔离级别

与不少传统数据库不一样,TiDB 支持的隔离级别是 Snapshot Isolation(SI,快照隔离级别),采用“乐观锁+MVCC”的实现方式。它和 Repeatable Read(RR)隔离级别基本等价但也有必定的差别,详细状况以下:

①TiDB 的 SI 隔离级别能够避免幻读(Phantom Reads),但 ANSI/ISO SQL 标准中的 RR 隔离级别不能避免。

所谓幻读是指:事务 A 首先根据条件查询获得 n 条记录,而后事务 B 改变了这 n 条记录以外的 m 条记录或者增添了 m 条符合事务 A 查询条件的记录,致使事务 A 再次发起请求时发现有 n+m 条符合条件记录,就产生了幻读。

②TiDB 的 SI 隔离级别不能避免写偏斜(Write Skew),须要使用 select for update 语法来避免写偏斜。

写偏斜是指:两个并发的事务读取了两行不一样但相关的记录,接着这两个事务各自更新了本身读到的那行数据,并最终都提交了事务,若是这两行相关的记录之间存在着某种约束,那么最终结果多是违反约束的。下图的“黑白球”经常被用来讲明写偏斜问题:

TiDB 在默认配置下不能避免丢失更新(Lost Updates)。

所谓丢失更新是指:两个事务 A、B 读取相同记录并更新同一列的值,若 A 先于 B 提交事务,当 B 事务提交后 A 再次查询时发现本身的更新丢失了。

TiDB 在默认配置下不能避免丢失更新是因为在事务冲突中提交较晚的事务被自动重试致使的(重试时会获取最新的 tso,至关于从新开启了一个事务),能够将参数 tidb_disable_txn_auto_retry 设成 1 来避免丢失更新,可是修改后发生冲突的事务将会失败并回滚,而不进行自动重试。

(2)显式事务中 DML 语句返回的 affected rows 不可信

与全部使用了乐观锁机制的分布式数据库同样,在显式执行的事务中(设置为非自动提交 autocommit=0,或使用 begin 语句显式声明事务开始),DML 操做所返回的 affected rows 并不保证与最终提交事务时所影响的数据行数一致。

以下案例,事务 B 在并发中丢失了它的更新,它的 affected rows 并不可靠。

这是因为在显式执行的事务中 DML 操做与提交操做分开被执行,在事务提交过程当中,若是因为事务冲突、找不到 TiKV、网络不稳定等缘由而发生了重试,TiDB 将获取新的时间戳从新执行本事务中的 DML 操做,本来的 SI 隔离级别在重试后会产生相似 RC(Read Committed)隔离级别的不可重复读与幻读异常现象。因为重试机制在内部完成,若是最终事务提交成功,用户通常是没法感知到是否发生了重试的,所以不能经过 affected rows 来做为程序执行逻辑的判断条件。而隐式事务中(以单条 SQL 为单位进行提交),语句的返回是提交以后的结果,所以隐式事务中的 affected rows 是可信的。

( 3)不支持 Spring 的 PROPAGATION_NESTED 传播行为

Spring 支持的 PROPAGATION_NESTED 传播行为会启动一个嵌套的事务,它是当前事务之上独立启动的一个子事务。嵌套事务开始时会记录一个 savepoint,若是嵌套事务执行失败,事务将会回滚到 savepoint 的状态,嵌套事务是外层事务的一部分,它将会在外层事务提交时一块儿被提交。下面案例展现了 savepoint 机制:

mysql> BEGIN;
mysql> INSERT INTO T2 VALUES(100);
mysql> SAVEPOINT svp1;
mysql> INSERT INTO T2 VALUES(200);
mysql> ROLLBACK TO SAVEPOINT svp1;
mysql> RELEASE SAVEPOINT svp1;
mysql> COMMIT;
mysql> SELECT * FROM T2;
+------+
| ID |
+------+
| 100 |
+------+
复制代码

TiDB 不支持 savepoint 机制,所以也不支持 PROPAGATION_NESTED 传播行为。基于 Java Spring 框架的应用若是使用了 PROPAGATION_NESTED 传播行为,须要在应用端作出调整,将嵌套事务的逻辑移除。

(4)对大事务的限制

基于日志的数据库在面对大事务时,须要手动调大可用日志的容量,以免日志被单一事务占满。因为 TiDB 分布式两阶段提交的要求,修改数据的大事务可能会出现一些问题。所以,TiDB 对事务大小设置了一些限制以减小这种影响:

  • 每一个键值对不超过 6MB

  • 键值对的总数不超过 300000

  • 键值对的总大小不超过 100MB

一行数据是一个键值对,一行索引也是一个键值对,当一张表只有 2 个索引时,每 insert 一行数据会写入 3 个键值对。据此,涉及大量数据增删改的事务(如批量的对帐事务等),须要进行缩减事务量的改造,最佳实践是将大事务改写为分页 SQL,分段提交,TiDB 中能够利用 order by 配合 limit 的 offset 实现分页功能,写法以下:

update tab set value=’new_value’ where id in (select id from tab order by id limit 0,10000);
commit;
update tab set value=’new_value’ where id in (select id from tab order by id limit 10000,10000);
commit;
update tab set value=’new_value’ where id in (select id from tab order by id limit 20000,10000);
commit;
... ...
复制代码

2.2 自增 ID

TiDB 的自增 ID(auto_increment)只保证自增且惟一,并不保证连续分配。TiDB 目前采用批量分配的方式,因此若是在多台 TiDB 上同时插入数据,分配的自增 ID 会不连续。当多个线程并发往不一样的 tidb-server 插入数据的时候,有可能会出现后插入的数据自增 ID 小的状况。此外,TiDB 容许给整型类型的列指定 auto_increment,且一个表只容许一个属性为 auto_increment的列。

2.3 惟一性约束

和其余数据库同样,TiDB 中的主键和惟一索引都是表中数据的惟一性约束,可是有以下不一样点:

  • TiDB 中的主键必须在建表时声明,目前版本(v2.1.0)还不能为已有的表添加、修改或删除主键;惟一索引没有此限制

  • Drop Column 操做不支持删除主键列

TiDB 不支持外键,要去掉全部表结构中建立外键的相关语句。外键的级联操做多表数据的功能须要在应用中完成。

2.4 索引

和表中的数据同样,TiDB 中表的索引在存储引擎中也被做为 KV 来存储,一行索引是一个 KV 对。例如一张有 2 个索引的表,每插入一行数据的时候,会写入 3 个 KV 对。

TiDB 支持主键索引、惟一索引,也支持二级索引,构成以上索引的能够是单一列,也能够是多个列(复合索引)。TiDB 目前(v2.1.0)还不支持双向索引、全文索引、分区表的全局索引。

TiDB 中在查询的谓词是 =,>,<,>=,<=,like ‘...%’,not like ‘...%’,in,not in,<>,!=,is null,is not null 时可以使用索引,使用与否由优化器来决策。TiDB 中在查询的谓词是 like ‘%...’,like ‘%...%’,not like ‘%...’,not like ‘%...%’,<=>时没法使用索引。

TiDB 目前(v2.1.0)对于一张表的查询还不能同时利用到这张表上的两个索引。

TiDB 中的复合索引与其余数据库同样,设计的通常原则是尽量的把数据值区分度高的列排在前面,这样就可让 SQL 在执行时尽快筛选出更少的数据行。在当前版本(v2.1.0 及如下的所有版本)使用中须要特别注意,复合索引中前一列的范围查询会停止后续索引列的使用,能够经过下面的案例来理解这个特性。在以下的查询中:

select a,b,c from tablename where a<predicate>’<value1>’ and b<predicate>’<value2>’and c<predicate>’<value3>’;
复制代码

若是 a 条件的谓词是 =in,那么在 b 的查询条件上就能够利用到组合索引(a,b,c)。例:select a,b,c from tablename where a = 1 and b<5 and c=’abc’

一样的,若是 a 条件和 b 条件的谓词都是 =in,那么在 c 上的查询就能够利用到组合索引(a,b,c)。例:select a,b,c from tablename where a in (1,2,3) and b = 5 and c=’abc’

若是 a 条件的谓词不是 = 也不是 in,那么 b 上的查询就没法利用到组合索引(a,b,c)。此时 b 条件将在 a 条件筛选后的数据中进行无索引的数据扫描。例:select a,b,c from tablename where a > 1 and b<5 and c=’abc’

这是因为在 TiDB 中,复合索引中排在前面的列若是被用于范围查询,那么后续列的查询就会在前一列筛选后的数据范围中进行非索引的扫描。

综上,在 TiDB 中进行复合索引设计时,须要尽量的将区分度高的列排在前面,将常常进行范围查询的列排在后面。

2.5 写入热点

TiKV 是一个按 range 切分的 KV 系统,KV 的 Key 决定了写入位置在哪一个 region。对于主键为非整数或没有主键的表,TiDB 会使用一个隐式的自增 rowid,大量 INSERT 时会把数据集中写入单个 region,形成写入热点。经过设置表级别选项 SHARD_ROW_ID_BITS(以下所示)能够把 rowid 打散写入多个不一样的 region,缓解写入热点问题。可是设置的过大会形成 RPC 请求数放大,增长 CPU 和网络开销。

SHARD_ROW_ID_BITS = 4 表示 2^4=16 个分片

SHARD_ROW_ID_BITS = 6 表示 2^6=64 个分片

SHARD_ROW_ID_BITS = 0 表示 2^0,就是默认值 1 个分片

CREATE TABLE 语句示例:

CREATE TABLE t (c int) SHARD_ROW_ID_BITS = 4;

ALTER TABLE 语句示例:

ALTER TABLE t SHARD_ROW_ID_BITS = 4;

分区表能够将一张表的数据分散到多张物理表中,经过合理的设计分区规则,能够进一步避免写入热点问题。

2.6 暂不支持的特性

TiDB 在大部分状况下能保证与 MySQL 的兼容,不过一些特性因为在分布式环境下无法很好的实现,目前暂时不支持,好比:

  • 存储过程

  • 视图

  • 触发器

  • 自定义函数

  • 外键约束

  • 全文索引

  • 空间索引

  • 非 UTF8 字符集

  • CREATE TABLE tblName AS SELECT stmt 语法

  • … …

3. 实践机器

咱们从 2017 年初就开始了 TiDB 的调研与测试工做,到目前为止,已经在多个业务系统测试了 TiDB 的功能与性能。TiDB 也从最初运行不稳定、性能很差、周边工具缺失的年轻产品,慢慢成长为了产品稳定、性能可随节点数目线性扩展、周边工具丰富、社区火热的金融级分布式 NewSQL 数据库。

2019 年 4 月下旬,咱们上线了第一套 TiDB 生产集群,采用 6 台服务器构成“3 TiDB Server + 3 TiKV Server”的架构。PD Server 与 TiDB Server 共享服务器。每台服务器配置 128 GB 内存,2 路共 12 核 CPU,6 块 960GB SSD 盘作成 RAID 10。

目前接入生产 TiDB 集群的业务系统分为如下几个阶段进行实施:

1)经过 TiDB Lightning 工具将当月之前的历史存量数据以文件的方式导入 TiDB 集群

2)上线当日,经过 TiDB Lightning 工具将当月的数据以文件的方式导入 TiDB 集群

3)上线以后,业务端进行双写,利用 kafka 将新的数据同步到 TiDB 生产集群

4)稳定运行几个月后,将查询流量逐步切到 TiDB

5)继续稳定运行几个月后,将查询流量和写入流量所有切到 TiDB,经过业务双写将新的数据同步到原 Mycat+MySQL 环境

6)完全下线原 Mycat+MySQL 环境

当前处于第三个阶段。自上线以来,TiDB 集群运行稳定,最高 QPS 达到每秒 3.4 万笔。写入速度与原 MySQL 环境至关,kafka 端未出现数据积压,系统资源使用均衡,而且尚有余量。

总结

从咱们实践的结果来看,TiDB 这种 NewSQL 数据库确实展示了不错的技术优点。其提供的 MySQL 兼容性让业务系统的改造代价大大下降,对分布式事务的支持让业务系统能够像访问单个 MySQL 库同样访问 TiDB。基于 raft 协议的多副本机制,极大的保证了数据的一致性和可用性。其云原生的设计理念,让扩容缩容变得很是方便,大大解放了运维人员的时间。

可是咱们也须要看到它的缺点,TiDB 最大的缺点是还比较年轻,很多功能还没有完善,所以咱们的思路是先小范围试用,选择非交易类系统进行推广,待稳定运行后再扩大推广范围。