一、同步性能卡壳,先抓两个核心排查点
做数据同步的人大概率都遇到过这种糟心事:明明源端数据量不大、目标端也没跑满资源,可整个同步任务就是慢得像蜗牛,甚至半天才动一下。之前踩过不少坑,发现很多时候性能卡壳的核心问题,都出在「序列化」和「目标端锁等待」这两个环节——尤其是用SeaTunnel做同步的时候,这俩问题还特别容易被忽略。
先给大家掰明白这俩问题到底是啥,别觉得专业就难理解:序列化说白了就是把内存里的「活数据」(比如Java里的对象、Python里的字典)转成能传去目标端的「死格式」(比如二进制、JSON串);目标端锁等待就是目标数据库、消息队列这类存数据的地方,正在被别的操作占着「锁」,咱们的同步任务得排队等,啥时候轮到啥时候才能写数据。
二、排查第一步:先确认是不是序列化环节卡了
序列化慢的核心原因,要么是序列化工具选得烂,要么是序列化的逻辑有问题,比如转出来的格式太冗余、或者处理数据的时候做了没必要的复杂操作。咱们拿SeaTunnel里的实际场景来举例,一步步说怎么查。
2.1 先看序列化的耗时占比
不管用什么工具做同步,第一步都是先看日志里的耗时分布——要是序列化环节的耗时占了整个任务的60%以上,那基本就是这的问题了。比如SeaTunnel跑任务的时候,会打印每个阶段的耗时,比如下面这段日志:
2024-05-20 10:00:00,123 INFO [Task-Executor-0] org.apache.seatunnel.core.starter.SeaTunnelTask - Task: test-sync-task, Stage: serialize, Duration: 12000ms
2024-05-20 10:00:12,124 INFO [Task-Executor-0] org.apache.seatunnel.core.starter.SeaTunnelTask - Task: test-sync-task, Stage: network-transport, Duration: 1500ms
2024-05-20 10:00:13,625 INFO [Task-Executor-0] org.apache.seatunnel.core.starter.SeaTunnelTask - Task: test-sync-task, Stage: write-to-target, Duration: 1000ms
你看,序列化用了12秒,后面的网络传输加写目标端才2.5秒,这明显是序列化拖了后腿。
2.2 怎么改序列化的配置(拿SeaTunnel的实际配置举例)
很多人用SeaTunnel的时候,会随便选序列化工具,比如默认用Jackson转JSON,但要是数据里有大字段(比如10KB以上的字符串、复杂的嵌套对象),Jackson转起来会特别慢。这时候换个高效的序列化工具,比如Protobuf,速度能提好几倍。
先给大家看之前用Jackson的配置(SeaTunnel的配置文件是JSON格式,咱们就用这个当示例):
{
"env": {
"parallelism": 1
},
"source": {
"mysql": {
"username": "root",
"password": "123456",
"database": "test_db",
"table": "test_table",
"columns": ["id", "name", "content"]
}
},
"transform": {
"serialize": {
"type": "jackson", // 用Jackson做序列化
"format": "json"
}
},
"sink": {
"kafka": {
"bootstrap.servers": "localhost:9092",
"topic": "test_topic"
}
}
}
这个配置跑的时候,要是content字段是个大的JSON串,转的时候就会特别慢。那换成Protobuf的配置怎么改?首先得定义Protobuf的消息格式,比如:
syntax = "proto3";
message TestData {
int32 id = 1;
string name = 2;
string content = 3;
}
然后把SeaTunnel的序列化配置改成Protobuf:
{
"env": {
"parallelism": 1
},
"source": {
"mysql": {
"username": "root",
"password": "123456",
"database": "test_db",
"table": "test_table",
"columns": ["id", "name", "content"]
}
},
"transform": {
"serialize": {
"type": "protobuf", // 换成Protobuf序列化
"message_class": "com.example.TestData", // 对应刚才定义的Protobuf类
"schema_file": "test_data.proto" // 指向Protobuf的模式文件
}
},
"sink": {
"kafka": {
"bootstrap.servers": "localhost:9092",
"topic": "test_topic"
}
}
}
改完之后,再跑任务,你会发现序列化的耗时可能直接降到2秒以内,整个任务速度能快好几倍。这里给大家提个注意点:要是你的数据里有很多重复的字段,或者字段的类型很固定,Protobuf比Jackson、Gson这类转JSON的工具快很多;但要是数据结构特别灵活,经常变字段类型,那Protobuf可能就不太适合,因为得频繁改模式文件。
三、排查第二步:确认是不是目标端锁等待卡了
要是序列化的耗时正常,那就要看是不是目标端的锁等待问题了。这个问题更隐蔽,因为你看SeaTunnel的日志,可能只会看到「写数据的阶段耗时特别长」,但不知道为啥慢——其实是目标端的锁被别的操作占着,同步任务一直在等锁释放。
3.1 什么是目标端锁等待?举个实际例子
咱们拿最常见的目标端MySQL来举例。比如你有个同步任务,要往MySQL的test_table里写数据;同时还有个定时任务,每天凌晨会跑一个批量更新的操作,比如update test_table set status = 1 where create_time < '2024-01-01'。这个批量更新的操作,要是没有加合适的索引,就会锁整个test_table的表锁;或者哪怕加了索引,要是更新的范围特别大,也会锁很多行锁。这时候你的同步任务要往test_table里写数据,就得等这个批量更新的锁释放,不然写不进去。
再比如用Redis当目标端的场景:要是别的任务在执行multi、exec的事务操作,或者执行keys *这种慢命令,也会占着Redis的锁,导致同步任务写不进去。
3.2 怎么排查目标端锁等待?
不同的目标端,排查锁等待的方法不一样,咱们拿最常用的MySQL、Kafka来举例。
3.2.1 排查MySQL的锁等待
MySQL里有个专门的命令,能看当前的锁等待情况,咱们可以用这个命令查:
-- 查看当前所有的锁等待信息
SELECT * FROM performance_schema.data_locks WHERE LOCK_STATUS = 'WAITING';
要是这个命令查出来有结果,说明确实有锁等待。比如你查出来的结果里,锁的对象是test_table,锁的类型是TABLE(表锁),那就是有操作占着整个表的锁;要是锁的类型是RECORD(行锁),那就是占着某几行的锁。
再给大家看一个实际的排查场景:之前有个同步任务,往MySQL写数据特别慢,查SeaTunnel日志,写数据的阶段每次都要等几十秒。然后跑上面的锁等待命令,发现有个批量更新的操作占着表锁,那个批量更新的SQL是update test_table set status = 1 where create_time < '2024-01-01',但是create_time字段没有加索引,所以这个SQL执行的时候,会扫整个表,占着表锁。后来给create_time加了索引,批量更新的速度从原来的20秒降到了1秒以内,同步任务的写数据阶段也正常了。
3.2.2 排查Kafka的锁等待
Kafka的锁等待主要出现在分区的锁上。Kafka的每个分区,同一时间只能有一个生产者往里面写数据(哪怕是同一个主题的不同分区,也是独立的锁)。要是你的同步任务往一个Kafka主题的某个分区写数据,同时还有别的生产者往同一个分区写,就会出现锁等待。
排查Kafka的锁等待,可以用Kafka自带的命令:
# 查看指定主题的分区锁情况
kafka-topics.sh --describe --topic test_topic --bootstrap-server localhost:9092
# 查看当前的生产者连接情况,判断有没有多个生产者往同一个分区写
kafka-consumer-groups.sh --describe --group test_group --bootstrap-server localhost:9092
要是发现有多个生产者往同一个分区写,那就要调整,比如把同步任务的生产者配置改成用分区的哈希策略,或者给主题加更多的分区。
3.3 怎么解决目标端锁等待?
不同的目标端,解决方法不一样,给大家总结几个通用的方法:
- 给目标端的操作加合适的索引:比如MySQL的批量更新、删除操作,一定要给条件字段加索引,避免扫全表占锁;
- 调整操作的时间:比如把批量更新这种占锁的操作,放到业务量最少的时间段(比如凌晨2点),避免和同步任务抢锁;
- 优化目标端的配置:比如Kafka的主题,要是同步任务的速度上不去,可以加更多的分区,让锁的粒度更小;
- 调整同步任务的配置:比如SeaTunnel的同步任务,要是目标端是MySQL,可以开启批量写入(batch size),减少写的次数,也就减少了等锁的次数。
给大家看一个SeaTunnel开启批量写入的配置示例:
{
"env": {
"parallelism": 1
},
"source": {
"mysql": {
"username": "root",
"password": "123456",
"database": "test_db",
"table": "test_table",
"columns": ["id", "name", "content"]
}
},
"sink": {
"mysql": {
"username": "root",
"password": "123456",
"database": "target_db",
"table": "target_table",
"batch_size": 1000, // 每1000条数据批量写一次,减少写的次数
"batch_interval": 1000 // 每1秒没攒够1000条也写一次,避免数据延迟
}
}
}
这个配置能有效减少同步任务写目标端的次数,也就减少了等锁的概率。
四、应用场景、优缺点和注意事项
4.1 应用场景
这两个排查方法,几乎适用于所有的SeaTunnel同步场景,尤其是下面这几种:
- 实时数据同步:比如把业务库的数据同步到数据仓库、Kafka,要求速度快、延迟低;
- 批量数据同步:比如每天同步前一天的业务数据,数据量比较大,容易出现性能问题;
- 跨系统数据同步:比如把MySQL的数据同步到Redis、Elasticsearch,目标端的类型比较多,容易出现锁等待问题。
4.2 两种排查方法的优缺点
4.2.1 序列化排查的优缺点
优点:排查方法简单,只要看日志的耗时分布,就能快速定位;改配置也比较容易,换个序列化工具或者调整配置就能解决问题。 缺点:要是序列化的逻辑比较复杂(比如自定义的序列化逻辑),排查起来会比较麻烦;而且换序列化工具可能会带来兼容性问题,比如之前的旧数据用的是Jackson转的JSON,换成Protobuf之后,旧数据读不出来。
4.2.2 目标端锁等待排查的优缺点
优点:能从根源上解决问题,不会出现改了之后又复发的情况;而且排查出来的问题,不仅能解决同步任务的性能问题,还能优化目标端的其他操作(比如批量更新的速度)。 缺点:排查方法比较复杂,需要对目标端的锁机制有一定的了解;而且解决问题可能需要改别的操作(比如批量更新的SQL、时间),需要协调别的开发人员,比较麻烦。
4.3 注意事项
- 排查的时候,一定要先看日志的耗时分布,再针对性排查,不要上来就乱改配置;
- 改序列化配置的时候,一定要先做测试,避免出现兼容性问题;
- 排查目标端锁等待的时候,一定要确认锁的来源,不要上来就停别的操作,避免影响业务;
- 调整同步任务的配置(比如batch size)的时候,一定要根据实际的资源情况调整,比如batch size太大,会占太多的内存,导致任务报错。
五、总结
同步性能卡壳是数据同步里最常见的问题之一,很多人遇到这种情况,要么乱加并行度,要么换工具,其实很多时候只要排查序列化和目标端锁等待这两个环节,就能快速解决问题。
排查的逻辑很简单:先看SeaTunnel的日志,要是序列化的耗时占比高,就优化序列化的配置,比如换Protobuf、减少序列化的冗余;要是序列化的耗时正常,就排查目标端的锁等待,比如MySQL的表锁、行锁,Kafka的分区锁,然后针对性解决。
给大家提个小建议:平时做同步任务的时候,一定要关注日志的耗时分布,定期排查序列化和锁等待的情况,提前发现问题,避免出现大的性能问题。
评论
围绕“同步性能上不去,排查SeaTunnel任务是否卡在序列化环节与目标端锁等待”参与讨论