其实大多数场景下,各种大数据框架预定义的InputFormat(数据读取器)是够用的,除了一些比较特殊的情况,特殊的数据格式,我们才会需要自定义读取数据的方式。
然后有一天,我在接入一个hdfs上gz格式数据的时候,遇到了一个报错:
仔细看了报错,是输入流在read数据的时候,调用LineRecordReader的nextKeyValue方法报错了,百度了下,没有什么太准确的答案,大致来说是读取的gz文件块异常,但是这部分hadoop的原生代码,并没有异常的捕获或者往外抛,导致遇到异常,spark任务直接挂掉,所以,有没有办法丢弃损坏的数据,继续往后读呢?
所以我们就需要自定义InputFormat,然后在关键关键位置做异常处理(LineRecordReader的nextKeyValue)
1、使用spark的默认的方式如下(无法处理异常):
//如果该路径下有多个文件,都可以读到
Dataset<String> stringDataset = spark.read().textFile(路径);
2、解决异常:
-1.自定义InputFormat数据读取方式
//new Configuration()这个是引入org.apache.hadoop.conf.Configuration
//数据返回的是rdd<偏移量,具体数据>,处理数据的时候,直接拿具体数据处理就好了
JavaRDD<Tuple2<LongWritable, Text>> tuple2RDD = spark
.sparkContext()
.newAPIHadoopFile(路径,
TextInputFormatSelf.class,
LongWritable.class,
Text.class,
new Configuration())
.toJavaRDD();
-2.自定义的inputFormat
import com.google.common.base.Charsets;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
public class TextInputFormatSelf extends TextInputFormat {
@Override
public RecordReader<LongWritable, Text> createRecordReader(InputSplit split, TaskAttemptContext context) {
String delimiter = context.getConfiguration().get(
"textinputformat.record.delimiter");
byte[] recordDelimiterBytes = null;
if (null != delimiter)
recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
return new LineRecordReaderSelf(recordDelimiterBytes);
}
@Override
protected boolean isSplitable(JobContext context, Path file) {
return super.isSplitable(context, file);
}
}
-3.自定义LineRecordReader
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.Seekable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.Decompressor;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.LineRecordReader;
import org.apache.hadoop.util.LineReader;
import java.io.IOException;
public class LineRecordReaderSelf extends LineRecordReader {
private static final Log LOG = LogFactory.getLog(LineRecordReaderSelf.class);
private long start;
private long pos;
private long end;
private LineReader in;
private FSDataInputStream fileIn;
private Seekable filePosition;
private int maxLineLength;
private LongWritable key;
private Text value;
private boolean isCompressedInput;
private Decompressor decompressor;
private byte[] recordDelimiterBytes;
public LineRecordReaderSelf() {
}
public LineRecordReaderSelf(byte[] recordDelimiter) {
this.recordDelimiterBytes = recordDelimiter;
}
@Override
public boolean nextKeyValue() throws IOException {
boolean flag = false;
try {
flag = super.nextKeyValue();
}catch (Exception e){
LOG.error("============解析gz压缩文件异常==============");
//e.printStackTrace();
LOG.error(e.getMessage());
}
return flag;
}
private int maxBytesToConsume(long pos) {
return isCompressedInput
? Integer.MAX_VALUE
: (int) Math.min(Integer.MAX_VALUE, end - pos);
}
private long getFilePosition() throws IOException {
long retVal;
if (isCompressedInput && null != filePosition) {
retVal = filePosition.getPos();
} else {
retVal = pos;
}
return retVal;
}
@Override
public void initialize(InputSplit genericSplit, TaskAttemptContext context) throws IOException {
super.initialize(genericSplit, context);
}
@Override
public LongWritable getCurrentKey() {
return super.getCurrentKey();
}
@Override
public Text getCurrentValue() {
return super.getCurrentValue();
}
@Override
public float getProgress() throws IOException {
return super.getProgress();
}
@Override
public synchronized void close() throws IOException {
super.close();
}
}
可以看到我基本什么方法都是继承父类的,唯一就是加了trycatch,所以这代码简陋到啥都没做,就是捕获了异常,为了让spark不会挂掉,当然如果你有其他想法,可以尝试添加更多的东西。举例:想找到异常数据的原因,是否可以将异常数据存到其他地方统一人工处理。
==========================再更新下=============================
有朋友说遇到如下的报错:
我们经过讨论后,觉得有两个方法
第一:
这个Could not obtain block:XXX报错,是hdfs的文件块有损坏缺快的问题,讲道理,应该去修复有异常的块来解决这个问题,基本任何框架读这个块的时候都会出问题,并不是spark的问题
第二:
但是如果我就是想跳过无视这个块,该怎么做呢,注意看报错,报错的是initialize,也就是说在做初始化的时候报错了
那讲道理,我们也可以在初始化方法的时候加上trycatch跳过这个文件块