异常现象截图
canal的坑:CanalParseException: column size is not match for table (表结构缓存异常阻塞问题)
早期的canal版本(<=1.0.24),在处理表结构的DDL变更时采用了一种简单的策略,在内存里维护了一个当前数据库内表结构的镜像(通过desc table获取)。
这样的内存表结构镜像的维护存在问题,如果当前在处理的binlog为历史时间段T0,当前时间为T1,存在的一些异常分支情况:
补充一下MySQL binlog的一些技术背景:
ps. 针对复杂的一条update中包含多张表的更新时,大家可以观察一下Table_map的特殊情况,留待有兴趣的同学发挥
扯了一堆的背景之后,再来看一下我们如何解决canal上一版本存在的表结构一致性的问题,这里会把我们的思考过程都记录出来,方便大家辩证的看一下方案.
解决这个问题,第一个最直接的思考:canal在订阅binlog时,尽可能保持准实时,不做延迟回溯消费. 这样的方式会有对应的优点和缺点:
整个方案上,基本是想避开表结构的问题,在遇到一些容灾场景下一定也会遇上,不是一个技术解决的方案,废弃.
经过了第一轮辩证的思考,基本确定想通过迂回的方式,简单绕过一致性的问题不是正解,所以这次的思考主要就是如何正面解决一致性的问题. 基本思路:基于binlog中DDL的变化,来动态维护一份表结构,比如DDL中增加一个列,在本地表结构中也动态增加一列,解析binlog时都从本地表结构中获取
实现方案:
整个方案上,可以绝大部分的解决DDL的问题,但也存在一些缺点:
有了之前的两次思考,思路基本明确了,在一次偶然的机会中和alibaba Druid的作者高铁,交流中得到了一些灵感,是否可以基于Druid对DDL的支持能力,来构建一份动态的表结构.
大致思路:
名词解释:
接口设计:
public interface TableMetaTSDB {
/**
* 初始化
*/
public boolean init(String destination);
/**
* 获取当前的表结构
*/
public TableMeta find(String schema, String table);
/**
* 添加ddl到时间表结构库中
*/
public boolean apply(BinlogPosition position, String schema, String ddl, String extra);
/**
* 回滚到指定位点的表结构
*/
public boolean rollback(BinlogPosition position);
/**
* 生成快照内容
*/
public Map snapshot();
}
持久化存储的思考:
canal.instance.tsdb.spring.xml=classpath:spring/tsdb/h2-tsdb.xml
#canal.instance.tsdb.spring.xml=classpath:spring/tsdb/mysql-tsdb.xml
参数名 | 默认值 | 描述 |
---|---|---|
canal.instance.tsdb.enable | true | 是否开启时序表结构的能力 |
canal.instance.tsdb.dir | ${canal.file.data.dir:…/conf}/{canal.instance.destination:} | 默认存储到conf/$instance |
canal.instance.tsdb.url | jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL; | jdbc链接串 |
canal.instance.tsdb.dbUsername | canal | jdbc用户名,因为有自动创建表的能力,所以对该用户需要有create table的权限 |
canal.instance.tsdb.dbPassword | canal | jdbc密码 |
例子:
# table meta tsdb info
canal.instance.tsdb.enable=true
canal.instance.tsdb.dir=${canal.file.data.dir:../conf}/${canal.instance.destination:}
canal.instance.tsdb.url=jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
canal.instance.tsdb.dbUsername=canal
canal.instance.tsdb.dbPassword=canal
CREATE TABLE `meta_snapshot` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`destination` varchar(128) DEFAULT NULL COMMENT '通道名称',
`binlog_file` varchar(64) DEFAULT NULL COMMENT 'binlog文件名',
`binlog_offest` bigint(20) DEFAULT NULL COMMENT 'binlog偏移量',
`binlog_master_id` varchar(64) DEFAULT NULL COMMENT 'binlog节点id',
`binlog_timestamp` bigint(20) DEFAULT NULL COMMENT 'binlog应用的时间戳',
`data` longtext COMMENT '表结构数据',
`extra` text COMMENT '额外的扩展信息',
PRIMARY KEY (`id`),
UNIQUE KEY `binlog_file_offest` (`destination`,`binlog_master_id`,`binlog_file`,`binlog_offest`),
KEY `destination` (`destination`),
KEY `destination_timestamp` (`destination`,`binlog_timestamp`),
KEY `gmt_modified` (`gmt_modified`)
) COMMENT='表结构记录表快照表';
CREATE TABLE `meta_history` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`destination` varchar(128) DEFAULT NULL COMMENT '通道名称',
`binlog_file` varchar(64) DEFAULT NULL COMMENT 'binlog文件名',
`binlog_offest` bigint(20) DEFAULT NULL COMMENT 'binlog偏移量',
`binlog_master_id` varchar(64) DEFAULT NULL COMMENT 'binlog节点id',
`binlog_timestamp` bigint(20) DEFAULT NULL COMMENT 'binlog应用的时间戳',
`use_schema` varchar(1024) DEFAULT NULL COMMENT '执行sql时对应的schema',
`sql_schema` varchar(1024) DEFAULT NULL COMMENT '对应的schema',
`sql_table` varchar(1024) DEFAULT NULL COMMENT '对应的table',
`sql_text` longtext COMMENT '执行的sql',
`sql_type` varchar(256) DEFAULT NULL COMMENT 'sql类型',
`extra` text COMMENT '额外的扩展信息',
PRIMARY KEY (`id`),
UNIQUE KEY `binlog_file_offest` (`destination`,`binlog_master_id`,`binlog_file`,`binlog_offest`),
KEY `destination` (`destination`),
KEY `destination_timestamp` (`destination`,`binlog_timestamp`),
KEY `gmt_modified` (`gmt_modified`)
) COMMENT='表结构变化明细表';
canal.instance.tsdb.enable = true
canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
#canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
canal.instance.tsdb.url = jdbc:mysql://hostname:3306/tsdb?useUnicode=true&characterEncoding=UTF-8&useSSL=false
canal.instance.tsdb.dbUsername = username
canal.instance.tsdb.dbPassword = passwd
#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml