TiDB 的正确使用姿式

最近这几个月,特别是 TiDB RC1 发布后,愈来愈多的用户已经开始测试起来,也有不少朋友已经在生产环境中使用,咱们这边也陆续的收到了不少用户的测试和使用反馈。很是感谢各位小伙伴和早期用户的厚爱,并且看了这么多场景后,也总结出了一些 TiDB 的使用实践 (其实 Spanner 的最佳实践大部分在 TiDB 中也是适用的,MySQL 最佳实践也是),也是借着 Google Cloud Spanner 发布的东风,看了一下 Spanner 官方的一些最佳实践文档,写篇文章讲讲 TiDB 以及分布式关系型数据库的一些正确的使用姿式,固然,时代也在一直发展,TiDB 也在不停的进化,这篇文章基本上只表明近期的一些观察。算法

首先谈谈 Schema 设计的一些比较好的经验。因为 TiDB 是一个分布式的数据库,可能在表结构设计的时候须要考虑的事情和传统的单机数据库不太同样,须要开发者可以带着「这个表的数据会分散在不一样的机器上」这个前提,才能作更好的设计。数据库

和 Spanner 同样,TiDB 中的一张表的行(Rows)是按照主键的字节序排序的(整数类型的主键咱们会使用特定的编码使其字节序和按大小排序一致),即便在 CREATE TABLE 语句中不显式的建立主键,TiDB 也会分配一个隐式的。
有四点须要记住:网络

  1. 按照字节序的顺序扫描的效率是比较高的;架构

  2. 连续的行大几率会存储在同一台机器的邻近位置,每次批量的读取和写入的效率会高;并发

  3. 索引是有序的(主键也是一种索引),一行的每一列的索引都会占用一个 KV Pair,好比,某个表除了主键有 3 个索引,那么在这个表中插入一行,对应在底层存储就是 4 个 KV Pairs 的写入:数据行以及 3 个索引行。框架

  4. 一行的数据都是存在一个 KV Pair 中,不会被切分,这点和类 BigTable 的列式存储很不同。分布式

表的数据在 TiDB 内部会被底层存储 TiKV 切分红不少 64M 的 Region(对应 Spanner 的 Splits 的概念),每一个 Region 里面存储的都是连续的行,Region 是 TiDB 进行数据调度的单位,随着一个 Region 的数据量愈来愈大和时间的推移,Region 会分裂/合并,或者移动到集群中不一样的物理机上,使得整个集群可以水平扩展。函数

  • 建议:高并发

    1. 尽量批量写入,可是一次写入总大小不要超过 Region 的分裂阈值(64M),另外 TiDB 也对单个事务有大小的限制。性能

    2. 存储超宽表是比较不合适的,特别是一行的列很是多,同时不是太稀疏,一个经验是最好单行的总数据大小不要超过 64K,越小越好。大的数据最好拆到多张表中。

    3. 对于高并发且访问频繁的数据,尽量一次访问只命中一个 Region,这个也很好理解,好比一个模糊查询或者一个没有索引的表扫描操做,可能会发生在多个物理节点上,一来会有更大的网络开销,二来访问的 Region 越多,遇到 stale region 而后重试的几率也越大(能够理解为 TiDB 会常常作 Region 的移动,客户端的路由信息可能更新不那么及时),这些可能会影响 .99 延迟;另外一方面,小事务(在一个 Region 的范围内)的写入的延迟会更低,TiDB 针对同一个 Region 内的跨行事务是有优化的。另外 TiDB 对经过主键精准的点查询(结果集只有一条)效率更高。

关于索引

除了使用主键查询外,TiDB 容许用户建立二级索引以加速访问,就像上面提到过的,在 TiKV 的层面,TiDB 这边的表里面的行数据和索引的数据看起来都是 TiKV 中的 KV Pair,因此不少适用于表数据的原则也适用于索引。和 Spanner 有点不同的是,TiDB 只支持全局索引,也就是 Spanner 中默认的 Non-interleaved indexes。全局索引的好处是对使用者没有限制,能够 scale 到任意大小,不过这意味着,索引信息不必定和实际的数据在一个 Region 内。

  • 建议:
    对于大海捞针式的查询来讲 (海量数据中精准定位某条或者某几条),务必经过索引。

固然也不要盲目的建立索引,建立太多索引会影响写入的性能。

反模式 (最好别这么干!)

其实 Spanner 的白皮书已经写得很清楚了,我再赘述一下:

第一种,过分依赖单调递增的主键,AUTO INCREMENT ID
在传统的关系型数据库中,开发者常常会依赖自增 ID 来做为 PRIMARY KEY,可是其实大多数场景你们想要的只是一个不重复的 ID 而已,至因而不是自增其实无所谓,可是这个对于分布式数据库来讲是不推荐的,随着插入的压力增大,会在这张表的尾部 Region 造成热点,并且这个热点并无办法分散到多台机器。TiDB 在 GA 的版本中会对非自增 ID 主键进行优化,让 insert workload 尽量分散。

  • 建议:
    若是业务没有必要使用单调递增 ID 做为主键,就别用,使用真正有意义的列做为主键(通常来讲,例如:邮箱、用户名等)

使用随机的 UUID 或者对单调递增的 ID 进行 bit-reverse (位反转)

第二种,单调递增的索引 (好比时间戳)
不少日志类型的业务,由于常常须要按照时间的维度查询,因此很天然须要对 timestamp 建立索引,可是这类索引的问题本质上和单调递增主键是同样的,由于在 TiDB 的内部实现里,索引也是一堆连续的 KV Pairs,不断的插入单调递增的时间戳会形成索引尾部的 Region 造成热点,致使写入的吞吐受到影响。

  • 建议:
    由于不可避免的,不少用户在使用 TiDB 存储日志,毕竟 TiDB 的弹性伸缩能力和 MySQL 兼容的查询特性是很适合这类业务的。另外一方面,若是发现写入的压力实在扛不住,可是又很是想用 TiDB 来存储这种类型的数据,能够像 Spanner 建议的那样作 Application 层面的 Sharding,以存储日志为例,原来的可能在 TiDB 上建立一个 log 表,更好的模式是能够建立多个 log 表,如:log_1, log_2 … log_N,而后业务层插入的时候根据时间戳进行 hash ,随机分配到 1..N 这几个分片表中的一个。

相应的,查询的时候须要将查询请求分发到各个分片上,最后在业务层汇总结果。

查询优化

TiDB 的优化分为基于规则的优化(Rule Based Optimization)和基于代价的优化(Cost Based Optimization), 本质上 TiDB 的 SQL 引擎更像是一个分布式计算框架,对于大表的数据由于自己 TiDB 会将数据分散到多个存储节点上,能将查询逻辑下推,会大大的提高查询的效率。

TiDB 基于规则的优化有:
谓词下推

谓词下推会将 where/on/having 条件推到离数据表尽量近的地方,好比:

select * from t join s on t.id = s.id where t.c1 < 10

能够被 TiDB 自动改写成

select * from (select * from t where t.c1 < 10) as t join s on t.id = s.id

关联子查询消除

关联子查询可能被 TiDB 改写成 Join,例如:

select * from t where t.id in (select id from s where s.c1 < 10 and s.name = t.name)

能够被改写成:

select * from t semi join s on t.id = s.id and s.name = t.name and s.c1 < 10

聚合下推
聚合函数能够被推过 Join,因此相似带等值链接的 Join 的效率会比较高,例如:

select count(s.id) from t join s on t.id = s.t_id

能够被改写成:

select sum(agg0) from t join (select count(id) as agg0, t_id from s group by t_id) as s on t.id = s.t_id

基于规则的优化有时能够组合以产生意想不到的效果,例如:

select s.c2 from s where 0 = (select count(id) from t where t.s_id = s.id)

在TiDB中,这个语句会先经过关联子查询消除的优化,变成:

select s.c2 from s left outer join t on t.s_id = s.id group by s.id where 0 = count(t.id)

而后这个语句会经过聚合下推的优化,变成:

select s.c2 from s left outer join (select count(t.id) as agg0 from t group by t.s_id) t on t.s_id = s.id group by s.id where 0 = sum(agg0)

再通过聚合消除的判断,语句能够优化成:

select s.c2 from s left outer join (select count(t.id) as agg0 from t group by t.s_id) t on t.s_id = s.id where 0 = agg0

基于代价的优化有:

读取表时,若是有多条索引能够选择,咱们能够经过统计信息选择最优的索引。例如:

select * from t where age = 30 and name in ( ‘小明’, ‘小强’)
对于包含 Join 的操做,咱们能够区分大小表,TiDB 的对于一个大表和一个小表的 Join 会有特殊的优化。
例如
select * from t join s on s.id = t.id
优化器会经过对表大小的估计来选择 Join 的算法:即选择把较小的表装入内存中。
对于多种方案,利用动态规划算法选择最优者,例如:

(select * from t where c1 < 10) union all (select * from s where c2 < 10) order by c3 limit 10

t 和 s 能够根据索引的数据分布来肯定选择索引 c3 仍是 c2。

总之正确使用 TiDB 的姿式,或者说 TiDB 的典型的应用场景是:

大数据量下,MySQL 复杂查询很慢;

大数据量下,数据增加很快,接近单机处理的极限,不想分库分表或者使用数据库中间件等对业务侵入性较大,架构反过来约束业务的 Sharding 方案;

大数据量下,有高并发实时写入、实时查询、实时统计分析的需求;

有分布式事务、多数据中心的数据 100% 强一致性、auto-failover 的高可用的需求。

若是整篇文章你只想记住一句话,那就是数据条数少于 5000w 的场景下一般用不到 TiDB,TiDB 是为大规模的数据场景设计的。若是还想记住一句话,那就是单机 MySQL 能知足的场景也用不到 TiDB。

相关文章
相关标签/搜索