在上一篇文章中,我們已經把客戶端的頁面日誌,啟動日誌,曝光日誌分別傳送到kafka對應的主題中。在本文中,我們將把業務資料也傳送到對應的kafka主題中。
2. 消費kafka資料及ETL操作
專案地址:https://github.com/zhangbaohpu/gmall-flink-parent/tree/master/gmall-realtime
在模組 gmall-realtime 的dwd包下建立類:BaseDbTask.java
具體步驟就看程式碼了
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zhangbao.gmall.realtime.utils.MyKafkaUtil;
import org.apache.flink.runtime.state.filesystem.FsStateBackend;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.LocalStreamEnvironment;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
/**
* 從kafka讀取業務資料
* @author: zhangbao
* @date: 2021/8/15 21:10
* @desc:
**/
public class BaseDbTask {
public static void main(String[] args) {
//1.獲取flink環境
LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
//設定並行度
env.setParallelism(4);
//設定檢查點
env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setCheckpointTimeout(60000);
env.setStateBackend(new FsStateBackend("hdfs://hadoop101:9000/gmall/flink/checkpoint/baseDbApp"));
//指定哪個使用者讀取hdfs檔案
System.setProperty("HADOOP_USER_NAME","zhangbao");
//2.從kafka獲取topic資料
String topic = "ods_base_db_m";
String group = "base_db_app_group";
FlinkKafkaConsumer<String> kafkaSource = MyKafkaUtil.getKafkaSource(topic, group);
DataStreamSource<String> jsonStrDs = env.addSource(kafkaSource);
//3.對資料進行json轉換
SingleOutputStreamOperator<JSONObject> jsonObjDs = jsonStrDs.map(jsonObj -> JSON.parseObject(jsonObj));
//4.ETL, table不為空,data不為空,data長度不能小於3
SingleOutputStreamOperator<JSONObject> filterDs = jsonObjDs.filter(jsonObject -> jsonObject.getString("table") != null
&& jsonObject.getJSONObject("data") != null
&& jsonObject.getString("data").length() > 3);
filterDs.print("json str --->>");
try {
env.execute("base db task");
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. 動態分流
由於MaxWell是把全部資料統一寫入一個Topic中, 這樣顯然不利於日後的資料處理。所以需要把各個表拆開處理。但是由於每個表有不同的特點,有些表是維度表,有些表是事實表,有的表既是事實表在某種情況下也是維度表。
在實時計算中一般把維度資料寫入儲存容器,一般是方便通過主鍵查詢的資料庫比如HBase,Redis,MySQL 等。一般把事實資料寫入流中,進行進一步處理,最終形成寬表。但是作為 Flink 實時計算任務,如何得知哪些表是維度表,哪些是事實表呢?而這些表又應該採集哪些欄位呢?
我們可以將上面的內容放到某一個地方,集中配置。這樣的配置不適合寫在配置檔案中,因為業務端隨著需求變化每增加一張表,就要修改配置重啟計算程式。所以這裡需要一種動態配置方案,把這種配置長期儲存起來,一旦配置有變化,實時計算可以自動感知。
這種可以有兩個方案實現
-
一種是用 Zookeeper 儲存,通過 Watch 感知資料變化。
-
另一種是用 mysql 資料庫儲存,週期性的同步或使用flink-cdc實時同步。
這裡選擇第二種方案,週期性同步,flink-cdc方式可自行嘗試,主要是 mysql 對於配置資料初始化和維護管理,用 sql 都比較方便,雖然週期性操作時效性差一點,但是配置變化並不頻繁。
所以就有了如下圖:
業務資料儲存到Kafka 的主題中,維度資料儲存到Hbase 的表中。
4. mysql配置
① 在 gmall-realtime 模組新增依賴
<!--lomback 外掛依賴-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<!--commons-beanutils 是 Apache 開源組織提供的用於操作 JAVA BEAN 的工具包。
使用 commons-beanutils,我們可以很方便的對 bean 物件的屬性進行操作-->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
<!--Guava 工程包含了若干被 Google 的 Java 專案廣泛依賴的核心庫,方便開發-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
② 單獨建立資料庫gmall2021_realtime
create database gmall2021_realtime;
CREATE TABLE `table_process` (
`source_table` varchar(200) NOT NULL COMMENT '來源表',
`operate_type` varchar(200) NOT NULL COMMENT '操作型別 insert,update,delete',
`sink_type` varchar(200) DEFAULT NULL COMMENT '輸出型別 hbase kafka',
`sink_table` varchar(200) DEFAULT NULL COMMENT '輸出表(主題)',
`sink_columns` varchar(2000) DEFAULT NULL COMMENT '輸出欄位',
`sink_pk` varchar(200) DEFAULT NULL COMMENT '主鍵欄位',
`sink_extend` varchar(200) DEFAULT NULL COMMENT '建表擴充套件',
PRIMARY KEY (`source_table`,`operate_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
③ 建立實體類
package com.zhangbao.gmall.realtime.bean;
import lombok.Data;
/**
* @author: zhangbao
* @date: 2021/8/22 13:06
* @desc:
**/
@Data
public class TableProcess {
//動態分流 Sink 常量 改為小寫和指令碼一致
public static final String SINK_TYPE_HBASE = "hbase";
public static final String SINK_TYPE_KAFKA = "kafka";
public static final String SINK_TYPE_CK = "clickhouse";
//來源表
private String sourceTable;
//操作型別 insert,update,delete
private String operateType;
//輸出型別 hbase kafka
private String sinkType;
//輸出表(主題)
private String sinkTable;
//輸出欄位
private String sinkColumns;
//主鍵欄位
private String sinkPk;
//建表擴充套件
private String sinkExtend;
}
④ mysql工具類
package com.zhangbao.gmall.realtime.utils;
import com.google.common.base.CaseFormat;
import com.zhangbao.gmall.realtime.bean.TableProcess;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.reflect.FieldUtils;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author: zhangbao
* @date: 2021/8/22 13:09
* @desc:
**/
public class MysqlUtil {
private static final String DRIVER_NAME = "com.mysql.jdbc.Driver";
private static final String URL = "jdbc:mysql://192.168.88.71:3306/gmall2021_realtime?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8";
private static final String USER_NAME = "root";
private static final String USER_PWD = "123456";
public static void main(String[] args) {
String sql = "select * from table_process";
List<TableProcess> list = getList(sql, TableProcess.class, true);
for (TableProcess tableProcess : list) {
System.out.println(tableProcess.toString());
}
}
public static <T> List<T> getList(String sql,Class<T> clz, boolean under){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
Class.forName(DRIVER_NAME);
conn = DriverManager.getConnection(URL, USER_NAME, USER_PWD);
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
List<T> resultList = new ArrayList<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
while (rs.next()){
System.out.println(rs.getObject(1));
T obj = clz.newInstance();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i);
String propertyName = "";
if(under){
//指定資料庫欄位轉換為駝峰命名法,guava工具類
propertyName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL,columnName);
}
//通過guava工具類設定屬性值
BeanUtils.setProperty(obj,propertyName,rs.getObject(i));
}
resultList.add(obj);
}
return resultList;
} catch (Exception throwables) {
throwables.printStackTrace();
new RuntimeException("msql 查詢失敗!");
} finally {
if(rs!=null){
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if(ps!=null){
try {
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
return null;
}
}
5. 程式分流
如圖定義一個mapFunction函式
-
1.在open方法中初始化配置資訊,並週期開啟一個任務重新整理配置
-
2.在任務中根據配置建立資料表
-
3.分流
主任務流程
package com.zhangbao.gmall.realtime.app.dwd;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zhangbao.gmall.realtime.app.func.TableProcessFunction;
import com.zhangbao.gmall.realtime.bean.TableProcess;
import com.zhangbao.gmall.realtime.utils.MyKafkaUtil;
import org.apache.flink.runtime.state.filesystem.FsStateBackend;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.LocalStreamEnvironment;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.util.OutputTag;
/**
* 從kafka讀取業務資料
* @author: zhangbao
* @date: 2021/8/15 21:10
* @desc:
**/
public class BaseDbTask {
public static void main(String[] args) {
//1.獲取flink環境
LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
//設定並行度
env.setParallelism(4);
//設定檢查點
env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setCheckpointTimeout(60000);
env.setStateBackend(new FsStateBackend("hdfs://hadoop101:9000/gmall/flink/checkpoint/baseDbApp"));
//指定哪個使用者讀取hdfs檔案
System.setProperty("HADOOP_USER_NAME","zhangbao");
//2.從kafka獲取topic資料
String topic = "ods_base_db_m";
String group = "base_db_app_group";
FlinkKafkaConsumer<String> kafkaSource = MyKafkaUtil.getKafkaSource(topic, group);
DataStreamSource<String> jsonStrDs = env.addSource(kafkaSource);
//3.對資料進行json轉換
SingleOutputStreamOperator<JSONObject> jsonObjDs = jsonStrDs.map(jsonObj -> JSON.parseObject(jsonObj));
//4.ETL, table不為空,data不為空,data長度不能小於3
SingleOutputStreamOperator<JSONObject> filterDs = jsonObjDs.filter(jsonObject -> jsonObject.getString("table") != null
&& jsonObject.getJSONObject("data") != null
&& jsonObject.getString("data").length() > 3);
//5.動態分流,事實表寫會kafka,維度表寫入hbase
OutputTag<JSONObject> hbaseTag = new OutputTag<JSONObject>(TableProcess.SINK_TYPE_HBASE){};
//建立自定義mapFunction函式
SingleOutputStreamOperator<JSONObject> kafkaTag = filterDs.process(new TableProcessFunction(hbaseTag));
DataStream<JSONObject> hbaseDs = kafkaTag.getSideOutput(hbaseTag);
filterDs.print("json str --->>");
try {
env.execute("base db task");
} catch (Exception e) {
e.printStackTrace();
}
}
}
建立TableProcessFunction自定義任務
這裡包括上面說的四個步驟
-
初始化並週期讀取配置資料
-
執行每條資料
-
過濾欄位
-
標記資料流向,根據配置寫入對應去向,維度資料就寫入hbase,事實資料就寫入kafka
package com.zhangbao.gmall.realtime.app.func;
import com.alibaba.fastjson.JSONObject;
import com.zhangbao.gmall.realtime.bean.TableProcess;
import com.zhangbao.gmall.realtime.common.GmallConfig;
import com.zhangbao.gmall.realtime.utils.MysqlUtil;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;
/**
* @author: zhangbao
* @date: 2021/8/26 23:24
* @desc:
**/
@Log4j2(topic = "gmall-logger")
public class TableProcessFunction extends ProcessFunction<JSONObject,JSONObject> {
//定義輸出流標記
private OutputTag<JSONObject> outputTag;
//定義配置資訊
private Map<String , TableProcess> tableProcessMap = new HashMap<>();
//在記憶體中存放已經建立的表
Set<String> existsTable = new HashSet<>();
//phoenix連線物件
Connection con = null;
public TableProcessFunction(OutputTag<JSONObject> outputTag) {
this.outputTag = outputTag;
}
//只執行一次
@Override
public void open(Configuration parameters) throws Exception {
//初始化配置資訊
log.info("查詢配置表資訊");
//建立phoenix連線
Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
con = DriverManager.getConnection(GmallConfig.PHOENIX_SERVER);
refreshDate();
//啟動一個定時器,每隔一段時間重新獲取配置資訊
//delay:延遲5000執行,每隔5000執行一次
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
refreshDate();
}
},5000,5000);
}
//每進來一個元素,執行一次
@Override
public void processElement(JSONObject jsonObj, Context context, Collector<JSONObject> collector) throws Exception {
//獲取表的修改記錄
String table = jsonObj.getString("table");
String type = jsonObj.getString("type");
JSONObject data = jsonObj.getJSONObject("data");
if(type.equals("bootstrap-insert")){
//maxwell更新歷史資料時,type型別是bootstrap-insert
type = "insert";
jsonObj.put("type",type);
}
if(tableProcessMap != null && tableProcessMap.size()>0){
String key = table + ":" + type;
TableProcess tableProcess = tableProcessMap.get(key);
if(tableProcess!=null){
//資料傳送到何處,如果是維度表,就傳送到hbase,如果是事實表,就傳送到kafka
String sinkType = tableProcess.getSinkType();
jsonObj.put("sink_type",sinkType);
String sinkColumns = tableProcess.getSinkColumns();
//過濾掉不要的資料列,sinkColumns是需要的列
filterColumns(data,sinkColumns);
}else {
log.info("no key {} for mysql",key);
}
if(tableProcess!=null && tableProcess.getSinkType().equals(TableProcess.SINK_TYPE_HBASE)){
//根據sinkType判斷,如果是維度表就分流,傳送到hbase
context.output(outputTag,jsonObj);
}else if(tableProcess!=null && tableProcess.getSinkType().equals(TableProcess.SINK_TYPE_KAFKA)){
//根據sinkType判斷,如果是事實表就傳送主流,傳送到kafka
collector.collect(jsonObj);
}
}
}
//過濾掉不要的資料列,sinkColumns是需要的列
private void filterColumns(JSONObject data, String sinkColumns) {
String[] cols = sinkColumns.split(",");
//轉成list集合,用於判斷是否包含需要的列
List<String> columnList = Arrays.asList(cols);
Set<Map.Entry<String, Object>> entries = data.entrySet();
Iterator<Map.Entry<String, Object>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<String, Object> next = iterator.next();
String key = next.getKey();
//如果不包含就刪除不需要的列
if(!columnList.contains(key)){
iterator.remove();
}
}
}
//讀取配置資訊,並建立表
private void refreshDate() {
List<TableProcess> processList = MysqlUtil.getList("select * from table_process", TableProcess.class, true);
for (TableProcess tableProcess : processList) {
String sourceTable = tableProcess.getSourceTable();
String operateType = tableProcess.getOperateType();
String sinkType = tableProcess.getSinkType();
String sinkTable = tableProcess.getSinkTable();
String sinkColumns = tableProcess.getSinkColumns();
String sinkPk = tableProcess.getSinkPk();
String sinkExtend = tableProcess.getSinkExtend();
String key = sourceTable+":"+operateType;
tableProcessMap.put(key,tableProcess);
//在phoenix建立表
if(TableProcess.SINK_TYPE_HBASE.equals(sinkType) && operateType.equals("insert")){
boolean noExist = existsTable.add(sinkTable);//true則表示沒有建立表
if(noExist){
createTable(sinkTable,sinkColumns,sinkPk,sinkExtend);
}
}
}
}
//在phoenix中建立表
private void createTable(String table, String columns, String pk, String ext) {
if(StringUtils.isBlank(pk)){
pk = "id";
}
if(StringUtils.isBlank(ext)){
ext = "";
}
StringBuilder sql = new StringBuilder("create table if not exists " + GmallConfig.HBASE_SCHEMA + "." + table +"(");
String[] split = columns.split(",");
for (int i = 0; i < split.length; i++) {
String field = split[i];
if(pk.equals(field)){
sql.append(field + " varchar primary key ");
}else {
sql.append("info." + field +" varchar ");
}
if(i < split.length-1){
sql.append(",");
}
}
sql.append(")").append(ext);
//建立phoenix表
PreparedStatement ps = null;
try {
log.info("建立phoenix表sql - >{}",sql.toString());
ps = con.prepareStatement(sql.toString());
ps.execute();
} catch (SQLException throwables) {
throwables.printStackTrace();
}finally {
if(ps!=null){
try {
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
throw new RuntimeException("建立phoenix表失敗");
}
}
}
if(tableProcessMap == null || tableProcessMap.size()==0){
throw new RuntimeException("沒有從配置表中讀取配置資訊");
}
}
}
6. 重啟策略
flink程式在執行時,有錯誤會丟擲異常,程式就停止了,但當開始checkpoint檢查點時,flink重啟策略就是開啟的,如果程式出現異常了,程式就會一直重啟,並且重啟次數是Integer.maxValue,這個過程也看不到錯誤資訊,是很不友好的。
flink可以設定重啟策略,所以在我們開啟checkpoint檢查點時,設定不需要重啟就可以看到錯誤資訊了:
env.setRestartStrategy(RestartStrategies.noRestart());
下面我們測試一下。
package com.zhangbao.gmall.realtime.app.dwd;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zhangbao.gmall.realtime.app.func.TableProcessFunction;
import com.zhangbao.gmall.realtime.bean.TableProcess;
import com.zhangbao.gmall.realtime.utils.MyKafkaUtil;
import org.apache.flink.api.common.restartstrategy.RestartStrategies;
import org.apache.flink.runtime.executiongraph.restart.RestartStrategy;
import org.apache.flink.runtime.state.filesystem.FsStateBackend;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.LocalStreamEnvironment;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.util.OutputTag;
/**
* 從kafka讀取業務資料
* @author: zhangbao
* @date: 2021/8/15 21:10
* @desc:
**/
public class Test {
public static void main(String[] args) {
//1.獲取flink環境
LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
//設定並行度
env.setParallelism(4);
//設定檢查點
env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setCheckpointTimeout(60000);
env.setStateBackend(new FsStateBackend("hdfs://hadoop101:9000/gmall/flink/checkpoint/baseDbApp"));
//指定哪個使用者讀取hdfs檔案
System.setProperty("HADOOP_USER_NAME","zhangbao");
//flink重啟策略,
// 如果開啟上面的checkpoint,重啟策略就是自動重啟,程式有問題不會有報錯,
// 如果沒有開啟checkpoint,就不會自動重啟,所以這裡設定不需要重啟,就可以檢視錯誤資訊
env.setRestartStrategy(RestartStrategies.noRestart());
//2.從kafka獲取topic資料
String topic = "ods_base_db_m";
String group = "test_group";
FlinkKafkaConsumer<String> kafkaSource = MyKafkaUtil.getKafkaSource(topic, group);
DataStreamSource<String> jsonStrDs = env.addSource(kafkaSource);
jsonStrDs.print("轉換前-->");
//3.對資料進行json轉換
SingleOutputStreamOperator<JSONObject> jsonObjDs = jsonStrDs.map(jsonObj ->{
System.out.println(4/0);
JSONObject jsonObject = JSON.parseObject(jsonObj);
return jsonObject;
});
jsonObjDs.print("轉換後-->");
try {
env.execute("base db task");
} catch (Exception e) {
e.printStackTrace();
}
}
}
在程式對資料進行轉換過程中,我們加了 System.out.println(4/0);
這樣一行程式碼,肯定會丟擲異常的。
在設定不需要重啟後,就可以看到錯誤資訊了,當你把設定不需要重啟一行程式碼註釋掉,就會發現程式是一直在執行中的,並且沒有任何錯誤資訊。