共计 4485 个字符,预计需要花费 12 分钟才能阅读完成。
前言
Relay log 相似 binary log,是指一组蕴含数据库变更事件的文件,加上相干的 index 和 mata 文件,具体细节参考 官网文档。在 DM 中针对某个上游开启 relay log 后,相比不开启,有如下劣势:
不开启 relay log 时,每个 subtask 都会连贯上游数据库拉取 binlog 数据,会对上游数据库造成较大压力,而开启后,只需创立一个连贯拉取 binlog 数据到本地,各个 subtask 可读取本地的 relay log 数据。
上游数据库对 binlog 个别会有一个生效工夫,或者会被动 purge binlog,以清理空间。在不开启 relay log 时,如果 DM 同步进度较为落后,一旦 binlog 被清理,会导致同步失败,只能从新进行全量迁徙;开启 relay log 后,binlog 数据会被实时拉取并写入到本地,与以后同步进度无关,可无效防止后面的问题。
但在 DM 版本 <= v2.0.7 中,开启 relay log 后有如下问题:
数据同步提早相比不开启 relay log 有显著回升,上面的表格是一个单 task 的 benchmark 的测试后果,可看出均匀 latency 有显著的增长。下表中以“.”结尾的是提早的百分位数据。
开启 relay 后 CPU 耗费减少。(因为 latency 的增长,在一些简略场景下(比方只有 1 个 task)相比不开启 relay log,资源使用率反而是降落的。但当 task 增多时,开启 relay 的 CPU 耗费就减少了)。
因为以上问题,在新的版本中,咱们对 DM 的 relay log 做了一些性能优化。
以后 relay 实现
在开始介绍具体的优化之前,首先简略介绍下以后 DM 中 relay 的实现,具体实现,详情可参阅 DM 源码浏览系列文章(六)relay log 的实现,本文在此不做过多形容。
以后 relay 模块能够分为两个局部,relay writer 和 relay reader 局部,其构造如下所示:
Relay writer
relay writer 顺次做了下述 3 个事件:
应用 binlog reader 从上游 MySQL/MariaDB 读取 binlog event;
将读取到的 binlog event 应用 binlog transformer 进行转换;
将转换后的 binlog event 应用 binlog writer 以 relay log file 的模式存储在本地的 relay directory 中。
Relay reader
开启 relay 后,Syncer 会通过 relay reader 获取 binlog event,relay reader 次要是做了如下工作:
读取 realy directory 中的 binlog 文件,发送给 syncer;
当读取到文件尾时,定时(目前是每隔 100ms)查看以后 binlog 文件大小和 meta 文件内容是否存在变动,如果扭转则持续读取(binlog 文件变动)或者切换到新的文件(meta 文件变动)。
从下面的介绍能够看出,relay reader 跟 relay writer 是互相独立的,彼此通过 relay directory 中的 binlog、meta 和 index 文件进行交互。
测试环境阐明
在开始介绍优化内容前,先介绍下优化时应用的环境状况
上游为 MySQL,版本为 5.7.35-log;
上游为单实例的 TiDB,版本为 5.7.25-TiDB-v5.2.1;
DM 应用了 1 个 master 和 1 个 worker
Latency 基准测试版本为 2021-10-14 号的 master 分支(commit hash 为 d2dc22d)
CPU 基准测试版本为 2021-11-01 号的 relay 重构分支(commit hash 为 9f7ce1d6)
在小规模测试(<= 4 task)下 MySQL/TiDB/DM 运行在同一主机,不便测试,主机环境为 8 核 16G;
较大规模测试(> 4 task)下 MySQL/TiDB/DM 别离运行在一台主机上,主机都是 8 核 16G
测量迁徙提早采纳上游自更新工夫列的形式,具体参考 多种形式通知你如何计算 DM 同步数据到 TiDB 的延时工夫 中的第三种形式。
Latency 优化
从下面的“以后 relay 实现”局部能够看出,有两个可能影响 latency 的点:
以后 relay reader 的定时 check 的实现形式自身就会对 latency 有肯定影响,在最坏状况下一个 binlog event 至多要提早 100ms 能力同步到上游;
relay writer 会写到磁盘,之后 relay reader 从磁盘读取,这一读一写是否会对 latency 有较大影响?
调研发现 linux 零碎(Mac OS 零碎下也有相似机制)下存在 page cache 的机制,读取最近写入的文件并非通过磁盘,而是读取 OS 内存中的缓存,因而实践上影响无限。
通过调研以及对上述问题的测试,咱们总结了两个计划:
在内存中缓存 relay writer 在最近一段时间内准备写入的 binlog,如果 relay reader 申请这段 binlog,relay reader 间接从内存读取;
relay reader 依然采纳读取文件的形式,relay writer 在写入新的 event 时,告诉 relay reader。
计划 1 须要依据上游的写入速度在读内存和读文件之间进行切换,实现起来绝对简单,而且因为 OS 层 page cache 的存在,利用自身再减少一层缓存对 latency 的影响无限。
计划 2,咱们做了一些初步的测试,在减少 relay reader check 的频率时,开启 relay 根本能达到不开启 relay 时的 latency,调研了下 MySQL 的 relay log,发现也是通过读取文件的形式,因而咱们抉择了计划 2。
实现绝对较简略,在 relay writer 减少了 Listener,在有新 binlog event 时告诉该 Listener(往 channel 中发送一个音讯),而后在 relay reader 中,将定时 check 改为监听 channel 中的音讯。
下图是在 4 table 4 task 测试下的 latency 后果,能够看出开启 relay 后的 latency 跟不开启很靠近:
CPU 优化
Latency 优化完后,咱们也对开启 relay log 后的 CPU 占用状况做了测试,发现开启后 CPU 占用率也较高,下图是在 4 table 4 task 下测试后果(注:后续没有非凡阐明的话,都是在该场景下的测试后果),能够看出开启 relay 后 CPU 耗费有显著增长,而且尖刺变得更大:
应用 golang 自带的 pprof 做了一个 CPU profile,从下图能够看出占比拟大的次要是 syncer/relay reader/relay writer 等局部,比照代码逻辑后,发现:
Relay reader 应用了 go-mysql 的 ParseFile 接口,该接口每次调用都会从新关上文件,并读取第一个 FORMAT_DESCRIPTION 事件,也就是下图中第一个蓝色标注的地位;
在优化 latency 时,因为 relay reader 和 writer 是互相独立的,为了简化实现,仅通过 channel 告诉是否存在新的 binlog 写入,而新写入的 binlog 可能在上次读取的时候曾经读取过了,这导致了很多有效的 meta、index 文件的查看。
针对下面的问题,咱们做了如下优化:
应用 go-mysql 的 ParseReader 来打消反复关上和读取的耗费;
重构 relay 模块,将 relay writer 和 reader 整合在一起,不便两者间通信。relay reader 在通过 channel 收到告诉后,查看以后 relay writer 正在写入的文件是否跟正在读取的文件雷同,即该文件是否为 active 写入状态,并获取以后文件写入的地位,通过这些信息,能够防止有效的 meta、index 文件的查看。
从下图能够看出优化后 CPU 有较大幅度的升高,然而尖刺依然较大:
因为咱们测试用的 sysbench 产生 write 事件的速率是比拟安稳的,DM 中也没有特地的执行代码,而 Golang 是一个编译型带 GC 的语言,因而咱们猜想尖刺次要来自于 GC,然而这一点从 CPU profile 并不显著,见下图:
应用 GODEBUG=gctrace= 1 开启 GC 日志,见下图,能够发现:
开启 relay 后,驻留内存大了靠近一倍(239 MB -> 449 MB),Heap 总空间也大了近一倍。
通过调研该问题是因为 DM 内嵌的 tidb 导致的内存透露,临时未解决。
开启 relay 后,GC 占用的 CPU 大幅减少,特地是 background GC time 和 idle GC time。
下图是下面优化后做的 heap profile 中 alloc_space 局部的火焰图:
阐明:pprof 的 heap profile 是程序运行至今的一个分析,并不是某一段时间内,所以从下图中也能够看到一些驻留内存的申请。
通过 heap profile 并比照代码,发现了以下可优化的点:
Go-mysql 在从文件解析 binlog event 时,每个 event 都会从新申请一个 bytes.Buffer,并在读取过程中一直扩容。优化后改为应用一个 buffer pool,缩小一直扩容带来的开销;
Local streamer 中 heatbeat event 应用了 time.After,应用该接口代码会更简洁,然而该接口创立的 channel 只在触发 timer 时才会开释,另外 Local streamer 读取事件是一个高频调用,每次调用都创立一个 timer channel 开销也较大。优化后改为复用 timer;
Relay 从上游读取事件时应用了一个 timeout context,每次读取都会创立一个额定的 channel,而在以后场景下,该 timeout context 并没必要。优化后去掉了该 timeout context;
Relay reader、relay writer 写入 debug 日志未检测 log level,每次都会创立一些 Field 对象,尽管不大,然而因为这些操作调用频率较高,也会带来一些开销。优化后,对高频调用的 debug 日志,减少 log level 判断。
阐明:DM 写入日志应用的 zap logger,该 logger 性能较好,在非高频调用下,间接调用 log.Debug 个别是没问题的。
优化后的后果如下,能够看出 CPU 又升高了不少,尖刺也少了很多:
下图为在 20 table 20 task 场景下的测试后果:
遗留问题 & 将来工作
通过下面的优化,开启 relay 相比不开 relay,在 latency 上的差距曾经很小,CPU 的增长也在一个绝对低的程度,然而仍有一些点是能够持续优化的,预期会在后续版本中逐渐增加,如下:
go-mysql 读文件应用的 io.CopyN,这个函数外部会申请一个小对象,高频应用状况下还是对 GC 有一些影响的,但不大,这次临时没改;
有些对 no relay 和 relay 同时失效的优化这次没做,比方 streamer 读取 event 时创立的 timeout context;
目前看多个 reader 读同一个文件还是有不少开销的,再优化的可能计划:
缩小读的次数,比方一个 reader 读完、其余的 reader 读内存之类的,或者像之前构想的减少内存缓存的形式;
合并雷同上游的 task,缩小 task 数量。