写点什么

数据抽取在 tidb 中的应用总结

  • 2024-02-23
    北京
  • 本文字数:6658 字

    阅读完需:约 22 分钟

作者: sustyle 原文来源:https://tidb.net/blog/4fd82d71

1 背景

我们公司有一种特殊的数据分析场景,大数据部门会定期抽取业务数据进行分析然后 T+1 更新报表。对于 MySQL 来说,可以通过添加一个从库专门用来给大数据抽数,这样就能避免因为抽数导致对业务正常访问请求受影响。


对于 tidb 集群来说,就不太方便添加从库来实现了,或者说成本较大,所以抽数任务对我们公司的 tidb 集群来说是一直都是痛点。直到近期才总结出一个影响最小的方案来解决我们公司的这类业务场景。

2、抽数的影响

先来介绍一下我们抽数场景对业务的影响,主要是基于时间线去介绍。


先简单介绍一下抽数用到的 SQL:


  • 抽取全量数据

  • 抽取增量数据


可以看到上述两类 SQL 中,都是通过分页的方式去遍历。其中第一类是全表扫描,第二类即便不是全表扫描但是如果过滤区间的数据量比较大的话性能也很差。


  • 业务很喜欢使用 limit offset 的方式进行分页遍历,但是这种性能堪忧,尤其是 tidb 这种分布式系统,所以慎用。

  • 假如[‘2023-02-01’,‘2023-02-06’) 区间有 1000w 行数据,就需要查 1000 次,然后每次都是需要将这 1000w 从 tikv 汇总到 tidb 层进行排序分页,可想而知,性能会很差,而且还存在大量的网络资源浪费。


一看到这个 SQL 血压就飙升,第一反应就是联系业务进行优化,希望业务将 limit offset 改成 limit 的方式,即每次去改表过滤条件进行分页而不是依赖 offset 进行分页。但是很遗憾,这套系统太老了,而且几乎涉及了整个公司的业务线,所以不可能去动。


别问,问就是因为历史遗留问题。


下面就介绍一下这种访问方式带来的影响。


(1)tidb


早在 tidb 2.0 版本我们就遇到过抽数会对业务的正常访问产生影响,主要是体现在 tidb 网络带宽上,因为当时我们的大部分机器都是千兆网卡。在抽数的时候很容易将 tidb 组件所在的机器的网络跑满,然后这台机器的正常业务访问受影响。


(2)tikv


在解决完 tidb 的问题后,很长一段时间都没有再遇到因为抽数导致业务的正常访问受影响。直到我们在完成机房迁移后,再次遇到了抽数导致正常的业务受影响。


这时候我们的集群已经升级到 4.0.13 了。


原因可能是机房迁移后,千兆网卡机器都换成了双万兆网卡,所以 tidb 的网络不再成为瓶颈,在抽数并发上来后,tikv 成为了瓶颈,出现大量的慢查询,然后导致业务正常访问大量超时。

3、解决问题

(1)解决 tidb 层问题


这个问题的解决也比较简单:


  • 第一,加钱。将服务器的千兆网卡升级至万兆网卡,但是这个解决方案还是没法避免问题,因为抽数任务并发上来,万兆网卡也可能会跑满。

  • 第二,隔离。将抽数任务与业务访问的 tidb 进行隔离,就是单独提供一个 tidb 节点(etl 节点)提供给抽数任务,这样该节点网络跑满后不会影响业务的正常请求。


综合分析后,我们采取了第二种方案进行解决,这个方案成本更低,效果更明显,在很大一段时间内都没再遇到抽数导致业务的正常访问受影响。


(2)解决 tikv 层问题


这个问题的解决也比较简单,因为此时我们的集群已经是 4.0 版本,开始支持 tiflash 组件,支持 TP/AP 物理隔离,所以对于存在问题的表,直接添加 tiflash 组件即可。


经过大概大半年的时间将这类问题彻底解决,但是回过头来做核算的时候发现,成本飙升,tikv 本身已经是三份数据,tiflash 又是两份数据,这样算下来成本太高了,业务不能接受,所以解决方案还需要进一步优化。


(3)解决成本问题


使用 tiflash 组件可以彻底解决因抽数导致业务正常请求受影响的问题,但是成本很高,几乎是属于通过加钱来提升性能,这是我们所不能接受的,那么有没有什么方案成本低,又能解决此类问题呢?


答案当然是肯定的,那就是:


  • 使用 tiflash 组件,提供全量数据。

  • 使用 ticdc 组件,提供增量数据(kafka)。


使用 ticdc 往 kafka 同步数据的时候要格外注意 kafka 的 max.message.bytes 参数是否做了限制,要求 cdc max-message-bytes * max-batch-size <= Kafka 的 max.message.bytes,不满足此条件的环境可能会导致同步报错。


等全量数据抽完,补全增量数据后就可以删除 tiflash 的数据,相当于 tiflash 只是临时用一下。这样就解决了抽取增量数据带来的影响,还降低了成本。


(4)其他问题这个问题是关于在使用 tiflash 组件的时候发现的,抽数任务的 SQL 并没有走 tiflash,然后经过测试发现,使用索引的查询条件就没法使用 tiflash。所以从使用体验来看,tiflash 并没有那么舒服,也不像网上吹虚的那样,具体测试如下:


mysql> show create table tb_monitor_disk_info\G*************************** 1. row ***************************       Table: tb_monitor_disk_infoCreate Table: CREATE TABLE `tb_monitor_disk_info` (  `id` bigint(20) NOT NULL AUTO_INCREMENT,  `rshost` varchar(20) NOT NULL DEFAULT '' COMMENT '主机地址',  `part` varchar(50) NOT NULL DEFAULT '' COMMENT '分区信息',  `disk_info` json DEFAULT NULL COMMENT 'disk信息 json串, 单位是GB',  `a_time` datetime NOT NULL DEFAULT '2022-01-01 00:00:00',  PRIMARY KEY (`id`),  KEY `idx_rshost` (`rshost`),  KEY `idx_a_time` (`a_time`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=3074940241 row in set (0.00 sec)
mysql> desc select * from tb_monitor_disk_info;+-----------------------+-------------+--------------+----------------------------+----------------------+| id | estRows | task | access object | operator info |+-----------------------+-------------+--------------+----------------------------+----------------------+| TableReader_7 | 53220210.00 | root | | data:TableFullScan_6 || └─TableFullScan_6 | 53220210.00 | cop[tiflash] | table:tb_monitor_disk_info | keep order:false |+-----------------------+-------------+--------------+----------------------------+----------------------+2 rows in set (0.00 sec)
mysql> desc select count(*) from tb_monitor_disk_info;+----------------------------+-------------+-------------------+----------------------------+----------------------------------+| id | estRows | task | access object | operator info |+----------------------------+-------------+-------------------+----------------------------+----------------------------------+| StreamAgg_28 | 1.00 | root | | funcs:count(Column#14)->Column#6 || └─TableReader_29 | 1.00 | root | | data:StreamAgg_8 || └─StreamAgg_8 | 1.00 | batchCop[tiflash] | | funcs:count(1)->Column#14 || └─TableFullScan_25 | 53220210.00 | batchCop[tiflash] | table:tb_monitor_disk_info | keep order:false |+----------------------------+-------------+-------------------+----------------------------+----------------------------------+4 rows in set (0.01 sec)
mysql> select count(*) from tb_monitor_disk_info;+----------+| count(*) |+----------+| 52916325 |+----------+1 row in set (0.08 sec)
mysql>
复制代码


可以看到 tiflash 是可以正常工作的,按照官方的文档,全表扫描及 count 这种统计是可以使用到 tiflash,而且很快,5000w 的数据 80 毫秒就出结果了。


但是下面的查询,也是业务的抽数 SQL,就没法使用 tiflash 了。


mysql> desc select count(*) from tb_monitor_disk_info where a_time >= '2023-02-10' and a_time < '2023-02-11';+-----------------------------+------------+-----------+------------------------------------------------------+-------------------------------------------------------------------+| id                          | estRows    | task      | access object                                        | operator info                                                     |+-----------------------------+------------+-----------+------------------------------------------------------+-------------------------------------------------------------------+| StreamAgg_24                | 1.00       | root      |                                                      | funcs:count(Column#8)->Column#6                                   || └─IndexReader_25            | 1.00       | root      |                                                      | index:StreamAgg_9                                                 ||   └─StreamAgg_9             | 1.00       | cop[tikv] |                                                      | funcs:count(1)->Column#8                                          ||     └─IndexRangeScan_23     | 1513586.15 | cop[tikv] | table:tb_monitor_disk_info, index:idx_a_time(a_time) | range:[2023-02-10 00:00:00,2023-02-11 00:00:00), keep order:false |+-----------------------------+------------+-----------+------------------------------------------------------+-------------------------------------------------------------------+4 rows in set (0.00 sec)
mysql> select count(*) from tb_monitor_disk_info where a_time >= '2023-02-10' and a_time < '2023-02-11';+----------+| count(*) |+----------+| 1775971 |+----------+1 row in set (0.25 sec)
mysql> desc select * from tb_monitor_disk_info where a_time >= '2023-02-10' and a_time < '2023-02-11';+--------------------------------+------------+-----------+------------------------------------------------------+-------------------------------------------------------------------+| id | estRows | task | access object | operator info |+--------------------------------+------------+-----------+------------------------------------------------------+-------------------------------------------------------------------+| IndexLookUp_13 | 1513586.15 | root | | || ├─IndexRangeScan_11(Build) | 1513586.15 | cop[tikv] | table:tb_monitor_disk_info, index:idx_a_time(a_time) | range:[2023-02-10 00:00:00,2023-02-11 00:00:00), keep order:false || └─TableRowIDScan_12(Probe) | 1513586.15 | cop[tikv] | table:tb_monitor_disk_info | keep order:false |+--------------------------------+------------+-----------+------------------------------------------------------+-------------------------------------------------------------------+3 rows in set (0.00 sec)
mysql> desc select * from tb_monitor_disk_info ignore index(idx_a_time) where a_time >= '2023-02-10' and a_time < '2023-02-11';+-------------------------+-------------+--------------+----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+| id | estRows | task | access object | operator info |+-------------------------+-------------+--------------+----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+| TableReader_10 | 1513586.15 | root | | data:Selection_9 || └─Selection_9 | 1513586.15 | cop[tiflash] | | ge(dbzz_monitor.tb_monitor_disk_info.a_time, 2023-02-10 00:00:00.000000), lt(dbzz_monitor.tb_monitor_disk_info.a_time, 2023-02-11 00:00:00.000000) || └─TableFullScan_8 | 53220210.00 | cop[tiflash] | table:tb_monitor_disk_info | keep order:false |+-------------------------+-------------+--------------+----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------+3 rows in set (0.00 sec)
mysql>
复制代码


针对上述现象,只能使用 ignore index 语法,使其不使用索引才会走 tiflash。虽然这样比较慢,但是总体上还是不影响业务的正常访问。


也可以使用 set SESSION tidb_isolation_read_engines = “tiflash”;


总结起来,我们现在处理抽数需求的操作流程如下:


  • 添加 ticdc 任务,将目标表的增量数据打到 kafka。

  • 添加 tiflash 任务,为目标表配置 tiflash 副本。

  • 提供一个单独的 tidb 节点(etl 节点),支持抽数任务。

  • 大数据访问 etl 节点,抽取全量数据(要求抽数 SQL 不能使用索引)后,再去消费 kafka 的消息补全增量数据。

  • 删除 tiflash 中目标表的副本数据,下掉 etl 节点。


以上步骤要求串行完成,任何步骤遇到问题都要终止操作。

  • 抽数 SQL 不能使用索引,否则还是会访问 tikv。

  • tiflash、ticdc 与 tikv 组件,etl 节点与业务访问的 tidb 节点都是物理隔离,即部署的机器不是同一台(批)机器。

4、总结

抽数是我们公司一个重要的业务场景,具有一定的特殊性,也十分消耗资源,所以在支持这类业务的过程中遇到了很多问题,不断的总结迭代解决方案。最终形成了一套适合我们公司的解决方案。


(1)使用单独的 tidb 节点支持抽数任务,目的是解决 tidb 的网络带宽瓶颈。


(2)使用 tiflash 组件支持全量数据抽取,目的是将抽数(AP)与正常的业务(TP)进行物理隔离。


降低慢查询,降低 tikv 的 io 压力及网络压力,需要注意,全量抽取完毕后要及时删除 tiflash 的数据副本,以降低存储成本。


不过在具体使用 tiflash 的体验上并没有那么好,如果目标查询使用了索引,就会导致 tiflash 失效,还是会影响 TP 业务。


(3)使用 ticdc 组件支持增量数据的抽取,目的是释放 tiflash 组件以此降低存储成本。


4.0 版本的 ticdc 可能会有很多问题,我们就踩了很多坑(4.0.13),所以建议使用 6.1 版本(踩过坑的集群升级到了 6.1.7)。


综上所述,tidb 集群支持抽数的业务场景比较麻烦,不像 mysql 直接挂一个从库就行,随便下游抽数。tidb 集群需要打一套完整的组合拳才行。


如果存在上下游关系的架构中,一定要评估好风险,避免因调整了 tidb 的大小写敏感问题导致上下游同步异常。

5、写在最后

本文对数据抽取在 tidb 中的应用做了简单总结,各公司的业务场景也不一样,需求也不同,还可能碰上其他未知的问题,本文所有内容仅供参考。


发布于: 刚刚阅读数: 2
用户头像

TiDB 社区官网:https://tidb.net/ 2021-12-15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
数据抽取在tidb中的应用总结_实践案例_TiDB 社区干货传送门_InfoQ写作社区