介绍如何借助于 MySQL 的 Master-slave 协议实现 MySQL 增量数据获取服务
做过后端开发的同学都知道, 经常会遇到如下场景:
所有涉及到类似需求的代码中都写了各种发送消息中间件的代码, 冗余, 易错, 而且难以保证一致性. 那么问题来了:
数据都在 MySQL 中, 是否可以实现仅仅更新 MySQL 就实现数据更新和发布逻辑?
最早我听说的解决方案是 Linkedin 实现的, 参见
核心思路就是通过数据库的 binary log(简称: binlog) 来实现数据库更新的自动获取. Linkedin 自己实现了 MySQL 版本和 Oracle 版本。
以 MySQL 为例, 数据库为了主从复制结构和容灾,都会有一份提交日志 (commit log),通过解析这份日志,理论上说可以获取到每次数据库的数据更新操作。获取到这份日志有两种方式:
这里有一个注意的点: MySQL 的 binlog 支持三种格式: Statement
、 Row
和 Mixed
格式:
Statement
格式就是说日志中记录 Master 执行的 SQLRow
格式就是说每次讲更改的数据记录到日志中Mixed
格式就是让 Master 自主决定是使用 Row
还是 Statement
格式由于伪装成 Slave 的解析程序很难像 MySQL slave 一样通过 Master 执行的 SQL 来获取数据更新,因此要将 MySQL Master 的 binlog 格式调整成 Row
格式才方便实现数据更新获取服务
至于 Oracle 的实现,我厂没用 Oracle。。。。
好了, 如果想自己写一个 Databus 服务, 就需要如下几个核心模块:
时至今日, 已经有很多大厂开源了自己的 MySQL binlog 解析方案,Java 语言可选的有:
想自己造轮子实现协议的,也可以参考 MySQL 官方文档
由于 binlog 可以通过网络协议获取,也可以直接通过读取磁盘上的 binlog 文件获取, 因此同步服务就有两种部署方式:
在 MySQL 中, Master-slave 之间只用标识:
由于同步服务会重启,因此必须自行维护 binlog 的状态。一般存储到 MySQL 或者 Zookeeper 中。当服务重启后,自动根据存储的 binlog 位置,继续同步数据。
虽然现在 Kafka 如日中天,大多数情况下大家都会选择 Kafka 作为消息中间件缓冲数据。选择其他的消息中间件也未尝不可。 但有一点注意:
由于上诉原因,类似 AWS SQS 这样的消息队列就不满足此处对消息队列的需求(参见: AWS SQS 官方文档关于保序方面的解释)
解析到了数据,现在要做的就是将数据发布到消息中间件中。有一下几个方面需要注意:
一个 MySQL 节点中可以有多个数据库, 每个数据库有多张表,是采用一个节点一个 Kafka Topic,还是一个数据库一个 Topic, 还是一张表一个 Topic?
Kafka 中数据是根据 key 进行分区, 同一个分区下保证消息的顺序。
如何选择数据的key的限制因素就是看数据消费端是否希望同一个表的同一条数据的更新记录都落到同一个 Kafka 分区上,进而不需要消费端做多进程间的状态维护, 简化消费端逻辑。例如: 一个Kafka Topic 有20个分区,同一个表 table_1 中 ID 为1的数据前后两次更新被发送到了不同的 partition,这就要求消费端必须每个 partition 保持lag一致, 并且及时同步数据状态到其他消费进程可见才可以保证保序; 但如果同一个表 table_1 中 ID 为1的数据前后两次更新被发送到了同一个 partition, 由于 Kafka 保证同一个 partition 保序,消费端就简化了很多。
如下图展示数据乱序问题:
A2
为新的数据, A1
为同一个 ID 的老数据慢消费进程
数据堆积,导致 A2
这个新数据先被消费, 当老数据 A1
被消费时有可能覆盖之前的结果 要实现上述的逻辑, 就要求在 Kafka 数据的 Key 的选择上做文章:
如下图,消息无乱序情况:
A
和 C
的每个版本由于 Hash 值 % 分区数量相同,同属于同一个分区, 并且按数据版本保序B
和 D
同 A
、 C
, 数据按修改时间顺序保序但属于不同分区读取到 binlog 数据后, 需要将数据序列化成更简单易用的格式,发送到 Kafka。如果选择 Avro 作为序列化方式的话,可以考虑集成 Kafka 背后的公司 Confluent 提出的一个新的方法: Schema Registry,具体信息参见 Confluent 公司官网。
随着业务的扩展,越来越多的 MySQL 接入了数据同步服务。运维管理的压力也就随之而来。因此可能最后系统演变成如下结构:
服务的监控必不可少。除了基础的进程监控,数据同步服务的关键是 binlog 解析服务与 MySQL master 之间的延迟监控,避免在 MySQL 写入高峰期导致数据延迟,影响后面的数据消费服务。
获取延迟的方法也很简单:
SHOW MASTER STATUS
获取到 Master 节点当前的文件 ID 和 binlog 位置Canal Blob 类型字段编码
由于 Canal 将 binlog 中的值序列化成了 String 格式给下游程序,因此在 Blob 格式的数据序列化成 String 时为了节省空间,强制使用了 IOS_8859_0
作为编码。因此,在如下情况下会造成中文乱码:
参见:
// com.alibaba.otter.canal.parse.inbound.mysql.dbsync.LogEventConvert 第541行起:
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
// fixed text encoding
// https://github.com/AlibabaTech/canal/issues/18
// mysql binlog中blob/text都处理为blob类型,需要反查table
// meta,按编码解析text
if (fieldMeta != null && isText(fieldMeta.getColumnType())) {
columnBuilder.setValue(new String((byte[]) value, charset));
javaType = Types.CLOB;
} else {
// byte数组,直接使用iso-8859-1保留对应编码,浪费内存
columnBuilder.setValue(new String((byte[]) value, ISO_8859_1));
javaType = Types.BLOB;
}
break;
通过实现数据同步服务,可以在一定程度上实现数据消费端与后端程序解耦。但凡事皆有成本,是否值得引入到现有系统架构中,还需要架构师自己斟酌。
-- EOF --