这是我见过最好的canal文章

149 阅读5分钟

什么是canal

canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。我们可以简单地把canal理解为一个用来同步增量数据的一个工具

工作原理

Canal模拟MySQL的从服务器协议,并将自己伪装为MySQL的从服务器,向主服务器发送dump请求。然后,MySQL主服务器在收到dump请求后,开始推送二进制日志(binary log)给从服务器即Canal。最后Canal解析从主服务器接收到的二进制日志对象(原始字节流),再发送到存储目的地,比如MySQL,Kafka,ElasticSearch等等。

canal的输出对象目前也比较多,参看架构图:

image.png

组件架构

image.png

  • 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日志如下说明部署成功了

image.png

canal-server 配置

properties配置分为两部分:

  1. canal.properties  (系统根配置文件)
  2. instance.properties  (instance级别的配置文件,每个instance一份)

canal.properties

image.png

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

image.png

下发归集

当数据变更时需要通知其他依赖系统。其原理是任务系统监听数据库变更,然后将变更的数据写入 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)

打印内容如下: image.png