……

第一章、canal入门

一、什么是canal

canal是纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL

img

 

 

如上图:canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议

二、canal 搭建

1、搭建mysql环境
  • 对于自建 MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下

[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
  • 授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant

CREATE USER canal IDENTIFIED BY 'canal'; 
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;

这个第一步还是蛮简单的,就是要自己搭建一个mysql,修改一下mysql的配置,这个配置一般是再/etc/my.cnf中,还是得要点小基础的哈,至少mysql得会搭

2、搭建canal环境
  • 下载 canal, 访问 release 页面 , 选择需要的包下载, 如以 1.0.17 版本为例

wget https://github.com/alibaba/canal/releases/download/canal-1.0.17/canal.deployer-1.0.17.tar.gz
  • 解压缩

mkdir /tmp/canal
tar zxvf canal.deployer-$version.tar.gz -C /tmp/canal

解压完成后,进入 /tmp/canal 目录,可以看到如下结构

img

 

  • 配置修改

vi conf/example/instance.properties
#################################################
## mysql serverId , v1.0.26+ will autoGen
canal.instance.mysql.slaveId=8

# enable gtid use true/false
canal.instance.gtidon=false

# position info 需要改成自己的数据库信息
canal.instance.master.address=10.0.98.186:3306
canal.instance.master.journal.name=
canal.instance.master.position=
canal.instance.master.timestamp=
canal.instance.master.gtid=

# rds oss binlog
canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=

# table meta tsdb info
canal.instance.tsdb.enable=true
#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
#canal.instance.tsdb.dbUsername=canal
#canal.instance.tsdb.dbPassword=canal

#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =
#canal.instance.standby.gtid=

# username/password 需要改成自己的数据库信息
canal.instance.dbUsername=root
canal.instance.dbPassword=root
canal.instance.connectionCharset=UTF-8
canal.instance.defaultDatabaseName=expert-online-school
# table regex
canal.instance.filter.regex=.*\\..*
# table black regex
canal.instance.filter.black.regex=
#################################################

注意: canal.instance.connectionCharset 代表数据库的编码方式对应到 java 中的编码类型,比如 UTF-8,GBK , ISO-8859-1 如果系统是1个 cpu,需要将 canal.instance.parser.parallel 设置为 false

  • 启动

sh bin/startup.sh

到目前为止 canal的服务端我们已经搭建好了 但是到目前 我们只是把数据库的binlog 拉到canal中,我们还得把数据用otter去消费

三、写个简单的Demo 去监听mysql 数据的变动

1、Jar包
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.3</version>
</dependency>

四、测试代码

package com.hq.eos.sync.client;

import java.net.InetSocketAddress;
import java.util.List;


import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
import com.alibaba.otter.canal.protocol.Message;



public class CanalTest {

public static void main(String[] args) throws Exception {

CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("10.0.98.186", 11111), "expert", "root", "root");
connector.connect();
connector.subscribe(".*\\..*");
connector.rollback();

while (true) {
Message message = connector.getWithoutAck(100); // 获取指定数量的数据
long batchId = message.getId();
if (batchId == -1 || message.getEntries().isEmpty()) {
Thread.sleep(1000);
continue;
}
// System.out.println(message.getEntries());
printEntries(message.getEntries());
connector.ack(batchId);// 提交确认,消费成功,通知server删除数据
// connector.rollback(batchId);// 处理失败, 回滚数据,后续重新获取数据
}
}

private static void printEntries(List<Entry> entries) throws Exception {
for (Entry entry : entries) {
if (entry.getEntryType() != EntryType.ROWDATA) {
continue;
}

RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());

EventType eventType = rowChange.getEventType();
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType));

for (RowData rowData : rowChange.getRowDatasList()) {
switch (rowChange.getEventType()) {
case INSERT:
System.out.println("INSERT ");
printColumns(rowData.getAfterColumnsList());
break;
case UPDATE:
System.out.println("UPDATE ");
printColumns(rowData.getAfterColumnsList());
break;
case DELETE:
System.out.println("DELETE ");
printColumns(rowData.getBeforeColumnsList());
break;

default:
break;
}
}
}
}

private static void printColumns(List<Column> columns) {
for(Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
}

测试结果

================> binlog[mysql-bin.000017:240485980] , name[xxl_job,xxl_job_registry] , eventType : UPDATE
UPDATE
id : 402 update=false
registry_group : EXECUTOR update=false
registry_key : hq-eos-crawler update=false
registry_value : 172.27.0.1:15674 update=false
update_time : 2019-12-03 17:54:42 update=true
================> binlog[mysql-bin.000017:240486374] , name[xxl_job,xxl_job_registry] , eventType : UPDATE
UPDATE
id : 82 update=false
registry_group : EXECUTOR update=false
registry_key : hq-eos-inf-config update=false
registry_value : 172.18.0.1:15672 update=false
update_time : 2019-12-03 17:54:42 update=true
================> binlog[mysql-bin.000017:240486774] , name[xxl_job,xxl_job_registry] , eventType : UPDATE

注意一下

 CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("10.0.98.186", 11111), "expert", "root", "root");

这里的配置来自于 canal.properties 我把这个配置也贴出来吧

canal.id= 8
canal.ip=
canal.port=11111
canal.metrics.pull.port=11112
canal.zkServers=10.0.14.36:2181,10.0.14.39:2181,10.0.14.49:2181
# flush data to zk
canal.zookeeper.flush.period = 1000
canal.withoutNetty = false
# flush meta cursor/parse position to file
canal.file.data.dir = ${canal.conf.dir}
canal.file.flush.period = 1000
## memory store RingBuffer size, should be Math.pow(2,n)
canal.instance.memory.buffer.size = 16384
## memory store RingBuffer used memory unit size , default 1kb
canal.instance.memory.buffer.memunit = 1024
## meory store gets mode used MEMSIZE or ITEMSIZE
canal.instance.memory.batch.mode = MEMSIZE

## detecing config
canal.instance.detecting.enable = false
#canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()
canal.instance.detecting.sql = select 1
canal.instance.detecting.interval.time = 3
canal.instance.detecting.retry.threshold = 3
canal.instance.detecting.heartbeatHaEnable = false

# support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery
canal.instance.transaction.size = 1024
# mysql fallback connected to new master should fallback times
canal.instance.fallbackIntervalInSeconds = 60

# network config
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
canal.instance.network.soTimeout = 30
# binlog filter config
canal.instance.filter.druid.ddl = true
canal.instance.filter.query.dcl = false
canal.instance.filter.query.dml = false
canal.instance.filter.query.ddl = false
canal.instance.filter.table.error = false
canal.instance.filter.rows = false
canal.instance.filter.transaction.entry = false

# binlog format/image check
canal.instance.binlog.format = ROW,STATEMENT,MIXED
canal.instance.binlog.image = FULL,MINIMAL,NOBLOB

# binlog ddl isolation
canal.instance.get.ddl.isolation = false

# parallel parser config
canal.instance.parser.parallel = true
## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()
#canal.instance.parser.parallelThreadSize = 16
## disruptor ringbuffer size, must be power of 2
canal.instance.parser.parallelBufferSize = 256

# table meta tsdb info
canal.instance.tsdb.enable=true
canal.instance.tsdb.dir=${canal.file.data.dir:../conf}/${canal.instance.destination:}
canal.instance.tsdb.url=jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;
canal.instance.tsdb.dbUsername=root
canal.instance.tsdb.dbPassword=root
# rds oss binlog account
canal.instance.rds.accesskey =
canal.instance.rds.secretkey =

#################################################
######### destinations #############
#################################################
canal.destinations= expert
# conf root dir
canal.conf.dir = ../conf
# auto scan instance dir add/remove and start/stop instance
canal.auto.scan = true
canal.auto.scan.interval = 5

canal.instance.tsdb.spring.xml=classpath:spring/tsdb/h2-tsdb.xml
#canal.instance.tsdb.spring.xml=classpath:spring/tsdb/mysql-tsdb.xml

canal.instance.global.mode = spring
canal.instance.global.lazy = false
#canal.instance.global.manager.address = 127.0.0.1:1099
#canal.instance.global.spring.xml = classpath:spring/memory-instance.xml
canal.instance.global.spring.xml = classpath:spring/file-instance.xml
#canal.instance.global.spring.xml = classpath:spring/default-instance.xml

第二章、canal实战

一、canal连接kafka实现实时同步mysql数据

1、构建maven依赖
<dependency>
   <groupId>com.alibaba.otter</groupId>
   <artifactId>canal.client</artifactId>
   <version>1.0.25</version>
</dependency>
<dependency>
   <groupId>org.apache.kafka</groupId>
   <artifactId>kafka-clients</artifactId>
   <version>1.1.0</version>
</dependency>

注意版本对应

2、SimpleCanalClient(客户端)
package com.unigroup.client.canal;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.List;

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;

import com.alibaba.otter.canal.protocol.Message;
import com.unigroup.core.canal.CanalToKG;

/**  

* @Title: SimpleCanalClient.java

* @Package com.unigroup.canal

* @Description: canal單實例接口

* @author 大码王  

* @date 2019年12月29日 上午11:56:09

* @version V1.0  
 */
 public class SimpleCanalClient {

   private CanalConnector connector=null;

   public SimpleCanalClient(String ip,String port,String instance) {

       // 创建链接
       connector = CanalConnectors.newSingleConnector(new InetSocketAddress(ip, Integer.parseInt(port)),instance, "", "");

  }
   public List<Entry> execute(int batchSize,Class<?> clazz ) throws InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException {

       //int batchSize = 1;
       int emptyCount = 0;
       Object obj = clazz.newInstance();
       Method method = clazz.getMethod("send",Message.class);
       try {
           connector.connect();
           // connector.subscribe(".*\\..*");
           connector.subscribe("test.test1");
     
           connector.rollback();
           int totalEmptyCount = 120;
           while (emptyCount < totalEmptyCount) {
               Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
               long batchId = message.getId();
               int size = message.getEntries().size();
               if (batchId == -1 || size == 0) {
                   emptyCount++;
                   System.out.println("empty count : " + emptyCount);
                   try {
                       Thread.sleep(1000);
                  } catch (InterruptedException e) {
                  }
              } else {
                   emptyCount = 0;
                   method.invoke(obj, message);            
              }
               connector.ack(batchId); // 提交确认
     
               // connector.rollback(batchId); // 处理失败, 回滚数据
          }
     
           System.out.println("empty too many times, exit");
      } catch (IllegalAccessException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
      } catch (IllegalArgumentException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
      } catch (InvocationTargetException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
      } finally {
           connector.disconnect();
      }
       return null;

  }
}

 

3、CanalKafkaProducer(生产者)
package com.unigroup.kafka.producer;

import java.io.IOException;
import java.util.Properties;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.otter.canal.protocol.Message;
import com.unigroup.kafka.producer.KafkaProperties.Topic;
import com.unigroup.utils.MessageSerializer;

/**  

* @Title: CanalKafkaProducer.java

* @Package com.unigroup.kafka.producer

* @Description:

* @author 大码王

* @date 2019年12月3日 上午11:53:35

* @version V1.0  
 */
 public class CanalKafkaProducer {

   private static final Logger logger = LoggerFactory.getLogger(CanalKafkaProducer.class);

   private Producer<String, Message> producer;

   public void init(KafkaProperties kafkaProperties) {
       Properties properties = new Properties();
       properties.put("bootstrap.servers", kafkaProperties.getServers());
       properties.put("acks", "all");
       properties.put("retries", kafkaProperties.getRetries());
       properties.put("batch.size", kafkaProperties.getBatchSize());
       properties.put("linger.ms", kafkaProperties.getLingerMs());
       properties.put("buffer.memory", kafkaProperties.getBufferMemory());
       properties.put("key.serializer", StringSerializer.class.getName());
       properties.put("value.serializer", MessageSerializer.class.getName());
       producer = new KafkaProducer<String, Message>(properties);
  }

   public void stop() {
       try {
           logger.info("## stop the kafka producer");
           producer.close();
      } catch (Throwable e) {
           logger.warn("##something goes wrong when stopping kafka producer:", e);
      } finally {
           logger.info("## kafka producer is down.");
      }
  }

   public void send(Topic topic, Message message) throws IOException {

       ProducerRecord<String, Message> record;
       if (topic.getPartition() != null) {
           record = new ProducerRecord<String, Message>(topic.getTopic(), topic.getPartition(), null, message);
      } else {
           record = new ProducerRecord<String, Message>(topic.getTopic(), message);
      }
       producer.send(record);
       if (logger.isDebugEnabled()) {
           logger.debug("send message to kafka topic: {} \n {}", topic.getTopic(), message.toString());
      }

  }
}
4、canalToKafkaServer(服务端)
package com.unigroup.kafka.server;

import com.unigroup.client.canal.SimpleCanalClient;
import com.unigroup.kafka.producer.CanalKafkaProducer;
import com.unigroup.utils.GetProperties;

/**  

* @Title: canal.java
* @Package com.unigroup.kafka.server
* @Description:
* @author 大码王  
* @date 2019年12月3日 上午11:23:35
* @version V1.0  
 */
 public class canalToKafkaServer {
   public static void execute() {
       SimpleCanalClient simpleCanalClient = new SimpleCanalClient(GetProperties.getValue("MYSQL_HOST"),
               GetProperties.getValue("MTSQL_PORT"), GetProperties.getValue("INSTANCE"));
       try {
           simpleCanalClient.execute(1,CanalKafkaProducer.class);
      } catch (Exception e) {
           e.printStackTrace();
      }
  }
}

至此一个简单的canal到kafka的demo已经完成。这些都只是测试代码,实际应用中根据不同的情况,可以自己开发更多功能。

二、canal增量同步mysql数据库信息到ElasticSearch

0、 运作原理

原理很简单:

Canal模拟MySQL的slave的交互协议,伪装成mysql slave,并将转发协议发送到MySQL Master服务器。 MySQL Master接收到转储请求并开始将二进制日志推送到slave(即canal)。 Canal将二进制日志对象解析为自己的数据类型(原始字节流) 如图所示: image

1、同步es

在同步数据到es的时候需要使用适配器:canal adapter。目前最新版本1.1.3,下载地址:https://github.com/alibaba/canal/releases

目前es貌似支持6.x版本,不支持7.x版本!!!

2、准备工作
2.1 es和jdk

安装es如下:

2 安装Elasticsearch

2.1 创建elasticsearch目录

cd /usr/local/
mkdir tool
cd tool
mkdir elasticsearch
cd elasticsearch

2.1 下载Elasticsearch

2.1.1 在刚刚创建好的文件夹内下载Elasticsearch(以下简称es)

curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.4.2.tar.gz

2.1.2 解压es包

tar -xvf elasticsearch-5.4.2.tar.gz

2.1.3 进入es/bin包

cd elasticsearch-5.4.2/bin

2.1.4 启动es

./elasticsearch

2.2 解决问题

2.2.1 问题一

直接启动,遇到如图问题,如下:

img

这个问题是由于内存分配不够造成的,修改适合本机的内存,修改文件config/jvm.options

vi ../config/jvm.options

修改如下位置

img

由于我的服务器内存较小,修改为512m,具体可以根据情况修改,如下:

img

修改后在次启动:

./elasticsearch

2.2.2 问题二

出现如下错误:

org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root

如图

img

这个问题很明显,不允许使用root用户启动,那么我们新建一个es用户,并赋予权限:

添加es用户

useradd es

添加es用户密码

passwd es

将文件夹elasticsearch-5.4.2赋予es权限

chown -R es:es /usr/local/tool/elasticsearch/elasticsearch-5.4.2

切换为es用户

su es

再次启动es

./elasticsearch

这次启动成功了,我们在使用一个窗口登录root用户,输入命令:

curl -X GET http://localhost:9200

如图所示,可以成功访问

img

2.2.3 问题三

在浏览器访问http://118.24.242.170:9200/拒绝访问(118.24.242.170为服务器ip)

使用root用户,打开elasticsearch.yml文件,如下:

vi /usr/local/tool/elasticsearch/elasticsearch-5.4.2/config/elasticsearch.yml

文件内增加如下代码

network.host: 0.0.0.0

使用es用户启动,发现又出现了错误如下,得到错误信息如图

img

使用root用户打开如下文件:

vim /etc/sysctl.conf

添加如下配置:

vm.max_map_count = 655360

使配置生效

/sbin/sysctl -p

然后使用es用户启动Elasticsearch,这次可以成功启动了,如果需要后台启动的话,在启动命令后加&,如下所示:

./elasticsearch &

安装jdk如下

1.查看yum中管理的可用的JDK软件包列表:

yum search java | grep -i --color JDK

结果如下图所示:

image

2.选择合适版本,安装jdk,本人选择的是java-1.8.0-openjdk-devel.x86_64

yum install java-1.8.0-openjdk-devel.x86_64

3配置环境变量,打开etc文件下profile

vi  /etc/profile

在文件内添加

export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.71-2.b15.el7_2.x86_64
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

保存关闭后,执行,让配置生效:

source  /etc/profile

然后分别输入下面命令确认jdk是否安装成功:

java

image

javac

image

java -version

image

能显示以上信息,就说明安装成功了。

2.2 安装canal server

下载canal.deployer-1.1.3.tar.gz

wget https://github.com/alibaba/canal/releases/download/canal-1.1.3/canal.deployer-1.1.3.tar.gz

解压文件

tar -zxvf canal.deployer-1.1.3.tar.gz

进入解压后的文件夹

cd canal.deployer-1.1.3

修改conf/example/instance.properties文件,主要注意以下几处:

canal.instance.master.address:数据库地址,例如127.0.0.1:3306
canal.instance.dbUsername:数据库用户
canal.instance.dbPassword:数据库密码

完整内容如下:

#################################################

## mysql serverId , v1.0.26+ will autoGen

# canal.instance.mysql.slaveId=0

# enable gtid use true/false

canal.instance.gtidon=false

# position info

canal.instance.master.address=127.0.0.1:3306
canal.instance.master.journal.name=
canal.instance.master.position=
canal.instance.master.timestamp=
canal.instance.master.gtid=

# rds oss binlog

canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=

# table meta tsdb info

canal.instance.tsdb.enable=true
#canal.instance.tsdb.url=
#canal.instance.tsdb.dbUsername=
#canal.instance.tsdb.dbPassword=

#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =
#canal.instance.standby.gtid=

# username/password

canal.instance.dbUsername=root
canal.instance.dbPassword=12345678
canal.instance.connectionCharset = UTF-8

# enable druid Decrypt database password

canal.instance.enableDruid=false
#canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==

# table regex

canal.instance.filter.regex=.*\\..*

# table black regex

canal.instance.filter.black.regex=

# mq config

#canal.mq.topic=example

# dynamic topic route by schema or table regex

#canal.mq.dynamicTopic=mytest1.user,mytest2\\..*,.*\\..*
#canal.mq.partition=0

# hash partition config

#canal.mq.partitionsNum=3
#canal.mq.partitionHash=test.table:id^name,.*\\..*
#################################################

回到canal.deployer-1.1.3目录下,启动canal:

sh bin/startup.sh

查看日志:

vi logs/canal/canal.log

查看具体instance日志:

 vi logs/example/example.log

关闭命令

sh bin/stop.sh

 

2.3 安装canal-adapter

下载canal.adapter-1.1.3.tar.gz

wget https://github.com/alibaba/canal/releases/download/canal-1.1.3/canal.adapter-1.1.3.tar.gz

解压

tar -zxvf canal.adapter-1.1.3.tar.gz

进入解压后的文件夹

cd canal.adapter-1.1.3

修改conf/application.yml文件,主要注意如下内容,由于是yml文件,注意我这里说明的属性名称:

server.port:canal-adapter端口号
canal.conf.canalServerHost:canal-server地址和ip
canal.conf.srcDataSources.defaultDS.url:数据库地址
canal.conf.srcDataSources.defaultDS.username:数据库用户名
canal.conf.srcDataSources.defaultDS.password:数据库密码
canal.conf.canalAdapters.groups.outerAdapters.hosts:es主机地址,tcp端口

完整内容如下:

      • server:
        port: 8081
        spring:
        jackson:
          date-format: yyyy-MM-dd HH:mm:ss
          time-zone: GMT+8
          default-property-inclusion: non_null


        canal.conf:
        mode: tcp
        canalServerHost: 127.0.0.1:11111
        batchSize: 500
        syncBatchSize: 1000
        retries: 0
        timeout:
        accessKey:
        secretKey:
        srcDataSources:
          defaultDS:
            url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true
            username: root
            password: 12345678
        canalAdapters:

        - instance: example
          groups:
          - groupId: g1
            outerAdapters:
            - name: es
              hosts: 127.0.0.1:9300
              properties:
                cluster.name: elasticsearch

        另外需要配置conf/es/*.yml文件,adapter将会自动加载conf / es下的所有.yml结尾的配置文件。在介绍配置前,需要先介绍一下本案例使用的表结构,如下:

CREATE TABLE `test` (
 `id` int(11) NOT NULL,
 `name` varchar(200) NOT NULL,
 `address` varchar(1000) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

需要手动去es中创建索引,比如这里使用es-head创建,如下图:

image

test索引结构如下:

{
   "mappings":{
       "_doc":{
           "properties":{
               "name":{
                   "type":"text"
              },
               "address":{
                   "type":"text"
              }
          }
      }
  }
}

接下来创建test.yml(文件名随意),内容很好理解_

index为索引名称,sql为对应语句,内容如下:

dataSourceKey: defaultDS
destination: example
groupId:
esMapping:
_index: test
_type: _doc
_id: _id
upsert: true
sql: "select a.id as _id,a.name,a.address from test a"
commitBatch: 3000

配置完成后,回到canal-adapter根目录,执行命令启动

bin/startup.sh

查看日志

vi logs/adapter/adapter.log

关闭canal-adapter命令

bin/stop.sh

3.测试 都启动成功后,先查看一下es-head,如图,现在是没有任何数据的。

img

接下来,我们在数据库中插入一条数据进行测试,语句如下:

INSERT INTO `test`.`test`(`id`, `name`, `address`) VALUES (7, '北京', '北京市朝阳区');

然后在看一下es-head,如下

img

接下来看一下日志,如下:

2019-06-22 17:54:15.385 [pool-2-thread-1] DEBUG c.a.otter.canal.client.adapter.es.service.ESSyncService - DML: {"data":[{"id":7,"name":"北京","address":"北京市朝阳区"}],"database":"test","destination":"example","es":1561197255000,"groupId":null,"isDdl":false,"old":null,"pkNames":["id"],"sql":"","table":"test","ts":1561197255384,"type":"INSERT"} 
Affected indexes: test

小知识点:上面介绍的查看日志的方法可能不是很好用,推荐使用如下语法,比如查看日志最后200行:

tail -200f logs/adapter/adapter.log
3.总结

1.全量更新不能实现,但是增删改都是可以的。 2.一定要提前创建好索引。 3.es配置的是tcp端口,比如默认的9300

在这里插入图片描述

三、mysql+canal+kafka+elasticsearch构建数据查询平台

1. 实验环境

CPU:4 内存:8G ip:192.168.0.187

开启iptables防火墙 关闭selinux java >=1.5 使用yum方式安装的java,提前配置好JAVA_HOME环境变量

vim /etc/profile.d/java.sh
#!/bin/bash

export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk # 路径根据实际情况而定
export PATH=$PATH:$JAVA_HOME/bin
source /etc/profile.d/java.sh
2. MySQL信息

mysql账号 root MySQL密码 liykpntuu9?C

1、操作
vim /etc/my.cnf
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

service mysqld restart

登陆数据库后操作

CREATE USER canal IDENTIFIED BY 'canal!%123AD';  
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
3. canal操作
# 下载
wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.deployer-1.1.4.tar.gz
mkdir -p /usr/local/canal
tar -zxv -f canal.deployer-1.1.4.tar.gz -C /usr/local/canal

# 修改连接数据库的配置文件
cd /usr/local/canal
vim conf/example/instance.properties
## mysql serverId
canal.instance.mysql.slaveId = 123
#position info,需要改成自己的数据库信息
canal.instance.master.address = 127.0.0.1:3306
canal.instance.master.journal.name =
canal.instance.master.position =
canal.instance.master.timestamp =
#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =
#username/password,需要改成自己的数据库信息
canal.instance.dbUsername = canal  
canal.instance.dbPassword = canal!%123AD
canal.instance.defaultDatabaseName =
canal.instance.connectionCharset = UTF-8
#table regex
canal.instance.filter.regex = .\*\\\\..\*

# 启动
bash bin/startup.sh

# 查看 server 日志
tail -n 30 logs/canal/canal.log
2019-09-20 09:48:46.987 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## set default uncaught exception handler
2019-09-20 09:48:47.019 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## load canal configurations
2019-09-20 09:48:47.028 [main] INFO com.alibaba.otter.canal.deployer.CanalStarter - ## start the canal server.
2019-09-20 09:48:47.059 [main] INFO com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[192.168.0.187(192.168.0.187):11111]
2019-09-20 09:48:48.228 [main] INFO com.alibaba.otter.canal.deployer.CanalStarter - ## the canal server is running now ......

# 查看 instance 的日志
2019-09-20 09:48:47.395 [main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [canal.properties]
2019-09-20 09:48:47.399 [main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [example/instance.properties]
2019-09-20 09:48:47.580 [main] WARN o.s.beans.GenericTypeAwarePropertyDescriptor - Invalid JavaBean property 'connectionCharset' being accessed! Ambiguous write methods found next to actually used [public void com.alibaba.otter.canal.parse.inbound.mysql.AbstractMysqlEventParser.setConnectionCharset(java.lang.String)]: [public void com.alibaba.otter.canal.parse.inbound.mysql.AbstractMysqlEventParser.setConnectionCharset(java.nio.charset.Charset)]
2019-09-20 09:48:47.626 [main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [canal.properties]
2019-09-20 09:48:47.626 [main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [example/instance.properties]
2019-09-20 09:48:48.140 [main] INFO c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start CannalInstance for 1-example
2019-09-20 09:48:48.147 [main] WARN c.a.o.canal.parse.inbound.mysql.dbsync.LogEventConvert - --> init table filter : ^.*\..*$
2019-09-20 09:48:48.147 [main] WARN c.a.o.canal.parse.inbound.mysql.dbsync.LogEventConvert - --> init table black filter :
2019-09-20 09:48:48.165 [main] INFO c.a.otter.canal.instance.core.AbstractCanalInstance - start successful....
2019-09-20 09:48:48.288 [destination = example , address = /127.0.0.1:3306 , EventParser] WARN c.a.o.c.p.inbound.mysql.rds.RdsBinlogEventParserProxy - ---> begin to find start position, it will be long time for reset or first position
2019-09-20 09:48:48.288 [destination = example , address = /127.0.0.1:3306 , EventParser] WARN c.a.o.c.p.inbound.mysql.rds.RdsBinlogEventParserProxy - prepare to find start position just show master status
2019-09-20 09:48:49.288 [destination = example , address = /127.0.0.1:3306 , EventParser] WARN c.a.o.c.p.inbound.mysql.rds.RdsBinlogEventParserProxy - ---> find start position successfully, EntryPosition[included=false,journalName=mysql-bin.000004,position=4,serverId=1,gtid=<null>,timestamp=1568943354000] cost : 989ms , the next step is binlog dump

# 关闭
bash bin/stop.sh

# 端口使用情况
ss -tlnp
State       Recv-Q Send-Q           Local Address:Port     Peer Address:Port              
LISTEN      0      50                   *:11110                 *:*                   users:(("java",pid=2078,fd=109))
LISTEN      0      50                   *:11111                 *:*                   users:(("java",pid=2078,fd=105))
LISTEN      0      3                   *:11112                 *:*                   users:(("java",pid=2078,fd=87))

# 端口号说明
# admin端口:11110
# tcp端口:11111
# metric端口:11112
# canal-admin 使用WEB UI界面查看管理canal

# canal-admin的限定依赖:
#   MySQL,用于存储配置和节点等相关数据
#   canal版本,要求>=1.1.4 (需要依赖canal-server提供面向admin的动态运维管理接口)
wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.admin-1.1.4.tar.gz
tar -zxv -f canal-1.1.4/canal.admin-1.1.4.tar.gz -C /usr/local/src/canal_admin
vim conf/application.yml
server:
port: 8089 # 端口号,防火墙放行该端口号
spring:
jackson:
  date-format: yyyy-MM-dd HH:mm:ss
  time-zone: GMT+8

spring.datasource:
address: 127.0.0.1:3306 # 数据库地址和端口
database: canal_manager # 数据库名
username: canal_admin   # 数据库账号 ,注意跟一开始创建的canal账号区分开,需要修改一下
password: ABC123,.abc@#11 # 数据库密码
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
hikari:
  maximum-pool-size: 30
  minimum-idle: 1

canal:
adminUser: admin   # 平台账号
adminPasswd: admin # 平台密码

# 注意,数据库名,账号和密码需要提前创建好
# 若修改默认的数据库名,则示例sql文件中也需要修改
# 这里只修改默认的数据库账号和密码,其余保持默认

# 初始化元数据库
# 初始化SQL脚本里会默认创建canal_manager的数据库,建议使用root等有超级权限的账号进行初始化 b. canal_manager.sql默认会在conf目录下
mysql -hlocalhost -uroot -p
mysql> source /usr/local/canal_admin/conf/canal_manager.sql;

# 启动
bash bin/startup.sh

# 查看 admin 日志
tail -n 30 logs/admin.log
2019-09-20 14:50:54.595 [main] INFO org.apache.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8089"]
2019-09-20 14:50:54.624 [main] INFO org.apache.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
2019-09-20 14:50:54.812 [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer - Tomcat started on port(s): 8089 (http) with context path ''
2019-09-20 14:50:54.818 [main] INFO com.alibaba.otter.canal.admin.CanalAdminApplication - Started CanalAdminApplication in 11.057 seconds (JVM running for 12.731)

# 浏览器访问,防火墙放行8089端口号
# 地址:http://192.168.0.187:8089/ 访问,默认密码:admin/123456

# 使用
# 创建一个集群,添加已有的canal
# 因为端口的问题,暂时只能添加一个
# 另外canal是否可以组件集群,还有待研究

# 停止
bash bin/stop.sh
4. zookeeper
# 设置zookeeper集群
cd /usr/local/src
wget http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.5.5/apache-zookeeper-3.5.5-bin.tar.gz
tar -zxvf apache-zookeeper-3.5.5-bin.tar.gz -C /usr/local
cd /usr/local/apache-zookeeper-3.5.5-bin

mkdir -p /zkdata/{zookeeper-1,zookeeper-2,zookeeper-3}

cp conf/zoo_sample.cfg conf/zoo-1.cfg
# vim conf/zoo-1.cfg
dataDir=/zkdata/zookeeper-1
clientPort=2181

server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

cp conf/zoo-1.cfg conf/zoo-2.cfg
cp conf/zoo-1.cfg conf/zoo-3.cfg

vim conf/zoo-2.cfg
dataDir=/zkdata/zookeeper-2
clientPort=2182

server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

vim conf/zoo-3.cfg
dataDir=/zkdata/zookeeper-3
clientPort=2183

server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

echo '1' > /zkdata/zookeeper-1/myid
echo '2' > /zkdata/zookeeper-2/myid
echo '3' > /zkdata/zookeeper-3/myid

# 修改启动文件,避免后续出现如下错误
# stat is not executed because it is not in the whitelist.
# envi is not executed because it is not in the whitelist.

# nc命令需要安装其他软件
yum install nmap-ncat

# envi命令执行报错提示:envi is not executed because it is not in the whitelist.
# 解决办法 修改启动指令 zkServer.sh ,往里面添加 :ZOOMAIN="-Dzookeeper.4lw.commands.whitelist=* ${ZOOMAIN}"

else
   echo "JMX disabled by user request" >&2
   ZOOMAIN="org.apache.zookeeper.server.quorum.QuorumPeerMain" # 注意找到这个信息
fi

# 如果不想添加在这里,注意位置和赋值的顺序
ZOOMAIN="-Dzookeeper.4lw.commands.whitelist=* ${ZOOMAIN}"

# 然后重启zookeeper

# 集群启动脚本
vim start.sh
bash bin/zkServer.sh start conf/zoo-1.cfg
bash bin/zkServer.sh start conf/zoo-2.cfg
bash bin/zkServer.sh start conf/zoo-3.cfg

# 集群关闭脚本
vim start.sh
bash bin/zkServer.sh stop conf/zoo-1.cfg
bash bin/zkServer.sh stop conf/zoo-2.cfg
bash bin/zkServer.sh stop conf/zoo-3.cfg

# 检测集群状态
[root@bogon apache-zookeeper-3.5.5-bin]# bash bin/zkServer.sh status conf/zoo-1.cfg
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: conf/zoo-1.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

[root@bogon apache-zookeeper-3.5.5-bin]# bash bin/zkServer.sh status conf/zoo-2.cfg
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: conf/zoo-2.cfg
Client port found: 2182. Client address: localhost.
Mode: leader

[root@bogon apache-zookeeper-3.5.5-bin]# bash bin/zkServer.sh status conf/zoo-3.cfg
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: conf/zoo-3.cfg
Client port found: 2183. Client address: localhost.
Mode: follower
# 使用WEB UI查看监控集群-zk ui安装
cd /usr/local

git clone https://github.com/DeemOpen/zkui.git

yum install -y maven

# 更换使用阿里云maven源
vim /etc/maven/settings.xml
<mirrors>  

  <mirror>
      <id>nexus-aliyun</id>
      <mirrorOf>central</mirrorOf>
      <name>Nexus aliyun</name>
      <url>http://maven.aliyun.com/nexus/content/groups/public</url>
  </mirror>

</mirrors>

cd zkui/

mvn clean install

# 修改配置文件默认值
vim config.cfg
   serverPort=9090     #指定端口
   zkServer=localhost:2181,localhost:2182,localhost:2183 # 不使用127.0.0.1
   sessionTimeout=300

   # userSet中是登陆web界面的用户名和密码
#管理员
#admin:manager
#用户
#appconfig:appconfig

# 启动程序至后台
vim start.sh
#!/bin/bash

nohup java -jar target/zkui-2.0-SNAPSHOT-jar-with-dependencies.jar &

# 浏览器访问
# 防火墙放行9090端口,后期改用nginx代理
http://192.168.0.187:9090/
5. Kafka
# kafka集群,伪集群
cd /usr/local/src
wget http://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.3.0/kafka_2.12-2.3.0.tgz
tar -zxv -f kafka_2.12-2.3.0.tgz -C /usr/local/
cd /usr/local/kafka_2.12-2.3.0/config
mkdir -p /kafkadata/{kafka-1,kafka-2,kafka-3}
cp server.properties server-1.properties
vim server-1.properties
broker.id=1
delete.topic.enable=true
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://localhost:9092
log.dirs=/kafkadata/kafka-1
zookeeper.connect=localhost:2181,localhost:2182,localhost:2183

cp server-1.properties server-2.properties
vim server-2.properties
broker.id=2
delete.topic.enable=true
listeners=PLAINTEXT://:9093
log.dirs=/kafkadata/kafka-2
zookeeper.connect=localhost:2181,localhost:2182,localhost:2183

cp server-1.properties server-3.properties
vim server-3.properties
broker.id=3
delete.topic.enable=true
listeners=PLAINTEXT://:9094
log.dirs=/kafkadata/kafka-3
zookeeper.connect=localhost:2181,localhost:2182,localhost:2183

# 启动集群
vim start.sh
#!/bin/bash

bash bin/kafka-server-start.sh -daemon config/server-1.properties
bash bin/kafka-server-start.sh -daemon config/server-2.properties
bash bin/kafka-server-start.sh -daemon config/server-3.properties

# 停止集群
vim stop.sh
#!/bin/bash

bash bin/kafka-server-stop.sh -daemon config/server-1.properties
bash bin/kafka-server-stop.sh -daemon config/server-2.properties
bash bin/kafka-server-stop.sh -daemon config/server-3.properties
# 监控kafka集群
# 有一个问题,需要在kafka-server-start.sh文件中配置端口,有如下三种办法
# 第一种:复制并修改kafka目录,比如kafka-1,kafka-2,kafka-3,然后再每个目录下修改kafka-server-start.sh文件
# 第二种:在启动脚本start.sh中添加指定端口
# 第三种:多复制几个kafka-server-start.sh文件,然后进行修改,最后在start.sh中修改一下

# 以下三种方法任选其一即可

# 第一种方式办法,相应行修改成如下形式,注意端口号不同
# 使用的是不同目录下的不同kafka-server-start.sh文件
# start.sh文件也需要做相应的修改
# kafka-1/bin/kafka-server-start.sh
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
  # export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
   export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
   export JMX_PORT="9997"
fi
# kafka-2/bin/kafka-server-start.sh
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
  # export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
   export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
   export JMX_PORT="9998"
fi
# kafka-3/bin/kafka-server-start.sh
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
  # export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
   export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
   export JMX_PORT="9999"
fi

# start.sh
#!/bin/bash
bash kafka-1/bin/kafka-server-start.sh -daemon config/server-1.properties
bash kafka-2/bin/kafka-server-start.sh -daemon config/server-2.properties
bash kafka-3/bin/kafka-server-start.sh -daemon config/server-3.properties

# 第二种方法
# 使用的用一个目录下的同一个文件,只是在每个命令前指定端口号
vim start.sh
#!/bin/bash

JMX_PORT=9997 bash bin/kafka-server-start.sh -daemon config/server-1.properties
JMX_PORT=9998 bash bin/kafka-server-start.sh -daemon config/server-2.properties
JMX_PORT=9999 bash bin/kafka-server-start.sh -daemon config/server-3.properties

# 第三种方法
# 使用的是同一个目录下的不同kafka-server-start文件
# start.sh文件也需要做相应的修改
cp kafka-server-start.sh kafka-server-start-1.sh
cp kafka-server-start.sh kafka-server-start-2.sh
cp kafka-server-start.sh kafka-server-start-3.sh

vim kafka-server-start-1.sh
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
  # export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
   export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
   export JMX_PORT="9997"
fi
vim kafka-server-start-2.sh
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
  # export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
   export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
   export JMX_PORT="9998"
fi
vim kafka-server-start-3.sh
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
  # export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
   export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
   export JMX_PORT="9999"
fi

vim start.sh
#!/bin/bash

bash bin/kafka-server-start-1.sh -daemon config/server-1.properties
bash bin/kafka-server-start-2.sh -daemon config/server-2.properties
bash bin/kafka-server-start-3.sh -daemon config/server-3.properties
cd /usr/local/src
wget https://github.com/smartloli/kafka-eagle-bin/archive/v1.3.9.tar.gz

# 多次解压缩后得到kafka-eagle-web-1.3.9目录,然后把该目录复制到/usr/local目录下

cd /usr/local/kafka-eagle-web-1.3.9/conf
vim system-config.properties
kafka.eagle.zk.cluster.alias=cluster1
cluster1.zk.list=localhost:2181,localhost:2182,localhost:2183
kafka.eagle.metrics.charts=true
# 其余保持默认,数据库使用sqlite,注意路径需要事先创建好或修改成当前目录
# 数据库也可以更换成MySQL
kafka.eagle.url=jdbc:sqlite:/usr/local/kafka-eagle-web-1.3.9/db/ke.db

# 注意
# kafka.eagle.zk.cluster.alias的值需要跟下面的这个cluster1.zk.list小数点第一个保持一致,比如都是cluster1,否则获取不到数据

# 添加环境变量
vim /etc/profile.d/kafka_eagle.sh
#!/bin/bash

export KE_HOME=/usr/local/kafka-eagle-web-1.3.9
export PATH=$PATH:$KE_HOME/bin
source /etc/profile.d/kafka_eagle.sh


# 命令相关
bash bin/ke.sh start|stop|status|stats|restart

# 启动
bash bin/ke.sh start
*******************************************************************
* Kafka Eagle system monitor port successful...
*******************************************************************
[2019-09-20 12:10:32] INFO: Status Code[0]
[2019-09-20 12:10:32] INFO: [Job done!]
Welcome to
  __ __   ___     ____   __ __   ___           ______   ___   ______   __     ______
  / //_/   /   |   / __/   / //_/   /   |         / ____/   /   | / ____/   / /   / ____/
/ ,<     / /| | / /_   / ,<     / /| |         / __/     / /| | / / __   / /   / __/  
/ /| |   / ___ | / __/   / /| |   / ___ |       / /___   / ___ |/ /_/ /   / /___ / /___  
/_/ |_| /_/ |_|/_/     /_/ |_| /_/ |_|       /_____/   /_/ |_|\____/   /_____//_____/  
                                                                                           

Version 1.3.9
*******************************************************************
* Kafka Eagle Service has started success.
* Welcome, Now you can visit 'http://127.0.0.1:8048/ke'
* Account:admin ,Password:123456
*******************************************************************
* <Usage> ke.sh [start|status|stop|restart|stats] </Usage>
* <Usage> https://www.kafka-eagle.org/ </Usage>
*******************************************************************

# 浏览器访问,防火墙放行该端口,后期改用Nginx代理
地址:192.168.0.187:8048/ke
账号:admin,密码:123456
6. 投递数据到Kafka
# 先进行canal配置,改动配置文件canal.properties
# serverMode改为kafka
vim conf/canal.properties
canal.serverMode = kafka
canal.mq.servers = localhost:9092,localhost:9093,localhost:9094

vim conf/example/instance.propties
# mq config
canal.mq.topic=canal_manager # 填写数据库库名,表示这个数据库的所有表的操作都在这个topic下
# dynamic topic route by schema or table regex
# canal.mq.dynamicTopic=.*\\..*
canal.mq.partition=0
# hash partition config
# canal.mq.partitionsNum=10
# canal.mq.partitionHash=.*\\..*

# 以上具体规则详看官方文档

# kafka开启消息队列的自动创建topic模式,相关配置在kafka的server.properties
echo 'auto.create.topics.enable=true' >> config/server-1.properties
echo 'auto.create.topics.enable=true' >> config/server-2.properties
echo 'auto.create.topics.enable=true' >> config/server-3.properties

# 相关改动完成后重启canal和kafka

# 使用canal_admin平台查看canal的状态
# Server管理,操作,日志

# 使用zu ui平台查看kafka的topic情况
# 左侧导航Topic-List查看生成的topic,这里显示的是canal_manager,上面设置的那个数据库库名
# 点开Topic Name可以查看具体的数据个数

# 使用命令行kafka-console-consumer.sh --topic canal_manager --bootstrap-server localhost:9092 --from-beginning查看canal传递给kafka的数据
# 插入一条数据
{"data":[{"id":"13","username":"13","password":"6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9","name":"Canal Manager","roles":"admin","introduction":null,"avatar":null,"creation_date":"2019-07-14 00:05:28"}],"database":"canal_manager","es":1568972329000,"id":10,"isDdl":false,"mysqlType":{"id":"bigint(20)","username":"varchar(31)","password":"varchar(128)","name":"varchar(31)","roles":"varchar(31)","introduction":"varchar(255)","avatar":"varchar(255)","creation_date":"timestamp"},"old":null,"pkNames":["id"],"sql":"","sqlType":{"id":-5,"username":12,"password":12,"name":12,"roles":12,"introduction":12,"avatar":12,"creation_date":93},"table":"canal_user","ts":1568972329456,"type":"INSERT"}
# 删除一条数据
{"data":[{"id":"13","username":"13","password":"6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9","name":"Canal Manager","roles":"admin","introduction":null,"avatar":null,"creation_date":"2019-07-14 00:05:28"}],"database":"canal_manager","es":1568972368000,"id":11,"isDdl":false,"mysqlType":{"id":"bigint(20)","username":"varchar(31)","password":"varchar(128)","name":"varchar(31)","roles":"varchar(31)","introduction":"varchar(255)","avatar":"varchar(255)","creation_date":"timestamp"},"old":null,"pkNames":["id"],"sql":"","sqlType":{"id":-5,"username":12,"password":12,"name":12,"roles":12,"introduction":12,"avatar":12,"creation_date":93},"table":"canal_user","ts":1568972369005,"type":"DELETE"}

后续增加使用logstash从Kafka中拉取数据传输到elastic中且指定索引

第三章、canal面试

一、canal日志复制

  1. master将改变记录到二进制日志(binary log)中;

  2. slave将master的binary log events拷贝到它的中继日志(relay log);

  3. slave重做中继日志中的事件,将改变反映它自己的数据。

img

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议

  2. mysql master收到dump请求,开始推送binary log给slave(也就是canal)

  3. canal解析binary log对象(原始为byte流)

img

基于日志增量订阅&消费支持的业务:

  1. 数据库镜像

  2. 数据库实时备份

  3. 多级索引 (卖家和买家各自分库索引)

  4. search build

  5. 业务cache刷新

  6. 价格变化等重要业务消息

二、canal介绍

名称:canal [kə'næl]

译意: 水道/管道/沟渠

语言: 纯java开发

定位: 基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了mysql

关键词: mysql binlog parser / real-time / queue&topic

三、工作原理

1、mysql主备复制实现

img

从上层来看,复制分成三步:

  1. master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看);

  2. slave将master的binary log events拷贝到它的中继日志(relay log);

  3. slave重做中继日志中的事件,将改变反映它自己的数据。

2、canal的工作原理:

img

原理相对比较简单:

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议

  2. mysql master收到dump请求,开始推送binary log给slave(也就是canal)

  3. canal解析binary log对象(原始为byte流)

四、架构

img

说明:

  • server代表一个canal运行实例,对应于一个jvm

  • instance对应于一个数据队列 (1个server对应1..n个instance)

instance模块:

  • eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)

  • eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)

  • eventStore (数据存储)

  • metaManager (增量订阅&消费信息管理器)

五、知识科普

mysql的Binlay Log介绍

简单点说:

  • mysql的binlog是多文件存储,定位一个LogEvent需要通过binlog filename + binlog position,进行定位

  • mysql的binlog数据格式,按照生成的方式,主要分为:statement-based、row-based、mixed。

 

mysql> show variables like 'binlog_format';
   +---------------+-------+
   | Variable_name | Value |
   +---------------+-------+
   | binlog_format | ROW   |
   +---------------+-------+
   1 row in set (0.00 sec)

目前canal支持所有模式的增量订阅(但配合同步时,因为statement只有sql,没有数据,无法获取原始的变更日志,所以一般建议为ROW模式)

六、EventParser设计

大致过程:

img

整个parser过程大致可分为几步:

  1. Connection获取上一次解析成功的位置 (如果第一次启动,则获取初始指定的位置或者是当前数据库的binlog位点)

  2. Connection建立链接,发送BINLOG_DUMP指令 // 0. write command number // 1. write 4 bytes bin-log position to start at // 2. write 2 bytes bin-log flags // 3. write 4 bytes server id of the slave // 4. write bin-log file name

  3. Mysql开始推送Binaly Log

  4. 接收到的Binaly Log的通过Binlog parser进行协议解析,补充一些特定信息 // 补充字段名字,字段类型,主键信息,unsigned类型处理

  5. 传递给EventSink模块进行数据存储,是一个阻塞操作,直到存储成功

  6. 存储成功后,定时记录Binaly Log位置

七、EventSink设计

img

说明:

  • 数据过滤:支持通配符的过滤模式,表名,字段内容等

  • 数据路由/分发:解决1:n (1个parser对应多个store的模式)

  • 数据归并:解决n:1 (多个parser对应1个store)

  • 数据加工:在进入store之前进行额外的处理,比如join

数据1:n业务

为了合理的利用数据库资源, 一般常见的业务都是按照schema进行隔离,然后在mysql上层或者dao这一层面上,进行一个数据源路由,屏蔽数据库物理位置对开发的影响,阿里系主要是通过cobar/tddl来解决数据源路由问题。

所以,一般一个数据库实例上,会部署多个schema,每个schema会有由1个或者多个业务方关注

数据n:1业务

同样,当一个业务的数据规模达到一定的量级后,必然会涉及到水平拆分和垂直拆分的问题,针对这些拆分的数据需要处理时,就需要链接多个store进行处理,消费的位点就会变成多份,而且数据消费的进度无法得到尽可能有序的保证。

所以,在一定业务场景下,需要将拆分后的增量数据进行归并处理,比如按照时间戳/全局id进行排序归并.

八、EventStore设计

  • \1. 目前仅实现了Memory内存模式,后续计划增加本地file存储,mixed混合模式

  • \2. 借鉴了Disruptor的RingBuffer的实现思路

RingBuffer设计:

img

定义了3个cursor

  • Put : Sink模块进行数据存储的最后一次写入位置

  • Get : 数据订阅获取的最后一次提取位置

  • Ack : 数据消费成功的最后一次消费位置

借鉴Disruptor的RingBuffer的实现,将RingBuffer拉直来看:

 

img

image.png

实现说明:

  • Put/Get/Ack cursor用于递增,采用long型存储

  • buffer的get操作,通过取余或者与操作。(与操作: cusor & (size - 1) , size需要为2的指数,效率比较高)

九、Instance设计

img

instance代表了一个实际运行的数据队列,包括了EventPaser,EventSink,EventStore等组件。

抽象了CanalInstanceGenerator,主要是考虑配置的管理方式:

  • manager方式: 和你自己的内部web console/manager系统进行对接。(目前主要是公司内部使用)

  • spring方式:基于spring xml + properties进行定义,构建spring配置.

十、Server设计

img

server代表了一个canal的运行实例,为了方便组件化使用,特意抽象了Embeded(嵌入式) / Netty(网络访问)的两种实现

  • Embeded : 对latency和可用性都有比较高的要求,自己又能hold住分布式的相关技术(比如failover)

  • Netty : 基于netty封装了一层网络协议,由canal server保证其可用性,采用的pull模型,当然latency会稍微打点折扣,不过这个也视情况而定。(阿里系的notify和metaq,典型的push/pull模型,目前也逐步的在向pull模型靠拢,push在数据量大的时候会有一些问题)

十一、增量订阅/消费设计

img

具体的协议格式,可参见:CanalProtocol.proto

get/ack/rollback协议介绍:

  • Message getWithoutAck(int batchSize),允许指定batchSize,一次可以获取多条,每次返回的对象为Message,包含的内容为: a. batch id 唯一标识 b. entries 具体的数据对象,对应的数据对象格式:EntryProtocol.proto

  • void rollback(long batchId),顾命思议,回滚上次的get请求,重新获取数据。基于get获取的batchId进行提交,避免误操作

  • void ack(long batchId),顾命思议,确认已经消费成功,通知server删除数据。基于get获取的batchId进行提交,避免误操作

canal的get/ack/rollback协议和常规的jms协议有所不同,允许get/ack异步处理,比如可以连续调用get多次,后续异步按顺序提交ack/rollback,项目中称之为流式api.

流式api设计的好处:

  • get/ack异步化,减少因ack带来的网络延迟和操作成本 (99%的状态都是处于正常状态,异常的rollback属于个别情况,没必要为个别的case牺牲整个性能)

  • get获取数据后,业务消费存在瓶颈或者需要多进程/多线程消费时,可以不停的轮询get数据,不停的往后发送任务,提高并行化. (作者在实际业务中的一个case:业务数据消费需要跨中美网络,所以一次操作基本在200ms以上,为了减少延迟,所以需要实施并行化)

流式api设计:

img

  • 每次get操作都会在meta中产生一个mark,mark标记会递增,保证运行过程中mark的唯一性

  • 每次的get操作,都会在上一次的mark操作记录的cursor继续往后取,如果mark不存在,则在last ack cursor继续往后取

  • 进行ack时,需要按照mark的顺序进行数序ack,不能跳跃ack. ack会删除当前的mark标记,并将对应的mark位置更新为last ack cusor

  • 一旦出现异常情况,客户端可发起rollback情况,重新置位:删除所有的mark, 清理get请求位置,下次请求会从last ack cursor继续往后取

十二、数据对象格式:EntryProtocol.proto

 

Entry
   Header
       logfileName [binlog文件名]
       logfileOffset [binlog position]
       executeTime [binlog里记录变更发生的时间戳]
       schemaName [数据库实例]
       tableName [表名]
       eventType [insert/update/delete类型]
   entryType   [事务头BEGIN/事务尾END/数据ROWDATA]
   storeValue [byte数据,可展开,对应的类型为RowChange]

RowChange
isDdl       [是否是ddl变更操作,比如create table/drop table]
sql     [具体的ddl sql]
rowDatas   [具体insert/update/delete的变更数据,可为多条,1个binlog event事件可对应多条变更,比如批处理]
beforeColumns [Column类型的数组]
afterColumns [Column类型的数组]

Column
index       [column序号]
sqlType     [jdbc type]
name       [column name]
isKey       [是否为主键]
updated     [是否发生过变更]
isNull     [值是否为null]
value       [具体的内容,注意为文本]

说明:

  • 可以提供数据库变更前和变更后的字段内容,针对binlog中没有的name,isKey等信息进行补全

  • 可以提供ddl的变更语句

十三、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生命周期绑定),可以看下我之前zookeeper的相关文章。

Canal Server:

img

大致步骤:

  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节点的方式进行控制.

 posted on 2020-05-12 09:55  大码王  阅读(1319)  评论(0编辑  收藏  举报
复制代码