一 高可用架构设计
配置说明:
zookeeper x 3 + canal x 2 + mysql x 2
组件说明:
-
- linux内核版本(CentOS Linux 7):(命令:uname -a)
Linux slave1 3.10.0-693.el7.x86_64 #1 SMP Tue Aug 22 21:09:27 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
- linux内核版本(CentOS Linux 7):(命令:uname -a)
-
- mysql版本:(SQL命令:select version(); 或 status)
Server version: 5.6.43-log MySQL Community Server (GPL)
- mysql版本:(SQL命令:select version(); 或 status)
-
- canal版本:canal-1.1.3
-
- zookeeper版本:zookeeper-3.4.5-cdh5.7.0
-
- JDK版本: 1.8
canal工作原理:
-
- 模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议;
-
- mysql master收到dump请求,开始推送binary log给slave(也就是canal);
-
- 解析binary log对象(原始为byte流)
了解更多详细更新可以查看文章:【了解canal,看这个就够了】
二 配置与部署流程
2.1 安装mysql数据库
1. 下载安装
在192.168.175.21和192.168.175.22两台服务器上分别安装mysql,具体安装流程可参考文章:Linux-安装MySQL.
2. 创建canal账户
在创建root账号并设置远程访问之后,接着创建canal账号并设置远程访问和权限:
mysql> CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
mysql> GRANT ALL ON canal.* TO 'canal'@'%';
mysql> GRANT SELECT, REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'canal'@'%';
mysql>FLUSH PRIVILEGES;
3. 验证登录
#远程登录
mysql -h 192.168.175.22 -P 3306 -u canal -pcanal
#本地登录
mysql -ucanal -pcanal
4. 修改my.cnf配置(这一步非常关键!!!)
分别在175.21和175.22两台服务器修改my.conf配置,查找my.cnf配置位置命令:whereis my
.
示例,在192.168.175.21的my.cnf配置新增如下内容:
log_bin=mysql-bin #指定bin-log的名称,尽量可以标识业务含义
binlog_format=row #选择row模式,必须!!!
server_id=1 #mysql服务器id
2.2 搭建zookeeper集群
搭建zookeeper集群地址为192.168.175.20:2181,192.168.175.21:2181,192.168.175.22:2181
,具体搭建流程,可查看文章【Zookeepr3.4.5集群搭建】。
2.3 搭建canal server集群
前提: mysql已打开binlog功能,且配置binlog模式为row.
1. 下载最新canal安装包
下载地址: https://github.com/alibaba/canal/releases/download/canal-1.1.3/canal.deployer-1.1.3.tar.gz
2.上传并解压
进入192.168.175.20服务器,使用rz命令上传,使用如下命令进行解压至/usr/local/hadoop/app/canal
:
tar xzvf canal.deployer-1.1.3.tar.gz -C canal
3. 修改配置instance.properties
新解压的文件夹/usr/local/hadoop/app/canal/conf/
有一个example
文件夹,一个example就代表一个instance实例.而一个instance实例就是一个消息队列,所以这里可以将文件名改为example1,同时再复制出来一个叫example2.(命名可以使用监听的数据库名)
修改/usr/local/hadoop/app/canal/conf/example1/instance.properties
配置文件:
canal.instance.master.address=192.168.175.21:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
canal.mq.topic=example1
修改/usr/local/hadoop/app/canal/conf/example2/instance.properties
配置文件:
canal.instance.master.address=192.168.175.22:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
canal.mq.topic=example2
配置文件参数说明,可查看:https://github.com/alibaba/canal/wiki/AdminGuide
4. 修改配置canal.properties
配置/usr/local/hadoop/app/canal/conf/canal.properties
是一个对应canal server的全局配置(instance.properties是对应canal instance的配置)。
canal.id = 2 #保证每个canal server的id不同
canal.port = 11111
canal.zkServers =192.168.175.20:2181,192.168.175.21:2181,192.168.175.22:2181
canal.instance.global.spring.xml = classpath:spring/default-instance.xml
#其他配置默认即可.
注意: 两台机器上的instance目录的名字需要保证完全一致,HA模式是依赖于instance name进行管理,同时必须都选择default-instance.xml配置。
配置完成,将文件从192.168.175.20远程复制一份到192.168.175.22上:
#需要确保已开通免密
scp -rp /usr/local/hadoop/app/canal slave2:/usr/local/hadoop/app/
5. 启动canal server
进入文件夹/usr/local/hadoop/app/canal/bin
执行如下启动命令:
./startup.sh
查看日志/usr/local/hadoop/app/canal/logs/canal/canal.log
,出现如下内容,即表示启动成功:
2019-06-07 21:15:03.372 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## load canal configurations
2019-06-07 21:15:03.427 [main] INFO c.a.o.c.d.monitor.remote.RemoteConfigLoaderFactory - ## load local canal configurations
2019-06-07 21:15:03.529 [main] INFO com.alibaba.otter.canal.deployer.CanalStater - ## start the canal server.
2019-06-07 21:15:06.251 [main] INFO com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[192.168.175.22:11111]
2019-06-07 21:15:22.245 [main] INFO com.alibaba.otter.canal.deployer.CanalStater - ## the canal server is running now ......
在zk集群中查看canal节点注册情况:
[zk: localhost:2181(CONNECTED) 27] ls2 /otter/canal/destinations
[example2, example1]
[zk: localhost:2181(CONNECTED) 26] ls2 /otter/canal/cluster
[192.168.175.22:11111, 192.168.175.20:11111]
可以看到canal server节点已经在zk集群上注册成功.
当停掉一个canal server时,可以看到zk上对应的临时节点也会删除.
2.4 使用canal client通过zookeeper连接canal server集群
注意运行canal客户端代码时,一定要先启动canal server!!!
(1) 添加pom依赖
com.alibaba.otter
canal.client
1.1.3
(2) canal client代码:
package com.xgh.canal;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import java.net.InetSocketAddress;
import java.util.List;
public class TestCanalByZk {
public static void main(String args[]) {
String zkHost="192.168.175.20:2181,192.168.175.21:2181,192.168.175.22:2181";
// 创建链接
CanalConnector connector = CanalConnectors.newClusterConnector(zkHost,"example1","","");
/*CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.175.22", 11111),
"example", "", "");*/
int batchSize = 1000;
int emptyCount = 0;
long batchId = 0;
//外层死循环:在canal节点宕机后,抛出异常,等待zk对canal处理切换,切换完后,继续创建连接处理数据
while(true) {
try {
connector.connect();
connector.subscribe(".*\\..*");//订阅所有库下面的所有表
//connector.subscribe("canal.t_canal");//订阅库canal库下的表t_canal
connector.rollback();
//内层死循环:按频率实时监听数据变化,一旦收到变化数据,立即做消费处理,并ack,考虑消费速度,可以做异步处理并ack.
while (true) {
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
batchId = message.getId();
int size = message.getEntries().size();
//// 偏移量不等于-1 或者 获取的数据条数不为0 时,认为拿到消息,并处理
if (batchId == -1 || size == 0) {
emptyCount++;
System.out.println("empty count : " + emptyCount);//此時代表當前數據庫無遍更數據
Thread.sleep(200); //200ms拉一次变动数据
} else {
emptyCount = 0;
System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);
printEntry(message.getEntries());
}
connector.ack(batchId); // 提交确认
}
}catch(Exception e){
e.printStackTrace();
connector.rollback(batchId); // 处理失败, 回滚数据
} finally {
connector.disconnect();
}
}
}
private static void printEntry(List entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN
|| entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
e);
}
System.out.println("rowChare ======>"+rowChage.toString());
EventType eventType = rowChage.getEventType(); //事件類型,比如insert,update,delete
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(),//mysql的my.cnf配置中的log-bin名稱
entry.getHeader().getLogfileOffset(), //偏移量
entry.getHeader().getSchemaName(),//庫名
entry.getHeader().getTableName(), //表名
eventType));//事件名
for (RowData rowData : rowChage.getRowDatasList()) {
if (eventType == EventType.DELETE) {
printColumn(rowData.getBeforeColumnsList());
} else if (eventType == EventType.INSERT) {
printColumn(rowData.getAfterColumnsList());
} else {
System.out.println("-------> before");
printColumn(rowData.getBeforeColumnsList());
System.out.println("-------> after");
printColumn(rowData.getAfterColumnsList());
}
}
}
}
private static void printColumn(List columns) {
for (Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
}
canal client运行实例:
empty count : 1
empty count : 2
empty count : 3
empty count : 4
6. 触发数据库变更
创建库:create database canal;
创建表:create table t_canal (id int,name varchar(20),status int);
插入数据:insert into t_canal values(11,'xxiao',1);
canal client输出日志:
================> binlog[mysql-bin.000001:6973] , name[canal,t_canal] , eventType : INSERT
id : 11 update=true
name : xxiao update=true
status : 1 update=true
7. 其他
将canal client代码CanalConnectors.newClusterConnector(zkHost,"example1","","");
中的队列名example1
换成example2
再执行,就可以监听example2对应数据变化了.
8. 问题:为何设置了数据表的过滤条件,但貌似没有生效?
答:首先看文档AdminGuide,了解canal.instance.filter.regex的书写格式。mysql 数据解析关注的表,Perl正则表达式.多个正则之间以逗号(,)分隔,转义符需要双斜杠(\) 。常见例子:
-
- 所有表:.* or .\..
-
- canal schema下所有表: canal\..*
-
- canal下的以canal打头的表:canal\.canal.*
-
- canal schema下的一张表:canal.test1
-
- 多个规则组合使用:canal\..*,mysql.test1,mysql.test2 (逗号分隔)
检查binlog格式,过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提取tableName进行过滤)。
检查下CanalConnector是否调用subscribe(filter)方法;有的话,filter需要和instance.properties的canal.instance.filter.regex一致,否则subscribe的filter会覆盖instance的配置,如果subscribe的filter是...,那么相当于你消费了所有的更新数据 【特别注意】
三 运行测试及总结
1. 启动两个监听example1的canal client,启动两个监听example2的canal client:
在example1或example2对应的数据发生变化时,两个canal client只有一个消费消息。
当两个监听同一个队列的canal client有一个宕掉时,再有数据变化时,剩下的一个canal client就会开始消费数据。
这就验证了canal client的HA机制:为了保证有序性,一份instance同一时间只能由一个canal client进行get/ack/rollback操作,否则客户端接收无法保证有序.
2. 启动两个canal server并在zk上注册
当停掉其中一个canal server时,当产生数据变化时,整个canal server集群仍可以正常对外提供服务。
这就验证了canal server的HA机制:为了减少对mysql dump的请求,不同server上的instance要求同一时间只能有一个处于running,其他的处于standby状态.
3. 在canal server切换过程中,canal client存在重复消费数据的问题
这点需要在消费端自行进行处理。
参考文章:
- https://www.2cto.com/database/201609/547661.html
- https://www.cnblogs.com/yulu080808/p/8819260.html
- https://github.com/alibaba/canal
- https://blog.csdn.net/my201110lc/article/details/78836270