实时数仓搭建和flink分析Day02
昨日回顾
了解实时数仓的整体背景/涉及技术/业务主线
实时数仓的整体架构图.
使用Canal采集MySQL中的数据变更信息.
使用Java开发Canal客户端.
ProtoBuf序列化
- 编写Protobuf配置文件
- 对protobuf进行编译.
- 使用Protobuf创建的类来封装消息
- 将消息转换为字节数组
- 使用字节数组转换为对象
Canal原理.
依赖于MySQL的主从复制功能.
一个服务端里面可以有多个实例,每个实例中都有自己的EventParser/EventSink/EventStore/MetaManager.
今日课程介绍
Canal高可用: Server端高可用/Client高可用
进入数仓业务开发.
搭建实时数仓的项目环境
开发Canal客户端,负责将MySQL的变更信息通过Protobuf发送给Kafka.
实时ETL工程的项目环境初始化.
Canal的高可用
Canal高可用分为2种:
- 服务端高可用: 可以保证随时从MySQL中获取数据.
- 客户端高可用: 可以保证随时可以将数据发送到Kafka.
Canal的高可用依赖于zookeeper
服务端高可用
修改Canal配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z61rKCRW-1591285941152)(assets/image-20200426095826172.png)]
改过之后,将Canal分发到2节点一份,需要修改实例的slaveID.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwqUQSsA-1591285941155)(assets/image-20200426095926674.png)]
启动Canal
- 启动ZK
- 分别启动node1和node2上面的Canal.
验证:
可以使用zookeeper客户端zkCli.sh获取节点信息:
/export/servers/zookeeper-3.4.9/bin/zkCli.sh
get /otter/canal/destinations/example/running
{"active":true,"address":"192.168.88.120:11111","cid":1}
使用高可用版本的API获取Canal信息:
CanalConnector connector = CanalConnectors.newClusterConnector(
"node1:2181,node2:2181,node3:2181",
"example",
"root",
"123456"
);
可以尝试终止掉目前正在使用的Canal,看zk上面是否能够自动切换到新的canal服务器端.
客户端高可用
客户端的高可用主要依赖于zk,会在zk上注册消费者的节点信息:
![image-20200426102017812](assets/image-20200426102017812.png)
客户端高可用本身不需要进行任何配置,直接去启动2个客户端就可以了.
我们可以在IDEA中,将当前的客户端启动配置直接复制一份.
![image-20200426102752904](assets/image-20200426102752904.png)
启动第二个客户端程序.之后触发MySQL的数据变更,可以看到第一个Canal程序已经消费到了数据,另一个处于等待状态,当第一个程序挂掉后,第二个程序就可以开始进行消费,他们是通过zk中的cursor里面记录的position实现数据的索引记录,继续消费.
![image-20200426102946933](assets/image-20200426102946933.png)
实时数仓项目环境初始化
依赖仓库下载地址(如果遇到依赖下载不了,可以使用下面的仓库.):
链接:https://pan.baidu.com/s/1oRZpVHAZgJKZ8zC26VAxiw
提取码:0w6l
复制这段内容后打开百度网盘手机App,操作更方便哦
创建项目,创建子模块:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-esl1Seik-1591285941157)(assets/image-20200426104738010.png)]
导入依赖:
导入依赖,参考讲义,或者今天的代码.
Canal模块开发
创建包结构:
包名 | 说明 |
---|---|
com.itcast.canal_client | 存放入口、Canal客户端核心实现 |
com.itcast.canal_client.util | 存放工具类 |
com.itcast.canal_client.kafka | 存放Kafka生产者实现 |
导入配置文件到resources文件夹下
在resources文件夹下,创建一个server.propertis配置文件,复制下面的内容到配置文件.
# canal配置
canal.server.ip=node1
canal.server.port=11111
canal.server.destination=example
canal.server.username=root
canal.server.password=123456
canal.subscribe.filter=itcast_shop.*
# zookeeper配置
zookeeper.server.ip=node1:2181,node2:2181,node3:2181
# kafka配置
kafka.bootstrap_servers_config=node1:9092,node2:9092,node3:9092
kafka.batch_size_config=1024
# 1: 表示leader写入成功就返回确认信息,假如Leader写完之后宕机了,还没来得及同步到各个节点,数据丢失.
# 0: 异步操作,不管数据有没有写入成功.存在数据丢失.
# -1: 当Leader写入成功,同时从主节点同步成功之后才返回,可以保证数据不丢失.
# all: Leader会等待所有的Follower同步完成,确保消息不丢失,除非整个Kafka集群都挂掉了.
kafka.acks=all
kafka.retries=0
kafka.client_id_config=itcast_shop_canal_click
kafka.key_serializer_class_config=org.apache.kafka.common.serialization.StringSerializer
kafka.value_serializer_class_config=cn.itcast.canal.protobuf.ProtoBufSerializer
kafka.topic=ods_itcast_shop_mysql
编写ConfigUitl配置文件的读取工具类
为了方便配置文件的读取,我们可以编写一个工具类来获取配置文件的数据.
package com.itcast.canal_client.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 客户端程序配置文件的读取工具类
*/
public class ConfigUtil {
// 一般,我们配置文件都是通过类名.变量名的形式进行获取的.
// 为了方便后面的使用,这里我们也可以将配置文件都定义为静态的成员变量.
public static String CANAL_SERVER_IP = "";
public static String CANAL_SERVER_PORT = "";
public static String CANAL_SERVER_DESTINATION = "";
public static String CANAL_SERVER_USERNAME = "";
public static String CANAL_SERVER_PASSWORD = "";
public static String CANAL_SUBSCRIBE_FILTER = "";
public static String ZOOKEEPER_SERVER_IP = "";
public static String KAFKA_BOOTSTRAP_SERVERS_CONFIG = "";
public static String KAFKA_BATCH_SIZE_CONFIG = "";
public static String KAFKA_ACKS = "";
public static String KAFKA_RETRIES = "";
public static String KAFKA_CLIENT_ID_CONFIG = "";
public static String KAFKA_KEY_SERIALIZER_CLASS_CONFIG = "";
public static String KAFKA_VALUE_SERIALIZER_CLASS_CONFIG = "";
public static String KAFKA_TOPIC = "";
// 定义代码块,对上面的变量进行赋值.
{
//编写代码.
// 这个代码块随着对象的创建而加载.
}
static {
try {
// 静态代码块
// 随着类的加载而加载
// 读取config.properties,将数据赋值给上面的成员变量
// 将数据封装为properties对象.
Properties properties = new Properties();
// 读取config配置文件
// 2种方式: 1. 使用FileInputStream, 2. 使用类的加载器.
InputStream inputStream = ConfigUtil.class.getClassLoader().getResourceAsStream("config.properties");
// 将数据封装为properties对象.
properties.load(inputStream);
// 从properties对象中获取数据,赋值给上面的变量.
CANAL_SERVER_IP = properties.getProperty("canal.server.ip");
CANAL_SERVER_PORT = properties.getProperty("canal.server.port");
CANAL_SERVER_DESTINATION = properties.getProperty("canal.server.destination");
CANAL_SERVER_USERNAME = properties.getProperty("canal.server.username");
CANAL_SERVER_PASSWORD = properties.getProperty("canal.server.password");
CANAL_SUBSCRIBE_FILTER = properties.getProperty("canal.subscribe.filter");
ZOOKEEPER_SERVER_IP = properties.getProperty("zookeeper.server.ip");
KAFKA_BOOTSTRAP_SERVERS_CONFIG = properties.getProperty("kafka.bootstrap_servers_config");
KAFKA_BATCH_SIZE_CONFIG = properties.getProperty("kafka.batch_size_config");
KAFKA_ACKS = properties.getProperty("kafka.acks");
KAFKA_RETRIES = properties.getProperty("kafka.retries");
KAFKA_CLIENT_ID_CONFIG = properties.getProperty("kafka.client_id_config");
KAFKA_KEY_SERIALIZER_CLASS_CONFIG = properties.getProperty("kafka.key_serializer_class_config");
KAFKA_VALUE_SERIALIZER_CLASS_CONFIG = properties.getProperty("kafka.value_serializer_class_config");
KAFKA_TOPIC = properties.getProperty("kafka.topic");
} catch (IOException e) {
// 下面这一行是将数据打印到控制台,开发时,一般我们都是将错误信息写入到日志文件中.
// 这行代码建议不要删除,除非我们已经手动将日志保存起来.
e.printStackTrace();
} finally {
}
}
//测试配置的读取功能
public static void main(String[] args) {
System.out.println(ConfigUtil.CANAL_SERVER_IP);
System.out.println(ConfigUtil.CANAL_SERVER_PORT);
System.out.println(ConfigUtil.CANAL_SERVER_DESTINATION);
System.out.println(ConfigUtil.CANAL_SERVER_USERNAME);
System.out.println(ConfigUtil.CANAL_SERVER_PASSWORD);
System.out.println(ConfigUtil.CANAL_SUBSCRIBE_FILTER);
System.out.println(ConfigUtil.ZOOKEEPER_SERVER_IP);
System.out.println(ConfigUtil.KAFKA_BOOTSTRAP_SERVERS_CONFIG);
System.out.println(ConfigUtil.KAFKA_BATCH_SIZE_CONFIG);
System.out.println(ConfigUtil.KAFKA_ACKS);
System.out.println(ConfigUtil.KAFKA_RETRIES);
System.out.println(ConfigUtil.KAFKA_CLIENT_ID_CONFIG);
System.out.println(ConfigUtil.KAFKA_KEY_SERIALIZER_CLASS_CONFIG);
System.out.println(ConfigUtil.KAFKA_VALUE_SERIALIZER_CLASS_CONFIG);
System.out.println(ConfigUtil.KAFKA_TOPIC);
}
}
改造Canal发送的客户端程序
在Common工程中,创建protobuf的配置文件
syntax = "proto3";
option java_package = "cn.itcast.canal.protobuf";
option java_outer_classname = "CanalModel";
// 一行数据中包含的内容.
message RowData{
string logfileName = 2;
uint64 logfileOffset = 3;
string eventType = 4;
string dbName = 5;
string tableName = 6;
uint64 executeTime = 7;
//列信息
map<string, string> columns = 8;
}
编译配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFoyI5xd-1591285941160)(assets/image-20200426115642098.png)]
编写自定义数据传输的实体类: RowData
package cn.itcast.shop.bean;
import java.util.Map;
/**
* RowData就是一行数据的记录
*/
public class RowData {
private String logfileName;
private Long logfileOffset;
private String eventType;
private String dbName;
private String tableName;
private Long executeTime;
private Map<String, String> columns;
public RowData() {
}
public RowData(String logfileName, Long logfileOffset, String eventType, String dbName, String tableName, Long executeTime, Map<String, String> columns) {
this.logfileName = logfileName;
this.logfileOffset = logfileOffset;
this.eventType = eventType;
this.dbName = dbName;
this.tableName = tableName;
this.executeTime = executeTime;
this.columns = columns;
}
public String getLogfileName() {
return logfileName;
}
public void setLogfileName(String logfileName) {
this.logfileName = logfileName;
}
public Long getLogfileOffset() {
return logfileOffset;
}
public void setLogfileOffset(Long logfileOffset) {
this.logfileOffset = logfileOffset;
}
public String getEventType() {
return eventType;
}
public void setEventType(String eventType) {
this.eventType = eventType;
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getTableName() {
return tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public Long getExecuteTime() {
return executeTime;
}
public void setExecuteTime(Long executeTime) {
this.executeTime = executeTime;
}
public Map<String, String> getColumns() {
return columns;
}
public void setColumns(Map<String, String> columns) {
this.columns = columns;
}
@Override
public String toString() {
return "RowData{" +
"logfileName='" + logfileName + '\'' +
", logfileOffset=" + logfileOffset +
", eventType='" + eventType + '\'' +
", dbName='" + dbName + '\'' +
", tableName='" + tableName + '\'' +
", executeTime=" + executeTime +
", columns=" + columns +
'}';
}
}
修改canal客户端发送程序
package com.itcast.canal_client;
import cn.itcast.canal.protobuf.CanalModel;
import cn.itcast.shop.bean.RowData;
import com.alibaba.fastjson.JSON;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.itcast.canal_client.util.ConfigUtil;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Canal客户端开发
*/
public class CanalClient {
public static void main(String[] args) throws Exception {
//2. 创建Canal连接对象
// CanalConnectors.newSingleConnector() // 单机版的Canal服务端
// CanalConnectors.newClusterConnector() // 高可用版本的Canal
// CanalConnector connector = CanalConnectors.newSingleConnector(
// new InetSocketAddress("node1", 11111),
// "example", "root", "123456");
CanalConnector connector = CanalConnectors.newClusterConnector(
ConfigUtil.ZOOKEEPER_SERVER_IP,
ConfigUtil.CANAL_SERVER_DESTINATION,
ConfigUtil.CANAL_SERVER_USERNAME,
ConfigUtil.CANAL_SERVER_PASSWORD
);
//3. 连接Canal服务端
connector.connect();
//4. 回滚到最后一次消费的位置
connector.rollback();
//5. 订阅我们要消费的数据.比如我们只消费itcast_shop数据库
connector.subscribe(ConfigUtil.CANAL_SUBSCRIBE_FILTER);
// 获取数据不是只获取1次,而是应该源源不断的获取数据,只要数据发生了变更,我们就要进行数据的采集.
// 所以我们应该用一个死循环,源源不断的获取数据.
boolean flag = true;
while (flag) {
//6. 获取数据
// connector.get() 从服务器端获取数据,并发送回执
// connector.getWithoutAck() 从服务器获取数据,但是不发送回执.注意: 这种方式需要手动的发送回执.
Message message = connector.getWithoutAck(1000);
//7. 解析数据
long id = message.getId();//本批次数据的ID
List<CanalEntry.Entry> entries = message.getEntries();// 本批次的数据
//判断当前批次有没有获取到数据.
if (id == -1 || entries.size() == 0) {
//没有数据,不执行任何操作
} else {
// 将binlog转换为protobuf格式
binlogToProtoBuf(message);
}
//8. 给服务器一个回执,告诉服务器本批次的数据已经消费过了.
connector.ack(id);
//每一批次都休息1秒钟
// Thread.sleep(1000);
}
//9. 关闭连接.
connector.disconnect();
}
// binlog解析为ProtoBuf
private static void binlogToProtoBuf(Message message) throws InvalidProtocolBufferException {
// 1. 构建CanalModel.RowData实体
RowData rowdata = new RowData();
// 1. 遍历message中的所有binlog实体
for (CanalEntry.Entry entry : message.getEntries()) {
// 只处理事务型binlog
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 eventType = entry.getHeader().getEventType().toString().toLowerCase();
rowdata.setLogfileName(logfileName);
rowdata.setLogfileOffset(logfileOffset);
rowdata.setExecuteTime(executeTime);
rowdata.setDbName(schemaName);
rowdata.setTableName(tableName);
rowdata.setEventType(eventType);
// 获取所有行上的变更
HashMap<String, String> map = new HashMap<>();
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
List<CanalEntry.RowData> columnDataList = rowChange.getRowDatasList();
for (CanalEntry.RowData rowData : columnDataList) {
if (eventType.equals("insert") || eventType.equals("update")) {
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
map.put(column.getName(), column.getValue().toString());
}
} else if (eventType.equals("delete")) {
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
map.put(column.getName(), column.getValue().toString());
}
}
}
rowdata.setColumns(map);
// 这里打印的rowdata是我们自己定义的对象.
System.out.println(rowdata);
// 将我们自己封装的rowdata使用自定义方式发送到Kafka.,
// KafkaUtil.sendToKafka(rowdata);
}
}
}
编写Kafka工具类
到目前为止,我们已经能够将Canal中的消息获取到,并且封装成了自定义的RowData对象,我们先需要的就是直接将RowData对象发送给Kafka.并且将数据发送到Kafka的时候需要使用Protobuf进行序列化.
现在我们需要将数据发送到Kafka中,那么Kafka里面是否能够直接接收自定义对象RowData呢?
Kafka中数据传递的格式: Kafka中只能传递字节数组类型.
因为Kafka不支持直接将RowData数据类型进行传递,所以我们可以借鉴StringSerializer,通过自定义的方式,实现将RowData转换为字节.
package com.itcast.canal_client.kafka;
import cn.itcast.shop.bean.RowData;
import com.itcast.canal_client.util.ConfigUtil;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
/**
* Kafka的数据发送工具类
*/
public class KafkaSender {
// 定义一个发送数据的对象
public static KafkaProducer<String, RowData> producer;
static {
// 在静态代码块中,对KafkaProducer进行初始化操作
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", ConfigUtil.KAFKA_BOOTSTRAP_SERVERS_CONFIG);
properties.setProperty("acks", ConfigUtil.KAFKA_ACKS);
properties.setProperty("retries", ConfigUtil.KAFKA_RETRIES);
properties.setProperty("batch.size", ConfigUtil.KAFKA_BATCH_SIZE_CONFIG);
properties.setProperty("key.serializer", ConfigUtil.KAFKA_KEY_SERIALIZER_CLASS_CONFIG);
//自定义value的序列化.待定
properties.setProperty("value.serializer", ConfigUtil.KAFKA_VALUE_SERIALIZER_CLASS_CONFIG);
producer = new KafkaProducer<String, RowData>(properties);
}
/**
* 将RowData对象发送到Kafka中.
* @param rowData
*/
public static void send(RowData rowData) {
producer.send(new ProducerRecord<String, RowData>(ConfigUtil.KAFKA_TOPIC, rowData));
}
}
编写自定义序列化器
我们可以先定义一个接口,后期,凡是想实现自定义对象发送到Kafka的需求,都需要让我们的对象实现此接口.也就是凡是实现此接口的实体类,都可以直接发送给Kafka.
- 定义一个接口
- 让RowData实现这个接口
- 编写自定义序列化器,实现数据发送Kafka的功能.泛型(就是上面我们自己定义的接口类型)
- 让Kafka的生产者使用我们自己的序列化器.
使用Lombok改造之前的RowData实体类.
lombok是帮助我们快速的创建实体类的一些工具方法,比如get/set方法/有参无参构造等.使用的时候需要注意以下细节:
-
要在项目中添加lombok依赖:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> </dependency>
-
在IDEA中添加插件.否则IDEA编译会报错,认为我们没有getset方法.
编写自定义序列化对象的接口
package cn.itcast.shop.protobuf;
/**
* 凡是需要通过protobuf实现序列化的实体类,都需要实现该接口
*/
public interface ProtoBufable {
/**
* 子类必须实现该方法,提供一个转字节的功能.
* @return
*/
byte[] toBytes();
}
改造之前的RowData实体类
package cn.itcast.shop.bean;
import cn.itcast.canal.protobuf.CanalModel;
import cn.itcast.shop.protobuf.ProtoBufable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.Map;
/**
* RowData就是一行数据的记录
*/
@Data //get/set方法
@NoArgsConstructor //无参构造
@AllArgsConstructor // 有参构造
@ToString
public class RowData implements ProtoBufable {
private String logfileName;
private Long logfileOffset;
private String eventType;
private String dbName;
private String tableName;
private Long executeTime;
private Map<String, String> columns;
@Override
public byte[] toBytes() {
// 使用protobuf将当前对象中的数据转换为字节.
CanalModel.RowData.Builder builder = CanalModel.RowData.newBuilder();
//向build中添加数据
builder.setLogfileName(logfileName);
builder.setLogfileOffset(logfileOffset);
builder.setEventType(eventType);
builder.setDbName(dbName);
builder.setTableName(tableName);
builder.setExecuteTime(executeTime);
builder.putAllColumns(columns);
//将数据转换为字节数组
byte[] bytes = builder.build().toByteArray();
return bytes;
}
}
自定义序列化器
package cn.itcast.shop.protobuf;
import org.apache.kafka.common.serialization.Serializer;
import java.util.Map;
/**
* 自定义的序列化器
*/
public class ProtoBufSerializer implements Serializer<ProtoBufable> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
// 自定义配置
}
/**
* 实现向Kafka中发送数据的核心方法
* @param topic
* @param data
* @return
*/
@Override
public byte[] serialize(String topic, ProtoBufable data) {
return data.toBytes();
}
@Override
public void close() {
// 关闭资源的方法,目前不需要
}
}
测试数据发送功能
环境准备
启动Kafka的相关工具
# 启动Kafka的服务端
nohup /export/servers/kafka_2.11-1.0.0/bin/kafka-server-start.sh /export/servers/kafka_2.11-1.0.0/config/server.properties > /dev/null 2>&1 &
# 启动Kafka的客户端消费者
/export/servers/kafka_2.11-1.0.0/bin/kafka-console-consumer.sh --zookeeper node1:2181 --topic ods_itcast_shop_mysql
使用KafkaManager的时候,只需要去conf文件夹下修改application.conf中的zk地址信息:
kafka-manager.zkhosts="node1:2181"
访问Kafka-Manager
![image-20200426152941097](assets/image-20200426152941097.png)
运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CrRi73ZF-1591285941162)(assets/image-20200426153641329.png)]
实时ETL项目开发
初始化ETL项目环境
导入配置文件
![image-20200426154612163](assets/image-20200426154612163.png)
全局配置文件
#
#kafka的配置
#
# Kafka集群地址
bootstrap.servers="node1:9092,node2:9092,node3:9092"
# ZooKeeper集群地址
zookeeper.connect="node1:2181,node2:2181,node3:2181"
# 消费组ID
group.id="itcast"
# 自动提交拉取到消费端的消息offset到kafka
enable.auto.commit="true"
# 自动提交offset到zookeeper的时间间隔单位(毫秒)
auto.commit.interval.ms="5000"
# 每次消费最新的数据
auto.offset.reset="latest"
# kafka序列化器
key.serializer="org.apache.kafka.common.serialization.StringSerializer"
# kafka反序列化器
key.deserializer="org.apache.kafka.common.serialization.StringDeserializer"
# ip库本地文件路径
ip.file.path="D:/workspace/flink/itcast_shop_parent34/data/qqwry.dat"
# Redis配置
redis.server.ip="node2"
redis.server.port=6379
# MySQL配置
mysql.server.ip="node1"
mysql.server.port=3306
mysql.server.database="itcast_shop"
mysql.server.username="root"
mysql.server.password="123456"
# Kafka Topic名称
input.topic.canal="ods_itcast_shop_mysql"
# Kafka click_log topic名称
input.topic.click_log="ods_itcast_click_log"
# Kafka 购物车 topic名称
input.topic.cart="ods_itcast_cart"
# kafka 评论 topic名称
input.topic.comments="ods_itcast_comments"
# Druid Kafka数据源 topic名称
output.topic.order="dwd_order"
output.topic.order_detail="dwd_order_detail"
output.topic.cart="dwd_cart"
output.topic.clicklog="dwd_click_log"
output.topic.goods="dwd_goods"
output.topic.ordertimeout="dwd_order_timeout"
output.topic.comments="dwd_comments"
# HBase订单明细表配置
hbase.table.orderdetail="dwd_order_detail"
hbase.table.family="detail"
日志配置文件
log4j.rootLogger=warn,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p - %m%n
Hbase配置文件
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<!--
/**
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<configuration>
<property>
<name>hbase.rootdir</name>
<value>hdfs://node1:8020/hbase</value>
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<!-- 0.98后的新变动,之前版本没有.port,默认端口为60000 -->
<property>
<name>hbase.master.port</name>
<value>16000</value>
</property>
<property>
<name>hbase.zookeeper.property.clientPort</name>
<value>2181</value>
</property>
<property>
<name>hbase.zookeeper.quorum</name>
<value>node1:2181,node2:2181,node3:2181</value>
</property>
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/export/servers/zookeeper-3.4.5-cdh5.14.0/zkdatas</value>
</property>
</configuration>
编写配置文件读取工具类
package cn.itcast.shop.realtime.etl.util
import com.typesafe.config.{Config, ConfigFactory}
/**
* 全局的配置工具类
*/
object GlobalConfigUtil {
//使用ConfigFactory加载application.conf配置文件
private val config: Config = ConfigFactory.load()
// 从config中获取配置信息
val `bootstrap.servers`: String = config.getString("bootstrap.servers")
val `zookeeper.connect`: String = config.getString("zookeeper.connect")
val `group.id`: String = config.getString("group.id")
val `enable.auto.commit`: String = config.getString("enable.auto.commit")
val `auto.commit.interval.ms`: String = config.getString("auto.commit.interval.ms")
val `auto.offset.reset`: String = config.getString("auto.offset.reset")
val `key.serializer`: String = config.getString("key.serializer")
val `key.deserializer`: String = config.getString("key.deserializer")
val `ip.file.path`: String = config.getString("ip.file.path")
val `redis.server.ip`: String = config.getString("redis.server.ip")
val `redis.server.port`: String = config.getString("redis.server.port")
val `mysql.server.ip`: String = config.getString("mysql.server.ip")
val `mysql.server.port`: String = config.getString("mysql.server.port")
val `mysql.server.database`: String = config.getString("mysql.server.database")
val `mysql.server.username`: String = config.getString("mysql.server.username")
val `mysql.server.password`: String = config.getString("mysql.server.password")
val `input.topic.canal`: String = config.getString("input.topic.canal")
val `input.topic.click_log`: String = config.getString("input.topic.click_log")
val `input.topic.cart`: String = config.getString("input.topic.cart")
val `input.topic.comments`: String = config.getString("input.topic.comments")
val `output.topic.order`: String = config.getString("output.topic.order")
val `output.topic.order_detail`: String = config.getString("output.topic.order_detail")
val `output.topic.cart`: String = config.getString("output.topic.cart")
val `output.topic.clicklog`: String = config.getString("output.topic.clicklog")
val `output.topic.goods`: String = config.getString("output.topic.goods")
val `output.topic.ordertimeout`: String = config.getString("output.topic.ordertimeout")
val `output.topic.comments`: String = config.getString("output.topic.comments")
val `hbase.table.orderdetail`: String = config.getString("hbase.table.orderdetail")
val `hbase.table.family`: String = config.getString("hbase.table.family")
def main(args: Array[String]): Unit = {
println(GlobalConfigUtil.`output.topic.cart`)
println(GlobalConfigUtil.`zookeeper.connect`)
println(GlobalConfigUtil.`hbase.table.family`)
println(GlobalConfigUtil.`ip.file.path`)
}
}
开发入口主程序.
入口主程序主要负责接收Kafka的数据,进行ETL处理,之后将处理后的数据推送给kafka或者保存到HBase等.
Flink流处理程序的开发步骤:
-
获取Flink的流处理运行环境
-
设置运行环境相关参数:
- 并行度(全局的)
- 设置时间的处理特性,设置为事件发生时间.
-
开启Checkpoint.
- 进行checkpoint的参数设置
- 比如超时时间/失败策略/并行度…
- 设置checkpoint的路径信息.
-
设置程序的重启策略:
如果Flink开启了Checkpoint,默认是无限重启的.所以我们应该配置一下重启策略
-
加载Kafka数据源.
-
进行业务开发
-
启动程序
package cn.itcast.shop.realtime.etl.app
import org.apache.flink.api.common.restartstrategy.RestartStrategies
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.streaming.api.environment.CheckpointConfig
import org.apache.flink.streaming.api.{CheckpointingMode, TimeCharacteristic}
import org.apache.flink.streaming.api.scala._
/**
* FlinkETL程序的入口
*/
object App {
def main(args: Array[String]): Unit = {
//1. 获取Flink的流处理运行环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
//2. 设置运行环境相关参数:
// * 并行度(全局的)
env.setParallelism(1)
// * 设置时间的处理特性,设置为事件发生时间.
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
//3. 开启Checkpoint.
env.enableCheckpointing(5000L)
env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// 配置两次checkpoint的最小时间间隔
env.getCheckpointConfig.setMinPauseBetweenCheckpoints(1000)
// 配置最大checkpoint的并行度
env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)
// 配置checkpoint的超时时长
env.getCheckpointConfig.setCheckpointTimeout(60000)
// 当程序关闭,触发额外的checkpoint
env.getCheckpointConfig.enableExternalizedCheckpoints(
CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)
// 1. 进行checkpoint的参数设置
// 2. 比如超时时间/失败策略/并行度....
// 3. 设置checkpoint的路径信息.
// checkpoint的HDFS保存位置
env.setStateBackend(new FsStateBackend("hdfs://node1:8020/flink_itcast_shop000"))
//4. 设置程序的重启策略:
// 如果Flink开启了Checkpoint,默认是无限重启的.所以我们应该配置一下重启策略
// 如果程序出错,间隔1秒钟后重启一次,如果还是失败,那么程序退出.
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(1, 1000))
//
//5. 加载Kafka数据源.
env.fromCollection(
List("hadoop", "hive", "spark")
).print()
//6. 进行业务开发
//7. 启动程序
env.execute("itcast_shop")
}
}
公共基类的抽取
因为目前有2种数据源,为了方便后面的业务开发,我们针对String和MySQL的数据开发2个基类,
- BaseETL
- MQBaseETL
- ClickLogETL
- CartETL
- MySQLBaseETL
- OrderETL
- DimETL
- OrderGoodsETL
- MQBaseETL
编写BaseETL
package cn.itcast.shop.realtime.etl.process.base
import org.apache.flink.streaming.api.scala.DataStream
/**
* 所有ETL的基类,主要负责定义整体流程.
* 比如定义数据源的方法,处理数据的方法...
* (爷爷)
*/
trait BaseETL[T] {
/**
* 获取数据源
* @param topic 需要消费的Topic名称
* @return
*/
def getDataSource(topic: String): DataStream[T]
/**
* 后续所有的ETL操作都需要将功能实现放在Process方法中.
*/
def process()
}
编写Kafka的配置工具类
package cn.itcast.shop.realtime.etl.util
import java.util.Properties
import org.apache.kafka.clients.consumer.ConsumerConfig
/**
* Kafka配置工具类
*/
object KafkaProps {
def getKafkaProps(): Properties = {
//定义Kafka配置
val properties = new Properties()
properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, GlobalConfigUtil.`bootstrap.servers`)
properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, GlobalConfigUtil.`group.id`)
properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, GlobalConfigUtil.`enable.auto.commit`)
properties.setProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, GlobalConfigUtil.`auto.commit.interval.ms`)
properties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, GlobalConfigUtil.`auto.offset.reset`)
properties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, GlobalConfigUtil.`key.deserializer`)
properties
}
}
编写MQBaseETL
package cn.itcast.shop.realtime.etl.process.base
import cn.itcast.shop.realtime.etl.util.{GlobalConfigUtil, KafkaProps}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011
import org.apache.flink.streaming.util.serialization.SimpleStringSchema
/**
* 负责处理String类型的数据源
* (爸爸)
*/
abstract class MQBaseETL(env: StreamExecutionEnvironment) extends BaseETL[String] {
/**
* 获取数据源
*
* @param topic 需要消费的Topic名称
* @return
*/
override def getDataSource(topic: String): DataStream[String] = {
val consumer = new FlinkKafkaConsumer011[String](
topic,
new SimpleStringSchema,
KafkaProps.getKafkaProps
)
// 添加Kafka数据源
val sourceStream: DataStream[String] = env.addSource(consumer)
sourceStream
}
}
编写自定义反序列化器
在RowData中添加一个构造方法,用户根据字节数组生成对象
/**
* 接收字节数组,转换为RowData对象
*
* @param bytes
*/
public RowData(byte[] bytes) {
try {
//先将字节数组转换为CanalModel.RowData对象
CanalModel.RowData rowData = CanalModel.RowData.parseFrom(bytes);
//对当前对象赋值
this.logfileName = rowData.getLogfileName();
this.logfileOffset = rowData.getLogfileOffset();
this.eventType = rowData.getEventType();
this.dbName = rowData.getDbName();
this.tableName = rowData.getTableName();
this.executeTime = rowData.getExecuteTime();
//先初始化Map
this.columns = new HashMap<>();
//将RowData的数据赋值到当前columns中
this.columns.putAll(rowData.getColumnsMap());
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
} finally {
}
}
编写自定义反序列化器
package cn.itcast.shop.realtime.etl.util
import cn.itcast.shop.bean.RowData
import org.apache.flink.api.common.serialization.{AbstractDeserializationSchema, DeserializationSchema}
import org.apache.flink.api.common.typeinfo.TypeInformation
/**
* 自定义RowData的转换类,将Kafka中的数据直接转换为RowData类型
*/
class CanalRowDataDeserializationSchema extends AbstractDeserializationSchema[RowData]{
/**
* 在这个方法中,将字节转换为RowData对象
* @param bytes
* @return
*/
override def deserialize(bytes: Array[Byte]): RowData = {
// 2种方式进行转换. 1: 在这里进行转换 2: 在RowData的构造构造方法中进行转换.
new RowData(bytes)
}
}
编写MySQLBaseETL
package cn.itcast.shop.realtime.etl.process.base
import cn.itcast.shop.bean.RowData
import cn.itcast.shop.realtime.etl.util.{CanalRowDataDeserializationSchema, GlobalConfigUtil, KafkaProps}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011
import org.apache.flink.streaming.util.serialization.SimpleStringSchema
/**
* 负责处理Canal相关类型的数据源(RowData)
* (爸爸)
*/
abstract class MySQLBaseETL(env: StreamExecutionEnvironment) extends BaseETL[RowData] {
/**
* 获取数据源
*
* @param topic 需要消费的Topic名称
* @return
*/
override def getDataSource(topic: String = GlobalConfigUtil.`input.topic.canal`): DataStream[RowData] = {
val consumer = new FlinkKafkaConsumer011[RowData](
topic,
new CanalRowDataDeserializationSchema,
KafkaProps.getKafkaProps
)
// 添加Kafka数据源
val sourceStream: DataStream[RowData] = env.addSource(consumer)
sourceStream
}
}
测试基类的抽取功能
编写测试类:
package cn.itcast.shop.realtime.etl.process.test
import cn.itcast.shop.realtime.etl.process.base.MQBaseETL
import cn.itcast.shop.realtime.etl.util.GlobalConfigUtil
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
class TestStringETL(env: StreamExecutionEnvironment) extends MQBaseETL(env){
/**
* 后续所有的ETL操作都需要将功能实现放在Process方法中.
*/
override def process(): Unit = {
val source: DataStream[String] = getDataSource(GlobalConfigUtil.`input.topic.click_log`)
source.print("点击流日志数据::")
}
}
package cn.itcast.shop.realtime.etl.process.test
import cn.itcast.shop.bean.RowData
import cn.itcast.shop.realtime.etl.process.base.MySQLBaseETL
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
class TestMySQLETL(env: StreamExecutionEnvironment) extends MySQLBaseETL(env){
/**
* 后续所有的ETL操作都需要将功能实现放在Process方法中.
*/
override def process(): Unit = {
val source: DataStream[RowData] = getDataSource()
source.print("MySQL中获取的数据::")
}
}
在主程序中引用:
//6. 进行业务开发
//获取String类型的数据
new TestStringETL(env).process()
//获取MySQL的数据
new TestMySQLETL(env).process()
使用Kafka发送点击流日志的测试数据:
/export/servers/kafka_2.11-1.0.0/bin/kafka-console-producer.sh --broker-list node1:9092,node2:9092,node3:9092 --topic ods_itcast_click_log
修改MySQL中的数据,看Flink能否打印出来
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xzOpAWCd-1591285941164)(assets/image-20200426175143190.png)]
序列化: 对象转字节.
反序列化: 字节转对象.