主要maven依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-filesystem_2.11</artifactId>
<version>${flink.version}</version>
</dependency>
先上代码
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.fs.bucketing.BucketingSink;
import org.apache.flink.streaming.connectors.fs.bucketing.DateTimeBucketer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer010;
import java.time.ZoneId;
import java.util.Properties;
public class WriteToHDFS {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
ParameterTool params = ParameterTool.fromArgs(args);
env.enableCheckpointing(30000L);
//消费kafka
String topic = "MyTestData";
Properties prop = new Properties();
prop.setProperty("bootstrap.servers", "node223:6667,node224:6667,node225:6667");
prop.setProperty("group.id", "xxxx");
FlinkKafkaConsumer010<String> myConsumer = new FlinkKafkaConsumer010<>(topic, new SimpleStringSchema(), prop);
DataStreamSource<String> dataSource = env.addSource(myConsumer);
BucketingSink<String> sink = new BucketingSink<>(params.get("path","/user/root/flink/writetohdfs"));
//创建一个按照时间创建目录的bucketer,默认是yyyy-MM-dd--HH,时区默认是美国时间。这里我都改了,一天创建一次目录,上海时间
sink.setBucketer(new DateTimeBucketer<String>("yyyy-MM-dd", ZoneId.of("Asia/Shanghai")));
/**
* 设置大小和滚动时间,只要满足一个,就会滚动,和flume差不多
*/
//设置每个文件的最大大小 ,默认是384M(1024 * 1024 * 384)
sink.setBatchSize(1024 * 20); //this is 20KB
//设置多少时间,就换一个文件写 但是ms 这里我设置的是 一个小时
sink.setBatchRolloverInterval(1000 * 60 * 60);
// sink.setPendingSuffix("ccc");
// sink.setInactiveBucketThreshold(60 * 1000L);
// sink.setInactiveBucketCheckInterval(60 * 1000L);
// sink.setAsyncTimeout(60 * 1000);
dataSource.addSink(sink);
env.execute("WriteToHDFS");
}
}
源码分析
1.写入HDFS时,不用加其他HDFS的依赖,只要是master是yarn-cluster即可
2.使用的是FSDataOutputStream,数据写入时,会写入缓冲区,放在内存中
3.每次做checkpoint的时候,会flush缓存区的数据,以及将pending(已经完成的文件,但未被checkpoint记录,可以通过sink.setPendingSuffix("ccc"); 设置)结尾的文件,记录下来
4.每60秒(可以通过sink.setInactiveBucketCheckInterval(60 * 1000L);去设置) 检测,如果一个文件的FSDataOutputStream在60秒内(可以通过sink.setInactiveBucketThreshold(60 * 1000L);去设置),都没有接受的数据,就会被认为是不活跃的bucket(可以认为是文件),那么就会flush然后关闭该文件。
这里如果说的再深入一点就是,实际上只是在processingTimeService中注册了当前时间 + inactiveBucketCheckInterval(60秒不写入时间),然后有个onProcessingTime方法去时时刻刻的判断是否满足60秒不写入,同时也会判断是否到了滚动时间。当然通俗的讲,也可以认为是每60秒去检测一次。源码如下:
@Override
public void onProcessingTime(long timestamp) throws Exception {
long currentProcessingTime = processingTimeService.getCurrentProcessingTime();
closePartFilesByTime(currentProcessingTime);
processingTimeService.registerTimer(currentProcessingTime + inactiveBucketCheckInterval, this);
}
5.flink内部封装了一个Map<String, BucketState<T>> bucketStates = new HashMap<>(); 用来记录当前正在使用的文件,key是文件的路径,BucketState内部封装了该文件的所有信息,包括创建时间,最后一次写入时间(这里的写入指的是写入缓存区的时间,不是flush的时间),文件大小,当前文件是打开还是关闭,写缓冲区的方法。都在这里。每次flink要对文件进行操作的时候,都会从这里拿到文件的封装对象
6.当程序别cancel的时候,当前正在操作的文件,会被flush,关闭。然后将文件的后缀名从in-progress改为pending。这个前后缀都能设置,但如果没有什么特殊需求,默认即可。这里拿文件,用的就是5里面说的bucketStates这个map。它在close方法中,会去遍历这个map,去做上述的操作。
@Override
public void close() throws Exception {
if (state != null) {
for (Map.Entry<String, BucketState<T>> entry : state.bucketStates.entrySet()) {
closeCurrentPartFile(entry.getValue());
}
}
}
7.每次写入的时候,都是会bucketStates这个map中获取对应的对象,如果没有,就新建一个该对象。然后先判断是否需要滚动(通过当前文件大小和滚动时间去判断),然后才将数据写入缓冲区,更新最后写入时间。
@Override
public void invoke(T value) throws Exception {
Path bucketPath = bucketer.getBucketPath(clock, new Path(basePath), value);
long currentProcessingTime = processingTimeService.getCurrentProcessingTime();
BucketState<T> bucketState = state.getBucketState(bucketPath);
if (bucketState == null) {
bucketState = new BucketState<>(currentProcessingTime);
state.addBucketState(bucketPath, bucketState);
}
if (shouldRoll(bucketState, currentProcessingTime)) {
openNewPartFile(bucketPath, bucketState);
}
bucketState.writer.write(value);
bucketState.lastWrittenToTime = currentProcessingTime;
}
8.写入和关闭HDFS是通过异步的方式的,异步的超时时间默认是60秒,可以通过 sink.setAsyncTimeout(60 * 1000)去设置。
比较有意思的是,这里判断是否超时,用的是while 循环的方式,每500ms判断是否关闭完成,或者写入完成。
//判断关闭的
boolean isclosed = dfs.isFileClosed(partPath);
StopWatch sw = new StopWatch();
sw.start();
while (!isclosed) {
if (sw.getTime() > asyncTimeout) {
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e1) {
// ignore it
}
isclosed = dfs.isFileClosed(partPath);
}
//判断写入的
StopWatch sw = new StopWatch();
sw.start();
long newLen = fs.getFileStatus(partPath).getLen();
while (newLen != validLength) {
if (sw.getTime() > asyncTimeout) {
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e1) {
// ignore it
}
newLen = fs.getFileStatus(partPath).getLen();
}
目前初步的就先写到这里,欢迎大家来探讨。。