Hadoop - 映射器的构造函数args

时间:2011-11-15 18:59:13

标签: java scala hadoop

有没有办法在Hadoop中为Mapper提供构造函数args?可能通过一些包装工作创建的库?

这是我的情景:

public class HadoopTest {

    // Extractor turns a line into a "feature"
    public static interface Extractor {
        public String extract(String s);
    }

    // A concrete Extractor, configurable with a constructor parameter
    public static class PrefixExtractor implements Extractor {
        private int endIndex;

        public PrefixExtractor(int endIndex) { this.endIndex = endIndex; }

        public String extract(String s) { return s.substring(0, this.endIndex); }
    }

    public static class Map extends Mapper<Object, Text, Text, Text> {
        private Extractor extractor;

        // Constructor configures the extractor
        public Map(Extractor extractor) { this.extractor = extractor; }

        public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
            String feature = extractor.extract(value.toString());
            context.write(new Text(feature), new Text(value.toString()));
        }
    }

    public static class Reduce extends Reducer<Text, Text, Text, Text> {
        public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
            for (Text val : values) context.write(key, val);
        }
    }

    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = new Job(conf, "test");
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(Text.class);
        job.setMapperClass(Map.class);
        job.setReducerClass(Reduce.class);
        job.setInputFormatClass(TextInputFormat.class);
        job.setOutputFormatClass(TextOutputFormat.class);
        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        job.waitForCompletion(true);
    }
}

应该清楚,因为Mapper仅作为类引用(Configuration)提供给Map.class,所以Hadoop无法传递构造函数参数并配置特定的Extractor。

有一些Hadoop包装框架,比如Scoobi,Crunch,Scrunch(可能还有更多我不知道的)似乎有这种能力,但我不知道他们是如何实现的。 编辑:在与Scoobi合作之后,我发现我对此有些不对。如果在“映射器”中使用外部定义的对象,则Scoobi要求它是可序列化的,并且如果不是,则会在运行时进行投诉。所以也许正确的方法就是让我的Extractor可序列化并在Mapper的设置方法中反序列化...

另外,我实际上在Scala工作,因此非常欢迎基于Scala的解决方案(如果不鼓励的话)。

5 个答案:

答案 0 :(得分:7)

我建议您通过您正在创建的Configuration对象告诉您的mapper哪个提取器。映射器以setup方法(context.getConfiguration())接收配置。看起来您不能将对象放在配置中,因为它通常是从XML文件或命令行构造的,但您可以设置枚举值并让映射器自己构造其提取器。在创建映射器之后自定义映射器并不是很漂亮,但这就是我对API的解释。

答案 1 :(得分:5)

在提交作业时设置实施类名称

Configuration conf = new Configuration();
conf.set("PrefixExtractorClass", "com.my.class.ThreePrefixExtractor");

或使用命令行中的-D option来设置PrefixExtractorClass选项。

以下是mapper中的实现

Extractor extractor = null;
protected void setup(Context context) throws IOException,
            InterruptedException
{
    try {
        Configuration conf = context.getConfiguration();
        String className = conf.get("PrefixExtractorClass");
        extractor = Class.forName(className);
    } Catch (ClassNotFoundException e) {
        //handle the exception
    }
}

现在根据地图功能的需要使用extractor对象。

  • 包含com.my.class.ThreePrefixExtractor类的jar应该分发给所有节点。以下是来自Cloudera的article不同的做法。

  • 在上面的示例中,com.my.class.ThreePrefixExtractor应该扩展Extractor类。

使用这种方法可以使映射器实现成为通用的。这是大多数框架采用的方法(使用Class.forName)来实现可实现特定接口的可插入组件。

答案 2 :(得分:1)

我仍然在寻找好的答案,但我提出的一个(非理想的)解决方案是使用继承而不是组合,将Map转换为Extractor抽象类。然后它可以被一直子类化以包含所有构造函数args(如下所示)。

    public static abstract class Extractor extends Mapper<Object, Text, Text, Text> {
        public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
            String feature = extract(value.toString());
            context.write(new Text(feature), new Text(value.toString()));
        }

        public abstract String extract(String s);
    }

    public static abstract class PrefixExtractor extends Extractor {
        public String extract(String s) { return s.substring(0, getEndIndex()); }

        public abstract int getEndIndex();
    }

    public static class ThreePrefixExtractor extends PrefixExtractor {
        public int getEndIndex() { return 3; }
    }

然而,这并不是那么好,我真的觉得必须有一种方法以正确的方式做到这一点。

(我把它从原来的问题中移开,使事情变得不那么混乱。)

答案 3 :(得分:0)

到目前为止,我提出的最佳解决方案是将我想要的对象的序列化版本传递给Mapper,并使用反射在运行时构造对象。

所以,主要方法会说:

conf.set("ExtractorConstructor", "dicta03.hw4.PrefixExtractor(3)");

然后,在Mapper中我们使用辅助函数construct(在下面定义)并且可以说:

public void setup(Context context) {
    try {
        String constructor = context.getConfiguration().get("ExtractorConstructor");
        this.extractor = (Extractor) construct(constructor);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

使用反射在运行时从String递归构造对象的construct的定义:

public static Object construct(String s) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
    if (s.matches("^[A-Za-z0-9.#]+\\(.*\\)$")) {
        Class cls = null;
        List<Object> argList = new ArrayList<Object>();
        int parenCount = 0;
        boolean quoted = false;
        boolean escaped = false;
        int argStart = -1;
        for (int i = 0; i < s.length(); i++) {
            if (escaped) {
                escaped = false;
            } else if (s.charAt(i) == '\\') {
                escaped = true;
            } else if (s.charAt(i) == '"') {
                quoted = true;
            } else if (!quoted) {
                if (s.charAt(i) == '(') {
                    if (cls == null)
                        cls = Class.forName(s.substring(0, i));
                    parenCount++;
                    argStart = i + 1;
                } else if (s.charAt(i) == ')') {
                    if (parenCount == 1)
                        argList.add(construct(s.substring(argStart, i)));
                    parenCount--;
                } else if (s.charAt(i) == ',') {
                    if (parenCount == 1) {
                        argList.add(construct(s.substring(argStart, i)));
                        argStart = i + 1;
                    }
                }
            }
        }

        Object[] args = new Object[argList.size()];
        Class[] argTypes = new Class[argList.size()];
        for (int i = 0; i < argList.size(); i++) {
            argTypes[i] = argList.get(i).getClass();
            args[i] = argList.get(i);
        }
        Constructor constructor = cls.getConstructor(argTypes);
        return constructor.newInstance(args);
    } else if (s.matches("^\".*\"$")) {
        return s.substring(1, s.length() - 1);
    } else if (s.matches("^\\d+$")) {
        return Integer.parseInt(s);
    } else {
        throw new RuntimeException("Cannot construct " + s);
    }
}

(这可能不是最强大的解析器,但可以轻松扩展以涵盖更多类型的对象。)

答案 4 :(得分:0)

对于另一个类似的解决方案,请看一下:

https://github.com/NICTA/scoobi/blob/master/src/main/scala/com/nicta/scoobi/impl/rtt/ClassBuilder.scala

我们如何做到这一点。它使用反射来构建一些java-source-code,这些代码在运行时会创建一个相同的对象图。然后我们编译该源(使用javassist)并包含在发送到集群的jar中。

如果你想要它,它是非常强大的,它处理像循环对象图和所有特殊情况(有一些很少)的东西。