Hadoopジョブを毎日実行するときに、既存の出力ディレクトリを上書き/再利用したい。実際、出力ディレクトリには、毎日のジョブ実行結果の要約された出力が格納されます。同じ出力ディレクトリを指定すると、「出力ディレクトリが既に存在します」というエラーが発生します。
この検証をバイパスする方法は?
ジョブを実行する前にディレクトリを削除するのはどうですか?
シェルを介してこれを行うことができます:
hadoop fs -rmr /path/to/your/output/
またはJava API:
// configuration should contain reference to your namenode
FileSystem fs = FileSystem.get(new Configuration());
// true stands for recursively deleting the folder you gave
fs.delete(new Path("/path/to/your/output"), true);
ジャンブルートの答えはあなたの直接的な解決策です。私が個人的に削除する自動化されたプロセスを信頼することは決してないので、別の方法を提案します。
上書きしようとする代わりに、実行時間を含めて、ジョブの出力名を動的にすることをお勧めします。
「/path/to/your/output-2011-10-09-23-04/
」のようなもの。このようにして、再訪問が必要になった場合に備えて、古いジョブの出力を保持できます。毎日10件以上のジョブを実行する私のシステムでは、出力を/output/job1/2011/10/09/job1out/part-r-xxxxx
、/output/job1/2011/10/10/job1out/part-r-xxxxx
などに構成します。 。
HadoopのTextInputFormat
(使用していると思います)では、既存のディレクトリを上書きできません。おそらく、あなた(そしてあなたのクラスター)が非常に懸命に取り組んだ何かを誤って削除してしまったことを発見する苦痛を許してしまうでしょう。
ただし、出力フォルダーをジョブで上書きしたい場合は、TextOutputFormat
を次のように少し変更するのが最もクリーンな方法だと思います。
_public class OverwriteTextOutputFormat<K, V> extends TextOutputFormat<K, V>
{
public RecordWriter<K, V>
getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException
{
Configuration conf = job.getConfiguration();
boolean isCompressed = getCompressOutput(job);
String keyValueSeparator= conf.get("mapred.textoutputformat.separator","\t");
CompressionCodec codec = null;
String extension = "";
if (isCompressed)
{
Class<? extends CompressionCodec> codecClass =
getOutputCompressorClass(job, GzipCodec.class);
codec = (CompressionCodec) ReflectionUtils.newInstance(codecClass, conf);
extension = codec.getDefaultExtension();
}
Path file = getDefaultWorkFile(job, extension);
FileSystem fs = file.getFileSystem(conf);
FSDataOutputStream fileOut = fs.create(file, true);
if (!isCompressed)
{
return new LineRecordWriter<K, V>(fileOut, keyValueSeparator);
}
else
{
return new LineRecordWriter<K, V>(new DataOutputStream(codec.createOutputStream(fileOut)),keyValueSeparator);
}
}
}
_
これで、overwrite = trueを使用してFSDataOutputStream
(fs.create(file, true)
)を作成しています。
Hadoopは、ジョブへの複数の入力パスを許可することで、達成しようとしている効果をすでにサポートしています。ファイルを追加するファイルの単一のディレクトリを作成する代わりに、新しいディレクトリを追加するディレクトリのディレクトリを作成します。集計結果を入力として使用するには、入力グロブをサブディレクトリのワイルドカードとして指定するだけです(例:my-aggregate-output/*
)。新しいデータをアグリゲートに出力として「追加」するには、単にアグリゲートの新しい一意のサブディレクトリを出力ディレクトリとして指定します。通常、タイムスタンプまたは入力データから派生したシーケンス番号を使用します(例:my-aggregate-output/20140415154424
)。
Hadoopは哲学に従います1回書き込み、何度も読み取ります。したがって、もう一度ディレクトリに書き込もうとすると、新しいディレクトリを作成する必要がある(1回書き込み)と見なされますが、すでに存在しているため、文句を言う。 hadoop fs -rmr /path/to/your/output/
から削除できます。データを保持するために、(タイムスタンプまたはハッシュ値に基づいて)動的ディレクトリを作成することをお勧めします。
私は同様のユースケースを持っていました、私はこれを解決するためにMultipleOutputs
を使用しました。
たとえば、異なるMapReduceジョブが同じディレクトリ/outputDir/
に書き込む場合。ジョブ1は/outputDir/job1-part1.txt
に書き込み、ジョブ2は/outputDir/job1-part2.txt
に書き込みます(既存のファイルを削除せずに)。
メインで、出力ディレクトリをランダムに設定します(新しいジョブを実行する前に削除できます)
FileInputFormat.addInputPath(job, new Path("/randomPath"));
レデューサー/マッパーでMultipleOutputs
を使用し、目的のディレクトリに書き込むようにライターを設定します。
public void setup(Context context) {
MultipleOutputs mos = new MultipleOutputs(context);
}
そして:
mos.write(key, value, "/outputDir/fileOfJobX.txt")
しかし、私のユースケースはそれより少し複雑でした。同じフラットディレクトリに書き込むだけの場合は、別のディレクトリに書き込み、次のようなスクリプトを実行してファイルを移行できます:hadoop fs -mv /tmp/* /outputDir
私の使用例では、各MapReduceジョブは、書き込み中のメッセージの値に基づいて、異なるサブディレクトリに書き込みます。ディレクトリ構造は、次のように多層にすることができます。
/outputDir/
messageTypeA/
messageSubTypeA1/
job1Output/
job1-part1.txt
job1-part2.txt
...
job2Output/
job2-part1.txt
...
messageSubTypeA2/
...
messageTypeB/
...
各Mapreduceジョブは、数千のサブディレクトリに書き込むことができます。また、tmpディレクトリに書き込み、各ファイルを正しいディレクトリに移動するコストは高くなります。
私はこの正確な問題に遭遇しました、それはクラスcheckOutputSpecs
のFileOutputFormat
で発生した例外に起因します。私の場合、既存のディレクトリにファイルを追加する多くのジョブを実行したかったので、ファイルに一意の名前が付けられることを保証しました。
checkOutputSpecs
メソッドのみをオーバーライドし、ディレクトリがすでに存在するかどうかをチェックする場所でスローされるFileAlreadyExistsException
を窒息(無視)する出力フォーマットクラスを作成することで、これを解決しました。
public class OverwriteTextOutputFormat<K, V> extends TextOutputFormat<K, V> {
@Override
public void checkOutputSpecs(JobContext job) throws IOException {
try {
super.checkOutputSpecs(job);
}catch (FileAlreadyExistsException ignored){
// Suffocate the exception
}
}
}
そして、ジョブ構成では、LazyOutputFormat
とMultipleOutputs
も使用しました。
LazyOutputFormat.setOutputFormatClass(job, OverwriteTextOutputFormat.class);
ローカルファイルシステムから入力ファイルを(たとえば、追加されたエントリとともに)ロードして、Hadoop分散ファイルシステムにロードしている場合:
hdfs dfs -put /mylocalfile /user/cloudera/purchase
次に、既存の出力ディレクトリを-f
で上書き/再利用することもできます。フォルダを削除または再作成する必要はありません
hdfs dfs -put -f /updated_mylocalfile /user/cloudera/purchase
時間ごとに実行ごとに出力サブディレクトリを作成できます。たとえば、ユーザーからの出力ディレクトリを想定しているとすると、次のように設定します。
FileOutputFormat.setOutputPath(job, new Path(args[1]);
これを次の行で変更します。
String timeStamp = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss", Locale.US).format(new Timestamp(System.currentTimeMillis()));
FileOutputFormat.setOutputPath(job, new Path(args[1] + "/" + timeStamp));