Jni在Hadoop上的使用(2)

这个阶段的尝试我吃了不少苦头,主要是路径问题:hadoop将我写好的jar包分发到每个tasknode上,同时,我们要把.so也分发到相同路径下,并“告诉”tasknode,使得jvm在运行jar包的时候能够找到这个动态库。

还是按顺序说。写hadoop程序。

map函数:

public static class MapTestJni extends Mapper<Writable, Text, Text, Text> { 
         
        protected String s; 
        protected void setup(Context context) throws IOException, InterruptedException 
        { 
            s = FakeSegmentForJni.SegmentALine("jni-value"); 
        } 
         
        protected void map(Writable key, Text value, Context context) 
        throws IOException, InterruptedException { 
             
            // the format of input value is: 
            //    mcid totaltimes item1 item2(itemkey=itemvalue) 
             
            context.write(new Text("key"), new Text(s.toString())); 
        } 
    } 

在setup函数中,调用动态库初始化了一个字符串。这个字符串s的值应该是“jni-value--copy that”(参考上一篇动态库的实现)。在map函数中,简单的输出这个字符串。

reduce函数:

public static class ReduceTestJni extends Reducer<Text, Text, Text, Text> { 
         
        protected void reduce(Text key, Iterable<Text> values, Context context) 
        throws IOException, InterruptedException { 
             
            String outString = ""; 
            for (Text value: values) 
            { 
                outString = value.toString(); 
            } 
             
            context.write(key, new Text(outString)); 
        } 
    } 

reduce函数也很简单,直接输出这个字符串。

控制函数:先列出来正确的代码吧,然后再吐苦水。

public void runTestJni (String[] args) throws Exception { 
         
        //  the configuration 
        Configuration conf = new Configuration(); 
        <STRONG>GenericOptionsParser goparser = new GenericOptionsParser(conf, args); 
        String otherargs [] = goparser.getRemainingArgs();</STRONG> 
         
        // the job 
        Job job; 
        job = new Job(conf, "@here-TestFakeSegmentForJni-hadoopJni"); 
        job.setJarByClass(TestFakeSegmentForJni.class); 
         
        // the mapper 
        job.setMapperClass(MapTestJni.class); 
        job.setMapOutputKeyClass(Text.class); 
        job.setMapOutputValueClass(Text.class); 
         
        // the reducer 
        job.setReducerClass(ReduceTestJni.class); 
        job.setOutputKeyClass(Text.class); 
        job.setOutputValueClass(Text.class); 
        job.setNumReduceTasks(1); 
         
        // the path 
        FileInputFormat.addInputPath(job, new Path(otherargs[1])); 
        FileOutputFormat.setOutputPath(job, new Path(otherargs[2])); 
         
        job.waitForCompletion(true); 
    } 

其中GenericOptionsParser那两行是关键,用来分析和执行hadoop命令中传进来的特殊参数。配合命令行中的命令(下文写),他把动态库分发到tasknode上,路径与jar的执行路径相同。

通常用jar包的形式运行hadoop程序,所需的参数,如:输入路径、输出路径、mapper、combiner、reducer等都可以用Job来设置,不需要额外的参数。命令行中少了这些参数,会显得短很多。尤其hadoop命令行一般都挺长,就很方便。相反地,采用c++ streaming的方式来运行程序的时候,就需要用-input、-output等参数来指定相关参数。不过,在jar包中,除了用Job外,也可以用GenericOptionsParser来解析上述命令行中的参数,只要命令行配合有相应的输入,GenericOptionsParser就可以解析。对于-input、-output等来讲,没有必要这样做。还有一个参数是-files,就是把-files后面的文件(多个文件用逗号间隔)同jar包一起分发到tasknode中,这个参数,刚好可以将我们的动态库分发下去。

“goparser.getRemainingArgs();”这条语句,是在GenericOptionsParser解析完特殊参数之后,获得剩下的参数列表,对于我们来讲,剩下的参数就是main函数所在的类名、输入路径和输出路径,参见下面的命令行。

main函数:

public static void main(String[] args) throws Exception { 
 
        System.out.println ("In this project, we test jni!\n"); 
         
        // test jni on linux local 
        /*String s = FakeSegmentForJni.SegmentALine("now we test FakeSegmentForJni");
        System.out.print(s);*/ 
         
        // test jni on hadoop 
        new TestFakeSegmentForJni().runTestJni(args); 
         
    } // main 

这个不用解释了。然后直接打成jar包。

在命令行中提交hadoop任务:

hadoop jar /xxx/TestFakeSegmentForJniHadoop.jar -files /xxx/TestJni/libFakeSegmentForJni.so FakeSegmentForJni.TestFakeSegmentForJni /input/xxx.txt /outputJNI

这个也是关键。说一下几个需要注意的地方吧:

hadoop jar 命令后面跟随的第一个参数一定是打好的jar包,在本例中是TestFakeSegmentForJniHadoop.jar文件及其路径

由于在控制函数中用了GenericOptionsParser,jar包后面就必须紧跟需要设定的参数,这里,我们的参数是“-files /xxx/TestJni/libFakeSegmentForJni.so”,表示把本地路径“/xxx/TestJni/”中的libFakeSegmentForJni.so文件随jar包分发下去。

剩下的就比较容易了,分别是main函数所在的类名、输入路径、输出路径

运行的时候,因为当前路径是默认搜索路径之一,所以jvm能够找到动态库

接下来,说一下我踩到的“坑”们,不怕丢人哈。

我尝试过在控制函数里面设置tasknode的java.library.path属性,语句为“conf.set("java.library.path", ".")”,失败。

我尝试过在控制函数里面设置tasknode的jvm的hadoop.native.lib属性,语句为“conf.set("hadoop.native.lib", "true");”,失败。

我尝试过在控制函数里面设置tasknode的mapred.job.classpath属性,语句为“conf.set("mapred.job.classpath", "./")”,失败。

我尝试过在控制函数里面采用DistributedCache,用来将.so文件分发到tasknode上,语句为“DistributedCache.addFileToClassPath(new Path("/xxx/TestJni/libFakeSegmentForJni.so"), conf);”,失败。

我尝试过在class FakeSegmentForJni中用System.setProperty来设置java.library.path,语句为“System.setProperty("java.library.path", "./");”,失败。

我尝试了各种可能的路径,失败。

列一个我整理之前的控制函数(片段),如下:

public void runTestJni (String[] args) throws Exception { 
         
        //  the configuration 
        Configuration conf = new Configuration(); 
        // conf.set("java.library.path", "./Jars"); 
        // DistributedCache.addFileToClassPath(new Path("/xxx/libFakeSegmentForJni.so"), conf); 
        /*conf.set("hadoop.native.lib", "true");
        conf.set("java.library.path", "./");
        conf.set("mapred.job.classpath", "./");
        DistributedCache.createSymlink(conf);
        DistributedCache.addFileToClassPath(new Path("/xxx/libFakeSegmentForJni.so"), conf);*/ 
        GenericOptionsParser goparser = new GenericOptionsParser(conf, args); 
        String otherargs [] = goparser.getRemainingArgs(); 
         
        // the job 
        Job job; 
        job = new Job(conf, "@xiaojinghui-TestFakeSegmentForJni-hadoopJni"); 
        job.setJarByClass(TestFakeSegmentForJni.class); 
...... 

可以看到,我在"Job job"之前注释的有多乱。这还是删除了一些绝对不靠谱的尝试语句呢。可想而知,我当时的郁闷。

最后提一句,设置java.library.path失败,有两种说法:

这是一个bug,至今未解决。大家可以搜搜去,在似乎Oracle的一个网站上,有个人遇到了和我一样的问题,似乎是官方人员回答的。

这是一个design,当tasknode的jvm一旦运行起来,就没有办法动态更改它的属性了。

参考文档:Hadoop JAVA程序-files功能测试 

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/54b0de2cbcb60fb830d0765872a93cbc.html