Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

canal整体性能优化 #726

Closed
agapple opened this issue Jun 30, 2018 · 42 comments
Closed

canal整体性能优化 #726

agapple opened this issue Jun 30, 2018 · 42 comments
Assignees
Milestone

Comments

@agapple
Copy link
Member

agapple commented Jun 30, 2018

之前有较多的小伙伴, 反馈在大规模数据DML变更时消费速度有点跟不上, 反馈的问题列表:

  1. Mysql产生的binlog,大量数据的时候,同步到Canal慢,这个怎么解决? #672
  2. Canal Server 解析性能问题 #547
  3. 当数据库有大量的删除时,canal解析变慢 #355
  4. canal收集 由一条update语句更新多行 产生的binlog的速度很慢 什么原因呢? #267

这里会做一个相对完整的测试,进行针对性的优化,同时也非常欢迎大家的参与和代码MR,一起努力解决好性能和稳定性的问题. ps. 1.0.26会是一个里程碑式的版本。


最后的优化结果:https://github.com/alibaba/canal/wiki/Performance

image

@agapple agapple added this to the v1.0.26 milestone Jun 30, 2018
@agapple agapple self-assigned this Jun 30, 2018
@zwangbo
Copy link

zwangbo commented Jul 2, 2018

赞赞赞

@mjjian0
Copy link

mjjian0 commented Jul 2, 2018

期待ing,期待ing

@lcybo
Copy link
Collaborator

lcybo commented Jul 2, 2018

赞!
我先来抛个砖,说说以前遇到的issue和一些思路:
场景是,使用load backup file的方式灌数据,同时使用canal进行同步。
每个表一个或多个文件,由于每个文件中的行都同属于一张表的同一事件(INSERT),mysql-binlog会merge一定量的行(取决于binlog-row-event-max-size)到同一event。
*单句DML影响到多行数据的情况应该也适用。
压缩的event经过解析为protobuf对象,内存占用急剧膨胀,造成fgc频繁,甚至oom。
在MySQL 5.6的版本,官方默认将binlog-row-event-max-size从1024调高到了8192。
当时我们通过将binlog-row-event-max-size设回1024,问题被绕过。
如果将server/client 的netty3升级为4,用PooledByteBuf应该可以缓解一小部分。但大头还是protobuf这个memory hogs.

@lcybo
Copy link
Collaborator

lcybo commented Jul 3, 2018

补充一下,ringbuffer size被设置为4096。内存是4g.由于load backup file以后就是平稳的正常traffic。因此并不想调整内存或进一步减小ringbuffer

@agapple
Copy link
Member Author

agapple commented Jul 3, 2018

第一步网络优化(只做header解析,识别包大小、位点等信息,不做具体的记录级别解析)
结果:默认设置的32k的socket buffer,针对大吞吐量传输时有点过小,去掉了默认设置,让socket自我协调,默认在24c96g的物理机上测试,receiveBuffer协调结果为180k.

调整前后的性能吞吐量对比: 18MB VS 117MB,提供6倍多,socket buffer优化之后基本可以跑满网卡

@agapple
Copy link
Member Author

agapple commented Jul 3, 2018

第二步优化解析的能力,跑了下简单的对象解析(不做protobuf的对象构建),刚开始速度是20MB,主要优化了时间字段的解析上,提升到了45MB.

目前遇到瓶颈了,观察系统的负载cpu最高在1.5核左右,jvm gc主要集中在young区,下一步的优化思路:多线程并行解析,最大化的使用系统负载进行优化

@agapple
Copy link
Member Author

agapple commented Jul 3, 2018

第三步整体并发模型设计,按照前面的优化网络和对象解析,瓶颈都在对象的深度解析上,如果要最大化性能,必须得引入并发设计,整体设计思路如下:

image

基于ringbuffer的并发模型,整个过程会分为4个阶段,多个阶段之间互相隔离和依赖,把最耗瓶颈的解析这块采用多线程的模型进行加速,预计整个优化完成,整个解析可以跑满整个千兆网卡.

ps. 如果各位有万兆网卡的机型,到时候可以也帮忙跑一下效果

@agapple
Copy link
Member Author

agapple commented Jul 3, 2018

@lcybo 针对内存占用的问题,你有什么优化建议么?换掉protobuf?

@lcybo
Copy link
Collaborator

lcybo commented Jul 3, 2018

个人一点不成熟的想法:
因为不跨语言,可以用基于protobuf和java的protostuff。
相对来说,优点:

  • 比protobuf快,占用内存更少,相同的data size。
  • 可以使用POJO,不用proto文件。protobuf的generated code对人太不友好了。

不足的地方:

  • 社区不太活跃。
  • 待补充哈哈。

贴出一些细节:
// Re-use (manage) this buffer to avoid allocating on every serialization
LinkedBuffer buffer = LinkedBuffer.allocate(512);
对比protobuf:
byte[] result = new byte[this.getSerializedSize()];

此外,POJO还可以参考netty的Recycler或其他的对象池,理论上,ringbuffer中event占用Entry对象的峰值就是ObjectPool需要缓存的对象个数。
ProtostuffIOUtil.mergeFrom(protostuff, fooParsed, schema); //fooParsed是可以提供的,反序列化可以从ObjectPool受益。
当然这么做代码会复杂些。

@agapple
Copy link
Member Author

agapple commented Jul 3, 2018

byte[]的复用倒是有, 基于ObjectPool的思路以前还真没考虑过, 对象里的各种嵌套结构体也不太一样, 不太理解复用的原理 @lcybo

@lan1994
Copy link

lan1994 commented Jul 4, 2018

并行解析如何保证binlog event的有序性?@agapple

@lcybo
Copy link
Collaborator

lcybo commented Jul 4, 2018

办法挺多,举个栗子,把结果作为卫星数据,countdownlatch或其他的闭锁或parallelstream,最后还不用reorder

@lcybo
Copy link
Collaborator

lcybo commented Jul 4, 2018

@agapple,在外面爪机不太方便,回去再码字。

@agapple
Copy link
Member Author

agapple commented Jul 4, 2018

@lan1994 串行和并行的结合体,串行把基本的对象解析做好,并行主要处理一些耗时比较长的,比如DML的具体字段解析、protobuf对象构造等.

目前初步的测试结果,24个并发基本可以把一台24core的物理机cpu给跑满,吞吐量能跑到80MB/s (从binlog接收到生成CanalEntry整体,优化前大概7MB/s,提升一个数量级),目前瓶颈基本在cpu上了(字符串的序列化占比非常高),下午会继续测试一下从mysql -> canal server -> canal client的整体吞吐量

@lcybo
Copy link
Collaborator

lcybo commented Jul 4, 2018

关于嵌套结构:
假设Row嵌套Column的list
两个类都使用了ObjectPool,都实现了Recycable接口和recycle方法。
对Row调用recycle时,清楚数据fields,对每个Column嵌套调用recycle,再list.clear()
要注意的是内嵌的Column生命周期不应该久于Row。

关于netty的Recycler,每个对象new出来会bind ThreadLocal的回收stack,如果在同一线程进行recycle/get,那么就完全不会有竞争。如果两者分离,则会涉及到潜在线程间共享的WeakOrderQueue。
按:串行浅解析->并发深度解析&proto对象->ringbuffer串行get 的处理逻辑,序列化的点在SessionHandler的get,会引发深度解析线程之间的竞争(解析线程stack空的时候它们会去WeakOrderQueue偷一些对象回来),看起来至少netty的Recycler不适合这里应用。(除非提早序列化,把序列化结果放到ringbuffer)
参考:
io.netty.util.Recycler
对象参考:
io.netty.util.internal.RecyclableArrayList

BTW,sessionhandler里面的序列化:
NettyUtils.write(ctx.getChannel(), packetBuilder.build().toByteArray(), null);// 输出数据
packetBuilder.build().toByteArray()每次都new byte[],应该有优化的空间。

@agapple
Copy link
Member Author

agapple commented Jul 4, 2018

第三步优化已经完成,解析这块引入了ringbufer模型,分成了多个阶段:网络接收、简单解析、DML深度解析、投递到store,把中间最耗时的DML深度解析换成了并发解析的模型.

从binlog接收到生成Entry对象,测试了一下对比:

  1. 未做并行化改造前,大概是5MB/s (单线程)
  2. 做了并行化改造,16个并发解析,大概是80MB/S,基本是正相关的线性扩展 (基本跑满了cpu的瓶颈,如果要进一步优化,只能优化Entry对象的构造协议了)

@agapple
Copy link
Member Author

agapple commented Jul 4, 2018

第四步优化已经开始,主要是关注binlog解析到client收到数据的吞吐量,目前逐步压测的情况,大概是binlog下载吞吐在8MB/s,Entry对象的吞吐大概在55MB/s,大概是1:7的数据膨胀率. 目前profile看到的瓶颈主要在server在序列化Entry对象时,相比于网络传包占了50%,测试的记录大概是100字节,换算到记录的tps,目前大概是在20w record/s左右. (我测试数据是批量insert和update,binlog里会更加紧凑)


如果优化掉这块,理论上Entry对象可以跑满网络带宽,预计可以整体提升150% (相比于未优化前,因为最后端的网络传输瓶颈比较大,所以吃掉了前面的几个优化带来的提升,如果要进一步优化,得改动protobuf协议设计)


初步优化思路

  1. 多线程提前构造好Entry的序列化结果,避免在client get操作时临时做序列化 (因为单线程,会有瓶颈)
  2. 如果改动protobuf协议,可以通过设计一些字典表,压缩部分的数据存储
    image

@lcybo
Copy link
Collaborator

lcybo commented Jul 5, 2018

拜读了代码,parallel参数有些细节:Pr#737

@withlin
Copy link
Contributor

withlin commented Jul 6, 2018

赞!!!!!👍

@agapple
Copy link
Member Author

agapple commented Jul 6, 2018

非常欢迎大家提交PR哈,最近在思考protobuf的一些优化细节,目前主要瓶颈点在于protobuf的序列化和字节放大问题。 我会尽可能去保证兼容性,但也不排除极端情况下在协议设计改动出现不兼容的情况

so. 如果有好的想法,可以尽快反馈到我这里.

@lcybo
Copy link
Collaborator

lcybo commented Jul 6, 2018

enum Compression {
NONE = 1;
ZLIB = 2;
GZIP = 3;
LZF = 4;
}
proto里预留了压缩,如果在并发深度解析这一步额外进行序列化和并发压缩。
SessionHandler那里: packetBuilder.setBody(//压缩后数据);
然后,client那里串行接收+并行解压和反序列化。

并行压缩+网络传输 VS 无压缩网络传输,不知道表现如何。

@agapple
Copy link
Member Author

agapple commented Jul 6, 2018

  1. 单线程压缩和网络传输,这会是一个平衡点
  2. protobuf序列化之后的数据,压缩比也需要评测

@agapple
Copy link
Member Author

agapple commented Jul 18, 2018

第四步优化已经进行了一大半,大致的效果相比于完全未优化之前提升了35%的吞吐量,可以跑到8~9万的TPS (本次压测数据和之前的批量insert不同,选择了业务上随机的一个库进行跑,19MB的binlog,大概网络传输在35MB左右)

优化点:

  1. CanalEntry对象的序列化提前在进入ringbuffer之前就完成,最后sessionHandler只做ByteString拷贝,有部分提升
  2. SimpleCanalConnector增加了lazyParseEntry参数,支持lazy解析CanalEntry对象,减少整个get/ack串行操作的成本,最大化提升串行的吞吐量.

目前的profile分析来看,最大的瓶颈就在于构造网络传输时有多次数组拷贝。

现在的代码:

Packet.Builder packetBuilder = CanalPacket.Packet.newBuilder();
                        packetBuilder.setType(PacketType.MESSAGES);

                        Messages.Builder messageBuilder = CanalPacket.Messages.newBuilder();
                        messageBuilder.setBatchId(message.getId());
                        if (message.getId() != -1) {
                            if (message.isRaw()) {
                                // for performance
                                if (!CollectionUtils.isEmpty(message.getRawEntries())) {
                                    messageBuilder.addAllMessages(message.getRawEntries());
                                }
                            } else {
                                if (!CollectionUtils.isEmpty(message.getEntries())) {
                                    for (Entry entry : message.getEntries()) {
                                        messageBuilder.addMessages(entry.toByteString());
                                    }
                                }
                            }
                        }                        
                        packetBuilder.setBody(messageBuilder.build().toByteString()); 
                        NettyUtils.write(ctx.getChannel(), packetBuilder.build().toByteArray(), null);// 输出数据
  1. messageBuilder.build().toByteString(),会拷贝一次message.getRawEntries()的所有数据到一个byte[]里
  2. packetBuilder.build().toByteArray(),会拷贝一次message的整个数据到一个packet的byte[]里
  3. NettyUtils.write 网络发送

原始的10MB的记录,会至少是原先的3倍+,针对性的优化就是直代码一次性构造protobuf的数据格式,而不是通过builder + toByteString的方式,预计还能再提升10%左右.

@agapple
Copy link
Member Author

agapple commented Jul 19, 2018

尝试用一次byte[]数组绕过protobuf的多次拷贝,下一步可以优化如何复用byte[]数组,避免每次请求都开辟一个新的byte[]数组,一次性开10MB的内存块,还是有一些开销的,包括client层面

agapple added a commit that referenced this issue Jul 20, 2018
agapple added a commit that referenced this issue Jul 20, 2018
@agapple
Copy link
Member Author

agapple commented Jul 20, 2018

@agapple agapple closed this as completed Jul 20, 2018
wingerx added a commit to wingerx/canal that referenced this issue Jul 31, 2018
agapple added a commit that referenced this issue Jul 31, 2018
kafka producer 适配 row data for performance #726
@koshox
Copy link
Contributor

koshox commented Dec 6, 2018

666,这波优化,有打算增加负载均衡能力么?

@francisoliverlee
Copy link

第四步优化已经开始,主要是关注binlog解析到client收到数据的吞吐量,目前逐步压测的情况,大概是binlog下载吞吐在8MB/s,Entry对象的吞吐大概在55MB/s,大概是1:7的数据膨胀率. 目前profile看到的瓶颈主要在server在序列化Entry对象时,相比于网络传包占了50%,测试的记录大概是100字节,换算到记录的tps,目前大概是在20w record/s左右. (我测试数据是批量insert和update,binlog里会更加紧凑)

如果优化掉这块,理论上Entry对象可以跑满网络带宽,预计可以整体提升150% (相比于未优化前,因为最后端的网络传输瓶颈比较大,所以吃掉了前面的几个优化带来的提升,如果要进一步优化,得改动protobuf协议设计)

初步优化思路

  1. 多线程提前构造好Entry的序列化结果,避免在client get操作时临时做序列化 (因为单线程,会有瓶颈)
  2. 如果改动protobuf协议,可以通过设计一些字典表,压缩部分的数据存储
    image

@agapple 那个调用链的查看使用的啥工具呀?

@agapple
Copy link
Member Author

agapple commented Dec 14, 2018

jvm自带的visualvm啊

@wq19880601
Copy link

第一步网络优化(只做header解析,识别包大小、位点等信息,不做具体的记录级别解析)
结果:默认设置的32k的socket buffer,针对大吞吐量传输时有点过小,去掉了默认设置,让socket自我协调,默认在24c96g的物理机上测试,receiveBuffer协调结果为180k.

调整前后的性能吞吐量对比: 18MB VS 117MB,提供6倍多,socket buffer优化之后基本可以跑满网卡

你调整的这个参数是canal的配置canal.instance.network.receiveBufferSize和canal.instance.network.sendBufferSize 这两个参数对吗,我用的otter,在otter的manager后台,调整的这两个参数,机器配置是64核内存128g,感觉没有任何效果,是什么原因,最近otter性能总是上不去,感觉还是干canal有关系

@agapple
Copy link
Member Author

agapple commented Jan 10, 2019

@wq19880601 otter里使用canal目前默认没开并行解析的参数

@acuitong
Copy link

@agapple server端这个常驻内存,随着 数据量增加总有内存溢出的情况,1.3版本后请问有没有好的解决方案

@acuitong
Copy link

@agapple 为什么做大批量insert操作时canal-server端内存无明显增加,而update和delete操作,server端内存快速增加。他们有什么区别吗?

@agapple
Copy link
Member Author

agapple commented Feb 25, 2019

@acuitong 可以调低一下ringbuffer的bufferSize

@acuitong
Copy link

@agapple 你好,这个已经调到了128,请问那个insert操作和update,delete操作有什么不同之处,现在insert操作,server端内存没什么变化,但是update和delete操作,内存会随数据量升高,调了buffer.size,虽然幅度很小,但是总会有占满堆空间的时候,同样会产生内存溢出。

@f-zhao
Copy link
Contributor

f-zhao commented Apr 1, 2019

@wq19880601 otter里使用canal目前默认没开并行解析的参数

是不是通过修改MysqlEventParser.setParallel(true)就可以修改为并行读了?

@agapple
Copy link
Member Author

agapple commented Apr 1, 2019

@f-zhao parallel是有几个关联参数的,你可以看一下代

@f-zhao
Copy link
Contributor

f-zhao commented Apr 1, 2019

@f-zhao parallel是有几个关联参数的,你可以看一下代

@agapple

mysqlEventParser.setParallel(true);
mysqlEventParser.setParallelThreadSize(Runtime.getRuntime().availableProcessors() * 60 / 100);
mysqlEventParser.setParallelBufferSize(256);

这三个吗?

@littleneko
Copy link
Contributor

@agapple 请问这个测试的平均单行数据是多大?

@agapple
Copy link
Member Author

agapple commented Aug 21, 2019

@acuitong update的内存问题,以前还真没留意,我到时候关注一下

@q6413260
Copy link

q6413260 commented Jan 6, 2020

@agapple 你好,咨询个问题,如果一条sql修改了多条记录,对这多条记录的解析还是串行的吧?

@lanmaodage
Copy link

有没有碰到过每次从grafana看到延迟好大的时候,Store remain events快达到buffer size,怀疑是client消费不及时,这个要怎么调整呢

@shizhimin123
Copy link

我这边是同时使用logstash和canal进行同步 但是因为canal的消费慢一步 可能logstash同步到最新数据后 被canal的后消费又覆盖掉最新的 有什么好的同步方案吗

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests