数据异构重器之 Canal 初探

本文涉及的产品
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
RDS AI 助手,专业版
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: 数据异构重器之 Canal 初探

1、应用场景


提到 Canal,大家应该都能想到这是一个用于解析 MySQL binlog 日志的工具,并将 MySQL 数据库中数据同步到其他存储介质中,例如 Elasticsearch。


即 Canal 一个非常常用的使用场景:数据异构,一种更高级别的数据读写分离架构设计方法。


随着业务不断的发展,企业发展到一定阶段,发现单体的关系型数据库已无法支撑业务高速发展带来数据不断累积的压力,从而会诞生出一种设计架构:分库分表。分库分表对缓解单库数据库压力确实是一种非常好的解决方案,但又衍生出另外一种困境,关联查询不友好,甚至跨库JOIN就更加如此。


举例说明如下:例如一个订单系统,通常有两类用户需要去查询订单,一类是顾客,一类是商家,在对数据库进行分库分表时,如果以顾客(buy_id)进行分库的话,同一个商家的订单数据会分布在不同的库中,如果以商家(shop_id)进行分库的话,同一个用户购买的所有订单数据将会分布在不同的库中,这样进行关联查询,就必然需要跨库进行join,其成本都会偏高。而且上面的场景只能满足一方的需求,那如何是好呢?


Canal 这个时候就闪亮登场了,在电商设计中,其实商家、顾客会被拆分成两个不同的服务,我们可以为两个不同的服务搭建不同的数据库集群,我们可以用户订单库、商家订单库进行分库,以用户订单库为主库,当用户在订单系统下单后,数据进入到用户订单库中,然后可以通过 canal 监听数据库的binlog日志,然后将数据再同步到商家订单库,而用户订单库以用户ID为维度进行分库,商家订单库以商家ID做分库,完美解决问题。


2、架构设计原理


在了解到 Canal 的基本使用场景后,我们通过 canal 官方文档,去探究一下其核心架构设计理念,以此打开进入 Canal 的神秘世界中。


首先我们简单看一下 MySQL 的主从同步原理:

06f75a37fa9e654784cc9915914c2972.png

从上面的图中可以看成主从复制主要分成三个步骤:


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


基于 MySQL 这种数据同步机制,那 Canal 的设计目标主要就是实现数据的同步,即数据的复制,从上面的图自然而然的想到了如下的设计:

0b83a82152bbdc51f1fddc419b9c762b.png

原理相对比较简单:


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


接下来我们来看一下 Canale 的整体组成部分:

804af27ff55559d40856874dff9a3479.png

说明:


  • server代表一个canal运行实例,对应于一个jvm
  • instance对应于一个数据队列 (1个server对应1..n个instance)


instance模块:


  • eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
  • eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
  • eventStore (数据存储)
  • metaManager (增量订阅&消费信息管理器)

这些组件我暂时不打算深入去研究,因为在目前这个阶段我自己也不清楚,但这个是我后续需要学习研究的重点。

3、在 IntelliJ IDEA 中运行 Demo


在 Linux 环境中安装 canal 比较简单,大家可以按照官方手册一步一步操作即可,在这里我就不重复介绍,本节主要的目的是希望在开发工具中运行 Canal 的 Demo,以便后续在研究源码的过程中遇到难题时可以进行 Debug。


温馨提示:大家在学习过程中,可以根据官方文档先安装一遍 canal,对理解 Canal 的核心组件有着非常重要的帮助。


首先先从 canal 源码中寻找官方提供的 Demo,其示例代码在 example 包中,如下图所示:

aade1e92eef039bbc592fd65b893ce2a.png


但是另外稍微遗憾的是 canal 提供提供的示例代码中只包含了 client 端相关的代码,并没有包含服务端(server),故我们将目光放到其单元测试中,如下图所示:

3e3ec722b702786c6277f40767727257.png

下来我根据官方的一些提示,结合自己的理解,编写出如下测试代码,在 IDEA 开发工具中实现运行 Canal 相关的 Demo。下面的代码已通过测试,可直接使用。


1、Canal Server Demo


package com.alibaba.otter.canal.server;
import com.alibaba.otter.canal.instance.core.CanalInstance;
import com.alibaba.otter.canal.instance.core.CanalInstanceGenerator;
import com.alibaba.otter.canal.instance.manager.CanalInstanceWithManager;
import com.alibaba.otter.canal.instance.manager.model.Canal;
import com.alibaba.otter.canal.instance.manager.model.CanalParameter;
import com.alibaba.otter.canal.server.embedded.CanalServerWithEmbedded;
import com.alibaba.otter.canal.server.netty.CanalServerWithNetty;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class CanalServerTestMain {
    protected static final String ZK_CLUSTER_ADDRESS      = "127.0.0.1:2181";
    protected static final String DESTINATION   = "example";
    protected static final String DETECTING_SQL = "select 1";
    protected static final String MYSQL_ADDRESS = "127.0.0.1";
    protected static final String USERNAME      = "canal";
    protected static final String PASSWORD      = "canal";
    protected static final String FILTER        = ".\\*\\\\\\\\..\\*";
    /** 默认 500s 后关闭 */
    protected static final long RUN_TIME = 120 * 1000;
    private final ByteBuffer header        = ByteBuffer.allocate(4);
    private CanalServerWithNetty nettyServer;
    public static void main(String[] args) {
        CanalServerTestMain test = new CanalServerTestMain();
        try {
            test.setUp();
            System.out.println("start");
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println("sleep");
            try {
                Thread.sleep(RUN_TIME);
            } catch (Throwable ee) {
            }
            test.tearDown();
            System.out.println("end");
        }
    }
    public void setUp() {
        CanalServerWithEmbedded embeddedServer = new CanalServerWithEmbedded();
        embeddedServer.setCanalInstanceGenerator(new CanalInstanceGenerator() {
            public CanalInstance generate(String destination) {
                Canal canal = buildCanal();
                return new CanalInstanceWithManager(canal, FILTER);
            }
        });
        nettyServer = CanalServerWithNetty.instance();
        nettyServer.setEmbeddedServer(embeddedServer);
        nettyServer.setPort(11111);
        nettyServer.start();
        // 启动 instance
        embeddedServer.start("example");
    }
    public void tearDown() {
        nettyServer.stop();
    }
    private Canal buildCanal() {
        Canal canal = new Canal();
        canal.setId(1L);
        canal.setName(DESTINATION);
        canal.setDesc("test");
        CanalParameter parameter = new CanalParameter();
        //parameter.setZkClusters(Arrays.asList(ZK_CLUSTER_ADDRESS));
        parameter.setMetaMode(CanalParameter.MetaMode.MEMORY);
        parameter.setHaMode(CanalParameter.HAMode.HEARTBEAT);
        parameter.setIndexMode(CanalParameter.IndexMode.MEMORY);
        parameter.setStorageMode(CanalParameter.StorageMode.MEMORY);
        parameter.setMemoryStorageBufferSize(32 * 1024);
        parameter.setSourcingType(CanalParameter.SourcingType.MYSQL);
        parameter.setDbAddresses(Arrays.asList(new InetSocketAddress(MYSQL_ADDRESS, 3306),
                new InetSocketAddress(MYSQL_ADDRESS, 3306)));
        parameter.setDbUsername(USERNAME);
        parameter.setDbPassword(PASSWORD);
        parameter.setSlaveId(1234L);
        parameter.setDefaultConnectionTimeoutInSeconds(30);
        parameter.setConnectionCharset("UTF-8");
        parameter.setConnectionCharsetNumber((byte) 33);
        parameter.setReceiveBufferSize(8 * 1024);
        parameter.setSendBufferSize(8 * 1024);
        parameter.setDetectingEnable(false);
        parameter.setDetectingIntervalInSeconds(10);
        parameter.setDetectingRetryTimes(3);
        parameter.setDetectingSQL(DETECTING_SQL);
        canal.setCanalParameter(parameter);
        return canal;
    }
}

2、Canal Client Demo

package com.alibaba.otter.canal.example;
import java.net.InetSocketAddress;
import java.util.List;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.common.utils.AddressUtils;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
public class SimpleCanalClientExample {
    public static void main(String[] args) {
        // 创建链接
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),
                11111), "example", "", "");
        int batchSize = 1000;
        int emptyCount = 0;
        try {
            connector.connect();
            connector.subscribe(".*\\..*");
            connector.rollback();
            int totalEmptyCount = 3000;
            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;
                    // System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);
                    printEntry(message.getEntries());
                }
                connector.ack(batchId); // 提交确认
                // connector.rollback(batchId); // 处理失败, 回滚数据
            }
            System.out.println("empty too many times, exit");
        } finally {
            connector.disconnect();
        }
    }
    private static void printEntry(List<CanalEntry.Entry> entrys) {
        for (CanalEntry.Entry entry : entrys) {
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }
            CanalEntry.RowChange rowChage = null;
            try {
                rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
                        e);
            }
            CanalEntry.EventType eventType = rowChage.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 (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == CanalEntry.EventType.DELETE) {
                    printColumn(rowData.getBeforeColumnsList());
                } else if (eventType == EventType.INSERT) {
                    printColumn(rowData.getAfterColumnsList());
                } else {
                    System.out.println("-------> before");
                    printColumn(rowData.getBeforeColumnsList());
                    System.out.println("-------> after");
                    printColumn(rowData.getAfterColumnsList());
                }
            }
        }
    }
    private static void printColumn(List<Column> columns) {
        for (Column column : columns) {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
        }
    }
}

运行 client 的效果如下图所示:


4ae3e6c55049f8960555f6d1808c3b18.png

在数据库中变更一条数据,以便产生新的binlog日志,其输出结果如下:

530fe710e7d0f0a37e632685c2fb0449.png

能在 IDEA 中搭建并运行 Demo,是我们踏入 canal 的第一步,后续将根据官方文档中的内容为提纲,尝试逐步解开 canal 的实现原理,以便更好的指导实践。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
canal 消息中间件 关系型数据库
系统重构数据同步利器之Canal实战篇
系统重构数据同步利器之Canal实战篇
1502 1
|
JSON 前端开发 JavaScript
layui介绍及入门基础(看我这篇就会了!!!)
layui介绍及入门基础(看我这篇就会了!!!)
780 0
|
Java 数据库
Apollo公共类型的Namespace配置需要进行关联才能用吗?
其实,这个问题在强哥看来应该有稍微深入了解Apollo的使用者应该都能回答的上来。 不过也是前些天,刚好看到公司新来的实习生在找Apollo配置时抓耳挠腮,了解情况之后,我发现,原来这个问题确实有许多人容易弄混。所以,强哥就在这里也简单科普一下。
Apollo公共类型的Namespace配置需要进行关联才能用吗?
|
2月前
|
存储 弹性计算 人工智能
2026年最新阿里云无影云电脑收费价格:企业版、个人版及免费版配置收费清单
阿里云无影云电脑2026年升级配置与计费体系,覆盖企业版、个人版及免费试用。企业版分四类,支持灵活时长包与高性能图形工作站;个人版推五档核时套餐,适配办公与游戏;新用户可享免费试用。按需选择,降本增效。
|
5月前
|
canal 关系型数据库 MySQL
数据同步神器-Canal
Canal是阿里巴巴开源的MySQL增量日志解析工具,通过模拟MySQL主从复制机制,实时捕获数据库变更,实现数据同步至Kafka、Elasticsearch等系统,广泛应用于数据同步、监控、备份与迁移场景。
3538 5
|
消息中间件 Java 测试技术
Java中的软件架构重构与升级策略
Java中的软件架构重构与升级策略
淘宝API接口( item_detail - 淘宝商品详情查询)
淘宝商品详情查询 API(item_detail)用于获取淘宝商品的详细信息。请求参数包括商品唯一 ID(num_iid)和是否获取促销价(is_promotion)。响应参数包含商品标题、价格、库存、图片链接、品牌等详细信息。
|
Java API Maven
使用 Smart-doc 记录 Spring REST API
使用 Smart-doc 记录 Spring REST API
539 0
|
关系型数据库 MySQL Linux
在CentOs7虚拟机Linux离线安装mysql5.6(亲测可用)
该博客文章详细记录了在CentOS 7虚拟机上离线安装MySQL 5.6版本的完整过程,包括下载安装包、导入虚拟机、卸载MariaDB、配置文件设置、服务启动和权限配置等步骤。
在CentOs7虚拟机Linux离线安装mysql5.6(亲测可用)
|
存储 消息中间件 JSON
DDD基础教程:一文带你读懂DDD分层架构
DDD基础教程:一文带你读懂DDD分层架构