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的主从复制:
从上层来看,复制分成三步:
- master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);
- slave将master的binary log events拷贝到它的中继日志(relay log);
- slave重做中继日志中的事件,将改变反映它自己的数据。
1.2.2 Canal工作原理
Canal的工作原理:
原理相对比较简单:
- canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
- mysql master收到dump请求,开始推送binary log给slave(也就是canal)
- canal解析binary log对象(原始为byte流)
1.2.3 Mysql的Binlog
MySQL 的二进制日志可以说是 MySQL 最重要的日志了,它记录了所有的 DDL 和DML (除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。
一般来说开启 binlog 日志大概会有 1% 的性能损耗。 binlog日志有两个最重要的使用场景:
- MySQL Replication 在 Master 端开启 binlog,Mster 把它的二进制日志传递给 slaves 来达到 master-slave 数据一致的目的。
- 自然就是数据恢复了,通过使用 mysql binlog 工具来使恢复数据。
1.2.4 binlog 格式
binlog 有 3 种格式: STATEMENT, ROW, MIXED
- statement
语句级别, binlog 会记录每次执行的写操作的语句, 注意记录的是语句, slave 会自己重新执行写操作的语句, 从而达到与 master 的一致.
但是有可能会出现主从不一致的情况: 比如存储当前时间戳, 存储一个随机值等.
• 优点: 节省空间
• 缺点: 有可能造成数据不一致。 - row
行级, binlog 会记录每次操作后每行记录的变化。
• 优点:保持数据的绝对一致性。因为不管sql是什么,引用了什么函数,他只记录执行后的效果。
• 缺点:占用较大空间。 如果一条语句执行之后导致很多行发生了变化, 则会产生很多条记录 - mixed
statement 的升级版,一定程度上解决了因为一些情况而造成的 statement模式不一致问题
在某些情况下会按照 ROW的方式进行处理
1)当函数中包含 UUID() 时;
2)包含 AUTO_INCREMENT 字段的表被更新时;
3)执行 INSERT DELAYED 语句时;
4)用 UDF 时;
• 优点:节省空间,同时兼顾了一定的一致性。
• 缺点:还有些极个别情况依旧会造成不一致,另外statement和mixed对于需要对binlog 的监控的情况都不方便。
由于 canal 是监控的数据的变化, 所以 binlog 的格式需要设置成 row 格式
1.3 CANAL的特点
Canal的主要特点:
- 避免全表扫描进行数据转移
- 能够实时的监控 Mysql 数据的变化
- 需要依赖Mysql的binlog
1.4 CANAL应用场景
- 实时监控并同步Mysql中数据的变化。
1.5 CANAL各个端口
端口名称 | 默认端口 | 说明 |
---|---|---|
Canal.port | 111111 | ntp端口 |
Canal.metrics.pull.port | 11112 | 主从机通信端口 |
1.6 中间件版本选取
中间件名称 | 版本号 |
---|---|
CentOS | CentOS 6.8 |
mysql | 5.6.24 |
Canal | 1.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:
大致步骤:
- canal server要启动某个canal instance时都先向zookeeper进行一次尝试启动判断 (实现:创建EPHEMERAL节点,谁创建成功就允许谁启动)
- 创建zookeeper节点成功后,对应的canal server就启动对应的canal instance,没有创建成功的canal instance就会处于standby状态
- 一旦zookeeper发现canal server A创建的节点消失后,立即通知其他的canal server再次进行步骤1的操作,重新选出一个canal server启动instance.
- canal client每次进行connect时,会首先向zookeeper询问当前是谁启动了canal instance,然后和其建立链接,一旦链接不可用,会重新尝试connect.
Canal Client的方式和canal server方式类似,也是利用zookeeper的抢占EPHEMERAL节点的方式进行控制.