实时数仓项目架构 Canal
学习目标
-
理解实时数仓项目的基本需求、整体架构。
-
了解常用实施方案。
-
能够编写Canal客户端采集binlog消息。
-
理解google ProtoBuf序列化方式。
-
理解Canal采集原理。
实时计算应用场景及技术选型
实时计算在公司的用处
公司内已经采用MR与spark之类的技术,做离线计算,为什么用实时计算?
-
离线的伤痛就是数据出的太慢
-
有对实时数据要求高的场景
-
比如:滴滴的风控、淘宝双十一营销大屏、电商购物推荐、春晚的观众数统计
-
实时计算技术选型
Spark streaming、 Structured streaming、Storm、JStorm(阿里)、Kafka Streaming、Flink技术栈这么多,到底选哪个?
-
公司员工的技术基础
-
流行
-
技术复用
-
场景
如果对延迟要求不高的情况下,可以使用 Spark Streaming,它拥有丰富的高级 API,使用简单,并且 Spark 生态也比较成熟,吞吐量大,部署简单,社区活跃度较高,从 GitHub 的 star 数量也可以看得出来现在公司用 Spark 还是居多的,并且在新版本还引入了 Structured Streaming,这也会让 Spark 的体系更加完善。
如果对延迟性要求非常高的话,可以使用当下最火的流处理框架 Flink,采用原生的流处理系统,保证了低延迟性,在 API 和容错性方面做的也比较完善,使用和部署相对来说也是比较简单的,加上国内阿里贡献的 Blink,相信接下来 Flink 的功能将会更加完善,发展也会更加好,社区问题的响应速度也是非常快的,另外还有专门的钉钉大群和中文列表供大家提问,每周还会有专家进行直播讲解和答疑。
本次项目:使用Flink来搭建实时计算平台
项目实施环境
数据
-
目前已经存在订单数据,业务系统会将订单写入到mysql
-
流量日志数据(访问日志)
硬件
-
4台物理服务器
-
服务配置
-
CPU x 2:志强E5 主频2.8 - 3.6,24核(12核 per CPU)
-
内存(768GB/1T)
-
硬盘(1T x 8 SAS盘)
-
网卡(4口 2000M)
-
人员
-
4人
-
前端(2人 JavaWeb + UI前端)
-
大数据(2人)
-
java开发(5人)
-
时间
-
一年左右
-
阶段
-
需求调研、评审(3周)
-
设计架构(2周)
-
编码、集成(2周)
-
测试、上线(1周)
-
需求分析
项目需求
-
目前已经有前端可视化项目,公司需要大屏用于展示订单数据与用户访问数据
数据来源
PV/UV数据来源
-
来自于页面埋点数据,将用户访问数据发送到web服务器
-
web服务器直接将该部分数据写入到kafka的click_log 的topic 中
销售金额与订单量数据来源
-
订单数据来源于mysql
-
订单数据来自binlog日志,通过canal 实时将数据写入到kafka的order的topic中
购物车数据和评论数据
-
购物车数据一般不会直接操作mysql,通过客户端程序写入到kafka(消息队列)中
-
评论数据也是通过客户端程序写入kafka(消息队列)中
常见的软件工程模型
瀑布模型
-
在一些银行、政府、等传统行业系统中,该模式应用较多
特点
-
上一项开发活动其成果作为本次活动的输入
-
给出本次活动的工作成果,作为输出传给下一项开发活动
-
对本次活动的实施工作成果进行评审
-
评审通过,则继续进行下一项开发活动
-
评审不通过,则返回前一项,甚至更前项的活动
-
使用范围
-
用户需求很清楚,在开发过程中变化较少
-
开发人员对业务很熟悉
-
用户的使用环境较稳定;
-
开发工作对用户参与的要求很低。
优点
-
人员职责明确、具体,有利于大型软件开发过程中人员的组织、管理
-
实施步骤清晰、有序,有利于软件开发方法和工具的研究,保障大型项目质量/效率
缺点
-
开发过程一般不能逆转,否则代价太大
-
实际的项目开发很难严格按该模型进行
-
客户很难清楚地给出所有的需求
-
软件实际情况必须到项目开发的后期才能看到,这要求客户有足够的耐心
敏捷开发
介绍
-
以用户的需求进化为核心,采用迭代、循序渐进的方法进行软件开发
-
把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成
-
在开发过程中软件一直处于可使用状态
优点
-
敏捷确实是项目进入实质开发迭代阶段,用户很快可以看到一个基线架构版的产品
-
注重市场快速反应能力,也即具体应对能力,客户前期满意度高
缺点
-
敏捷注重人员的沟通,忽略文档的重要性,若项目人员流动大太,又给维护带来不少难度,特别项目存在新手比较多时,老员工比较累
-
需要项目中存在经验较强的人,要不大项目中容易遇到瓶颈问题。
实现方案
JAVA 方式实现
-
一些中小企业当中,由于数据量较小(比如核心总量小于20万条),可通过Java程序定时查询mysql实现
-
比较简单,但是粗暴实用
-
仅仅需要对mysql做一些优化即可,比较增加索引
通过flink方案实现
-
数据量特别大、无法直接通过mysql查询完成,有时候根本查询不动
-
要求实时性高,比如阿里巴巴双十一监控大屏,要求延迟不超过1秒
实时数仓项目架构
Canal介绍
简介
-
基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费.
-
早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger(触发器) 获取增量变更
-
从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务,基于日志增量订阅和消费的业务包括
-
数据库镜像
-
数据库实时备份
-
索引构建和实时维护(拆分异构索引、倒排索引等)
-
业务 cache 刷新
-
带业务逻辑的增量数据处理
-
-
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
-
github地址:https://github.com/alibaba/canal
环境部署
MySQL
-
MySQL需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,/etc/my.cnf 中配置如下
-
二进制日志 binlog :
MySQL 的二进制日志 binlog 可以说是 MySQL 最重要的日志,它记录了所有的 DDL 和 DML 语句(除了数
据查询语句select、show等),以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是
事务安全型的。binlog 的主要目的是复制和恢复。
Binlog日志的两个最重要的使用场景
MySQL主从复制:MySQL Replication在Master端开启binlog,Master把它的二进制日志传递给slaves来达到master-slave数据一致的目的
-
数据恢复:通过使用 mysqlbinlog工具来使恢复数据。
-
环境配置: 需要注意的是配置my.ini文件,该文件位于:C:\ProgramData\MySQL\MySQL Server 5.7
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式,记录的是实际数据的变更
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
-
总结:MySQL二进制日志格式都配置成Row格式,以保证主库的变更能准确地在从库上重放,确保数据安全
以及数据的一致性。
-
授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant
CREATE USER canal IDENTIFIED BY 'canal';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' ;
FLUSH PRIVILEGES;
Canal安装
重要版本更新说明:
-
canal 1.1.x 版本(release_note),性能与功能层面有较大的突破,重要提升包括:
-
整体性能测试&优化,提升了150%. #726 参考: Performance
-
原生支持prometheus监控 #765 Prometheus QuickStart
-
原生支持kafka消息投递 #695 Canal Kafka/RocketMQ QuickStart
-
原生支持aliyun rds的binlog订阅 (解决自动主备切换/oss binlog离线解析) 参考: Aliyun RDS QuickStart
-
原生支持docker镜像 #801 参考: Docker QuickStart
-
-
canal 1.1.4版本,迎来最重要的WebUI能力,引入canal-admin工程,支持面向WebUI的canal动态管理能力,支持配置、任务、日志等在线白屏运维能力,具体文档:Canal Admin Guide
注意:本次学习使用的版本canal1.0.24
环境要求:
-
安装好ZooKeeper
-
解压缩
mkdir /opt/module/canal
tar -zxvf canal.deployer-1.0.24.tar.gz -C /opt/module/canal -
解压完成后,进入 /export/servers/canal/ 目录,可以看到如下结构
drwxr-xr-x. 2 root root 4096 2月 1 14:07 bin
drwxr-xr-x. 4 root root 4096 2月 1 14:07 conf
drwxr-xr-x. 2 root root 4096 2月 1 14:07 lib
drwxrwxrwx. 2 root root 4096 4月 1 2017 logs -
canal server的conf下有几个配置文件
[root@node1 canal]# tree conf/
conf/
├── canal.properties
├── example
│ └── instance.properties
├── logback.xml
└── spring
├── default-instance.xml
├── file-instance.xml
├── group-instance.xml
├── local-instance.xml
└── memory-instance.xml-
先来看
canal.properties
的common属性前四个配置项:
canal.id= 1
canal.ip= hadoop102
canal.port= 11111
canal.zkServers=canal.id是canal的编号,在集群环境下,不同canal的id不同,注意它和mysql的server_id不同。
ip这里不指定,默认为本机,比如上面是192.168.1.120,端口号是11111。zk用于canal cluster。
-
再看下
canal.properties
下destinations相关的配置:#################################################
######### destinations #############
#################################################
canal.destinations = example
canal.conf.dir = ../conf
canal.auto.scan = true
canal.auto.scan.interval = 5
canal.instance.global.mode = spring
canal.instance.global.lazy = false
canal.instance.global.spring.xml = classpath:spring/file-instance.xml
-
##################################################
######### Kafka Kerberos Info #############
##################################################
# 默认值tcp,这里改为投递到Kafka
canal.serverMode = kafka
# Kafka bootstrap.servers,可以不用写上全部的brokers
canal.mq.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
# 投递失败的重试次数,默认0,改为2
canal.mq.retries = 2
# Kafka batch.size,即producer一个微批次的大小,默认16K,这里加倍
canal.mq.batchSize = 32768
# Kafka max.request.size,即一个请求的最大大小,默认1M,这里也加倍
canal.mq.maxRequestSize = 2097152
# Kafka linger.ms,即sender线程在检查微批次是否就绪时的超时,默认0ms,这里改为200ms
# 满足batch.size和linger.ms其中之一,就会发送消息
canal.mq.lingerMs = 200
# Kafka buffer.memory,缓存大小,默认32M
canal.mq.bufferMemory = 33554432
# 获取binlog数据的批次大小,默认50
canal.mq.canalBatchSize = 50
# 获取binlog数据的超时时间,默认200ms
canal.mq.canalGetTimeout = 200
# 是否将binlog转为JSON格式。如果为false,就是原生Protobuf格式
canal.mq.flatMessage = true
# 压缩类型,官方文档没有描述
canal.mq.compressionType = none
# Kafka acks,默认all,表示分区leader会等所有follower同步完才给producer发送ack
# 0表示不等待ack,1表示leader写入完毕之后直接ack
canal.mq.acks = all
# Kafka消息投递是否使用事务
# 主要针对flatMessage的异步发送和动态多topic消息投递进行事务控制来保持和Canal binlog位置的一致性
# flatMessage模式下建议开启
canal.mq.transaction = true
•~~~
这里的canal.destinations = example可以设置多个,比如example1,example2,
则需要创建对应的两个文件夹,并且每个文件夹下都有一个instance.properties文件。
全局的canal实例管理用spring,这里的`file-instance.xml`最终会实例化所有的destinations instances:
-
全局的canal实例管理用spring,这里的
file-instance.xml
最终会实例化所有的destinations instances:
<!-- properties -->
<bean class="com.alibaba.otter.canal.instance.spring.support.PropertyPlaceholderConfigurer" lazy-init="false">
<property name="ignoreResourceNotFound" value="true" />
<property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/><!-- 允许system覆盖 -->
<property name="locationNames">
<list>
<value>classpath:canal.properties</value> <value>classpath:${canal.instance.destination:}/instance.properties</value>
</list>
</property>
</bean>
<bean id="socketAddressEditor" class="com.alibaba.otter.canal.instance.spring.support.SocketAddressEditor" />
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="socketAddressEditor" />
</list>
</property>
</bean>
<bean id="instance" class="com.alibaba.otter.canal.instance.spring.CanalInstanceWithSpring">
<property name="destination" value="${canal.instance.destination}" />
<property name="eventParser">
<ref local="eventParser" />
</property>
<property name="eventSink">
<ref local="eventSink" />
</property>
<property name="eventStore">
<ref local="eventStore" />
</property>
<property name="metaManager">
<ref local="metaManager" />
</property>
<property name="alarmHandler">
<ref local="alarmHandler" />
</property>
</bean>比如
canal.instance.destination
等于example,就会加载example/instance.properties
配置文件 -
修改instance 配置文件
vi conf/example/instance.properties
## mysql serverId,这里的slaveId不能和myql集群中已有的server_id一样
canal.instance.mysql.slaveId = 1234
# 按需修改成自己的数据库信息
#################################################
...
canal.instance.master.address=hadoop102:3306
# username/password,数据库的用户名和密码
...
canal.instance.dbUsername = root
canal.instance.dbPassword = root
################################################# -
启动
[foo@hadoop102 mysql]$ sh bin/startup.sh
[foo@hadoop102 mysql]$ jps
9991 CanalLauncher
10365 Jps -
查看 server 日志
vi logs/canal/canal.log
2013-02-05 22:45:27.967 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## start the canal server.
2013-02-05 22:45:28.113 [main] INFO com.alibaba.otter.canal.deployer.CanalController - ## start the canal server[10.1.29.120:11111]
2013-02-05 22:45:28.210 [main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## the canal server is running now ...... -
查看 instance 的日志
vi logs/example/example.log
2013-02-05 22:50:45.636 [main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [canal.properties]
2013-02-05 22:50:45.641 [main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [example/instance.properties]
2013-02-05 22:50:45.803 [main] INFO c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start CannalInstance for 1-example
2013-02-05 22:50:45.810 [main] INFO c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start successful.... -
关闭
sh bin/stop.sh
Canal客户端开发
创建client_demo项目
Maven依赖
<dependencies>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.0.24</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
</dependencies>
在canal_demo模块创建包结构
包名 | 说明 |
---|---|
com.foo.canal_demo | 代码存放目录 |
开发步骤
-
创建Connector
-
连接Cannal服务器,并订阅
-
解析Canal消息,并打印
Canal消息格式
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
sqlType [jdbc type]
name [column name]
isKey [是否为主键]
updated [是否发生过变更]
isNull [值是否为null]
value [具体的内容,注意为string文本]
参考代码:
public class CanalClientEntrance {
public static void main(String[] args) {
// 1. 创建链接
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.88.120",
11111), "example", "", "");
// 指定一次性获取数据的条数
int batchSize = 5 * 1024;
boolean running = true;
try {
while(running) {
// 2. 建立连接
connector.connect();
// 回滚上次的get请求,重新获取数据
connector.rollback();
// 订阅匹配日志
connector.subscribe("foo_shop.*");
while(running) {
// 批量拉取binlog日志,一次性获取多条数据
Message message = connector.getWithoutAck(batchSize);
// 获取batchId
long batchId = message.getId();
// 获取binlog数据的条数
int size = message.getEntries().size();
if(batchId == -1 || size == 0) {
}
else {
printSummary(message);
}
// 确认指定的batchId已经消费成功
connector.ack(batchId);
}
}
} finally {
// 断开连接
connector.disconnect();
}
}
private static void printSummary(Message message) {
// 遍历整个batch中的每个binlog实体
for (CanalEntry.Entry entry : message.getEntries()) {
// 事务开始
if(entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
continue;
}
// 获取binlog文件名
String logfileName = entry.getHeader().getLogfileName();
// 获取logfile的偏移量
long logfileOffset = entry.getHeader().getLogfileOffset();
// 获取sql语句执行时间戳
long executeTime = entry.getHeader().getExecuteTime();
// 获取数据库名
String schemaName = entry.getHeader().getSchemaName();
// 获取表名
String tableName = entry.getHeader().getTableName();
// 获取事件类型 insert/update/delete
String eventTypeName = entry.getHeader().getEventType().toString().toLowerCase();
System.out.println("logfileName" + ":" + logfileName);
System.out.println("logfileOffset" + ":" +