![](/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.