什么是canal
canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。我们可以简单地把canal理解为一个用来同步增量数据的一个工具。
工作原理
Canal模拟MySQL的从服务器协议,并将自己伪装为MySQL的从服务器,向主服务器发送dump请求。然后,MySQL主服务器在收到dump请求后,开始推送二进制日志(binary log)给从服务器即Canal。最后Canal解析从主服务器接收到的二进制日志对象(原始字节流),再发送到存储目的地,比如MySQL,Kafka,ElasticSearch等等。
canal的输出对象目前也比较多,参看架构图:
组件架构
- server 代表一个canal服务,管理多个instance
- instance 伪装成一个slave, 从mysql dump数据,可以看做是一个数据队列
- eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
- eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
- eventStore (数据存储)
- metaManager (增量订阅&消费信息管理器)
MySQL的Binlog MySQL Binlog 的格式有三种,分别是 STATEMENT,MIXED,ROW。在配置文件中可以选择配 置 binlog_format= statement|mixed|row。 statement:语句级,binlog 会记录每次一执行写操作的语句。比如
update user set create_date=now(); 优点:节省空间。 缺点:有可能造成数据不一致。 row:行级, binlog 会记录每次操作后每行记录的变化; 优点:保持数据的绝对一致性 缺点:占用较大空间。 mixed:statement 的升级版,一定程度上解决了,因为一些情况而造成的 statement 模式不一致问题,默认还是 statement,一些会产生不一致的情况还是会选择row。
Canal 想做监控分析,选择 row 格式比较合适,毕竟数据一致性非常重要,现如今磁盘便宜占用空间大也所谓。
部署安装
这里使用的版本如下:mysql8.1.0,canal1.1.6。
使用
canal1.1.4无法连接到 mysql8
mysql
修改配置
我们知道canal是通过把自己伪装成mysql slave,收集binlog做解析,然后再进行后续同步操作。所以我们的准备工作必须要求MySQL开启binlog日志:
vi /etc/mysql/conf.d/mysqld.cnf
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重
查看binlog是否开启:show variables like 'log_bin';
创建用户
CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
mysql8以上版本还需要执行下面命令:
ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal';
ALTER USER 'canal'@'%' IDENTIFIED BY 'canal' PASSWORD EXPIRE NEVER;
FLUSH PRIVILEGES;
canal-server
docker run -p 11111:11111 \
-e canal.instance.master.address=192.168.2.9:3306 \
-e canal.instance.dbUsername=canal \
-e canal.instance.dbPassword=canal \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false \
-e canal.instance.filter.regex=.\*\\\\..\* \
-d canal/canal-server
查看example日志如下说明部署成功了
canal-server 配置
properties配置分为两部分:
- canal.properties (系统根配置文件)
- instance.properties (instance级别的配置文件,每个instance一份)
canal.properties
instance.properties
## mysql serverId , v1.0.26+ will autoGen
## v1.0.26版本后会自动生成slaveId,所以可以不用配置
# canal.instance.mysql.slaveId=0
# 数据库地址
canal.instance.master.address=127.0.0.1:3306
# binlog日志名称
canal.instance.master.journal.name=mysql-bin.000001
# mysql主库链接时起始的binlog偏移量
canal.instance.master.position=154
# mysql主库链接时起始的binlog的时间戳
canal.instance.master.timestamp=
canal.instance.master.gtid=
# username/password
# 在MySQL服务器授权的账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
# 字符集
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
# table regex .*\..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\..*
# mysql 数据解析表的黑名单,多个表用,隔开
canal.instance.filter.black.regex=
应用场景
canal有很多种模式,可以把canal收集到的binlog发送到三大MQ中,或者tcp用于测试。
同步redis/elasticsearch
下发归集
当数据变更时需要通知其他依赖系统。其原理是任务系统监听数据库变更,然后将变更的数据写入 MQ/kafka 进行任务下发,比如商品数据变更后需要通知商品详情页、列表页、搜索页等相关系统。
这种方式可以保证数据下发的精确性,而且业务系统中不会散落着各种下发 MQ 的代码,从而实现了下发归集。
golang 操作canal
package main
import (
"fmt"
"github.com/golang/protobuf/proto"
"github.com/withlin/canal-go/client"
pbe "github.com/withlin/canal-go/protocol/entry"
"log"
"os"
"time"
)
func main() {
// 192.168.199.17 替换成你的canal server的地址
// example 替换成-e canal.destinations=example 你自己定义的名字
// 该字段名字在 canal\conf\example\meta.dat 文件中,NewSimpleCanalConnector函数参数配置,也在文件中
/**
NewSimpleCanalConnector 参数说明
client.NewSimpleCanalConnector("Canal服务端地址", "Canal服务端端口", "Canal服务端用户名", "Canal服务端密码", "Canal服务端destination", 60000, 60*60*1000)
Canal服务端地址:canal服务搭建地址IP
Canal服务端端口:canal\conf\canal.properties文件中
Canal服务端用户名、密码:canal\conf\example\instance.properties 文件中
Canal服务端destination :canal\conf\example\meta.dat 文件中
*/
connector := client.NewSimpleCanalConnector("127.0.0.1", 11115,
"canal", "canal", "example",
60000, 60*60*1000)
err := connector.Connect()
if err != nil {
log.Println(err)
os.Exit(1)
}
// https://github.com/alibaba/canal/wiki/AdminGuide
//mysql 数据解析关注的表,Perl正则表达式.
//
//多个正则之间以逗号(,)分隔,转义符需要双斜杠(\)
//
//常见例子:
//
// 1. 所有表:.* or .*\..*
// 2. canal schema下所有表: canal\..*
// 3. canal下的以canal打头的表:canal\.canal.*
// 4. canal schema下的一张表:canal\.test1
// 5. 多个规则组合使用:canal\..*,mysql.test1,mysql.test2 (逗号分隔)
err = connector.Subscribe(".*\..*")
if err != nil {
log.Println(err)
os.Exit(1)
}
for {
message, err := connector.Get(100, nil, nil)
if err != nil {
log.Println(err)
os.Exit(1)
}
batchId := message.Id
if batchId == -1 || len(message.Entries) <= 0 {
time.Sleep(300 * time.Millisecond)
fmt.Println(time.Now().Format("2006-01-02 15:04:05"), "===没有数据了===")
continue
}
printEntry(message.Entries)
}
}
func printEntry(entrys []pbe.Entry) {
for _, entry := range entrys {
if entry.GetEntryType() == pbe.EntryType_TRANSACTIONBEGIN || entry.GetEntryType() == pbe.EntryType_TRANSACTIONEND {
continue
}
rowChange := new(pbe.RowChange)
err := proto.Unmarshal(entry.GetStoreValue(), rowChange)
checkError(err)
if rowChange != nil {
eventType := rowChange.GetEventType()
header := entry.GetHeader()
fmt.Println(fmt.Sprintf("================> binlog[%s : %d],name[%s,%s], eventType: %s", header.GetLogfileName(), header.GetLogfileOffset(), header.GetSchemaName(), header.GetTableName(), header.GetEventType()))
for _, rowData := range rowChange.GetRowDatas() {
if eventType == pbe.EventType_DELETE {
printColumn(rowData.GetBeforeColumns())
} else if eventType == pbe.EventType_INSERT {
printColumn(rowData.GetAfterColumns())
} else {
fmt.Println("-------> before")
printColumn(rowData.GetBeforeColumns())
fmt.Println("-------> after")
printColumn(rowData.GetAfterColumns())
}
}
}
}
}
func printColumn(columns []*pbe.Column) {
for _, col := range columns {
fmt.Println(fmt.Sprintf("%s : %s update= %t", col.GetName(), col.GetValue(), col.GetUpdated()))
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
执行一条insert语句: insert into t values(40, 40, 40)
打印内容如下: