Flink Sql on Zeppelin(7)——Hive Streaming Sink之合并小文件(转载)

概述
之前和大家聊Hive Streaming Sink的时候说过,可以通过指定参数sink.partition-commit.policy.kind,来决定在提交分区时要做的事,比如合并小文件
本身Hive Streaming Sink是基于FileSystem Streaming Sink,FileSystem Streaming Sink其实已经做了保护,减少小文件的产生。主要是这两个参数
sink.rolling-policy.file-size:文件超过多大会切分
sink.rolling-policy.rollover-interval:如果文件大小一直没达到可以切分的大小,保持多久与这个文件的连接,一旦超过了这个时间,就会写到一个新文件
大家可能会觉得,既然做了保护了,又哪来的小文件呢?
因为在写入的时候,假设并行度为N,此时数据量不大,但是每个并行度都有数据的话,就会产生N个文件,每个文件都很小。这时候就会出现小文件过多的问题
秉着光说不干假把式的原则,这就和大家聊一下,如何通过Flink x Zeppelin来实现Hive Streaming Sink的时候,合并上一个小时的小文件
首先简单说一下如何实现的
在做完checkpoint,准备提交分区的时候,通过Http请求Zeppelin,建立一个合并小文件的任务,然后再提交分区,整个过程非常高
Zeppelin那边接收到请求后,执行合并小文件的任务
其实逻辑很简单,就是发送请求,具体执行任务的逻辑不在当前那个Hive Streaming Sink里面做,主要原因是合并小文件需要时间,如果在提交分区的时候去同步执行,会影响下游读取数据的任务
那为啥合并的是上一个小时的分区而不是当前分区呢?
第一点和上面一样,如果合并当前分区,那就得阻塞着等待合并分区完成之后再提交分区,会影响下游读取
第二点,当前分区都没提交怎么找得到当前分区呢?
总结来说就是三个优点:异步、解耦、快速返回
下面让我们开始看看怎么搞得吧
操作
我是通过改源码的方式去实现的合并小文件,所以如果打算跟着我的逻辑走的话,请先下载对应版本Flink源码
目前有些逻辑是写死的,比如固定是合并一小时之前的分区,而且分区得是分钟级别的三层分区,这些都可以写活,目前主要是为了测试所以暂时写死
下面直接贴代码吧
package org.apache.flink.connectors.hive;

import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.table.filesystem.PartitionCommitPolicy;
import org.apache.flink.util.CollectionUtil;
import org.apache.flink.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

public class CombineFileCommitPolicy implements PartitionCommitPolicy {
private static final Logger LOG = LoggerFactory.getLogger(CombineFileCommitPolicy.class);
private static final String URI_CREATE = “http://10.70.98.1:8080/api/notebook/2FDKM8CQH/paragraph/”;
private static final String URI_RUN = “http://10.70.98.1:8080/api/notebook/job/2FDKM8CQH/”;

@Override
public void commit(Context context) throws Exception {

	LinkedHashMap<String, String> map = context.partitionSpec();

	if (CollectionUtil.isNullOrEmpty(map))
		return;

	String condition = getCondition(map);

	LOG.info("condition is {}", condition);

	if (StringUtils.isNullOrWhitespaceOnly(condition)) {
		LOG.error("condition is null !!!");
		return;

	}
	StringBuffer sb = new StringBuffer();


	String sql = String.format("insert overwrite `%s` select * from `%s` where %s", context.tableName(), context.tableName(), condition);

	String text = String.format("%%flink.bsql\n%s ;", sql);
	String title = String.format("combine small file");

	String paragraphId = createParagraph(title, text);

	if (StringUtils.isNullOrWhitespaceOnly(paragraphId)) {
		LOG.error("paragraphId is null !!!");
		return;
	}

	String status = runParagraph(paragraphId);
	if ("OK".equals(status)) {
		return;
	} else {
		LOG.error("run paragraph error !!! status is {}", status);
	}

}

private String getCondition(Map<String, String> map) throws ParseException {
	StringBuffer sb = new StringBuffer();


	//因为知道分区是怎么样的,所以先写死,改起来也简单,先不改了
	try {
		String dt = map.get("dt");
		String hr = map.get("hr");
		String mi = map.get("mi");
		String datetime = String.format("%s %s:%s:00", dt, hr, mi);
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		Date date = format.parse(datetime);
		long timestamp = date.getTime() - 1 * 3600 * 1000 ;
		String finalDate = format.format(timestamp);

		dt = finalDate.substring(0, 10);
		hr = finalDate.substring(11, 13);
		mi = finalDate.substring(14, 16);

		sb.append("dt = '").append(dt).append("' and ").append("hr = '").append(hr).append("' and ").append("mi = '").append(mi).append("' ;");

	} catch (Exception e) {
		LOG.error("get condition error !!!,e is ", e);

	}


	return sb.toString();
}


private String createParagraph(String title, String text) {
	HttpURLConnection connection = null;
	StringBuilder result = null;
	ObjectMapper objectMapper = null;
	String paragraphId = null;
	try {
		URL url = new URL(URI_CREATE);
		connection = (HttpURLConnection) url.openConnection();

		connection.setDoInput(true); // 设置可输入
		connection.setDoOutput(true); // 设置该连接是可以输出的
		connection.setRequestMethod("POST"); // 设置请求方式
		connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");

		objectMapper = new ObjectMapper();
		Map<String, Object> data = new HashMap<String, Object>();
		data.put("title", title);
		data.put("text", text);
		PrintWriter pw = new PrintWriter(new BufferedOutputStream(connection.getOutputStream()));
		pw.write(objectMapper.writeValueAsString(data));
		pw.flush();
		pw.close();

		BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
		String line = null;
		result = new StringBuilder();
		while ((line = br.readLine()) != null) { // 读取数据
			result.append(line + "\n");
		}

		LOG.info("create paragraph result is {}", result);
		Map resMap = objectMapper.readValue(result.toString(), HashMap.class);
		paragraphId = resMap.get("body").toString();

	} catch (Exception e) {
		LOG.error("create paragraph error !!! ,e is ,", e);
	} finally {
		if (connection!=null)
			connection.disconnect();
	}

	return paragraphId;


}


private String runParagraph(String paragraphId) {
	HttpURLConnection connection = null;
	ObjectMapper objectMapper = null;
	String status = null;

	try {
		URL url = new URL(URI_RUN + paragraphId);
		connection = (HttpURLConnection) url.openConnection();

		connection.setDoInput(true); // 设置可输入
		connection.setDoOutput(true); // 设置该连接是可以输出的
		connection.setRequestMethod("POST"); // 设置请求方式
		connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
		connection.connect();

		BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"));
		String line = null;
		StringBuilder result = new StringBuilder();
		while ((line = br.readLine()) != null) { // 读取数据
			result.append(line + "\n");
		}
		LOG.info("run paragraph result is {}", result);
		objectMapper = new ObjectMapper();
		Map resMap = objectMapper.readValue(result.toString(), HashMap.class);
		status = resMap.get("status").toString();
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		if (connection!=null)
			connection.disconnect();

	}

	return status;
}

public static void main(String[] args) throws Exception {
}

逻辑很简单,通过当前要提交的分区获取一小时之前的分区,然后组装成Sql提交到Zeppelin上新建个任务,再把刚才新建的任务给Run起来
这里面用到了Zeppelin的REST API,具体可以参考Zeppelin官网的文档REST API
要注意一点的是,执行Stream任务和Batch任务的Flink集群不能是同一个集群,否则会报错,因为Batch任务不支持checkpoint,所以建议使用Yarn模式+Notebook per note模式,这样每个Notebook会对应一个Flink集群,可以通过Yarn的特点来快速拉起一个集群
还有一点,Batch任务的Notebook需要先执行一下这个代码
%flink
btenv.getConfig().getConfiguration().setBoolean(“table.exec.hive.infer-source-parallelism”,false);
1
2
这个参数table.exec.hive.infer-source-parallelism的意思是让读Hive表的时候,Source的并行度不跟着Hive文件个数走;为啥加这个参数呢?因为如果不加的话,还是会有多个并行度去写出文件,假设读两个文件再写出两个文件,那么就起不到合并小文件的作用了。可以参考Source Parallelism Inference
然后我们打包,到${FLINK_SRC}/flink-connectors/flink-connector-hive这个目录下mvn clean install -Dcheckstyle.skip=true -Dmaven.test.skip=true -Drat.skip=true -Pscala-2.11,然后用target目录下的flink-connector-hive_2.11-1.11.0.jar覆盖{FLINK_HOME}/lib下的同名jar包
如果上面的步骤没啥问题的话,下面开始我们的验证阶段
验证
启动一个Hive Streaming Sink的任务

%flink.ssql(parallelism=2)
SET table.dynamic-table-options.enabled=TRUE;
insert into hive_table2 select user_id,theme_id,item_id,leaf_cate_id,cate_level1_id,clk_cnt,reach_time,DATE_FORMAT(ts, ‘yyyy-MM-dd’), DATE_FORMAT(ts, ‘HH’) ,DATE_FORMAT(ts, ‘mm’) from kafka_table /+ OPTIONS(‘scan.startup.mode’ = ‘earliest-offset’ )/
1
2
3
记得配置一下checkpoint,否则任务一直会是In-progress状态,无法提交分区

%flink.conf

checkpoint 配置

pipeline.time-characteristic EventTime
execution.checkpointing.interval 120000
execution.checkpointing.min-pause 60000
execution.checkpointing.timeout 60000
execution.checkpointing.externalized-checkpoint-retention RETAIN_ON_CANCELLATION

依赖jar包配置

flink.execution.packages org.apache.flink:flink-connector-kafka_2.11:1.11.0,org.apache.flink:flink-connector-kafka-base_2.11:1.11.0
flink.execution.jars /Users/dijie/apps/hadoop-2.7.5/share/hadoop/common/lib/hadoop-lzo.jar

任务启动之后让我们看一下HDFS上的文件
1

因为我设置了两个并行度,所以写出了两个小文件

我这边设置的是合并一小时之前的分区,所以,先休息会儿,喝一杯 95 年的 Java☕️ 。

等时间到了之后,我们再瞄一眼HDFS上的文件
2

可以看到,小文件成功被合并了!

再让我们看一眼Zeppelin上的合并任务吧!
3

哦?还发现个自己代码的Bug,多了一个分号,还好Zeppelin这边本身就有容错~留个坑,我找到问题在哪了,大家可以自己修改一下

最后
最近被公众号Flink 中文社区约稿了,我写完这篇的时候还没发布,等发布了大家有兴趣可以看看,内容先神秘一下,留着大家自己一探究竟
然后大家也可以不必修改源码,可以自己搞个jar包,然后通过Zeppelin加载也行
不想自己编译也不想写代码的同学可以在这里下载到Flink 1.11.0版本的我改过源码的Flink-connector-hive的jar包(一直审核中,如果有需要可以留邮箱我给你发)

原文链接:https://blog.csdn.net/weixin_47482194/article/details/107696201?utm_medium=distribute.pc_feed.none-task-blog-personrec_tag-3.nonecase&depth_1-utm_source=distribute.pc_feed.none-task-blog-personrec_tag-3.nonecase&request_id=5f2489eb2405af26f814c7ed

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值