我们目前正在探索Apache Spark(使用Hadoop)以执行大规模 数据转换(使用Java)。
我们正在使用新的(和实验性的)DataSourceV2接口来构建我们的自定义
输出数据文件。其中的一个部分是org.apache.spark.sql.sources.v2.writer.DataWriter
的实现
接口。除了一个问题外,一切都运转良好:
org.apache.spark.sql.sources.v2.writer.DataWriter.write(record)
方法通常(但不总是)
为相同的输入记录调用了两次。
我希望这里有足够的代码让您了解我们正在做的事情:
基本上,我们有许多通过Spark应用程序登陆的输入数据集 使用类似于以下代码的代码将其放入Hadoop表:
final Dataset<Row> jdbcTableDataset = sparkSession.read()
.format("jdbc")
.option("url", sqlServerUrl)
.option("dbtable", tableName)
.option("user", jdbcUser)
.option("password", jdbcPassword)
.load();
final DataFrameWriter<Row> dataFrameWriter = jdbcTableDataset.write();
dataFrameWriter.save(hdfsDestination + "/" + tableName);
这些表的价值大概有五十个。我知道没有重复
在数据中,因为dataFrameWriter.count()
和dataFrameWriter.distinct().count()
返回相同的值。
转换过程包括对这些表执行联接操作并编写
结果以自定义格式保存到(共享)文件系统中的文件中。结果行包含唯一键,
dataGroup
列,dataSubGroup
列和其他40列。选择的记录是
由dataGroup
,dataSubGroup
和键排序。
每个输出文件都由dataGroup
列来区分,该列用于对write
操作进行分区:
final Dataset<Row> selectedData = dataSelector.selectData();
selectedData
.write()
.partitionBy("dataGroup")
.format("au.com.mycompany.myformat.DefaultSource")
.save("/path/to/shared/directory/");
为使您对规模有所了解,最终选择的数据包括五千六百万
记录,在大约3000 dataGroup
个文件之间分配不均。大,但不大。
partitionBy("dataGroup")
可以确保每个dataGroup
文件都由一个
单执行人。到目前为止一切顺利。
我的数据源实现了新的(和实验性的)DataSourceV2接口:
package au.com.mycompany.myformat;
import java.io.Serializable;
import java.util.Optional;
import org.apache.spark.sql.SaveMode;
import org.apache.spark.sql.sources.DataSourceRegister;
import org.apache.spark.sql.sources.v2.DataSourceOptions;
import org.apache.spark.sql.sources.v2.WriteSupport;
import org.apache.spark.sql.sources.v2.writer.DataSourceWriter;
import org.apache.spark.sql.types.StructType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefaultSource implements DataSourceRegister, WriteSupport , Serializable {
private static final Logger logger = LoggerFactory.getLogger(DefaultSource.class);
public DefaultSource() {
logger.info("created");
}
@Override
public String shortName() {
logger.info("shortName");
return "myformat";
}
@Override
public Optional<DataSourceWriter> createWriter(String writeUUID, StructType schema, SaveMode mode, DataSourceOptions options) {
return Optional.of(new MyFormatSourceWriter(writeUUID, schema, mode, options));
}
}
有一个DataSourceWriter
实现:
public class MyFormatSourceWriter implements DataSourceWriter, Serializable {
...
}
和DataSourceWriterFactory
实现:
public class MyDataWriterFactory implements DataWriterFactory<InternalRow> {
...
}
,最后是DataWriter
实现。似乎创建了DataWriter
并将其发送到
每个执行者。因此,每个DataWriter
将处理许多数据组。
每个记录都有一个唯一的键列。
public class MyDataWriter implements DataWriter<InternalRow>, Serializable {
private static final Logger logger = LoggerFactory.getLogger(XcdDataWriter.class);
...
MyDataWriter(File buildDirectory, StructType schema, int partitionId) {
this.buildDirectory = buildDirectory;
this.schema = schema;
this.partitionId = partitionId;
logger.debug("Created MyDataWriter for partition {}", partitionId);
}
private String getFieldByName(InternalRow row, String fieldName) {
return Optional.ofNullable(row.getUTF8String(schema.fieldIndex(fieldName)))
.orElse(UTF8String.EMPTY_UTF8)
.toString();
}
/**
* Rows are written here. Each row has a unique key column as well as a dataGroup
* column. Right now we are frequently getting called with the same record twice.
*/
@Override
public void write(InternalRow record) throws IOException {
String nextDataFileName = getFieldByName(record, "dataGroup") + ".myExt";
// some non-trivial logic for determining the right output file
...
// write the output record
outputWriter.append(getFieldByName(row, "key")).append(',')
.append(getFieldByName(row, "prodDate")).append(',')
.append(getFieldByName(row, "nation")).append(',')
.append(getFieldByName(row, "plant")).append(',')
...
}
@Override
public WriterCommitMessage commit() throws IOException {
...
outputWriter.close();
...
logger.debug("Committed partition {} with {} data files for zip file {} for a total of {} zip files",
partitionId, dataFileCount, dataFileName, dataFileCount);
return new MyWriterCommitMessage(partitionId, dataFileCount);
}
@Override
public void abort() throws IOException {
logger.error("Failed to collect data for schema: {}", schema);
...
}
}
现在,我正在通过跟踪最后处理的密钥并忽略该密钥来解决此问题 重复。