这次的故事起源于 SpeedyCloud 一个客户的请求日志分析系统需求,需求不复杂,客户把请求日志发到指定的服务器上,然后经过 ETL 阶段把数据加工之后保存到数据库中,然后通过一个 Web 系统配合各种条件和排序查看统计结果。
鉴于客户目前的需求最多查询前一天的统计信息,那么这个非实时系统设计起来也就简单多了:
通过 ETL 程序分析日志并生成单天的数据
把单天的数据保存到数据库中
通过 Web 系统连接数据库并进行查询
那么问题来了:应该选用什么数据库?
Hadoop + Hive
ElasticSearch
MySQL
这些是第一时间出现在脑子里的列表。对于 Hadoop + Hive 来说,查询速度是个硬伤;ElasticSearch 的查询速度不错,不过对于一些临时分析需求的支持其实并不友好;MySQL 对于开发和临时的分析支持很棒,但问题是单日数据量很大,保存长时间的数据会导致 MySQL 的查询速度下降。
为了能先搭建出来一个原型系统,最终还是选用了 ElasticSearch 来存储 ETL 的结果数据。不过 Web 后台的程序写的也是挺闹心的 @_@ 。
也算是机缘巧合,同事跟我提到了 TiDB,花了一些时间了解之后,我发现 TiDB 应该是这个日志分析系统最合适的存储引擎,然后花了一些时间改出了一个 TiDB 版本的 Web 后台程序,然后开始了 TiDB 和 ElasticSearch 的 PK 旅程。
日志分析系统的一个常用的功能是查询出某一天的数据,然后按照某个指标排序,然后分页展示。写出来的 SQL 类似:
SELECT * FROM `table` WHERE `date`="20170101" ORDER BY `some_column` LIMIT 0, 20;
但是一天的数据量基本上在百万级,所以 date
列加索引的意义不大。根据 TiDB 的架构(扫描完索引,然后在根据索引的handle
扫描表),做全表扫描会比在 date
上加索引要快很多。
但是不用索引,完全靠全表扫描会有引入外一个问题:随着数据量的增加,全表扫描的时间会随之变长,所以全表扫描虽然会在开始比较快,但最终还是会慢下来。在深入了解了 TiDB 的存储结构之后,发现如果能把日期通过某种编码方式转换成一个数字,然后把这个数字当成主键(Primary Key),那么 TiDB 在查询的时候就会根据主键进行区间扫描,从而把扫描的行数限制在了一个很小的范围,而且这个范围不受到表的总行数影响,因此优化后的查询语句就变成了:
SELECT * FROM `table` WHERE `id` BETWEEN "201701010000000000" AND "201701019999999999" ORDER BY `some_column` LIMIT 0, 20;
主键的编码方式选用了日期拼接上一个自增的ID。
当 TiDB 插入一行索引的时候,会根据索的引信息编码成 Key-Value Pair 存储在 TiKV 中 ,并按照 Key 的字节序列找到对应的 Region,然后将这个 Key-Value Pair 放在 Region 的对应位置。如果连续插入的索引是自增的话,会导致这些新的索引都落在一个 Region 当中。TiDB 官方推荐在插入索引的时候尽量让索引内容随机化。
针对这个日志分析系统,每一天的域名数据是唯一的,同时也有插叙某一天的某个域名的统计信息,因此日期和域名可以创建一个唯一的组合索引:
UNIQUE KEY `idx_domain_date` (`domain`, `date`)
因为数据是按天导入,但每次导入的域名都是不同的,那么根据随机化原则,组合索引的 domain
在前,date
在后可以大大分散索引插入时命中的 Region 的分散程度,从而提高索引的插入性能。
在最初部署的时候,用的是 TiDB 的 RC4 版本,Region 的大小是 256MB,在和 ElasticSearch 版本 PK 的时候发现性能差距很大,ES 的时间基本上在 1s 左右,而 TiDB 的版本在 4~5s。
在看了半天监控图,翻了一通 TiDB 的代码,外加各种脑补之后,突然意识到了一些问题:
日志表的数据是保存在 TiKV 的 N 个 Region 中
N 的大小跟 Region 的大小有关
TiDB 执行查询的时候会根据要扫描的 N 个 Region 数量生成 N 个 DistSQL 请求发给 TiKV
TiKV 会从 gRPC 的线程池中选择一个线程执行一个 DistSQL
TiDB 会根据系统变量控制 DistSQL 的并发数量
在想到这些之后,会发现要扫描的 Region 数量 N 越大,并行计算的优势就越能发挥出来。也就是说如果一天的数据最终映射到 4 个 Region 中,那么整个查询也只能使用 TiKV 集群中的 4 个 CPU,同时如果这 4 个 Region 的 Leader 又恰好在同一个服务器上,那么其他的服务器就不会收到任何请求。
所以如果有 10 台服务器跑 TiKV,那么最好的方案是一天的数据分散在 10 个 Region 当中,然后这 10 个 Region 的 Leader 分别在这 10 个服务器上,那么这个查询就可以利用 10 台服务器的 CPU 和 IO 资源。
基于以上的推断,在修改了 TiKV 的 Region Split 相关的配置,把 Region 的大小配置成 48MB 之后,又重新导入了一遍数据。再次测试查询的时间发现请求时间稳定在 1.5~1.2s。基本上跟 ElasticSearc 打了个平手。
对于一个 Region 只会生成一个 DistSQL 只有一个 TiKV 线程执行扫描的问题,想到了能不能对一个 Region 发起多个 DistSQL,然后就可以让这个 Region 可以并行扫描以提高查询性能。
鉴于上面这个设想,我自己 Hack 了一下 TiDB 的代码,然后在 Staging 环境上做了一个测试,得出的结论如下:
对于小 Region 而言,并行扫描提升不大,毕竟扫描完一个 Region 的时间很短
对于大 Region 而言,并行扫描有所提升,但更重要的问题在于 Region 的数据过于集中,导致一个查询只有 1~2 个 TiKV 执行,那么拆分的 N DistSQL 也会因为超过了 gRPC Concurrency 的上限而进行等待。
要了解 TiDB 和 TiKV 是怎么存储数据的,可以针对数据存储和查询方式选择最优的表结构。
要了解 TiDB 的查询方式和 TiKV 的存储数据分布程度,选择一个合适的 Region 大小,根据这几天优化的结果,一个好的数据分布可以大大提升 TiDB 在 OLAP 场景下的性能。
友情提示:这些经验并没有在 OLTP 场景下做过测试
经过一段时间的调优,TiDB 的查询速度已经可以满足这个日志系统的需求了,那么在 ElasticSearch 和 TiDB 之间就要做一个了断了。由于客户还会在查询界面之外提出一些特殊的查询需求,鉴于 TiDB 能很好地支持 SQL 查询,那么利用现有的工具,可以非常方便快捷的满足客户的临时查询需求。另外寻找懂 SQL 的同学还是比寻找懂 ElasticSearch 的同学要方便的多,同时从开发的便捷程度来说,TiDB 还是有更大的优势。所以最终上线的版本还是选择了 TiDB 作为数据存储引擎。(挺 ElasticSearch 的同学请轻拍)
后来和 PingCAP 团队沟通这篇文章的内容,PingCAP 官方认为减小 Region 并不是最终的解决之道,还是要改变单 Region 的扫描并发度,让对 Region 的扫描变为并发的,同时调整调度策略,让相邻的 Region 的 Leader 分散到不同的机器上,这样才能更好的解决这个问题。另外在和 PingCAP 同学的交流中得知 PD 的 Region 处理能力至少在百万级别。