DM 中 relay log 性能优化的实战经验 TiDB 工具分享

网友投稿 499 2024-01-30

前言Relay log 类似 binary log,是指一组包含数据库变更事件的文件,加上相关的 index 和 mata 文件,具体细节参考官方文档在 DM 中针对某个上游开启 relay log 后,相比不开启,有如下优势: 。

DM 中 relay log 性能优化的实战经验  TiDB 工具分享

不开启 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

测量迁移延迟采用下游自更新时间列的方式,详细参考多种方式告诉你如何计算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 数量。

DM

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Chaos Mesh:深入 Chaos Engineering as a Service 的探索之路
下一篇:DM 分库分表 DDL “乐观协调”模式深入介绍 TiDB 工具
相关文章