![](/img/trans.png)
[英]Write to multiple outputs by key Scalding Hadoop, one MapReduce Job
[英]Write to multiple outputs by key Spark - one Spark job
如何在单个Job中使用Spark写入取决于键的多个输出。
相关: 通过键扩展Hadoop(一个MapReduce作业)写入多个输出
例如
sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
.writeAsMultiple(prefix, compressionCodecOption)
将确保cat prefix/1
为
a
b
和cat prefix/2
将是
c
编辑:我最近添加了一个新的答案,其中包括完整的导入,pimp和压缩编解码器,请参阅https://stackoverflow.com/a/46118044/1586965 ,除了较早的答案外,它可能还会有所帮助。
如果使用Spark 1.4+,这要归功于DataFrame API ,它变得非常容易。 (DataFrames是在Spark 1.3中引入的,而我们所需的partitionBy()
是在1.4中引入的 。)
如果从RDD开始,则首先需要将其转换为DataFrame:
val people_rdd = sc.parallelize(Seq((1, "alice"), (1, "bob"), (2, "charlie")))
val people_df = people_rdd.toDF("number", "name")
在Python中,相同的代码是:
people_rdd = sc.parallelize([(1, "alice"), (1, "bob"), (2, "charlie")])
people_df = people_rdd.toDF(["number", "name"])
一旦有了DataFrame,就可以根据特定键写入多个输出,这很简单。 更重要的是-这就是DataFrame API的优点-Python,Scala,Java和R中的代码几乎相同:
people_df.write.partitionBy("number").text("people")
如果需要,您可以轻松使用其他输出格式:
people_df.write.partitionBy("number").json("people-json")
people_df.write.partitionBy("number").parquet("people-parquet")
在上述每个示例中,Spark将为我们对DataFrame进行分区的每个键创建一个子目录:
people/
_SUCCESS
number=1/
part-abcd
part-efgh
number=2/
part-abcd
part-efgh
我会这样做,可扩展
import org.apache.hadoop.io.NullWritable
import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.hadoop.mapred.lib.MultipleTextOutputFormat
class RDDMultipleTextOutputFormat extends MultipleTextOutputFormat[Any, Any] {
override def generateActualKey(key: Any, value: Any): Any =
NullWritable.get()
override def generateFileNameForKeyValue(key: Any, value: Any, name: String): String =
key.asInstanceOf[String]
}
object Split {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("Split" + args(1))
val sc = new SparkContext(conf)
sc.textFile("input/path")
.map(a => (k, v)) // Your own implementation
.partitionBy(new HashPartitioner(num))
.saveAsHadoopFile("output/path", classOf[String], classOf[String],
classOf[RDDMultipleTextOutputFormat])
spark.stop()
}
}
刚刚在上面看到了类似的答案,但实际上我们不需要自定义分区。 MultipleTextOutputFormat将为每个键创建文件。 可以将具有相同键的多个记录放入同一分区。
新的HashPartitioner(num),其中num是所需的分区号。 如果您有大量不同的键,可以将数字设置为big。 在这种情况下,每个分区将不会打开太多的hdfs文件处理程序。
如果给定键可能有很多值,我认为可伸缩的解决方案是每个分区的每个键写一个文件。 不幸的是,Spark中没有对此的内置支持,但是我们可以提高一些。
sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
.mapPartitionsWithIndex { (p, it) =>
val outputs = new MultiWriter(p.toString)
for ((k, v) <- it) {
outputs.write(k.toString, v)
}
outputs.close
Nil.iterator
}
.foreach((x: Nothing) => ()) // To trigger the job.
// This one is Local, but you could write one for HDFS
class MultiWriter(suffix: String) {
private val writers = collection.mutable.Map[String, java.io.PrintWriter]()
def write(key: String, value: Any) = {
if (!writers.contains(key)) {
val f = new java.io.File("output/" + key + "/" + suffix)
f.getParentFile.mkdirs
writers(key) = new java.io.PrintWriter(f)
}
writers(key).println(value)
}
def close = writers.values.foreach(_.close)
}
(将PrintWriter
替换为您选择的分布式文件系统操作。)
这样就可以在RDD上进行一次遍历,而不会进行随机播放。 它为每个键提供一个目录,每个目录中包含许多文件。
这包括请求的编解码器,必要的导入和请求的皮条客。
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SQLContext
// TODO Need a macro to generate for each Tuple length, or perhaps can use shapeless
implicit class PimpedRDD[T1, T2](rdd: RDD[(T1, T2)]) {
def writeAsMultiple(prefix: String, codec: String,
keyName: String = "key")
(implicit sqlContext: SQLContext): Unit = {
import sqlContext.implicits._
rdd.toDF(keyName, "_2").write.partitionBy(keyName)
.format("text").option("codec", codec).save(prefix)
}
}
val myRdd = sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c")))
myRdd.writeAsMultiple("prefix", "org.apache.hadoop.io.compress.GzipCodec")
与OP的一个细微差别是它将<keyName>=
作为目录名称的前缀。 例如
myRdd.writeAsMultiple("prefix", "org.apache.hadoop.io.compress.GzipCodec")
将给出:
prefix/key=1/part-00000
prefix/key=2/part-00000
其中prefix/my_number=1/part-00000
将包含行a
和b
,而prefix/my_number=2/part-00000
将包含行c
。
和
myRdd.writeAsMultiple("prefix", "org.apache.hadoop.io.compress.GzipCodec", "foo")
将给出:
prefix/foo=1/part-00000
prefix/foo=2/part-00000
应该清楚如何编辑parquet
。
最后,下面是Dataset
的示例,它也许比使用Tuples更好。
implicit class PimpedDataset[T](dataset: Dataset[T]) {
def writeAsMultiple(prefix: String, codec: String, field: String): Unit = {
dataset.write.partitionBy(field)
.format("text").option("codec", codec).save(prefix)
}
}
我有类似的需求,找到了一种方法。 但这有一个缺点(对我而言,这不是问题):您需要使用每个输出文件一个分区对数据进行重新分区。
要以这种方式进行分区,通常需要事先知道作业将输出多少文件,并找到将每个键映射到每个分区的函数。
首先,让我们创建基于MultipleTextOutputFormat的类:
import org.apache.hadoop.mapred.lib.MultipleTextOutputFormat
class KeyBasedOutput[T >: Null, V <: AnyRef] extends MultipleTextOutputFormat[T , V] {
override def generateFileNameForKeyValue(key: T, value: V, leaf: String) = {
key.toString
}
override protected def generateActualKey(key: T, value: V) = {
null
}
}
有了这个类,Spark将从一个分区(我想是第一个/最后一个)中获取一个密钥,并用该密钥命名文件,因此在同一分区上混合多个密钥是不好的。
对于您的示例,您将需要一个自定义分区程序。 这将完成工作:
import org.apache.spark.Partitioner
class IdentityIntPartitioner(maxKey: Int) extends Partitioner {
def numPartitions = maxKey
def getPartition(key: Any): Int = key match {
case i: Int if i < maxKey => i
}
}
现在,让我们将所有内容放在一起:
val rdd = sc.makeRDD(Seq((1, "a"), (1, "b"), (2, "c"), (7, "d"), (7, "e")))
// You need to know the max number of partitions (files) beforehand
// In this case we want one partition per key and we have 3 keys,
// with the biggest key being 7, so 10 will be large enough
val partitioner = new IdentityIntPartitioner(10)
val prefix = "hdfs://.../prefix"
val partitionedRDD = rdd.partitionBy(partitioner)
partitionedRDD.saveAsHadoopFile(prefix,
classOf[Integer], classOf[String], classOf[KeyBasedOutput[Integer, String]])
这将在前缀(名称分别为1、2和7)下生成3个文件,一次处理所有文件。
如您所见,您需要有关密钥的一些知识才能使用此解决方案。
对我来说,这很容易,因为每个密钥哈希都需要一个输出文件,并且文件的数量在我的控制之下,所以我可以使用常规的HashPartitioner来完成此任务。
我在Java中也需要同样的东西。 将我对Zhang Zhan的Scala答案的翻译发布给Spark Java API用户:
import org.apache.hadoop.mapred.lib.MultipleTextOutputFormat;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaSparkContext;
import scala.Tuple2;
import java.util.Arrays;
class RDDMultipleTextOutputFormat<A, B> extends MultipleTextOutputFormat<A, B> {
@Override
protected String generateFileNameForKeyValue(A key, B value, String name) {
return key.toString();
}
}
public class Main {
public static void main(String[] args) {
SparkConf conf = new SparkConf()
.setAppName("Split Job")
.setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
String[] strings = {"Abcd", "Azlksd", "whhd", "wasc", "aDxa"};
sc.parallelize(Arrays.asList(strings))
// The first character of the string is the key
.mapToPair(s -> new Tuple2<>(s.substring(0,1).toLowerCase(), s))
.saveAsHadoopFile("output/", String.class, String.class,
RDDMultipleTextOutputFormat.class);
sc.stop();
}
}
saveRDC数据是基于RDD数据实现的,特别是通过以下方法实现的: PairRDD.saveAsHadoopDataset ,该方法从PairRdd中获取执行数据。 我看到两个可能的选择:如果数据较小,则可以通过对RDD进行分组,从每个集合中创建一个新的RDD并使用该RDD写入数据来节省一些实现时间。 像这样:
val byKey = dataRDD.groupByKey().collect()
val rddByKey = byKey.map{case (k,v) => k->sc.makeRDD(v.toSeq)}
val rddByKey.foreach{ case (k,rdd) => rdd.saveAsText(prefix+k}
请注意,它不适用于大型数据集b / c, v.toSeq
处的迭代器的v.toSeq
可能不适合内存。
我看到的另一个选项(在这种情况下实际上是我推荐的)是:通过直接调用hadoop / hdfs api自己滚动。
这是我在研究此问题时开始的讨论: 如何从另一个RDD创建RDD?
我有一个类似的用例,其中我根据一个密钥(每个密钥1个文件)将Hadoop HDFS上的输入文件拆分为多个文件。 这是我的火花的scala代码
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
val hadoopconf = new Configuration();
val fs = FileSystem.get(hadoopconf);
@serializable object processGroup {
def apply(groupName:String, records:Iterable[String]): Unit = {
val outFileStream = fs.create(new Path("/output_dir/"+groupName))
for( line <- records ) {
outFileStream.writeUTF(line+"\n")
}
outFileStream.close()
}
}
val infile = sc.textFile("input_file")
val dateGrouped = infile.groupBy( _.split(",")(0))
dateGrouped.foreach( (x) => processGroup(x._1, x._2))
我已经根据密钥对记录进行了分组。 每个密钥的值都写入单独的文件。
如果您有多列并且要保存所有其他未按csv格式分区的列,这对于python用户来说是个好消息,如果您使用Nick的建议使用“文本”方法,则这将失败。
people_df.write.partitionBy("number").text("people")
错误消息是“ AnalysisException:u'Text数据源仅支持一列,而您有2列。
在spark 2.0.0中(我的测试环境是hdp的spark 2.0.0),软件包“ com.databricks.spark.csv”现在集成了,它允许我们保存仅一列分区的文本文件,请参见示例打击:
people_rdd = sc.parallelize([(1,"2016-12-26", "alice"),
(1,"2016-12-25", "alice"),
(1,"2016-12-25", "tom"),
(1, "2016-12-25","bob"),
(2,"2016-12-26" ,"charlie")])
df = people_rdd.toDF(["number", "date","name"])
df.coalesce(1).write.partitionBy("number").mode("overwrite").format('com.databricks.spark.csv').options(header='false').save("people")
[root@namenode people]# tree
.
├── number=1
│?? └── part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
├── number=2
│?? └── part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
└── _SUCCESS
[root@namenode people]# cat number\=1/part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
2016-12-26,alice
2016-12-25,alice
2016-12-25,tom
2016-12-25,bob
[root@namenode people]# cat number\=2/part-r-00000-6bd1b9a8-4092-474a-9ca7-1479a98126c2.csv
2016-12-26,charlie
在我的Spark 1.6.1环境中,代码没有引发任何错误,但是仅生成了一个文件。 它没有被两个文件夹分区。
希望这会有所帮助。
我有一个类似的用例。 我通过编写两个实现MultipleTextOutputFormat
和RecordWriter
自定义类在Java中解决了该RecordWriter
。
我的输入是JavaPairRDD<String, List<String>>
,我想将其存储在以其键命名的文件中,其值中包含所有行。
这是我的MultipleTextOutputFormat
实现的代码
class RDDMultipleTextOutputFormat<K, V> extends MultipleTextOutputFormat<K, V> {
@Override
protected String generateFileNameForKeyValue(K key, V value, String name) {
return key.toString(); //The return will be used as file name
}
/** The following 4 functions are only for visibility purposes
(they are used in the class MyRecordWriter) **/
protected String generateLeafFileName(String name) {
return super.generateLeafFileName(name);
}
protected V generateActualValue(K key, V value) {
return super.generateActualValue(key, value);
}
protected String getInputFileBasedOutputFileName(JobConf job, String name) {
return super.getInputFileBasedOutputFileName(job, name);
}
protected RecordWriter<K, V> getBaseRecordWriter(FileSystem fs, JobConf job, String name, Progressable arg3) throws IOException {
return super.getBaseRecordWriter(fs, job, name, arg3);
}
/** Use my custom RecordWriter **/
@Override
RecordWriter<K, V> getRecordWriter(final FileSystem fs, final JobConf job, String name, final Progressable arg3) throws IOException {
final String myName = this.generateLeafFileName(name);
return new MyRecordWriter<K, V>(this, fs, job, arg3, myName);
}
}
这是我的RecordWriter
实现的代码。
class MyRecordWriter<K, V> implements RecordWriter<K, V> {
private RDDMultipleTextOutputFormat<K, V> rddMultipleTextOutputFormat;
private final FileSystem fs;
private final JobConf job;
private final Progressable arg3;
private String myName;
TreeMap<String, RecordWriter<K, V>> recordWriters = new TreeMap();
MyRecordWriter(RDDMultipleTextOutputFormat<K, V> rddMultipleTextOutputFormat, FileSystem fs, JobConf job, Progressable arg3, String myName) {
this.rddMultipleTextOutputFormat = rddMultipleTextOutputFormat;
this.fs = fs;
this.job = job;
this.arg3 = arg3;
this.myName = myName;
}
@Override
void write(K key, V value) throws IOException {
String keyBasedPath = rddMultipleTextOutputFormat.generateFileNameForKeyValue(key, value, myName);
String finalPath = rddMultipleTextOutputFormat.getInputFileBasedOutputFileName(job, keyBasedPath);
Object actualValue = rddMultipleTextOutputFormat.generateActualValue(key, value);
RecordWriter rw = this.recordWriters.get(finalPath);
if(rw == null) {
rw = rddMultipleTextOutputFormat.getBaseRecordWriter(fs, job, finalPath, arg3);
this.recordWriters.put(finalPath, rw);
}
List<String> lines = (List<String>) actualValue;
for (String line : lines) {
rw.write(null, line);
}
}
@Override
void close(Reporter reporter) throws IOException {
Iterator keys = this.recordWriters.keySet().iterator();
while(keys.hasNext()) {
RecordWriter rw = (RecordWriter)this.recordWriters.get(keys.next());
rw.close(reporter);
}
this.recordWriters.clear();
}
}
大多数代码与FileOutputFormat
中的代码完全相同。 唯一的区别是那几行
List<String> lines = (List<String>) actualValue;
for (String line : lines) {
rw.write(null, line);
}
这些行允许我将输入List<String>
每一行写在文件上。 为了避免将密钥写在每一行上, write
函数的第一个参数设置为null
。
最后,我只需要执行此调用即可写入文件
javaPairRDD.saveAsHadoopFile(path, String.class, List.class, RDDMultipleTextOutputFormat.class);
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.