Canal技术实践V1.0


1 CANAL简述

Canal是阿里开源的一款基于数据库增量日志解析,提供增量数据订阅&消费的组件。目前主要支持Mysql,当然也支持MariaDB。

1.1 CANAL基本架构

在这里插入图片描述
• server代表一个canal运行实例,对应于一个jvm
• instance对应于一个数据队列 (1个server对应1…n个instance)
instance模块:
• eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
• eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
• eventStore (数据存储)
• metaManager (增量订阅&消费信息管理器)

1.2 CANAL工作原理

1.2.1 Mysql主从复制

Canal的工作原理主要利用了Mysql的主从复制。即,Canal伪装成Mysql的slave,假装从Master复制数据
Mysql的主从复制:
在这里插入图片描述
从上层来看,复制分成三步:

  1. master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);
  2. slave将master的binary log events拷贝到它的中继日志(relay log);
  3. slave重做中继日志中的事件,将改变反映它自己的数据。

1.2.2 Canal工作原理

Canal的工作原理:
在这里插入图片描述
原理相对比较简单:

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
  2. mysql master收到dump请求,开始推送binary log给slave(也就是canal)
  3. canal解析binary log对象(原始为byte流)

1.2.3 Mysql的Binlog

MySQL 的二进制日志可以说是 MySQL 最重要的日志了,它记录了所有的 DDL 和DML (除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。
一般来说开启 binlog 日志大概会有 1% 的性能损耗。 binlog日志有两个最重要的使用场景:

  1. MySQL Replication 在 Master 端开启 binlog,Mster 把它的二进制日志传递给 slaves 来达到 master-slave 数据一致的目的。
  2. 自然就是数据恢复了,通过使用 mysql binlog 工具来使恢复数据。

1.2.4 binlog 格式

binlog 有 3 种格式: STATEMENT, ROW, MIXED

  1. statement
    语句级别, binlog 会记录每次执行的写操作的语句, 注意记录的是语句, slave 会自己重新执行写操作的语句, 从而达到与 master 的一致.
    但是有可能会出现主从不一致的情况: 比如存储当前时间戳, 存储一个随机值等.
    • 优点: 节省空间
    • 缺点: 有可能造成数据不一致。
  2. row
    行级, binlog 会记录每次操作后每行记录的变化。
    • 优点:保持数据的绝对一致性。因为不管sql是什么,引用了什么函数,他只记录执行后的效果。
    • 缺点:占用较大空间。 如果一条语句执行之后导致很多行发生了变化, 则会产生很多条记录
  3. mixed
    statement 的升级版,一定程度上解决了因为一些情况而造成的 statement模式不一致问题
    在某些情况下会按照 ROW的方式进行处理
    1)当函数中包含 UUID() 时;
    2)包含 AUTO_INCREMENT 字段的表被更新时;
    3)执行 INSERT DELAYED 语句时;
    4)用 UDF 时;
    • 优点:节省空间,同时兼顾了一定的一致性。
    • 缺点:还有些极个别情况依旧会造成不一致,另外statement和mixed对于需要对binlog 的监控的情况都不方便。
    由于 canal 是监控的数据的变化, 所以 binlog 的格式需要设置成 row 格式

1.3 CANAL的特点

Canal的主要特点:

  1. 避免全表扫描进行数据转移
  2. 能够实时的监控 Mysql 数据的变化
  3. 需要依赖Mysql的binlog

1.4 CANAL应用场景

  1. 实时监控并同步Mysql中数据的变化。

1.5 CANAL各个端口

端口名称默认端口说明
Canal.port111111ntp端口
Canal.metrics.pull.port11112主从机通信端口

1.6 中间件版本选取

中间件名称版本号
CentOSCentOS 6.8
mysql5.6.24
Canal1.1.2

2 CANAL部署

2.1 环境准备

本次技术实践安装Canal集群,单独安装在hadoop102主机上,后续HA另做规划

2.1.1 CentOS6.8

CentOS6.8安过程省略。预先创建用户/用户组zhouchen
预先安装jdk1.8.0_92 +
预先安装mysql

2.1.2 关闭防火墙-root

针对CentOS7以下
1.查看防火墙状态
service iptables status
2.停止防火墙
service iptables stop
3.启动防火墙
service iptables start

2.2 集群安装

2.2.1 Mysql配置

1.赋权限

CREATE USER canal IDENTIFIED BY 'canal';
GRANT ALL PRIVILEGES ON *.* TO canal@'%' IDENTIFIED BY 'canal';
FLUSH PRIVILEGES;

2.开启 binlog
打开文件my.cnf, 如果没有就创建一个

[zhouchen@hadoop102 /]$ sudo find ./ -iname my.cnf
./usr/my.cnf

添加如下配置:

[zhouchen@hadoop102 /]$ sudo vim /usr/my.cnf
[mysqld]
server-id= 1
log-bin= mysql-bin #开启bin-log
binlog_format= row #选择row模式

3.重启 mysql 使 binlog 生效

[zhouchen@hadoop102 /]$ sudo service mysql restart

4.检查 binlog 是否生效
1)进入 mysql

mysql> show variables like 'log_%';
+----------------------------------------+--------------------------------+
| Variable_name                          | Value                          |
+----------------------------------------+--------------------------------+
| log_bin                                | ON                             |
| log_bin_basename                       | /var/lib/mysql/mysql-bin       |
| log_bin_index                          | /var/lib/mysql/mysql-bin.index |
| log_bin_trust_function_creators        | OFF                            |
| log_bin_use_v1_row_events              | OFF                            |
| log_error                              | /var/lib/mysql/hadoop201.err   |
| log_output                             | FILE                           |
| log_queries_not_using_indexes          | OFF                            |
| log_slave_updates                      | OFF                            |
| log_slow_admin_statements              | OFF                            |
| log_slow_slave_statements              | OFF                            |
| log_throttle_queries_not_using_indexes | 0                              |
| log_warnings                           | 1                              |
+----------------------------------------+--------------------------------+

*log_bin:on 表示开启成功
2)/var/lib/mysql下的mysql-bin文件
在这里插入图片描述

2.2.2 Canal安装

1.下载Canal

[zhouchen@hadoop102 software]$ wget https://github.com/alibaba/canal/releases/download/canal-1.1.2/canal.deployer-1.1.2.tar.gz

2.解压

[zhouchen@hadoop102 software]$ mkdir /opt/module/canal
[zhouchen@hadoop102 software]$ tar -zxvf canal.deployer-1.1.2.tar.gz -C /opt/module/canal

3.修改Canal通用配置conf/canal.properties

[zhouchen@hadoop102 canal]$ vim conf/canal.properties
canal.id = 1
canal.ip =
canal.port = 11111
canal.metrics.pull.port = 11112

4.修改mysql实例的配置conf/example/instance.properties

[zhouchen@hadoop102 canal]$ vim conf/example/instance.properties
# slaveId 必须配置, 不能和 mysql 的 id 重复
canal.instance.mysql.slaveId=100
# position info mysql 的位置信息
canal.instance.master.address=hadoop102:3306
# username/password用户名和密码
canal.instance.dbUsername=root
canal.instance.dbPassword=zhou59420
canal.instance.connectionCharset = UTF-8 #数据库编码格式
canal.instance.defaultDatabaseName =
# enable druid Decrypt database password
canal.instance.enableDruid=false

2.2.3 集群启动

启动:

[zhouchen@hadoop102 canal]$ bin/startup.sh

停止:

[zhouchen@hadoop102 canal]$ bin/stop.sh

2.2.4 安装检查

1.查看进程

[zhouchen@hadoop102 canal]$ jps
5351 CanalLauncher
5389 Jps

2.查看日志

[zhouchen@hadoop102 canal]$ ll logs/
总用量 8
drwxrwxr-x 2 zhouchen zhouchen 4096 12月 28 2019 canal
drwxrwxr-x 7 zhouchen zhouchen 4096 7月  22 10:32 example   

3 CANAL实时同步MYSQL数据到KAFKA

这里canal起到的作用是在业务模块,将保存在mysql中的结构化业务数据实时同步到kafka中。然后kafka到下游的大数据分析模块进程数据分析
在这里插入图片描述

3.1 从CANAL读取数据

3.1.1 添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>gmall</artifactId>
        <groupId>com.realtime.dw</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gmall-canal</artifactId>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.alibaba.otter/canal.client -->
        <!--canal 客户端, 从 canal 服务器读取数据-->
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
        <!-- kafka 客户端 -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>0.11.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.realtime.dw</groupId>
            <artifactId>gmall-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

3.1.2 从 Canal 服务读取数据的客户端

package com.gmall.canal

import java.net.InetSocketAddress
import java.util
import com.alibaba.otter.canal.client.{CanalConnector, CanalConnectors}
import com.alibaba.otter.canal.protocol.CanalEntry.{EntryType, RowChange}
import com.alibaba.otter.canal.protocol.{CanalEntry, Message}
import com.google.protobuf.ByteString

object CanalClient {

    def main(args: Array[String]): Unit = {
        import scala.collection.JavaConversions._
        // 1.创建连接
        val addr = new InetSocketAddress("hadoop102",11111)

        val connector: CanalConnector = CanalConnectors.newSingleConnector(addr,"example","","")

        connector.connect()
        connector.subscribe("gmall_realtime.*")
        while (true) {
            val msg: Message = connector.get(100)
            val entries: util.List[CanalEntry.Entry] = msg.getEntries
            if(entries != null && entries.nonEmpty){
                for(entry <- entries){
                    // 只处理ROWDATA这种类型的Entry
                    if(entry.getEntryType == EntryType.ROWDATA){
                        val storeValue: ByteString = entry.getStoreValue
                        val rowChange: RowChange = RowChange.parseFrom(storeValue)

                        val rowDataList: util.List[CanalEntry.RowData] = rowChange.getRowDatasList

                        CanalHandler.handler(entry.getHeader.getTableName, rowDataList, rowChange.getEventType)
                    }
                }
            }else{
                System.out.println("没有拉取到数据, 2s之后重新拉取");
                Thread.sleep(2000)
            }
        }
    }
}

• Message: 一次可以拉一个message, 一个message可以看成是由多条sql执行导致的变化组成的数据的封装
• Entry: 实体 一个message封装多个Entry. 一个Entry表示一条sql执行的结果(多行变化)
• StoreValue: 一个Entry封装一个StoreValue, 可以看成是序列化的数据
• RowChange: 表示一行变化的数据. 一个StoreValue会存储1个RowChange
• RowData: 变化的数据 对应以一行数据 一个RowChange对应多个RowData
• Column: 列 列名和值

3.2 读取的数据发送到 KAFKA

3.2.1 实现 Kafka 生产者

package com.gmall.canal

import java.util.Properties
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}

object MyKafkaUtil {
    val props = new Properties()
    // Kafka服务端的主机名和端口号
    props.put("bootstrap.servers", "hadoop102:9092,hadoop103:9092,hadoop104:9092")
    // key序列化
    props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
    // value序列化
    props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer")

    private val producer = new KafkaProducer[String, String](props)
    def send(topic :String, content: String) = {
        // 创建生产者, 然后写入
        val record = new ProducerRecord[String, String](topic, content)
        producer.send(record)
    }
}

3.2.2 使用生产者向 Kafka 生成数据

考虑到将来存储到 ES 中的数据是 Json 格式, 所以, 我们在 Kafka 存储的的时候也存储为 Json 格式的.
改写工具类: CanalHandler

package com.gmall.canal

import java.util
import com.alibaba.fastjson.JSONObject
import com.alibaba.otter.canal.protocol.CanalEntry
import com.alibaba.otter.canal.protocol.CanalEntry.EventType
import com.gmall.common.util.Constants
import scala.collection.JavaConversions._
import scala.util.Random

object CanalHandler {
    def handler(tableName: String, rowDataList: util.List[CanalEntry.RowData], eventType: CanalEntry.EventType) = {
        if ("order_info" == tableName && eventType == EventType.INSERT && rowDataList != null && !rowDataList.isEmpty) {
            println("order_info")
            sendRowDataListToKafka(rowDataList, Constants.TOPIC_ORDER)
        } else if ("order_detail" == tableName && eventType == EventType.INSERT && rowDataList != null && !rowDataList.isEmpty) {
            println("order_detail")
            sendRowDataListToKafka(rowDataList, Constants.TOPIC_DETAIL)
        }
    }


    private def sendRowDataListToKafka(rowDatasList: util.List[CanalEntry.RowData],topic:String) = {
        for (rowData <- rowDatasList) {
            val jsonObj = new JSONObject()

            // 变化后的列
            val columnList: util.List[CanalEntry.Column] = rowData.getAfterColumnsList

            for (column <- columnList) {
                jsonObj.put(column.getName, column.getValue)
            }

            //写入到kafka
            Thread.sleep(new Random().nextInt(1000*8))
            MyKafkaUtil.send(topic, jsonObj.toJSONString)
        }
    }
}

4 CANAL的HA设计

canal的ha分为两部分,canal server和canal client分别有对应的ha实现
• canal server: 为了减少对mysql dump的请求,不同server上的instance要求同一时间只能有一个处于running,其他的处于standby状态.
• canal client: 为了保证有序性,一份instance同一时间只能由一个canal client进行get/ack/rollback操作,否则客户端接收无法保证有序。
整个HA机制的控制主要是依赖了zookeeper的几个特性,watcher和EPHEMERAL节点(和session生命周期绑定)
Canal Server:
在这里插入图片描述
大致步骤:

  1. canal server要启动某个canal instance时都先向zookeeper进行一次尝试启动判断 (实现:创建EPHEMERAL节点,谁创建成功就允许谁启动)
  2. 创建zookeeper节点成功后,对应的canal server就启动对应的canal instance,没有创建成功的canal instance就会处于standby状态
  3. 一旦zookeeper发现canal server A创建的节点消失后,立即通知其他的canal server再次进行步骤1的操作,重新选出一个canal server启动instance.
  4. canal client每次进行connect时,会首先向zookeeper询问当前是谁启动了canal instance,然后和其建立链接,一旦链接不可用,会重新尝试connect.
    Canal Client的方式和canal server方式类似,也是利用zookeeper的抢占EPHEMERAL节点的方式进行控制.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值