课堂笔记

实时数仓搭建和flink分析Day02

昨日回顾

了解实时数仓的整体背景/涉及技术/业务主线

实时数仓的整体架构图.

使用Canal采集MySQL中的数据变更信息.

使用Java开发Canal客户端.

ProtoBuf序列化

  1. 编写Protobuf配置文件
  2. 对protobuf进行编译.
  3. 使用Protobuf创建的类来封装消息
  4. 将消息转换为字节数组
  5. 使用字节数组转换为对象

Canal原理.

依赖于MySQL的主从复制功能.

一个服务端里面可以有多个实例,每个实例中都有自己的EventParser/EventSink/EventStore/MetaManager.

今日课程介绍

Canal高可用: Server端高可用/Client高可用

进入数仓业务开发.

搭建实时数仓的项目环境

开发Canal客户端,负责将MySQL的变更信息通过Protobuf发送给Kafka.

实时ETL工程的项目环境初始化.

Canal的高可用

Canal高可用分为2种:

  1. 服务端高可用: 可以保证随时从MySQL中获取数据.
  2. 客户端高可用: 可以保证随时可以将数据发送到Kafka.

Canal的高可用依赖于zookeeper

服务端高可用

修改Canal配置文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z61rKCRW-1591285941152)(assets/image-20200426095826172.png)]

改过之后,将Canal分发到2节点一份,需要修改实例的slaveID.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwqUQSsA-1591285941155)(assets/image-20200426095926674.png)]

启动Canal
  1. 启动ZK
  2. 分别启动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

客户端高可用本身不需要进行任何配置,直接去启动2个客户端就可以了.

我们可以在IDEA中,将当前的客户端启动配置直接复制一份.

image-20200426102752904

启动第二个客户端程序.之后触发MySQL的数据变更,可以看到第一个Canal程序已经消费到了数据,另一个处于等待状态,当第一个程序挂掉后,第二个程序就可以开始进行消费,他们是通过zk中的cursor里面记录的position实现数据的索引记录,继续消费.

image-20200426102946933

实时数仓项目环境初始化

依赖仓库下载地址(如果遇到依赖下载不了,可以使用下面的仓库.):

链接: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.

  1. 定义一个接口
  2. 让RowData实现这个接口
  3. 编写自定义序列化器,实现数据发送Kafka的功能.泛型(就是上面我们自己定义的接口类型)
  4. 让Kafka的生产者使用我们自己的序列化器.

使用Lombok改造之前的RowData实体类.

lombok是帮助我们快速的创建实体类的一些工具方法,比如get/set方法/有参无参构造等.使用的时候需要注意以下细节:

  1. 要在项目中添加lombok依赖:

    <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.4</version>
    </dependency>
    
  2. 在IDEA中添加插件.否则IDEA编译会报错,认为我们没有getset方法.

    image-20200426144301384
编写自定义序列化对象的接口
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
运行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CrRi73ZF-1591285941162)(assets/image-20200426153641329.png)]

实时ETL项目开发

初始化ETL项目环境

导入配置文件
image-20200426154612163
全局配置文件
#
#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流处理程序的开发步骤:

  1. 获取Flink的流处理运行环境

  2. 设置运行环境相关参数:

    • 并行度(全局的)
    • 设置时间的处理特性,设置为事件发生时间.
  3. 开启Checkpoint.

    1. 进行checkpoint的参数设置
    2. 比如超时时间/失败策略/并行度…
    3. 设置checkpoint的路径信息.
  4. 设置程序的重启策略:

    如果Flink开启了Checkpoint,默认是无限重启的.所以我们应该配置一下重启策略

  5. 加载Kafka数据源.

  6. 进行业务开发

  7. 启动程序

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
编写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)]

序列化: 对象转字节.

反序列化: 字节转对象.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值