DM 中 relay log 性能优化实践丨TiDB 工具分享

Posted PingCAP

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DM 中 relay log 性能优化实践丨TiDB 工具分享相关的知识,希望对你有一定的参考价值。

将转换后的 binlog event 使用 binlog writer 以 relay log file 的形式存储在本地的 relay directory 中。

的机制,读取最近写入的文件并非通过磁盘,而是读取 OS 内存中的缓存,因此理论上影响有限。

的存在,应用本身再增加一层缓存对 latency 的影响有限。
改为监听 channel 中的消息。
做了一个 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 数量。

  • iOS 性能优化,被压测卡爆的语音房间(实践)

    ????????关注后回复 “进群” ,拉你进程序员交流群????????

    作者丨ZenonHuang

    来源丨掘金

    https://juejin.cn/post/6935830669549371423

    背景

    某天收到通知,有人气大主播要做语音房间活动,需要做质量保障工作。

    因为房间已经借鉴了之前做 IM 的预排版经验,加上 iPhone 机器本身性能都不错,我以为稳如老狗...

    然而 iPhone6 Plus 测试机随着压测数据上升到每秒上百条,直接卡爆了,整个屏幕没有任何响应。

    问题定位

    相信很多开发者都看过性能优化的文章,然而我们现在需要对症下药。

    按着文章一顿操作猛如虎,低头一看零杠五。

    关键问题的定位,并不是照猫画虎就能解决的,特别是在我们已经做了一些常规优化设计的情况下。

    CPU or GPU

    首先确定是属于哪种原因卡顿:

    CPU 卡顿

    GPU 卡顿

    我们可以利用工具观察卡顿发生时 CPU / GPU 的情况,Xcode 也自带这些功能。

    安利一下滴滴团队的 DoraemonKit ,集成了不少提升效率的开发工具,比如查看视图层级,网络请求,以及 CPU 变化等等..

    因为工程本身已经有了,我直接用它来观察 CPU 变化情况,方便的一匹。

    再次进行压测,CPU 的变化曲线一下子就升到 100%,肉眼可见的秒爆了。

    其实 CPU 消耗大还有一个特征就是发热,但我们要用数据说话,做业务的抓手是什么?数据 :)

    而关于卡顿发生的原因,这篇 《iOS 保持界面流畅的技巧》是非常推荐阅读的,值得反复学习,确实是佳作。

    哪些操作造成 CPU 卡顿问题?

    既然已经定位到了 CPU ,那么是不是就马上找到答案呢?

    答案是否定的,我们离问题仍然还有一段距离。

    如果直接按照造成 CPU 消耗问题的操作去搜索答案,不外乎是下面这七项:

    • 对象创建

    • 对象调整

    • 对象销毁

    • 文本计算

    • 文本渲染

    • 图片解码

    • 图片绘制

    实际上,真的一行行代码查看过去,你会发现几乎每行代码都被囊括在这七项当中:对象创建/调整/销毁,文本计算/渲染,图片/解码/绘制。

    到底哪里一行才是真正的问题代码,接下来该怎么办呢?如果是有十分丰富经验的工程师,或许能一眼扫出问题代码所在。

    如果不是大佬的话,显然我们还是需要一些方法。

    整理业务流程

    就像打仗一样,知己知彼才能百战百胜,我们要对问题的业务情况做掌握。

    先整理出问题场景的业务流程,详细了解整个过程。

    整理业务流程,比较建议的方式是画图,能直观看到业务里各部分的关系,处理流程,以及数据流向。

    对于大而复杂的模块,大多数时候是很多人都经手或共同维护的。这时候一定要学会和团队成员合作,找来每个部分最了解的人,能尽可能快的完成整理的步骤。

    针对这次出问题的语音房间/直播房间场景,就可以先做大的分类:礼物消息(带横幅动画,全屏动画等),图片消息,文字消息等几个大类型。

    再分别对大类型做梳理,例如文字消息类型的大概流程:

    当然上面的图只是一个比较粗略的例子????,真实场景里,光是 RoomChatHandle 里的处理流程都不少。

    整理流程是理顺业务的好机会,同时也能发现一些设计问题:如本应是上下层的关系,却产生互相依赖。如果将业务关系画图表示,就可以很好的暴露出来。

    总之,先把自己的业务思路理清。

    控制变量法

    整理完业务情况后,就可以开始真正动手了。

    建议使用控制变量法,更通俗具体的说,就是排除法!

    其实就是最简单的 Debug 方式了...大家只要中学做过一点实验,或者平时会 Debug 的应该都懂了。

    例如:

    a=白色
    b=白色
    c=蓝色
    //实际预期 d=白色
    d=a+b+c=蓝色
    
    

    那么这时分别去除一次 a/b/c 的影响,最后发现:

    d=a+b=白色
    d=a+c=蓝色
    d=b+c=蓝色
    
    

    就很容易知道 c 这个变量出问题了,影响了我们的预期结果。

    办法比较原始,毕竟最高端的食材往往需要最朴素的烹饪方法,高端的问题只需要最朴素的 Debug 功夫!

    利用控制变量来发现问题,确实也很像做饭,找问题的速度在于掌握火候,也就是粒度问题:

    每次做排除的是一行代码?还是1 个方法调用?还是好几个方法调用?

    通过逐渐缩小问题范围,然后一步步推进。

    说起来很轻松,其实整个过程也是比较枯燥,越复杂越大的模块越辛苦。

    最终发现了问题的关键代码:

    [tableView reload]
    
    

    对于原来的方式来说,来 1 条数据就会去刷 1 次 UI,1 秒 100 条数据就刷 100 次 UI。刷新频率过高,服务器来的数据很多,那么刷新的频繁,负荷太大直接造成卡死。

    解决问题

    定位问题之后,就可以针对性解决,目前核心问题在于:

    短时间内 UI 刷新次数过多,造成 CPU 压力过大

    那第一步要做的就是控制频率。

    控制频率

    很显然目标是尽可能减少刷新次数,不过仍然涉及到 2 个问题:

    • 控频策略

    • 取阈值

    关于控频策略,在 《iOS 上的函数防抖与节流》 有过说明,主要就是 Throttle / Debounce:

    • Throttle ,单位时间内只执行一次方法。

    • Debounce,单位时间内只要有方法调用,就再等一个周期,直到没有新调用,则执行方法。

    由于是刷新方法,如果消息一直不停过来刷,按照 Debounce 的方式,很可能会一直被推迟执行,UI 也就得不到刷新了。

    所以在控制频率上选择 Throttle 的话是比较合适的,单位时间内执行一次。

    经过实验,如果普通文本消息类型修改为 1 秒 1次 ,CPU 直接就减少了 30-40% 的压力。

    高/低性能机器区分

    通过控制刷新频率,已经非常有成效,然而粒度还是太粗了。

    出现卡死的机器都是低性能机器,对于高性能机器的话用一样的策略就不够好了。

    对于高性能机器要充分利用,适当放开限制数据处理,提高用户体验。

    所以可以用更精细的阀值来测试,对高性能机器选择合适的值,甚至部分强大的 CPU 可以放开管控,在 CPU 使用率到达一定临界点开启控制策略。

    数据的分发&处理

    数据批量分发

    虽然已经解决了主要问题,但发现原来的分发策略也比较直接:

    来 1 条数据,就直接传递 1 条,来 100 条数据就抛 100 次。

    对于数据做一个合并分发的策略,或者说是批量分发,例如:

    0.5 秒内,只来 1 条数据就传递 1 条,如果连续来 100 条,就等到最后一次,直接全部发出。

    这样也能大大减少分发的成本,将多次合并为 1 次,和上面 UI 刷新控制频率是相同的思想,不过现在是数据层面做控制。

    淘汰策略

    批量分发还存在一个问题:

    因为批量分发数据,中间到的数据会被存起来。

    如果到的数据太多,比如我们 1 秒处理 300 条,但是来了 500 条,随着时间过去,就会不断积压消息,甚至引发新的内存问题(OOM)。

    为了避免出现积压过多的消息,我们还需要做一个 淘汰策略 ,简单的说,就是丢弃一部分数据。

    丢弃不必要的消息,就涉及到产品上的问题,需要去与产品或者运营同学确认,哪些是不重要的:比如进出房间消息,普通聊天消息...

    当然我相信也有什么都不愿意舍弃的产品:全部都给我一次性刷出来!

    技术的提升,往往来源于业务的推动,变态的产品会对技术提出更高的要求。

    异步多线程处理

    原来处理数据也没有使用异步多线程,所以关于数据的处理,还能再做一次异步处理优化。

    异步多线程处理,利用 GCD 就可以了。

    看起来简单,其实也有坑,不断的开线程也会产生线程爆炸的问题,线程太多不一定就好。

    而且机器的核数也是有限的 ,真正能利用起来的线程数量也有限。简单的说,如果机器是双核的,那么我同时 2 个线程, 处理数据就很舒服,如果开 4 个线程,其实就是通过线程调度,来回不断的切换任务,并非真正的多线程。其实就是 并发 和 并行 的区别。

    我们可以直接站在前人的肩膀上,使用或仿照 YYKit 里的 YYDispatchQueuePool ,将处理数据的任务统一放到某个需要的优先级队列上。

    除了线程数量之外,还要注意处理死锁的问题。

    进一步思考

    做完上面的优化后,不仅是 iPhone6 Plus ,甚至 iPhone6 面对狂风暴雨的服务器消息也毫无波澜。

    在此基础上继续思考,如果数据量不断增大,并出现一个棘手的情况:剩下的重要消息,也超过我们的极限,例如全部都是 500 元以上的礼物消息,不能丢弃,全部展示,我们该怎么优化?

    协程

    在处理数据上,有没有比用多线程处理更好的方案呢?那可能就是协程了。

    查看阿里巴巴的 coobjc 这么介绍协程对于多线程的性能优势:

    • 调度性能更快:协程本身不需要进行内核级线程的切换,调度性能快,即使创建上万个协程也毫无压力

    • 减少卡顿卡死:协程的使用以帮助开发减少锁、信号量的滥用,通过封装会引起阻塞的 IO 等协程接口,可以从根源上减少卡顿、卡死,提升应用整体的性能

    异步绘制

    在 UI 显示上,再更进一步,采用异步绘制的方式,如 YYAsyncLayer 或者 Texture 都是可以借鉴的。

    由于我们本身也使用了 YYLabel,开启异步绘制后,会空白一阵后再显示,比较明显的滞后显示体验并不是太好,建议还是极端情况再选择性的开启。

    这也是一种性能与体验的取舍问题。

    总结

    虽然是一篇关于性能优化的文章,仍然要说一句:过度过早的优化是万恶之源。

    例如提到的多线程和异步绘制,可能优化后会带来别的体验问题。避免多线程问题的一个最好办法,就是不使用多线程。

    进行性能优化,更多的是得到查问题/改问题的经验,也掌握了一些方法与思考。

    最重要的是找问题的过程,往往可能就一行或者几行代码导致了影响,定位到却要花费不菲的时间。

    希望大家有更好的思考和经验多多交流!

    -End-

    最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

    点击????卡片,关注后回复【面试题】即可获取

    在看点这里好文分享给更多人↓↓

    以上是关于DM 中 relay log 性能优化实践丨TiDB 工具分享的主要内容,如果未能解决你的问题,请参考以下文章

    iOS 性能优化,被压测卡爆的语音房间(实践)

    大会回顾丨游戏用户体验优化如何实践,看大咖怎么说(附PPT下载)

    大会回顾丨游戏用户体验优化如何实践,看大咖怎么说(附PPT下载)

    大会回顾丨游戏用户体验优化如何实践,看大咖怎么说(附PPT下载)

    SPIChina丨京东商城代码质量平台建设实践

    关于Relay Log无法自动删除的问题