Je souhaite écraser/réutiliser le répertoire de sortie existant lorsque j'exécute mon travail Hadoop quotidiennement. En fait, le répertoire de sortie stockera la sortie récapitulative des résultats d'exécution du travail de chaque jour. Si je spécifie le même répertoire de sortie cela donne l'erreur "le répertoire de sortie existe déjà".
Comment contourner cette validation?
Qu'en est-il de la suppression du répertoire avant d'exécuter le travail?
Vous pouvez le faire via Shell:
hadoop fs -rmr /path/to/your/output/
ou via l'API Java:
// 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);
La réponse de Jungblut est votre solution directe. Comme je ne fais jamais confiance aux processus automatisés pour supprimer des éléments (moi personnellement), je proposerai une alternative:
Au lieu d'essayer d'écraser, je vous suggère de rendre le nom de sortie de votre travail dynamique, y compris l'heure d'exécution.
Quelque chose comme "/path/to/your/output-2011-10-09-23-04/
". De cette façon, vous pouvez conserver votre ancien travail au cas où vous auriez besoin de revenir. Dans mon système, qui exécute plus de 10 tâches quotidiennes, nous structurons la sortie en: /output/job1/2011/10/09/job1out/part-r-xxxxx
, /output/job1/2011/10/10/job1out/part-r-xxxxx
, etc.
La variable TextInputFormat
de Hadoop (que je suppose que vous utilisez) ne permet pas de remplacer un répertoire existant. Probablement pour vous excuser de la peine de découvrir que vous avez supprimé par erreur quelque chose sur lequel vous (et votre cluster) avez travaillé très dur.
Cependant, si vous êtes certain de vouloir écraser le travail dans votre dossier de sortie, je pense que le moyen le plus propre est de changer TextOutputFormat
un peu comme ceci:
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);
}
}
}
Vous créez maintenant la FSDataOutputStream
(fs.create(file, true)
) avec overwrite = true.
Hadoop prend déjà en charge l'effet que vous semblez atteindre en autorisant plusieurs chemins d'entrée vers un travail. Au lieu d'essayer d'avoir un seul répertoire de fichiers dans lequel vous ajoutez d'autres fichiers, utilisez un répertoire de répertoires auquel vous ajoutez de nouveaux répertoires. Pour utiliser le résultat d'agrégat en tant qu'entrée, spécifiez simplement le glob d'entrée en tant que caractère générique sur les sous-répertoires (par exemple, my-aggregate-output/*
). Pour "ajouter" de nouvelles données à l'agrégat en tant que sortie, spécifiez simplement un nouveau sous-répertoire unique de l'agrégat en tant que répertoire de sortie, en utilisant généralement un horodatage ou un numéro de séquence dérivé de vos données d'entrée (par exemple, my-aggregate-output/20140415154424
).
J'ai eu un cas d'utilisation similaire, j'utilise MultipleOutputs
pour résoudre ce problème.
Par exemple, si je veux que différents travaux MapReduce écrivent dans le même répertoire /outputDir/
. Le travail 1 écrit dans /outputDir/job1-part1.txt
, le travail 2 écrit dans /outputDir/job1-part2.txt
(sans supprimer les fichiers existants).
Dans l’ensemble, définissez le répertoire de sortie sur un répertoire aléatoire (il peut être supprimé avant l’exécution d’un nouveau travail).
FileInputFormat.addInputPath(job, new Path("/randomPath"));
Dans le réducteur/mappeur, utilisez MultipleOutputs
et configurez le rédacteur pour écrire dans le répertoire souhaité:
public void setup(Context context) {
MultipleOutputs mos = new MultipleOutputs(context);
}
et:
mos.write(key, value, "/outputDir/fileOfJobX.txt")
Cependant, mon cas d'utilisation était un peu compliqué. S'il ne s'agit que d'écrire dans le même répertoire plat, vous pouvez écrire dans un autre répertoire et exécuter un script pour migrer les fichiers, par exemple: hadoop fs -mv /tmp/* /outputDir
Dans mon cas d'utilisation, chaque travail MapReduce écrit dans différents sous-répertoires en fonction de la valeur du message en cours d'écriture. La structure de répertoire peut être multicouche comme:
/outputDir/
messageTypeA/
messageSubTypeA1/
job1Output/
job1-part1.txt
job1-part2.txt
...
job2Output/
job2-part1.txt
...
messageSubTypeA2/
...
messageTypeB/
...
Chaque travail Mapreduce peut écrire dans des milliers de sous-répertoires. Et le coût d'écriture dans un répertoire tmp et de déplacement de chaque fichier dans le bon répertoire est élevé.
Hadoop suit la philosophie Écrire une fois, lire plusieurs fois. Ainsi, lorsque vous essayez à nouveau d’écrire dans le répertoire, il suppose qu’il doit en créer un nouveau (Write once), mais il existe déjà et il se plaint. Vous pouvez le supprimer via hadoop fs -rmr /path/to/your/output/
. Il est préférable de créer un répertoire dynamique (basé sur l'horodatage ou une valeur de hachage, par exemple) afin de préserver les données.
Si l’on charge le fichier d’entrée (avec, par exemple, les entrées ajoutées) du système de fichiers local vers le système de fichiers distribué par hadoop en tant que tel:
hdfs dfs -put /mylocalfile /user/cloudera/purchase
Ensuite, vous pouvez également écraser/réutiliser le répertoire de sortie existant avec -f
. Pas besoin de supprimer ou de recréer le dossier
hdfs dfs -put -f /updated_mylocalfile /user/cloudera/purchase
Vous pouvez créer un sous-répertoire de sortie pour chaque exécution par heure. Par exemple, supposons que vous attendiez le répertoire de sortie de l'utilisateur, puis définissez-le comme suit:
FileOutputFormat.setOutputPath(job, new Path(args[1]);
Modifiez cela par les lignes suivantes:
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));