
暂无个人介绍
HBase是Hadoop数据库,能够实现随机、实时读写你的Big Data,它是Google的Bigtable的开源实现,可以参考Bigtable的论文Bigtable: A Distributed Storage System for Structured。HBase的存储模型可以如下三个词来概括:distributed, versioned, column-oriented。HBase并非只能在HDFS文件系统上使用, 你可以应用在你的本地文件系统上部署HBase实例来存储数据。 准备工作 hbase-0.90.4.tar.gz [http://labs.renren.com/apache-mirror//hbase/stable/hbase-0.90.4.tar.gz] zookeeper-3.3.4.tar.gz 下面介绍Standalone和Distributed安装过程。 Standalone模式 这种安装模式,是在你的本地文件系统上安装配置一个HBase实例,安装配置比较简单。 首先,要保证你的本地系统能够通过ssh无密码访问,配置如下: 1 ssh-keygen -t dsa 2 cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys 检查一下权限:你的~/.ssh目录的权限是否为755,~/.ssh/authorized_keys的权限是否为644,如果不是,执行下面的命令行: 1 chmod 755 ~/.ssh 2 chmod 644 ~/.ssh/authorized_keys 然后,安装配置HBase,过程如下: 1 cd /home/shirdrn/hadoop 2 tar -xvzf hbase-0.90.4.tar.gz 3 cd hbase-0.90.4 修改conf/hbase-env.sh中JAVA_HOME配置,指定为你的JAVA_HOME目录: 1 export JAVA_HOME=/usr/java/jdk1.6.0_16 其他配置,如HBASE*指定配置项,如果需要可以进行配置。 修改hbase-site.xml中配置,示例如下: 1 <?xml version="1.0"?> 2 <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> 3 4 <configuration> 5 <property> 6 <name>hbase.rootdir</name> 7 <value>file:///home/shirdrn/hadoop/hbase-0.90.4/data</value> 8 </property> 9 </configuration> 指定HBase的数据存储目录,使用的是本地文件系统的目录。 接着,就可以启动HBase实例,提供本地存储服务: 1 bin/start-hbase.sh 启动完成以后,你可以跟踪一下HBase日志,看看是否启动成功了: 1 tail -500f logs/hbase-shirdrn-master-localhost.log 或者查看一下HMaster进程是否存在: 1 ps -ef | grep HMaster 通过日志可以看出,HBase实例启动了所有的HBase和Zookeeper守护进程,并且这些进程都是在同一个JVM中。下面,可以启动HBase shell,来简单测试HBase的数据存储的基本命令: 01 cd bin 02 hbase shell 03 hbase(main):001:0> help 04 hbase(main):002:0> status 05 hbase(main):003:0> version 06 // 创建表'pagedb',列簇(Column Family)为metadata、text、status 07 hbase(main):004:0> create 'pagedb', 'metadata', 'text', 'status' 08 // 插入数据 09 hbase(main):005:0> put 'pagedb', 'http://www.mafengwo.cn/i/764197.html','metadata:site', 'www.mafengwo.cn' 10 hbase(main):006:0> put 'pagedb', 'http://www.mafengwo.cn/i/764197.html','metadata:pubdate', '2011-12-20 22:09' 11 hbase(main):007:0> put 'pagedb', 'http://www.mafengwo.cn/i/764197.html', 'text:title','南国之境' 12 hbase(main):008:0> put 'pagedb', 'http://www.mafengwo.cn/i/764197.html','text:content', '如果海會說话, 如果風愛上砂 我會聆聽浪花,...' 13 hbase(main):009:0> put 'pagedb', 'http://www.mafengwo.cn/i/764197.html','status:extracted', '0' 14 hbase(main):010:0> put 'pagedb', 'http://www.mafengwo.cn/i/764197.html','status:httpcode', '200' 15 hbase(main):011:0> put 'pagedb', 'http://www.mafengwo.cn/i/764197.html','status:indexed', '1' 16 // 扫描表'pagedb' 17 hbase(main):012:0> scan 'pagedb' 18 // 获取记录'http://www.mafengwo.cn/i/764197.html'的所有列的数据 19 hbase(main):013:0> get 'pagedb', 'http://www.mafengwo.cn/i/764197.html' 20 // 获取记录'http://www.mafengwo.cn/i/764197.html'的metadata列簇数据 21 hbase(main):014:0> get 'pagedb', 'http://www.mafengwo.cn/i/764197.html', 'metadata' 22 // 获取记录'http://www.mafengwo.cn/i/764197.html'的列metadata:site数据 23 hbase(main):015:0> get 'pagedb', 'http://www.mafengwo.cn/i/764197.html','metadata:site' 24 // 增加一个列status:state,并指定值为4 25 hbase(main):016:0> incr 'pagedb', 'http://www.mafengwo.cn/i/764197.html','status:state', 4 26 // 修改status:httpcode的值为500 27 hbase(main):017:0> put 'pagedb', 'http://www.mafengwo.cn/i/764197.html','status:httpcode', '500' 28 // 统计表'pagedb'中的记录行数 29 hbase(main):018:0> count 'pagedb' 30 // disable表'pagedb' 31 hbase(main):019:0> disable 'pagedb' 32 // enable表pagedb 33 hbase(main):020:0> enable 'pagedb' 34 // 清空表'pagedb' 35 hbase(main):021:0> truncate 'pagedb' 36 // 列出所有表 37 hbase(main):022:0> list 38 // 删除'http://www.mafengwo.cn/i/764197.html'数据行 39 hbase(main):023:0> deleteall 'pagedb','http://www.mafengwo.cn/i/764197.html' 40 // 删除表'pagedb',删除之前必须先disable表 41 hbase(main):024:0> drop 'pagedb' 如果想练习使用其他更多命令,可以通过help查看其他命令。 Distributed模式 基于分布式模式安装HBase,首先它是在安装在HDFS集群之上,所以,首先要做的就是能够正确配置分布式模式的HDFS集群:保证Nanemode和Datanode进程都正确启动。HBase是一个分布式NoSQL数据库,建立于HDFS之上,并且对于集群模式的HBase需要对各个结点之间的数据进行协调(Coordination),所以HBase直接将ZooKeeper作为一个分布式协调系统来实现HBase数据复制(Replication)存储。有关ZooKeeper的介绍可以参考官方文档:http://zookeeper.apache.org。 HBase的基于主从架构模式:HBase集群中存在一个Hbase Master Server,类似于HDFS中的Namenode的角色;而作为从结点的Region Server,类似于HDFS中的Datanode。 对于HBase分布式模式的安装,又基于Zookeeper的是否被HBase管理,分为两种模式: 基于HBase管理的Zookeeper集群,启动和关闭HBase集群,同时也控制Zookeeper集群 外部Zookeeper集群:一个完全独立于HBase的ZooKeeper集群,不受HBase管理控制(启动与停止ZooKeeper集群) 下面,我们基于一个单独安装的ZooKeeper集群,不基于HBase管理,进行安装。根据官网文档,很容易就能安装配置好,并尝试使用。1、安装配置HDFS集群 启动HDFS集群实例,一台master作为Namenode结点,其余3台slaves作为Datanode结点。 其中,master服务端口为9000。2、创建HBase存储目录 1 #创建目录hdfs://master:9000/hbase 2 hadoop fs -mkdir /hbase 3 #验证/hbase目录创建成功 4 hadoop fs -lsr / 3、配置HBase (1)解压缩HBase软件包,修改系统环境变量,在~/.bashrc中最后面加入如下配置: 1 export JAVA_HOME=/home/hadoop/installation/jdk1.6.0_30 2 export HADOOP_HOME=/home/hadoop/installation/hadoop-0.22.0 3 export HBASE_HEAPSIZE=128 4 export HBASE_MANAGES_ZK=false 使配置生效: 1 . ~/.bashrc 2)修改hbase-0.90.4/conf/hbase-env.sh脚本内容: 首先要重命名hbase-0.90.4目录下的一个目录: 1 hadoop@master:~/installation/hbase-0.90.4$ mv hbase-webapps/ webapps 默认会查找webapps目录。然后修改脚本,内容如下: 1 export JAVA_HOME=/home/hadoop/installation/jdk1.6.0_30 2 export HADOOP_HOME=/home/hadoop/installation/hadoop-0.22.0 3 export HBASE_HEAPSIZE=128 4 export HBASE_MANAGES_ZK=false 5 export HBASE_CLASSPATH=$HBASE_HOME/ 最后一个表示使用外部Zookeeper集群,而不让HBase集群去管理。 (3)修改conf/hbase-site.xml文件内容,如下所示: 01 <?xml version="1.0"?> 02 <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> 03 04 <configuration> 05 <property> 06 <name>hbase.rootdir</name> 07 <value>hdfs://master:9000/hbase</value> 08 <description>The directory shared by RegionServers.</description> 09 </property> 10 <property> 11 <name>hbase.cluster.distributed</name> 12 <value>true</value> 13 <description>The mode the cluster will be in. Possible values are false: standalone and pseudo-distributed setups with managed Zookeeper true: fully-distributed with unmanaged Zookeeper Quorum (see hbase-env.sh)</description> 14 </property> 15 <property> 16 <name>hbase.zookeeper.property.dataDir</name> 17 <value>/home/hadoop/storage/zookeeper</value> 18 <description>Property from ZooKeeper's config zoo.cfg. The directory where the snapshot is stored.</description> 19 </property> 20 <property> 21 <name>hbase.zookeeper.quorum</name> 22 <value>slave-01,slave-02,slave-03</value> 23 <description>The directory shared by RegionServers.</description> 24 </property> 25 </configuration> 上面配置中: hbase.rootdir 指定了HBase存储的根目录是在HDFS的hdfs://master:9000/hbase目录下,该目录要被HBase集群中Region Server共享。不要忘记了,在启动HBase集群之前,在HDFS集群中创建/hbase目录,在master上执行命令hadoop fs -mkdir /hbase即可。 hbase.cluster.distributed 指定了我们使用完全分布的模式进行安装 hbase.zookeeper.property.dataDir 指定了HBase集群使用的ZooKeeper集群的存储目录 hbase.zookeeper.quorum指定了用于协调HBase集群的ZooKeeper集群结点,必须配置奇数个结点,否则HBase集群启动会失败 所以,在启动HBase集群之前,首先要保证ZooKeeper集群已经成功启动。 (4)接下来,检查HBase的lib中的Hadoop的版本是否之前我们启动的HDFS集群使用的版本一致: 1 rm ~/installation/hbase-0.90.4/lib/hadoop-core-0.20-append-r1056497.jar 2 cp ~/installation/hadoop-0.22.0/*.jar ~/installation/hbase-0.90.4/lib/ 我直接将HBase解压缩包中的hadoop的jar文件删除,用当前使用版本的Hadoop的jar文件。这一步很重要,如果不细看官方文档,你可能会感觉很怪异,实际HBase软件包中lib下的Hadoop的版本默认是0.20的,如果你启动的HDFS使用的是0.22,则HBase启动会报版本不一致的错误。 (5)修改conf/regionservers文件,配置HBase集群中的从结点Region Server,如下所示: 1 slave-01 2 slave-02 3 slave-03 一行一个主机字符串,上面使用是从结点主机的域名。上面配置,与HDFS的从结点的配置非常类似。 (6)经过上面几个骤,基本已经在一台机器上(master)配置好HBase了,这时,需要将上述的全部环境变量配置,也在各个从结点上进行配置,然后将配置好的HBase安装文件拷贝分发到各个从结点上: 1 scp -r ~/installation/hbase-0.90.4 hadoop@slave-01:/home/hadoop/installation 2 scp -r ~/installation/hbase-0.90.4 hadoop@slave-02:/home/hadoop/installation 3 scp -r ~/installation/hbase-0.90.4 hadoop@slave-03:/home/hadoop/installation 4、配置Zookeeper集群 具体安装、配置和启动,详见文章 http://blog.csdn.net/shirdrn/article/details/7183503 的说明。 在开始启动HBase集群之前,要先启动Zookeeper集群,保证其运行正常。5、启动HBase集群 启动HBase集群了,执行如下脚本: 1 ./start-hbase.sh 你可以使用jps查看一下,当前master上启动的全部进程,如下所示: 1 hadoop@master:~/installation/hbase-0.90.4$ jps 2 15899 SecondaryNameNode 3 15553 NameNode 4 21677 Jps 5 21537 HMaster 其中,HMaster进程就是HBase集群的主结点服务进程。 slaves结点上启动的进程,以slave-03为例: 1 hadoop@slave-03:~/installation/hbase-0.90.4$ jps 2 6919 HRegionServer 3 4212 QuorumPeerMain 4 7053 Jps 5 3483 DataNode 上面,HReginServer是HBase集群的从结点服务进程,QuorumPeerMain是ZooKeeper集群的结点服务进程。 或者,查看日志,是否出现启动异常: 1 master上 : tail -500f $HBASE_HOME/logs/hbase-hadoop-master-master.log 2 slave-01上: tail -500f $HBASE_HOME/logs/hbase-hadoop-zookeeper-slave-01.log 3 slave-02上: tail -500f $HBASE_HOME/logs/hbase-hadoop-zookeeper-slave-02.log 4 slave-03上: tail -500f $HBASE_HOME/logs/hbase-hadoop-zookeeper-slave-03.log 6、验证HBase安装 启动HBase shell,如果能够显示如下信息则说明HBase集群启动成功: 01 hadoop@master:~/installation/hbase-0.90.4$ hbase shell 02 12/01/09 01:14:09 WARN conf.Configuration: hadoop.native.lib is deprecated. Instead, use io.native.lib.available 03 12/01/09 01:14:09 WARN conf.Configuration: hadoop.native.lib is deprecated. Instead, use io.native.lib.available 04 12/01/09 01:14:09 WARN conf.Configuration: hadoop.native.lib is deprecated. Instead, use io.native.lib.available 05 HBase Shell; enter 'help<RETURN>' for list of supported commands. 06 Type "exit<RETURN>" to leave the HBase Shell 07 Version 0.90.4, r1150278, Sun Jul 24 15:53:29 PDT 2011 08 09 10 hbase(main):001:0> help 11 HBase Shell, version 0.90.4, r1150278, Sun Jul 24 15:53:29 PDT 2011 12 Type 'help "COMMAND"', (e.g. 'help "get"' -- the quotes are necessary) for help on a specific command. 13 Commands are grouped. Type 'help "COMMAND_GROUP"', (e.g. 'help "general"') for help on a command group. 14 15 16 COMMAND GROUPS: 17 Group name: general 18 Commands: status, version 19 20 21 Group name: ddl 22 Commands: alter, create, describe, disable, drop, enable, exists, is_disabled, is_enabled, list 23 24 25 Group name: dml 26 Commands: count, delete, deleteall, get, get_counter, incr, put, scan, truncate 27 28 29 Group name: tools 30 Commands: assign, balance_switch, balancer, close_region, compact, flush, major_compact, move, split, unassign, zk_dump 31 32 33 Group name: replication 34 Commands: add_peer, disable_peer, enable_peer, remove_peer, start_replication, stop_replication 35 36 37 SHELL USAGE: 38 Quote all names in HBase Shell such as table and column names. Commas delimit 39 command parameters. Type <RETURN> after entering a command to run it. 40 Dictionaries of configuration used in the creation and alteration of tables are 41 Ruby Hashes. They look like this: 42 43 44 {'key1' => 'value1', 'key2' => 'value2', ...} 45 46 47 and are opened and closed with curley-braces. Key/values are delimited by the 48 '=>' character combination. Usually keys are predefined constants such as 49 NAME, VERSIONS, COMPRESSION, etc. Constants do not need to be quoted. Type 50 'Object.constants' to see a (messy) list of all constants in the environment. 51 52 53 If you are using binary keys or values and need to enter them in the shell, use 54 double-quote'd hexadecimal representation. For example: 55 56 57 hbase> get 't1', "key\x03\x3f\xcd" 58 hbase> get 't1', "key\003\023\011" 59 hbase> put 't1', "test\xef\xff", 'f1:', "\x01\x33\x40" 60 61 62 The HBase shell is the (J)Ruby IRB with the above HBase-specific commands added. 63 For more on the HBase Shell, see http://hbase.apache.org/docs/current/book.html 64 hbase(main):002:0> status 65 3 servers, 0 dead, 0.0000 average load 66 67 68 hbase(main):003:0> version 69 0.90.4, r1150278, Sun Jul 24 15:53:29 PDT 2011 70 71 72 hbase(main):004:0> 你可以按照前面使用本地文件系统安装过程中,使用的命令来进行相关的操作。 总结说明 1、出现版本不一致错误 如果启动时出现版本不一致的错误,如下所示: 01 2012-01-06 21:27:18,384 FATAL org.apache.hadoop.hbase.master.HMaster: Unhandled exception. Starting shutdown. 02 org.apache.hadoop.ipc.RemoteException: Server IPC version 5 cannot communicate with client version 3 03 at org.apache.hadoop.ipc.Client.call(Client.java:740) 04 at org.apache.hadoop.ipc.RPC$Invoker.invoke(RPC.java:220) 05 at $Proxy5.getProtocolVersion(Unknown Source) 06 at org.apache.hadoop.ipc.RPC.getProxy(RPC.java:359) 07 at org.apache.hadoop.hdfs.DFSClient.createRPCNamenode(DFSClient.java:113) 08 at org.apache.hadoop.hdfs.DFSClient.<init>(DFSClient.java:215) 09 at org.apache.hadoop.hdfs.DFSClient.<init>(DFSClient.java:177) 10 at org.apache.hadoop.hdfs.DistributedFileSystem.initialize(DistributedFileSystem.java:82) 11 at org.apache.hadoop.fs.FileSystem.createFileSystem(FileSystem.java:1378) 12 at org.apache.hadoop.fs.FileSystem.access$200(FileSystem.java:66) 13 at org.apache.hadoop.fs.FileSystem$Cache.get(FileSystem.java:1390) 14 at org.apache.hadoop.fs.FileSystem.get(FileSystem.java:196) 15 at org.apache.hadoop.fs.Path.getFileSystem(Path.java:175) 16 at org.apache.hadoop.hbase.util.FSUtils.getRootDir(FSUtils.java:364) 17 at org.apache.hadoop.hbase.master.MasterFileSystem.<init>(MasterFileSystem.java:81) 18 at org.apache.hadoop.hbase.master.HMaster.finishInitialization(HMaster.java:346) 19 at org.apache.hadoop.hbase.master.HMaster.run(HMaster.java:282) 20 2012-01-02 21:27:18,384 INFO org.apache.hadoop.hbase.master.HMaster: Aborting 这就是说明Hadoop和HBase版本不匹配,仔细阅读文档,你会在http://hbase.apache.org/book.html#hadoop发现,解释如下所示: 1 Because HBase depends on Hadoop, it bundles an instance of the Hadoop jar under its lib directory. The bundled jar is ONLY 2 3 for use in standalone mode. In 4 distributed mode, it is critical that the version of Hadoop that is out on your cluster match what is under HBase. Replace the hadoop jar found in the HBase lib 5 directory with the hadoop jar you are running on your cluster to avoid version mismatch issues. Make sure you replace the jar in HBase everywhere on your cluster. 6 Hadoop version mismatch issues have various manifestations but often all looks like its hung up. 将HBase解压缩包中lib的Hadoop Core jar文件替换为当前你所使用的Hadoop版本即可。2、HBase集群启动以后,执行相关操作时抛出异常 如果HBase集群正常启动,但是在想要创建一个table的时候,出现如下异常,如下所示: 01 ERROR: org.apache.hadoop.hbase.NotAllMetaRegionsOnlineException: org.apache.hadoop.hbase.NotAllMetaRegionsOnlineException: Timed out (10000ms) 02 at org.apache.hadoop.hbase.catalog.CatalogTracker.waitForMeta(CatalogTracker.java:334) 03 at org.apache.hadoop.hbase.master.HMaster.createTable(HMaster.java:769) 04 at org.apache.hadoop.hbase.master.HMaster.createTable(HMaster.java:743) 05 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 06 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) 07 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) 08 at java.lang.reflect.Method.invoke(Method.java:597) 09 at org.apache.hadoop.hbase.ipc.HBaseRPC$Server.call(HBaseRPC.java:570) 10 at org.apache.hadoop.hbase.ipc.HBaseServer$Handler.run(HBaseServer.java:1039) 解决方法就是,修改/etc/hosts文件,修改内容以master为例,如下所示: 01 #127.0.0.1 localhost 02 192.168.0.180 master 03 192.168.0.191 slave-01 04 192.168.0.190 slave-02 05 192.168.0.189 slave-03 06 # The following lines are desirable for IPv6 capable hosts 07 #::1 ip6-localhost ip6-loopback 08 #fe00::0 ip6-localnet 09 #ff00::0 ip6-mcastprefix 10 #ff02::1 ip6-allnodes 11 #ff02::2 ip6-allrouters 然后,再进行相关操作就没有问题了。
使用Maven来构建应用程序,可以非常方便地管理应用相关的资源。众所周知,应用程序中涉及到的一些依赖关系,如Java应用程序依赖jar文件,如果只是手动找到相应的资源,可能需要花费一些时间。而且,即使已经积累了库文件,在未来应用程序升级以后,还要考虑到依赖库文件的升级情况,再次搜索收集。 还有一个问题,对应用程序依赖文件的管理是个非常复杂工作,占用存储空间不说,还可能因为应用之间的版本问题导致依赖冲突。使用Maven的pom模型来构建应用程序,可以更加有效地的管理,而且配置内容非常清晰(有时多了,可能pom文件显得有点臃肿)。 下面将常用的Maven配置,整理如下,以备参考。首先,整理一个简单的目录,作为快速查询之用: 设置字符集 拷贝src/main/resources/资源文件 编译代码 、编译打包成jar文件 构建测试用例配置 输出依赖jar文件到指定目录 配置指定的repository 将应用及其依赖jar文件打成一个jar文件 具体配置的详细内容,如下所示: 1、设置字符集 1 <properties> 2 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 3 </properties> 在需要设置字符集的地方,引用${project.build.sourceEncoding}即可。 2、拷贝src/main/resources/资源文件 01 <build> 02 <pluginManagement> 03 <plugins> 04 <plugin> 05 <groupId>org.apache.maven.plugins</groupId> 06 <artifactId>maven-resources-plugin</artifactId> 07 <version>2.5</version> 08 <executions> 09 <execution> 10 <id>copy-resources</id> 11 <phase>package</phase> 12 <goals> 13 <goal>copy-resources</goal> 14 </goals> 15 <configuration> 16 <encoding>${project.build.sourceEncoding}</encoding> 17 <outputDirectory>${project.build.directory}</outputDirectory> 18 <resources> 19 <resource> 20 <directory>src/main/resources/</directory> 21 <includes> 22 <include>*.properties</include> 23 <include>*.xml</include> 24 </includes> 25 <filtering>true</filtering> 26 </resource> 27 </resources> 28 </configuration> 29 </execution> 30 </executions> 31 </plugin> 32 </plugins> 33 </pluginManagement> 34 </build> 3、编译代码 01 <build> 02 <pluginManagement> 03 <plugins> 04 <plugin> 05 <groupId>org.apache.maven.plugins</groupId> 06 <artifactId>maven-compiler-plugin</artifactId> 07 <version>2.5</version> 08 <configuration> 09 <source>1.7</source> 10 <target>1.7</target> 11 </configuration> 12 </plugin> 13 </plugins> 14 </pluginManagement> 15 </build> 可以指定源代码编译级别。 4、编译打包成jar文件 01 <build> 02 <pluginManagement> 03 <plugins> 04 <plugin> 05 <groupId>org.apache.maven.plugins</groupId> 06 <artifactId>maven-jar-plugin</artifactId> 07 <executions> 08 <execution> 09 <phase>package</phase> 10 <goals> 11 <goal>jar</goal> 12 </goals> 13 <configuration> 14 <classifier>without-configs</classifier> 15 <excludes> 16 <exclude>*.properties</exclude> 17 <exclude>*.xml</exclude> 18 </excludes> 19 </configuration> 20 </execution> 21 </executions> 22 </plugin> 23 </pluginManagement> 24 </build> 可以指定打包后jar文件的文件名后缀,同时可以设置是否将配置文件也打包到jar文件中。 5、构建测试用例配置 01 <build> 02 <pluginManagement> 03 <plugins> 04 <plugin> 05 <groupId>org.apache.maven.plugins</groupId> 06 <artifactId>maven-surefire-plugin</artifactId> 07 <version>2.9</version> 08 <configuration> 09 <skip>true</skip> 10 <testFailureIgnore>true</testFailureIgnore> 11 </configuration> 12 </plugin> 13 </plugins> 14 </pluginManagement> 15 </build> 构建应用时,可以配置是否执行测试用例代码,也可以配置如果测试用例未通过是否忽略。 6、输出依赖jar文件到指定目录 01 <build> 02 <pluginManagement> 03 <plugins> 04 <plugin> 05 <groupId>org.eclipse.m2e</groupId> 06 <artifactId>lifecycle-mapping</artifactId> 07 <version>1.0.0</version> 08 <configuration> 09 <lifecycleMappingMetadata> 10 <pluginExecutions> 11 <pluginExecution> 12 <pluginExecutionFilter> 13 <groupId>org.apache.maven.plugins</groupId> 14 <artifactId>maven-dependency-plugin</artifactId> 15 <versionRange>[2.0,)</versionRange> 16 <goals> 17 <goal>copy-dependencies</goal> 18 <goal>unpack</goal> 19 </goals> 20 </pluginExecutionFilter> 21 <action> 22 <ignore /> 23 </action> 24 </pluginExecution> 25 </pluginExecutions> 26 </lifecycleMappingMetadata> 27 </configuration> 28 </plugin> 29 </plugins> 30 </pluginManagement> 31 <plugins> 32 <plugin> 33 <groupId>org.apache.maven.plugins</groupId> 34 <artifactId>maven-dependency-plugin</artifactId> 35 <version>2.8</version> 36 <executions> 37 <execution> 38 <id>copy-dependencies</id> 39 <phase>package</phase> 40 <goals> 41 <goal>copy-dependencies</goal> 42 </goals> 43 <configuration> 44 <outputDirectory>${project.build.directory}/lib</outputDirectory> 45 <overWriteReleases>false</overWriteReleases> 46 <overWriteSnapshots>false</overWriteSnapshots> 47 <overWriteIfNewer>true</overWriteIfNewer> 48 </configuration> 49 </execution> 50 </executions> 51 </plugin> 52 </plugins> 53 </build> 上面,和pluginManagement并列的plugins元素中配置的是拷贝依赖jar文件到target/lib目录下面,如果在Eclipse中出现maven-dependency-plugin (goals “copy-dependencies”, “unpack”) is not supported by m2e错误,上面pluginManagement元素中的配置,可以解决这个错误提示。 7、配置指定的repository 1 <repositories> 2 <repository> 3 <id>cloudera</id> 4 <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url> 5 </repository> 6 </repositories> 如果我们需要要的一些依赖jar文件在maven中央repository中没有,可以在pom文件中配置特定的repository,一般需要配置id和url。 8、将应用及其依赖jar文件打成一个jar文件 查看源代码打印帮助 01 <build> 02 <plugins> 03 <plugin> 04 <artifactId>maven-assembly-plugin</artifactId> 05 <configuration> 06 <archive> 07 <manifest> 08 <mainClass>org.shirdrn.solr.cloud.index.hadoop.SolrCloudIndexer</mainClass> 09 </manifest> 10 </archive> 11 <descriptorRefs> 12 <descriptorRef>jar-with-dependencies</descriptorRef> 13 </descriptorRefs> 14 </configuration> 15 <executions> 16 <execution> 17 <id>make-assembly</id> 18 <phase>package</phase> 19 <goals> 20 <goal>single</goal> 21 </goals> 22 </execution> 23 </executions> 24 </plugin> 25 </plugins> 26 </build>
Apache Mina Server 是一个网络通信应用框架,也就是说,它主要是对基于 TCP/IP、UDP/IP协议栈的通信框架(当然,也可以提供 JAVA 对象的序列化服务、虚拟机管道通信服务等),Mina 可以帮助我们快速开发高性能、高扩展性的网络通信应用,Mina 提供了事件驱动、异步(Mina 的异步 IO 默认使用的是 JAVA NIO 作为底层支持)操作的编程模型。从官网文档“MINA based Application Architecture”中可以看到Mina作为一个通信层框架,在实际应用所处的位置,如图所示: Mina位于用户应用程序和底层Java网络API(和in-VM通信)之间,我们开发基于Mina的网络应用程序,就无需关心复杂的通信细节。 应用整体架构 再看一下,Mina提供的基本组件,如图所示: 也就是说,无论是客户端还是服务端,使用Mina框架实现通信的逻辑分层在概念上统一的,即包含如下三层: I/O Service – Performs actual I/O I/O Filter Chain – Filters/Transforms bytes into desired Data Structures and vice-versa I/O Handler – Here resides the actual business logic 想要开发基于MIna的应用程序,你只需要做如下事情: Create an I/O service – Choose from already available Services (*Acceptor) or create your own Create a Filter Chain – Choose from already existing Filters or create a custom Filter for transforming request/response Create an I/O Handler – Write business logic, on handling different messages 下面看一下使用Mina的应用程序,在服务器端和客户端的架构细节: 服务器端架构 服务器端监听指定端口上到来的请求,对这些请求经过处理后,回复响应。它也会创建并处理一个链接过来的客户会话对象(Session)。服务器端架构如图所示: 对服务器端的说明,引用官网文档,如下所示: IOAcceptor listens on the network for incoming connections/packets For a new connection, a new session is created and all subsequent request from IP Address/Port combination are handled in that Session All packets received for a Session, traverses the Filter Chain as specified in the diagram. Filters can be used to modify the content of packets (like converting to Objects, adding/removing information etc). For converting to/from raw bytes to High Level Objects, PacketEncoder/Decoder are particularly useful Finally the packet or converted object lands in IOHandler. IOHandlers can be used to fulfill business needs. 客户端架构 客户端主要做了如下工作: 连接到服务器端 向服务器发送消息 等待服务器端响应,并处理响应 客户端架构,如图所示: 对客户端架构的说明,引用官网文档内容,如下所示: Client first creates an IOConnector (MINA Construct for connecting to Socket), initiates a bind with Server Upon Connection creation, a Session is created and is associated with Connection Application/Client writes to the Session, resulting in data being sent to Server, after traversing the Filter Chain All the responses/messages received from Server are traverses the Filter Chain and lands at IOHandler, for processing 应用实例开发 下面根据上面给出的架构设计描述,看一下Mina(版本2.0.7)自带的例子,如何实现一个简单的C/S通信的程序,非常容易。服务端 首先,服务器端需要使用的组件有IoAdaptor、IoHandler、IoFilter,其中IoFilter可选. 我们基于Mina自带的例子进行了简单地修改,实现服务端IoHandler的代码如下所示: 01 package org.shirdrn.mina.server; 02 03 import org.apache.mina.core.service.IoHandlerAdapter; 04 import org.apache.mina.core.session.IdleStatus; 05 import org.apache.mina.core.session.IoSession; 06 import org.slf4j.Logger; 07 import org.slf4j.LoggerFactory; 08 09 public class TinyServerProtocolHandler extends IoHandlerAdapter { 10 private final static Logger LOGGER = LoggerFactory.getLogger(TinyServerProtocolHandler.class); 11 12 @Override 13 public void sessionCreated(IoSession session) { 14 session.getConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10); 15 } 16 17 @Override 18 public void sessionClosed(IoSession session) throws Exception { 19 LOGGER.info("CLOSED"); 20 } 21 22 @Override 23 public void sessionOpened(IoSession session) throws Exception { 24 LOGGER.info("OPENED"); 25 } 26 27 @Override 28 public void sessionIdle(IoSession session, IdleStatus status) { 29 LOGGER.info("*** IDLE #" + session.getIdleCount(IdleStatus.BOTH_IDLE) + " ***"); 30 } 31 32 @Override 33 public void exceptionCaught(IoSession session, Throwable cause) { 34 session.close(true); 35 } 36 37 @Override 38 public void messageReceived(IoSession session, Object message) 39 throws Exception { 40 LOGGER.info( "Received : " + message ); 41 if(!session.isConnected()) { 42 session.close(true); 43 } 44 } 45 } 这个版本中,IoHandlerAdapter实现了IoHandler接口,里面封装了一组用于事件处理的空方法,其中包含服务端和客户端的事件。在实际应用中,客户端可以选择客户端具有的事件,服务器端选择服务器端具有的事件,然后分别对这两类事件进行处理(有重叠的事件,如连接事件、关闭事件、异常事件等)。 客户端的IoHandler的具体实现也是类似的,不过多累述。 下面看启动服务器的主方法类,代码如下所示: 01 package org.shirdrn.mina.server; 02 03 import java.net.InetSocketAddress; 04 05 import org.apache.mina.filter.codec.ProtocolCodecFilter; 06 import org.apache.mina.filter.codec.textline.TextLineCodecFactory; 07 import org.apache.mina.transport.socket.SocketAcceptor; 08 import org.apache.mina.transport.socket.nio.NioSocketAcceptor; 09 import org.slf4j.Logger; 10 import org.slf4j.LoggerFactory; 11 12 public class TinyMinaServer { 13 14 private final static Logger LOG = LoggerFactory.getLogger(TinyMinaServer.class); 15 /** Choose your favorite port number. */ 16 private static final int PORT = 8080; 17 18 public static void main(String[] args) throws Exception { 19 SocketAcceptor acceptor = new NioSocketAcceptor(); 20 acceptor.setReuseAddress(true); 21 acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(newTextLineCodecFactory())); 22 23 // Bind 24 acceptor.setHandler(new TinyServerProtocolHandler()); 25 acceptor.bind(new InetSocketAddress(PORT)); 26 LOG.info("Listening on port " + PORT); 27 28 LOG.info("Server started!"); 29 30 for (;;) { 31 LOG.info("R: " + acceptor.getStatistics().getReadBytesThroughput() + ", W: " + acceptor.getStatistics().getWrittenBytesThroughput()); 32 Thread.sleep(3000); 33 } 34 } 35 36 } 客户端 实现客户端IoHandler的代码如下所示: 01 package org.shirdrn.mina.client; 02 03 import org.apache.mina.core.service.IoHandlerAdapter; 04 import org.apache.mina.core.session.IdleStatus; 05 import org.apache.mina.core.session.IoSession; 06 import org.slf4j.Logger; 07 import org.slf4j.LoggerFactory; 08 09 public class TinyClientProtocolHandler extends IoHandlerAdapter { 10 11 private final static Logger LOGGER = LoggerFactory 12 .getLogger(TinyClientProtocolHandler.class); 13 14 @Override 15 public void sessionCreated(IoSession session) { 16 LOGGER.info("CLIENT::CREATED"); 17 } 18 19 @Override 20 public void sessionClosed(IoSession session) throws Exception { 21 LOGGER.info("CLIENT::CLOSED"); 22 } 23 24 @Override 25 public void sessionOpened(IoSession session) throws Exception { 26 LOGGER.info("CLIENT::OPENED"); 27 } 28 29 @Override 30 public void sessionIdle(IoSession session, IdleStatus status) { 31 LOGGER.info("CLIENT::*** IDLE #" 32 + session.getIdleCount(IdleStatus.BOTH_IDLE) + " ***"); 33 } 34 35 @Override 36 public void exceptionCaught(IoSession session, Throwable cause) { 37 LOGGER.info("CLIENT::EXCEPTIONCAUGHT"); 38 cause.printStackTrace(); 39 } 40 41 public void messageSent(IoSession session, Object message) throws Exception { 42 LOGGER.info("CLIENT::MESSAGESENT: " + message); 43 } 44 } 下面看启动客户端的主方法类,代码如下所示: 01 package org.shirdrn.mina.client; 02 03 import java.net.InetSocketAddress; 04 05 import org.apache.mina.core.future.ConnectFuture; 06 import org.apache.mina.filter.codec.ProtocolCodecFilter; 07 import org.apache.mina.filter.codec.textline.TextLineCodecFactory; 08 import org.apache.mina.transport.socket.SocketConnector; 09 import org.apache.mina.transport.socket.nio.NioSocketConnector; 10 import org.slf4j.Logger; 11 import org.slf4j.LoggerFactory; 12 13 public class TinyMinaClient { 14 15 private final static Logger LOG = LoggerFactory.getLogger(TinyMinaClient.class); 16 /** Choose your favorite port number. */ 17 private static final int PORT = 8080; 18 19 public static void main(String[] args) throws Exception { 20 SocketConnector connector = new NioSocketConnector(); 21 22 // Connect 23 connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(newTextLineCodecFactory())); 24 connector.setHandler(new TinyClientProtocolHandler()); 25 26 for (int i = 0; i < 10; i++) { 27 ConnectFuture future = connector.connect(new InetSocketAddress(PORT)); 28 LOG.info("Connect to port " + PORT); 29 future.awaitUninterruptibly(); 30 future.getSession().write(String.valueOf(i)); 31 Thread.sleep(1500); 32 } 33 34 } 35 } 我们只是发送了十个数字,每发一次间隔1500ms。 测试上述服务器端与客户端交互,首先启动服务器端,监听8080端口。 接着启动客户端,连接到服务器端8080端口,然后发送消息,服务器端接收到消息后,直接将到客户端的连接关闭掉。
Mina从2.0版本以后,它的设计让人感觉到非常的优雅。它对网络应用通信框架的3个层进行了更好的抽象,以及在功能逻辑上的划分,同时又保证了 作为一个网络应用通信框架的统一。划分的3个层分别为: I/O Service层 I/O Filter Chain层 I/O Handler层 这里,我们重点关注I/O Service层。作为一个基于网络通信的应用,无论是服务器还是客户端角色,都要和网络I/O打交道,比如,服务器端需要创建服务器端Socket,监听指定端口并等待请求的带来,而客户端需要连接到服务器端指定的监听端口,使用网络服务。一般来说,这些I/O操作都比较复杂,而且很难在编 码中进行很好地控制,Mina的I/O Service层就是处理这些与实际的网络I/O相关的操作(事件)。 我们先看一下,对于服务器端和客户端,I/O Service层是如何设计的。类设计上的关系,作为这一层的最顶层抽象就是IoService接口类,如图所示: 通过上图,我们可以看到,IoService抽象的服务(功能)有如下几个: 管理IoSession:创建和删除IoSession,探测会话Idle状态 Filter Chain管理:处理过滤器链,允许用户修改过滤器链执行顺序 Handler的调用:当指定事件发生的时候,负责调用Handler进行处理 统计数据管理:更新消息发生的数量,以及传输的字节数,等等 监听器管理:管理用户设置的Listener 通信管理:管理端到端数据传输 具体可以参考源代码中定义的方法。 下面看看在服务器端和客户端,Mina是如何使用IoService进行抽象和设计的。 服务器端I/O Service层 在服务器端,对应于该层的抽象是IoAcceptor接口,IoAcceptor继承自IoService。具体的类设计上的关系,如图所示: 上图中,主要基于网络传输层协议TCP和UDP内置了对应IoAcceptor的实现,还附加了另外两个,如下所示: NioSocketAcceptor NioDatagramAcceptor AprSocketAcceptor VmPipeSocketAcceptor 根据实现类的命名就可知道各个类对应的应用场景。这里要说的是,前两个NioSocketAcceptor和NioDatagramAcceptor 都是基于非阻塞Socket的。AprSocketAcceptor是基于APR(Apache portable Run-time)的阻塞 Socket实现。 客户端I/O Service层 在客户端,对应于该层的抽象是IoConnector接口,IoConnector继承自IoService。具体的类设计上的关系,如图所示: 上图中给出了IoConnector的6个实现,如下所示: NioSocketConnector NioDatagramConnector AprSocketConnector ProxyConnector SerialConnector VmPipeConnector 上面的ProxyConnector是提供了代理支持的IoConnector,SerialConnector是支持串行传输数据的 IoConnector,VmPipeConnector就是in-VM的IoConnector。
I/O Filter Chain层是介于I/O Service层与I/O Handler层之间的一层,从它的命名上可以看出,这个层可以根据实际应用的需要,设置一组IoFilter来对I/O Service层与I/O Handler层之间传输数据进行过滤,任何需要在这两层之间进行处理的逻辑都可以放到IoFilter中。我们看一下IoFilter的抽象层次设计,如图所示: 通过上述类图可见,要实现一个自定义的IoFilter,一般是直接实现IoFilterAdapter类。同时,Mina也给出了几类常用的开发IoFilter的实现类,如下所示: LoggingFilter记录所有事件和请求 ProtocolCodecFilter将到来的ByteBuffer转换成消息对象(POJO) CompressionFilter压缩数据 SSLFilter增加SSL – TLS – StartTLS支持 想要实现一个自定义的IoFilter实现类,只需要基于上述给出的几个实现类即可。 如果想要实现自己的IoFilter,可以参考如下例子: 1 public class MyFilter extends IoFilterAdapter { 2 @Override 3 public void sessionOpened(NextFilter nextFilter, IoSession session) throwsException { 4 // Some logic here... 5 nextFilter.sessionOpened(session); 6 // Some other logic here... 7 } 8 } 下面通过一个例子来说明,如何使用IoFilter的实现类。 ProtocolCodecFilter 下面是Mina自带的例子,使用了ProtocolCodecFilter类: 01 package org.apache.mina.example.gettingstarted.timeserver; 02 03 import java.io.IOException; 04 import java.net.InetSocketAddress; 05 import java.nio.charset.Charset; 06 07 import org.apache.mina.core.service.IoAcceptor; 08 import org.apache.mina.core.session.IdleStatus; 09 import org.apache.mina.filter.codec.ProtocolCodecFilter; 10 import org.apache.mina.filter.codec.textline.TextLineCodecFactory; 11 import org.apache.mina.filter.logging.LoggingFilter; 12 import org.apache.mina.transport.socket.nio.NioSocketAcceptor; 13 14 public class MinaTimeServer { 15 /** 16 * We will use a port above 1024 to be able to launch the server with a 17 * standard user 18 */ 19 private static final int PORT = 9123; 20 21 /** 22 * The server implementation. It's based on TCP, and uses a logging filter 23 * plus a text line decoder. 24 */ 25 public static void main(String[] args) throws IOException { 26 // Create the acceptor 27 IoAcceptor acceptor = new NioSocketAcceptor(); 28 29 // Add two filters : a logger and a codec 30 acceptor.getFilterChain().addLast("logger", new LoggingFilter()); 31 acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(newTextLineCodecFactory(Charset.forName("UTF-8")))); 32 33 // Attach the business logic to the server 34 acceptor.setHandler(new TimeServerHandler()); 35 36 // Configurate the buffer size and the iddle time 37 acceptor.getSessionConfig().setReadBufferSize(2048); 38 acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10); 39 40 // And bind ! 41 acceptor.bind(new InetSocketAddress(PORT)); 42 } 43 } 上面设置了两个IoFilter,关键是看如果基于文本行的消息,使用一个ProtocolCodecFilter包裹了一TextLineCodecFactory类的实例,使用起来非常容易。 构造一个ProtocolCodecFilter实例,需要实现一个ProtocolCodecFactory实例,一个ProtocolCodecFactory包含了对消息进行编解码(Codec)的逻辑,这样实现的好处是将编解码的逻辑和IoFilter解耦合。下面看一下类图: LoggingFilter 如果需要记录通信过程中的事件以及请求,则可以直接使用LoggingFilter类,使用方法可以参考上面的例子。 CompressionFilter CompressionFilter是与压缩/解压缩数据相关的IoFilter,我们可以看一下该类的构造方法,如下所示: 01 /** 02 * Creates a new instance which compresses outboud data and decompresses 03 * inbound data with default compression level. 04 */ 05 public CompressionFilter() { 06 this(true, true, COMPRESSION_DEFAULT); 07 } 08 09 /** 10 * Creates a new instance which compresses outboud data and decompresses 11 * inbound data with the specified <tt>compressionLevel</tt>. 12 * 13 * @param compressionLevel the level of compression to be used. Must 14 */ 15 public CompressionFilter(final int compressionLevel) { 16 this(true, true, compressionLevel); 17 } 18 19 /** 20 * Creates a new instance. 21 * 22 * @param compressInbound <tt>true</tt> if data read is to be decompressed 23 * @param compressOutbound <tt>true</tt> if data written is to be compressed 24 * @param compressionLevel the level of compression to be used. Must 25 */ 26 public CompressionFilter(final boolean compressInbound, final booleancompressOutbound, final int compressionLevel) { 27 this.compressionLevel = compressionLevel; 28 this.compressInbound = compressInbound; 29 this.compressOutbound = compressOutbound; 30 } 基本上就构造方法参数中指定的3个参数与压缩/解压缩相关: compressionLevel compressInbound compressOutbound 使用的时候也比较简单,只需要创建一个CompressionFilter实例,加入到Filter Chain中即可。 DefaultIoFilterChainBuilder Mina自带的DefaultIoFilterChainBuilder可以非常容易就可以构建一个Filter Chain,默认在创建IoAcceptor和IoConnector的时候,可以直接通过他们获取到一个DefaultIoFilterChainBuilder的实例,然后调用add*方法设置IoFilter链,如下面代码中示例: 1 IoAcceptor acceptor = new NioSocketAcceptor(); 2 3 // Add two filters : a logger and a codec 4 acceptor.getFilterChain().addLast("logger", new LoggingFilter()); 5 acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(newTextLineCodecFactory(Charset.forName("UTF-8")))); 下面看一下来自Mina官网的表格,Mina框架也给出了一些典型的IoFilter的实现,引用如下所示: Filter class Description Blacklist BlacklistFilter Blocks connections from blacklisted remote addresses Buffered Write BufferedWriteFilter Buffers outgoing requests like the BufferedOutputStream does Compression CompressionFilter ConnectionThrottle ConnectionThrottleFilter ErrorGenerating ErrorGeneratingFilter Executor ExecutorFilter FileRegionWrite FileRegionWriteFilter KeepAlive KeepAliveFilter Logging LoggingFilter Logs event messages, like MessageReceived, MessageSent, SessionOpened, … MDC Injection MdcInjectionFilter Inject key IoSession properties into the MDC Noop NoopFilter A filter that does nothing. Useful for tests. Profiler ProfilerTimerFilter Profile event messages, like MessageReceived, MessageSent, SessionOpened, … ProtocolCodec ProtocolCodecFilter A filter in charge of encoding and decoding messages Proxy ProxyFilter Reference counting ReferenceCountingFilter Keeps track of the number of usages of this filter RequestResponse RequestResponseFilter SessionAttributeInitializing SessionAttributeInitializingFilter StreamWrite StreamWriteFilter SslFilter SslFilter WriteRequest WriteRequestFilter
使用libsvm,首先需要将实际待分类的内容或数据(训练数据,或预测数据)进行量化,然后通过libsvm提供的功能实现分类和预测。下面介绍使用libsvm的基本步骤。 准备训练数据 数据格式: 1 <label1> <index1>:<value11> <index2>:<value12>... 2 <label2> <index1>:<value21> <index2>:<value22>... 3 <label3> <index1>:<value31> <index2>:<value32>... 4 ... 每一行,表示以已定义的类别标签,以及属于该标签的各个属性值,每个属性值以“属性索引编号:属性值”的格式。一行内容表示一个类别属性以及与该类别相关的各个属性的值。属性的值,一般可以表示为“该属性隶属于该类别的程度”,越大,表示该属性更能决定属性该类别。 上面的数据必须使用数字类型,例如类别,可以通过不同的整数来表示不同的类别。 准备的原始训练样本数据存放在文件raw_data.txt中,内容如下所示: 1 1 1:0.4599 2:0.8718 3:0.1987 2 2 1:0.9765 2:0.2398 3:0.3999 3 3 1:0.0988 2:0.2432 3:0.7633 归一化 这一步对应于libsvm的缩放操作,即将量化的数据缩放到某一范围之内。首先,需要把原始的训练数据存放到文件中作为输入,如果实际应用中不需要从文件输入,可以根据需要修改libsvm的代码,来满足需要。 上面准备的文件raw_data.txt定义了三个类别,分别为1,2,3,其中有三个属性。正常情况下,每个属性值范围可能并不一定是在0到1之间,比如实际的温度数据,销售额数据,等等。 libsvm通过使用svm_scale来实现归一化,下面是svm_scale的使用说明: 1 用法:svmscale [-l lower] [-u upper] [-y y_lower y_upper] [-s save_filename] [-r restore_filename] filename 2 缺省值: lower = -1,upper = 1,没有对y进行缩放 3 -l:数据下限标记;lower:缩放后数据下限; 4 -u:数据上限标记;upper:缩放后数据上限; 5 -y:是否对目标值同时进行缩放;y_lower为下限值,y_upper为上限值;(回归需要对目标进行缩放,因此该参数可以设定为 –y -1 1 ) 6 -s save_filename:表示将缩放的规则保存为文件save_filename; 7 -r restore_filename:表示将缩放规则文件restore_filename载入后按此缩放; 8 filename:待缩放的数据文件(要求满足前面所述的格式) 我们输入如下参数,来执行数据的缩放操作: 1 -l 0 -u 1 -s src/s_rules.txt src/raw_data.txt 数据缩放的区间为[0, 1],生成的缩放规则的文件存放到文件src/s_rules.txt中,最后面的文件src/raw_data.txt就是我们进行分类的训练数据文件。 输入上面参数执行后,可以看到归一化的数据,如下所示: 1 1.0 1:0.4114162014355702 2:1.0 2 2.0 1:1.0 3:0.3563584838823946 3 3.0 2:0.005379746835443016 3:1.0 使用Eclipse的话,控制台输出的就是上面的内容,也就是我们可以直接用来训练的训练数据,将其存为文件train.txt。执行svm_scale命令,还输出一个规则文件(src/s_rules.txt): 1 x 2 0.000000000000000 1.000000000000000 3 1 0.09880000000000000 0.9765000000000000 4 2 0.2398000000000000 0.8718000000000000 5 3 0.1987000000000000 0.7633000000000000 训练分类模型 训练分类模型的过程,就是够呢局前面归一化的样本数据,建立一个分类模型,然后根据这个分类模型就能够进行分类的预测,这也是最终的目的。 我们看一下libsvm提供的训练模型的命令: 01 用法: svmtrain [options] training_set_file [model_file] 02 其中, options(操作参数):可用的选项即表示的涵义如下所示 03 -s svm类型:设置SVM 类型,默认值为0,可选类型有(对于回归只能选3或4): 04 0 -- C- SVC 05 1 -- n - SVC 06 2 -- one-class-SVM 07 3 -- e - SVR 08 4 -- n - SVR 09 -t 核函数类型:设置核函数类型,默认值为2,可选类型有: 10 0 -- 线性核:u'*v 11 1 -- 多项式核: (g*u'*v+ coef 0)deg ree 12 2 -- RBF 核:e( u v 2) g - 13 3 -- sigmoid 核:tanh(g*u'*v+ coef 0) 14 -d degree:核函数中的degree设置,默认值为3; 15 -g g :设置核函数中的g,默认值为1/k,其中k是指输入数据中的属性数; 16 -r coef 0:设置核函数中的coef 0,默认值为0; 17 -c cost:设置C- SVC、e - SVR、n - SVR中从惩罚系数C,默认值为1; 18 -n n :设置n - SVC、one-class-SVM 与n - SVR 中参数n ,默认值0.5; 19 -p e :设置n - SVR的损失函数中的e ,默认值为0.1; 20 -m cachesize:设置cache内存大小,以MB为单位,默认值为40; 21 -e e :设置终止准则中的可容忍偏差,默认值为0.001; 22 -h shrinking:是否使用启发式,可选值为0 或1,默认值为1; 23 -b 概率估计:是否计算SVC或SVR的概率估计,可选值0 或1,默认0; 24 -wi weight:对各类样本的惩罚系数C加权,默认值为1; 25 -v n:n折交叉验证模式,随机地将数据剖分为n部分并计算交叉检验准确度和均方根误差。 以上这些参数设置可以按照SVM的类型和核函数所支持的参数进行任意组合,如果设置的参数在函数或SVM 类型中没有也不会产生影响,程序不会接受该参数;如果应有的参数设置不正确,参数将采用默认值。 training_set_file是要进行训练的数据集;model_file是训练结束后产生的模型文件,该参数如果不设置将采用默认的文件名,也可以设置成自己惯用的文件名。 针对上面归一化操作得到的训练数据,我们通过输入如下参数并执行svmtrain命令进行训练: 1 src/train.txt src/model.txt 输入出的src/model.txt就是分类模型,模型数据的内容,如下所示: 01 svm_type c_svc 02 kernel_type rbf 03 gamma 0.3333333333333333 04 nr_class 3 05 total_sv 3 06 rho 0.0 0.0 0.0 07 label 1 2 3 08 nr_sv 1 1 1 09 SV 10 1.0 1.0 1:0.4114162014355702 2:1.0 11 -1.0 1.0 1:1.0 3:0.3563584838823946 12 -1.0 -1.0 2:0.005379746835443016 3:1.0 根据得出的分类模型,就可以进行分类预测了。 有关训练分类模型的优化,从参考链接中引用一段,有兴趣可以实际操作一下: 本实验中的参数-s取3,-t取2(默认)还需确定的参数是-c,-g,-p。 另外,实验中所需调整的重要参数是-c 和 –g,-c和-g的调整除了自己根据经验试之外,还可以使用gridregression.py对这两个参数进行优化。 该优化过程需要用到Python(2.5),Gnuplot(4.2),gridregression.py(该文件需要修改路径)。然后在命令行下面运行: python.exe gridregression.py -log2c -10,10,1 -log2g -10,10,1 -log2p -10,10,1 -s 3 –t 2 -v 5 -svmtrain E:/libsvm/libsvm-2.86/windows/svm-train.exe -gnuplot E:/libsvm/libsvm-2.86/gnuplot/bin/pgnuplot.exe E:/libsvm/libsvm-2.86/windows/train.txt > gridregression_feature.parameter 以上三个路径根据实际安装情况进行修改。 -log2c是给出参数c的范围和步长 -log2g是给出参数g的范围和步长 -log2p是给出参数p的范围和步长 上面三个参数可以用默认范围和步长。 -s选择SVM类型,也是只能选3或者4 -t是选择核函数 -v 10 将训练数据分成10份做交叉验证,默认为5 为了方便将gridregression.py是存放在python.exe安装目录下,trian.txt为训练数据,参数存放在gridregression_feature.parameter中,可以自己命名。 搜索结束后可以在gridregression_feature.parameter中最后一行看到最优参数。其中,最后一行的第一个参数即为-c,第二个为-g,第三个为-p,最后一个参数为均方误差。前三个参数可以直接用于模型的训练。然后,根据搜索得到的参数,重新训练,得到模型。 验证分类模型 预测分类的命令,说明如下所示: 1 用法:svmpredict [options] test_file model_file output_file 2 options(操作参数): 3 -b probability_estimates:是否需要进行概率估计预测,可选值为0 或者1,默认值为0。 4 model_file 是由svmtrain 产生的模型文件; 5 test_file 是要进行预测的数据文件; 6 output_file 是svmpredict 的输出文件,表示预测的结果值。 这个命令有两个主要的作用: 一个是在得出分类模型后,对分类模型进行验证评估,来确定分类模型的准确性。这种情况下,到输入的验证数据实际上也是已经知道分类结果的,可以通过指定的方式进行选取,最终将模型的精度优化到能够接受的程度。 另一个是,使用经过验证后的模型,对实际中未知的数据进行分类,得到分类结果,这也是分类预测的最终目的和结果。 这里,只有通过一组已经知道类别的数据来做验证,才能知道分类器(基于分类模型数据)的精度如何。如果分类器精度脚底,完全可以进行额外的参数寻优来调整模型。 准备验证分类器的数据(已知类标签,存为文件test.txt),如下所示: 1 1 1:0.0599 2:0.2718 3:0.1987 2 3 1:0.6765 2:0.1398 3:0.6999 3 2 1:0.0988 2:0.9432 3:0.7633 上面的数据是和训练数据属于同一类型的,即已经知道类别,通过将其作为模拟的待预测数据来验证分类模型的准确度。 输入如下参数,进行模拟预测: 1 src/test.txt src/model.txt src/predict.txt 结果会输出分类预测的精度: 1 Accuracy = 33.33333333333333% (1/3) (classification) 使用Eclipse的话会直接输出到控制台。然后看一下预测的结果,保存在文件src/predict.txt中,内容如下所示: 1 1.0 2 2.0 3 1.0 可见,模型的精度不是很高,只有一个预测与实际分类相符。我们这里只是举个例子,数据又很少。实际分类过程中,如果出现这种精度特别低的情况,需要对分类模型进行调整,达到一个满意的分类精度。 预测分类 实际上预测分类的数据是类别未知的,我们通过训练得出的分类器要做的事情就是确定待预测数据的类别。使用libsvm默认是以文件的方式输入数据,而且预测要求的数据格式必须和训练时相同,所以数据文件中第一列的类标签可以是随便给出的,分类器会处理数据,得出类别,然后输出到指定的文件中。 预测分类和前面的“验证分类模型”中的执行过程是一样的。 如果有其他需要,可以适当修改libsvm程序,使其支持你想要的输入输出方式。
其实,使用MapReduce计算最大值的问题,和Hadoop自带的WordCount的程序没什么区别,不过在Reducer中一个是求最大值,一个是做累加,本质一样,比较简单。下面我们结合一个例子来实现。 测试数据 我们通过自己的模拟程序,生成了一组简单的测试样本数据。输入数据的格式,截取一个片段,如下所示: 01 SG 253654006139495 253654006164392 619850464 02 KG 253654006225166 253654006252433 743485698 03 UZ 253654006248058 253654006271941 570409379 04 TT 253654006282019 253654006286839 23236775 05 BE 253654006276984 253654006301435 597874033 06 BO 253654006293624 253654006315946 498265375 07 SR 253654006308428 253654006330442 484613339 08 SV 253654006320312 253654006345405 629640166 09 LV 253654006330384 253654006359891 870680704 10 FJ 253654006351709 253654006374468 517965666 上面文本数据一行一行存储,一行包含4部分,分别表示: 国家代码 起始时间 截止时间 随机成本/权重估值 各个字段之间以空格号分隔。我们要计算的结果是,求各个国家(以国家代码标识)的成本估值的最大值。 编程实现 因为比较简单,直接看实际的代码。代码分为三个部分,当然是Mapper、Reducer、Driver。Mapper实现类为GlobalCostMapper,实现代码如下所示: 01 package org.shirdrn.kodz.inaction.hadoop.extremum.max; 02 03 import java.io.IOException; 04 05 import org.apache.hadoop.io.LongWritable; 06 import org.apache.hadoop.io.Text; 07 import org.apache.hadoop.mapreduce.Mapper; 08 09 public class GlobalCostMapper extends 10 Mapper<LongWritable, Text, Text, LongWritable> { 11 12 private final static LongWritable costValue = new LongWritable(0); 13 private Text code = new Text(); 14 15 @Override 16 protected void map(LongWritable key, Text value, Context context) 17 throws IOException, InterruptedException { 18 // a line, such as 'SG 253654006139495 253654006164392 619850464' 19 String line = value.toString(); 20 String[] array = line.split("\\s"); 21 if (array.length == 4) { 22 String countryCode = array[0]; 23 String strCost = array[3]; 24 long cost = 0L; 25 try { 26 cost = Long.parseLong(strCost); 27 } catch (NumberFormatException e) { 28 cost = 0L; 29 } 30 if (cost != 0) { 31 code.set(countryCode); 32 costValue.set(cost); 33 context.write(code, costValue); 34 } 35 } 36 } 37 } 上面实现逻辑非常简单,就是根据空格分隔符,将各个字段的值分离出来,最后输出键值对。 接着,Mapper输出了的键值对列表,在Reducer中就需要进行合并化简,Reducer的实现类为GlobalCostReducer,实现代码如下所示: 01 package org.shirdrn.kodz.inaction.hadoop.extremum.max; 02 03 import java.io.IOException; 04 import java.util.Iterator; 05 06 import org.apache.hadoop.io.LongWritable; 07 import org.apache.hadoop.io.Text; 08 import org.apache.hadoop.mapreduce.Reducer; 09 10 public class GlobalCostReducer extends 11 Reducer<Text, LongWritable, Text, LongWritable> { 12 13 @Override 14 protected void reduce(Text key, Iterable<LongWritable> values, 15 Context context) throws IOException, InterruptedException { 16 long max = 0L; 17 Iterator<LongWritable> iter = values.iterator(); 18 while (iter.hasNext()) { 19 LongWritable current = iter.next(); 20 if (current.get() > max) { 21 max = current.get(); 22 } 23 } 24 context.write(key, new LongWritable(max)); 25 } 26 } 上面计算一组键值对列表中代价估值的最大值,逻辑比较简单。为了优化,在Map输出以后,可以使用该Reducer进行合并操作,即作为Combiner,减少从Mapper到Reducer的数据传输量,在配置Job的时候可以指定。 下面看,如何来配置和运行一个Job,实现类为GlobalMaxCostDriver,实现代码如下所示: 01 package org.shirdrn.kodz.inaction.hadoop.extremum.max; 02 03 import java.io.IOException; 04 05 import org.apache.hadoop.conf.Configuration; 06 import org.apache.hadoop.fs.Path; 07 import org.apache.hadoop.io.LongWritable; 08 import org.apache.hadoop.io.Text; 09 import org.apache.hadoop.mapreduce.Job; 10 import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; 11 import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; 12 import org.apache.hadoop.util.GenericOptionsParser; 13 14 public class GlobalMaxCostDriver { 15 16 public static void main(String[] args) throws IOException, 17 InterruptedException, ClassNotFoundException { 18 19 Configuration conf = new Configuration(); 20 String[] otherArgs = new GenericOptionsParser(conf, args) 21 .getRemainingArgs(); 22 if (otherArgs.length != 2) { 23 System.err.println("Usage: maxcost <in> <out>"); 24 System.exit(2); 25 } 26 27 Job job = new Job(conf, "max cost"); 28 29 job.setJarByClass(GlobalMaxCostDriver.class); 30 job.setMapperClass(GlobalCostMapper.class); 31 job.setCombinerClass(GlobalCostReducer.class); 32 job.setReducerClass(GlobalCostReducer.class); 33 34 job.setOutputKeyClass(Text.class); 35 job.setOutputValueClass(LongWritable.class); 36 37 FileInputFormat.addInputPath(job, new Path(otherArgs[0])); 38 FileOutputFormat.setOutputPath(job, new Path(otherArgs[1])); 39 40 int exitFlag = job.waitForCompletion(true) ? 0 : 1; 41 System.exit(exitFlag); 42 } 43 } 运行程序 首先,需要保证Hadoop集群正常运行,我这里NameNode是主机ubuntu3。下面看运行程序的过程: 编译代码(我直接使用Maven进行),打成jar文件 1 shirdrn@SYJ:~/programs/eclipse-jee-juno/workspace/kodz-all/kodz-hadoop/target/classes$ jar -cvf global-max-cost.jar -C ./ org 拷贝上面生成的jar文件,到NameNode环境中 1 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ scpshirdrn@172.0.8.212:~/programs/eclipse-jee-juno/workspace/kodz-all/kodz-hadoop/target/classes/global-max-cost.jar ./ 2 global-max-cost.jar 上传待处理的数据文件 1 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ bin/hadoop fs -copyFromLocal /opt/stone/cloud/dataset/data_10m /user/xiaoxiang/datasets/cost/ 运行我们编写MapReduce任务,计算最大值 1 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ bin/hadoop jar global-max-cost.jar org.shirdrn.kodz.inaction.hadoop.extremum.max.GlobalMaxCostDriver /user/xiaoxiang/datasets/cost /user/xiaoxiang/output/cost 运行过程控制台输出内容,大概如下所示: 01 13/03/22 16:30:16 INFO input.FileInputFormat: Total input paths to process : 1 02 13/03/22 16:30:16 INFO util.NativeCodeLoader: Loaded the native-hadoop library 03 13/03/22 16:30:16 WARN snappy.LoadSnappy: Snappy native library not loaded 04 13/03/22 16:30:16 INFO mapred.JobClient: Running job: job_201303111631_0004 05 13/03/22 16:30:17 INFO mapred.JobClient: map 0% reduce 0% 06 13/03/22 16:30:33 INFO mapred.JobClient: map 22% reduce 0% 07 13/03/22 16:30:36 INFO mapred.JobClient: map 28% reduce 0% 08 13/03/22 16:30:45 INFO mapred.JobClient: map 52% reduce 9% 09 13/03/22 16:30:48 INFO mapred.JobClient: map 57% reduce 9% 10 13/03/22 16:30:57 INFO mapred.JobClient: map 80% reduce 9% 11 13/03/22 16:31:00 INFO mapred.JobClient: map 85% reduce 19% 12 13/03/22 16:31:10 INFO mapred.JobClient: map 100% reduce 28% 13 13/03/22 16:31:19 INFO mapred.JobClient: map 100% reduce 100% 14 13/03/22 16:31:24 INFO mapred.JobClient: Job complete: job_201303111631_0004 15 13/03/22 16:31:24 INFO mapred.JobClient: Counters: 29 16 13/03/22 16:31:24 INFO mapred.JobClient: Job Counters 17 13/03/22 16:31:24 INFO mapred.JobClient: Launched reduce tasks=1 18 13/03/22 16:31:24 INFO mapred.JobClient: SLOTS_MILLIS_MAPS=76773 19 13/03/22 16:31:24 INFO mapred.JobClient: Total time spent by all reduces waiting after reserving slots (ms)=0 20 13/03/22 16:31:24 INFO mapred.JobClient: Total time spent by all maps waiting after reserving slots (ms)=0 21 13/03/22 16:31:24 INFO mapred.JobClient: Launched map tasks=7 22 13/03/22 16:31:24 INFO mapred.JobClient: Data-local map tasks=7 23 13/03/22 16:31:24 INFO mapred.JobClient: SLOTS_MILLIS_REDUCES=40497 24 13/03/22 16:31:24 INFO mapred.JobClient: File Output Format Counters 25 13/03/22 16:31:24 INFO mapred.JobClient: Bytes Written=3029 26 13/03/22 16:31:24 INFO mapred.JobClient: FileSystemCounters 27 13/03/22 16:31:24 INFO mapred.JobClient: FILE_BYTES_READ=142609 28 13/03/22 16:31:24 INFO mapred.JobClient: HDFS_BYTES_READ=448913653 29 13/03/22 16:31:24 INFO mapred.JobClient: FILE_BYTES_WRITTEN=338151 30 13/03/22 16:31:24 INFO mapred.JobClient: HDFS_BYTES_WRITTEN=3029 31 13/03/22 16:31:24 INFO mapred.JobClient: File Input Format Counters 32 13/03/22 16:31:24 INFO mapred.JobClient: Bytes Read=448912799 33 13/03/22 16:31:24 INFO mapred.JobClient: Map-Reduce Framework 34 13/03/22 16:31:24 INFO mapred.JobClient: Map output materialized bytes=21245 35 13/03/22 16:31:24 INFO mapred.JobClient: Map input records=10000000 36 13/03/22 16:31:24 INFO mapred.JobClient: Reduce shuffle bytes=18210 37 13/03/22 16:31:24 INFO mapred.JobClient: Spilled Records=12582 38 13/03/22 16:31:24 INFO mapred.JobClient: Map output bytes=110000000 39 13/03/22 16:31:24 INFO mapred.JobClient: CPU time spent (ms)=80320 40 13/03/22 16:31:24 INFO mapred.JobClient: Total committed heap usage (bytes)=1535639552 41 13/03/22 16:31:24 INFO mapred.JobClient: Combine input records=10009320 42 13/03/22 16:31:24 INFO mapred.JobClient: SPLIT_RAW_BYTES=854 43 13/03/22 16:31:24 INFO mapred.JobClient: Reduce input records=1631 44 13/03/22 16:31:24 INFO mapred.JobClient: Reduce input groups=233 45 13/03/22 16:31:24 INFO mapred.JobClient: Combine output records=10951 46 13/03/22 16:31:24 INFO mapred.JobClient: Physical memory (bytes) snapshot=1706708992 47 13/03/22 16:31:24 INFO mapred.JobClient: Reduce output records=233 48 13/03/22 16:31:24 INFO mapred.JobClient: Virtual memory (bytes) snapshot=4316872704 49 13/03/22 16:31:24 INFO mapred.JobClient: Map output records=10000000 验证Job结果输出 001 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ bin/hadoop fs -cat/user/xiaoxiang/output/cost/part-r-00000 002 AD 999974516 003 AE 999938630 004 AF 999996180 005 AG 999991085 006 AI 999989595 007 AL 999998489 008 AM 999976746 009 AO 999989628 010 AQ 999995031 011 AR 999953989 012 AS 999935982 013 AT 999999909 014 AU 999937089 015 AW 999965784 016 AZ 999996557 017 BA 999949773 018 BB 999987345 019 BD 999992272 020 BE 999925057 021 BF 999999220 022 BG 999971528 023 BH 999994900 024 BI 999978516 025 BJ 999977886 026 BM 999991925 027 BN 999986630 028 BO 999995482 029 BR 999989947 030 BS 999980931 031 BT 999977488 032 BW 999935985 033 BY 999998496 034 BZ 999975972 035 CA 999978275 036 CC 999968311 037 CD 999978139 038 CF 999995342 039 CG 999788112 040 CH 999997524 041 CI 999998864 042 CK 999968719 043 CL 999967083 044 CM 999998369 045 CN 999975367 046 CO 999999167 047 CR 999971685 048 CU 999976352 049 CV 999990543 050 CW 999987713 051 CX 999987579 052 CY 999982925 053 CZ 999993908 054 DE 999985416 055 DJ 999997438 056 DK 999963312 057 DM 999941706 058 DO 999945597 059 DZ 999973610 060 EC 999920447 061 EE 999949534 062 EG 999980522 063 ER 999980425 064 ES 999949155 065 ET 999987033 066 FI 999966243 067 FJ 999990686 068 FK 999966573 069 FM 999972146 070 FO 999988472 071 FR 999988342 072 GA 999982099 073 GB 999970658 074 GD 999996318 075 GE 999991970 076 GF 999982024 077 GH 999941039 078 GI 999995295 079 GL 999948726 080 GM 999967823 081 GN 999951804 082 GP 999904645 083 GQ 999988635 084 GR 999999672 085 GT 999972984 086 GU 999919056 087 GW 999962551 088 GY 999999881 089 HK 999970084 090 HN 999972628 091 HR 999986688 092 HT 999970913 093 HU 999997568 094 ID 999994762 095 IE 999996686 096 IL 999982184 097 IM 999987831 098 IN 999914991 099 IO 999968575 100 IQ 999990126 101 IR 999986780 102 IS 999973585 103 IT 999997239 104 JM 999982209 105 JO 999977276 106 JP 999983684 107 KE 999996012 108 KG 999991556 109 KH 999975644 110 KI 999994328 111 KM 999989895 112 KN 999991068 113 KP 999967939 114 KR 999992162 115 KW 999924295 116 KY 999977105 117 KZ 999992835 118 LA 999989151 119 LB 999963014 120 LC 999962233 121 LI 999986863 122 LK 999989876 123 LR 999897202 124 LS 999957706 125 LT 999999688 126 LU 999999823 127 LV 999945411 128 LY 999992365 129 MA 999922726 130 MC 999978886 131 MD 999996042 132 MG 999996602 133 MH 999989668 134 MK 999968900 135 ML 999990079 136 MM 999987977 137 MN 999969051 138 MO 999977975 139 MP 999995234 140 MQ 999913110 141 MR 999982303 142 MS 999974690 143 MT 999982604 144 MU 999988632 145 MV 999961206 146 MW 999991903 147 MX 999978066 148 MY 999995010 149 MZ 999981189 150 NA 999961177 151 NC 999961053 152 NE 999990091 153 NF 999989399 154 NG 999985037 155 NI 999965733 156 NL 999949789 157 NO 999993122 158 NP 999972410 159 NR 999956464 160 NU 999987046 161 NZ 999998214 162 OM 999967428 163 PA 999924435 164 PE 999981176 165 PF 999959978 166 PG 999987347 167 PH 999981534 168 PK 999954268 169 PL 999996619 170 PM 999998975 171 PR 999906386 172 PT 999993404 173 PW 999991278 174 PY 999985509 175 QA 999995061 176 RE 999952291 177 RO 999994148 178 RS 999999923 179 RU 999894985 180 RW 999980184 181 SA 999973822 182 SB 999972832 183 SC 999973271 184 SD 999963744 185 SE 999972256 186 SG 999977637 187 SH 999983638 188 SI 999980580 189 SK 999998152 190 SL 999999269 191 SM 999941188 192 SN 999990278 193 SO 999973175 194 SR 999975964 195 ST 999980447 196 SV 999999945 197 SX 999903445 198 SY 999988858 199 SZ 999992537 200 TC 999969540 201 TD 999999303 202 TG 999977640 203 TH 999968746 204 TJ 999983666 205 TK 999971131 206 TM 999958998 207 TN 999963035 208 TO 999947915 209 TP 999986796 210 TR 999995112 211 TT 999984435 212 TV 999971989 213 TW 999975092 214 TZ 999992734 215 UA 999970993 216 UG 999976267 217 UM 999998377 218 US 999912229 219 UY 999989662 220 UZ 999982762 221 VA 999975548 222 VC 999991495 223 VE 999997971 224 VG 999949690 225 VI 999990063 226 VN 999974393 227 VU 999953162 228 WF 999947666 229 WS 999970242 230 YE 999984650 231 YT 999994707 232 ZA 999998692 233 ZM 999973392 234 ZW 999928087 可见,结果是我们所期望的。
Storm是一个分布式的、高容错的实时计算系统,在实时性要求比较强的应用场景下,可以用它来处理海量数据。我们尝试着搭建Storm平台,来实现实时计算。下面,我们在CentOS 6.4上安装配置Storm系统。 安装配置 安装配置过程,按照如下步骤进行: 1、安装配置sunjdk 下载sunjdk,并安装Java运行环境: 1 wget http://download.oracle.com/otn/java/jdk/6u45-b06/jdk-6u45-linux-x64.bin 2 chmod +x jdk-6u45-linux-x64-rpm.bin 3 ./jdk-6u45-linux-x64.bin 配置Java运行时环境: 1 vi ~/.bashrc 2 export JAVA_HOME=/usr/java/jdk1.6.0_45/ 3 export PATH=$PATH:$JAVA_HOME/bin 4 export CLASSPATH=$JAVA_HOME/lib/*.jar:$JAVA_HOME/jre/lib/*.jar 5 . ~/.bashrc 6 java -version 2、安装zeromq 执行如下命令,进行下载配置安装: 1 wget http://download.zeromq.org/zeromq-2.2.0.tar.gz 2 tar -zvxf zeromq-2.2.0.tar.gz 3 cd zeromq-2.2.0/ 4 ./configure 5 sudo make 6 sudo make install 3、安装jzmq 安装jzmq需要使用Git下载源码,从源代码编译安装: 1 sudo yum install git 2 git clone git://github.com/nathanmarz/jzmq.git 3 cd jzmq/ 4 sudo make 5 sudo make install 4、安装Storm 下载解压缩Storm软件包即可: 1 wget http://cloud.github.com/downloads/nathanmarz/storm/storm-0.8.1.zip 2 tar -xvzf storm-0.8.1.zip 然后配置环境变量: 1 cd storm-0.8.1/ 2 vi ~/.bashrc 3 export STORM_HOME=/home/shirdrn/programs/storm-0.8.1 4 export PATH=$PATH:$STORM_HOME/bin 5 . ~/.bashrc 5、安装构建storm-starter 首先需要下载代码,并使用Maven构建: 1 git clone https://github.com/nathanmarz/storm-starter.git 2 cd storm-starter/ 3 mvn -f m2-pom.xml package 如果需要把Storm的示例代码导入到Eclipse环境中,需要执行如下命令: 1 cd storm-starter/ 2 cp m2-pom.xml pom.xml 3 mvn eclipse:eclipse 6、配置Storm 修改配置文件conf/storm.yaml内容如下所示: 01 storm.zookeeper.servers: 02 - "nn" 03 storm.zookeeper.port: 2181 04 nimbus.host: "nn" 05 storm.local.dir: "/home/shirdrn/programs/storm-0.8.1/tmp" 06 supervisor.slots.ports: 07 - 6700 08 - 6701 09 - 6702 10 - 6703 启动运行 1、启动Storm相关服务 启动ZooKeeper 1 bin/zkServer.sh start 启动Nimbus 1 bin/storm nimbus 查看日志,确定Nimbus启动是否成功: 1 [shirdrn@nn storm-0.8.1]$ tail -100f logs/nimbus.log 启动Supervisor 1 bin/storm supervisor 查看日志,确定Supervisor启动是否成功: 1 [shirdrn@nn storm-0.8.1]$ tail -100f logs/supervisor.log 2、提交一个Topology 上面,已经使用Maven构建storm-starter工程,在target目录下生成一个jar文件,然后将该storm-starter工程中的WordCountTopology提交到Nimbus,执行如下命令: 1 bin/storm jar ../storm-starter/target/storm-starter-0.0.1-SNAPSHOT-jar-with-dependencies.jar storm.starter.WordCountTopology myFirstStormApp 上面myFirstStormApp是提交的Topology的名称,可以看到提交Topology的日志信息: 1 0 [main] INFO backtype.storm.StormSubmitter - Jar not uploaded to master yet. Submitting jar... 2 12 [main] INFO backtype.storm.StormSubmitter - Uploading topology jar ../storm-starter/target/storm-starter-0.0.1-SNAPSHOT-jar-with-dependencies.jar to assigned location: /home/shirdrn/programs/storm-0.8.1/tmp/nimbus/inbox/stormjar-0ae68c15-130d-46f9-a46a-69dd4de29a99.jar 3 119 [main] INFO backtype.storm.StormSubmitter - Successfully uploaded topology jar to assigned location: /home/shirdrn/programs/storm-0.8.1/tmp/nimbus/inbox/stormjar-0ae68c15-130d-46f9-a46a-69dd4de29a99.jar 4 119 [main] INFO backtype.storm.StormSubmitter - Submitting topology myFirstStormApp in distributed mode with conf {"topology.workers":3,"topology.debug":true} 5 423 [main] INFO backtype.storm.StormSubmitter - Finished submitting topology: myFirstStormApp 这时,可以通过查看worker的日志,来确定我们提交的Topology的执行情况: 1 [shirdrn@nn storm-0.8.1]$ tail -100f logs/worker-6700.log 2 [shirdrn@nn storm-0.8.1]$ tail -100f logs/worker-6701.log 3 [shirdrn@nn storm-0.8.1]$ tail -100f logs/worker-6702.log 4 [shirdrn@nn storm-0.8.1]$ tail -100f logs/worker-6703.log worker日志文件名称的后缀正好对应于我们在配置文件conf/storm.yaml中配置supervisor.slots.ports中的端口号。 如果只是上面的命令不带参数,表示虚拟测试该程序,如下所示: 1 bin/storm jar ../storm-starter/target/storm-starter-0.0.1-SNAPSHOT-jar-with-dependencies.jar storm.starter.WordCountTopology 可以看到具体模拟执行的情况,它并不将这个Topology提交给Nimbus。 3、Storm管理命令 可以通过如下命令查看Storm的管理操作命令: 1 bin/storm help 下面,给出一些常用的命令: 查询当前运行的Topology 1 bin/storm list 杀掉运行中的Topology 1 bin/storm kill myFirstStormApp 问题说明 在安装Storm的过程中,有关一些依赖安装包的问题,不像在Ubuntu系统下,可以模糊指定软件包名称,然后会给出一些提示信息,CentOS需要明确地指定软件包名称,记录下一下在安装过程中遇到的问题及其解决办法。 1、出现错误:configure: error: Unable to find a working C++ compiler 需要安装g++编译器: 1 sudo yum install gcc-c++ 2、出现错误:configure: error: cannot link with -luuid, install uuid-dev. 需要安装软件包uuid-devel和libuuid-devel: 1 sudo yum install uuid-devel 2 sudo yum install libuuid-devel 3、出现错误:autogen.sh: error: could not find libtool. libtool is required to run autogen.sh. 需要安装libtool: 1 sudo yum install libtool
IoService是对通信双方所进行的I/O操作的抽象,那么无论是在服务器端还是在客户端,都要进行I/O的读写操作,它们有一些共性,可以抽象出来。这里,我们主要详细说明IoAccectpr和IoConnector以及所基于的IoService抽象服务,都提供哪些操作和数据结构,都是如何构建的。首先,提供一个IoService服务接口相关的继承层次关系的类图,如图所示: 最终使用的Acceptor和Connector是上面继承层次中最下层的实现类。 IoService抽象 实际上,支持I/O操作服务的内容,集中在两个类中:IoService和AbstractIoService,看一下类图: 根据上图中IoService接口定义,我们给出接口中定义的方法,如下所示: 01 public interface IoService { 02 void addListener(IoServiceListener listener); 03 void removeListener(IoServiceListener listener); 04 boolean isDisposing(); 05 boolean isDisposed(); 06 void dispose(); 07 void dispose(boolean awaitTermination); 08 IoHandler getHandler(); 09 void setHandler(IoHandler handler); 10 Map<Long, IoSession> getManagedSessions(); 11 int getManagedSessionCount(); 12 IoSessionConfig getSessionConfig(); 13 IoFilterChainBuilder getFilterChainBuilder(); 14 void setFilterChainBuilder(IoFilterChainBuilder builder); 15 DefaultIoFilterChainBuilder getFilterChain(); 16 boolean isActive(); 17 long getActivationTime(); 18 Set<WriteFuture> broadcast(Object message); 19 IoSessionDataStructureFactory getSessionDataStructureFactory(); 20 void setSessionDataStructureFactory(IoSessionDataStructureFactory sessionDataStructureFactory); 21 int getScheduledWriteBytes(); 22 int getScheduledWriteMessages(); 23 IoServiceStatistics getStatistics(); 24 } 我们可以看到,IoService主要定义了两类服务,一类是提供I/O操作相关服务,另一类是会话 (IoSession)相关服务,这两类服务,无论是在服务端还是在客户端,都会提供,以此来保证双方通信。那么,具体地这两类服务中都包括哪些内容,我们总结如下: 管理IoService元数据,描述IoService本身,这些元数据都封装在TransportMetadata中,例如I/O 服务类型(如NIO,APR或RXTX),连接类型(如无连接接),地址类型等。 管理IoServiceListener,它是用来监听与一个IoService服务相关的事件的,比如服务的激活、会话的建立等 等,当然,这些监听服务不是提供给外部进行开发使用的,而是Mina内部使用的。 管理IoHandler,从Mina框架的架构我们知道,IoHandler的具体实现是与业务逻辑处理相关的,也是最靠近应用层的。 管理IoSession,即管理与一个IoService服务交互的会话对象,可以有一组会话同时使用该IoService服务。 管理IoFilter链,IoFilter链基于事件拦截模式,它位于IoHandler与IoService两层之间,Mina为 了方便使用IoFilter链,直接内置了一个IoFilterChainBuilder(具体实现为 DefaultIoFilterChainBuilder)。 管理一些相关的统计信息,如读写字节数、读写消息数、读写时间等。 上面类图中,AbstractIoService实现了IoService接口中定义的操作,同时增加了一些属性字段,可以通过这些字段看出,Mina框架IoService抽象服务层设计了哪些数据结构,用来辅助有关I/O操作的服务。我们通过如下几个方面来详述: IoServiceListener列表 管理服务于IoService的IoServiceListener,主要是通过IoServiceListenerSupport类,这 个类中定义了如下结构: 1 private final List<IoServiceListener> listeners = newCopyOnWriteArrayList<IoServiceListener>(); 2 private final ConcurrentMap<Long, IoSession> managedSessions = newConcurrentHashMap<Long, IoSession>(); 3 private final Map<Long, IoSession> readOnlyManagedSessions = Collections.unmodifiableMap(managedSessions); 当我们创建一个IoService实例时,可能是服务器端的IoAccectpr,也可能是客户端的IoConnector,可以分别通过调用如下两个方法来增加或者移除一个IoServiceListener: 1 void addListener(IoServiceListener listener); 2 void removeListener(IoServiceListener listener); 一个IoServiceListener定义如下操作: 1 public interface IoServiceListener extends EventListener { 2 void serviceActivated(IoService service) throws Exception; 3 void serviceIdle(IoService service, IdleStatus idleStatus) throws Exception; 4 void serviceDeactivated(IoService service) throws Exception; 5 void sessionCreated(IoSession session) throws Exception; 6 void sessionDestroyed(IoSession session) throws Exception; 7 } 通过接口中定义的方法名,可以了解到,一个IoService监听器都负责监听哪些事件。 构建IoFilter链 就像上面IoServiceListener与IoServiceListenerSupport的关系一样,IoFilter是通过另一 个工具类IoFilterChainBuilder来聚合起来,形成一个IoFilter链。通过实现IoFilterChainBuilder 接口的DefaultIoFilterChainBuilder可以对一组IoFilter进行创建。包含的数据结构如下所示: 1 private final List<Entry> entries; 2 3 public DefaultIoFilterChainBuilder() { 4 entries = new CopyOnWriteArrayList<Entry>(); 5 } 其中Entry包装了一个IoFilter以及为其定义的名称。从IoFilterChainBuilder的名称来看,它只是关注一个 IoFilterChain如何创建,而不关心一组注册的IoFilter调用顺序,也不关心被指定事件被触发时调用哪个操作,这些逻辑是由 IoFilterChain来定义,并通过实现这个接口的DefaultIoFilterChain类实现的。当我们调用DefaultIoFilterChainBuilder 实例的有关操作IoFilter的方法,如下所示(在DefaultIoFilterChainBuilder中实现): 1 public synchronized void addFirst(String name, IoFilter filter); 2 public synchronized void addLast(String name, IoFilter filter); 3 public synchronized void addBefore(String baseName, String name, IoFilter filter); 4 public synchronized void addAfter(String baseName, String name, IoFilter filter); 实际上最终在调用构建的方法buildFilterChain的时候,将已经组织到DefaultIoFilterChainBuilder 实例中的多个IoFilter实例添加到已经构造的IoFilterChain中(如默认的DefaultIoFilterChain),一 个IoFilterChain实例可以在IoService实例运行时被使用,下面是buildFilterChain方法的逻辑: 1 public void buildFilterChain(IoFilterChain chain) throws Exception { 2 for (Entry e : entries) { 3 chain.addLast(e.getName(), e.getFilter()); 4 } 5 } 也就是说,IoFilterChainBuilder是供使用Mina框架的开发网络应用程序的人员组织IoFilter链的,它只是一个运行前构建工具;而IoFilterChain是Mina框架运行服务所需要的,即是一个运行时辅助管理IoFilter链调用的工具。 IoSession内存数据结构 每当有一个新的会话被创建,及使用了IoService提供的服务,就对应创建了一个IoSession实例,而且,与IoSession 相关的一些实时数据需要在内存中保存,以便IoService实例能够随时访问并对该会话实例提供需要的I/O读写服务。Mina定义了 IoSessionDataStructureFactory,来保存会话相关数据,这个结构提供了如下两个方法: 1 public interface IoSessionDataStructureFactory { 2 IoSessionAttributeMap getAttributeMap(IoSession session) throws Exception; 3 WriteRequestQueue getWriteRequestQueue(IoSession session) throws Exception; 4 } 可以看出,上面方法中的IoSessionAttributeMap和WriteRequestQueue都是与一个IoSession相 关的数据对象,我们可以看一下,这几个类之间的关系,如图所示: 与一个IoSession有关的数据,都在上面的结构中保存着。其中主要包含两类:一类是用户在启动会话时定义的属性集合,另一类是会话期 间可能需要进行读写操作。每个IoSession实例调用write方法的时候,都会对应这一个WriteRequest对象,封装了写请求数据。而提供I/O服务的IoService实例在运行时会把对应的WriteRequest对象放入/移出IoSessionDataStructureFactory 结构所持有的队列。 Executor:处理I/O事件的执行 每个IoService都对应这一个Executor,用来处理被触发的I/O事件。 IoAcceptor与IoConnector抽象 IoAcceptor和IoConnector已经区分I/O操作相关的不同服务了,作为通信的服务器端和客户端,必然存在一些差异服务来维持各自在通信过程中的角色,比如,IoAcceptor需要监听指定服务端口,等待客户端的连接到服务器端,而IoConnector与服务器端进行通信,首先应该连接到服务器端Socket暴露的服务地址。下面,我们分别根据通信双方的这两种不同角色,来深入讨论一些细节。 IoAcceptor抽象 从IoAcceptor接口定义,可以很好地看出它具有的一些基本操作,如下所示: 01 public interface IoAcceptor extends IoService { 02 SocketAddress getLocalAddress(); 03 Set<SocketAddress> getLocalAddresses(); 04 SocketAddress getDefaultLocalAddress(); 05 List<SocketAddress> getDefaultLocalAddresses(); 06 void setDefaultLocalAddress(SocketAddress localAddress); 07 void setDefaultLocalAddresses(SocketAddress firstLocalAddress, SocketAddress... otherLocalAddresses); 08 void setDefaultLocalAddresses(Iterable<? extends SocketAddress> localAddresses); 09 void setDefaultLocalAddresses(List<? extends SocketAddress> localAddresses); 10 boolean isCloseOnDeactivation(); 11 void setCloseOnDeactivation(boolean closeOnDeactivation); 12 void bind() throws IOException; 13 void bind(SocketAddress localAddress) throws IOException; 14 void bind(SocketAddress firstLocalAddress, SocketAddress... addresses) throwsIOException; 15 void bind(SocketAddress... addresses) throws IOException; 16 void bind(Iterable<? extends SocketAddress> localAddresses) throws IOException; 17 void unbind(); 18 void unbind(SocketAddress localAddress); 19 void unbind(SocketAddress firstLocalAddress, SocketAddress... otherLocalAddresses); 20 void unbind(Iterable<? extends SocketAddress> localAddresses); 21 IoSession newSession(SocketAddress remoteAddress, SocketAddress localAddress); 22 } 可以看到上面定义的方法中,主要是与IP地址相关的操作,主要包括绑定和解绑定,这些操作的实现是在该接口的抽象实现类AbstractIoAcceptor中给予实现的,在AbstractIoAcceptor中并没有涉及到有关SocketChannel的I/O操作,有关如何基于轮询的策略去检查SocketChannel是否有相应的事件被触发,这些I/O相关的操作被封装到AbstractPollingIoAcceptor类中。以基于TCP的NIO通信为例,具体接收客户端到来的连接请求,这些逻辑是在AbstractPollingIoAcceptor的实现类NioSocketAcceptor中实现的,这里创建了用来管理与客户端通信的NioSocketSession对象(它是IoSession的NIO实现)。 IoConnector抽象 IoConnector的接口定义,如下所示: 01 public interface IoConnector extends IoService { 02 int getConnectTimeout(); 03 long getConnectTimeoutMillis(); 04 void setConnectTimeout(int connectTimeout); 05 void setConnectTimeoutMillis(long connectTimeoutInMillis); 06 SocketAddress getDefaultRemoteAddress(); 07 void setDefaultRemoteAddress(SocketAddress defaultRemoteAddress); 08 ConnectFuture connect(); 09 ConnectFuture connect(IoSessionInitializer<? extends ConnectFuture> sessionInitializer); 10 ConnectFuture connect(SocketAddress remoteAddress); 11 ConnectFuture connect(SocketAddress remoteAddress, IoSessionInitializer<? extendsConnectFuture> sessionInitializer); 12 ConnectFuture connect(SocketAddress remoteAddress, SocketAddress localAddress); 13 ConnectFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, IoSessionInitializer<? extends ConnectFuture> sessionInitializer); 14 } IoConnector定义的操作基本是与连接到服务端的。同样,AbstractIoConnector实现了Connector接口定义的基本操作。以基于TCP的NIO通信为例,客户端和服务端有部分操作非常类似,如轮询SocketChannel检查是否有事件触发,读写请求等,所以,客户端在AbstractIoConnector的抽象实现类AbstractPollingIoConnector中处理于此相关的逻辑。与NioSocketAcceptor对应,客户端有一个NioSocketConnector实现类。 通过上面IoAcceptor和IoConnector的说明,我们还不知道具体I/O操作是由谁来处理的。实际上,无论是服务端还是客户端,在处理轮询通道的抽象服务中,封装了一个IoProcessor抽象,它才是实际处理I/O操作的抽象部分。为了将通信的宏观抽象过程与通信过程中的处理细节分开,将IoProcessor独立出来,与宏观通信过程的逻辑解耦合。以基于TCP的NIO通信为例,在AbstractPollingIoAcceptor和AbstractPollingIoConnector中都有一个IoProcessor实例(这里是实现类NioProcessor的实例),通过调用它提供的处理操作来完成实际的I/O操作。
一般来说,基于Hadoop的MapReduce框架来处理数据,主要是面向海量大数据,对于这类数据,Hadoop能够使其真正发挥其能力。对于海量小文件,不是说不能使用Hadoop来处理,只不过直接进行处理效率不会高,而且海量的小文件对于HDFS的架构设计来说,会占用NameNode大量的内存来保存文件的元数据(Bookkeeping)。另外,由于文件比较小,我们是指远远小于HDFS默认Block大小(64M),比如1k~2M,都很小了,在进行运算的时候,可能无法最大限度地充分Locality特性带来的优势,导致大量的数据在集群中传输,开销很大。 但是,实际应用中,也存在类似的场景,海量的小文件的处理需求也大量存在。那么,我们在使用Hadoop进行计算的时候,需要考虑将小数据转换成大数据,比如通过合并压缩等方法,可以使其在一定程度上,能够提高使用Hadoop集群计算方式的适应性。Hadoop也内置了一些解决方法,而且提供的API,可以很方便地实现。 下面,我们通过自定义InputFormat和RecordReader来实现对海量小文件的并行处理。 基本思路描述如下: 在Mapper中将小文件合并,输出结果的文件中每行由两部分组成,一部分是小文件名称,另一部分是该小文件的内容。 编程实现 我们实现一个WholeFileInputFormat,用来控制Mapper的输入规格,其中对于输入过程中处理文本行的读取使用的是自定义的WholeFileRecordReader。当Map任务执行完成后,我们直接将Map的输出原样输出到HDFS中,使用了一个最简单的IdentityReducer。 现在,看一下我们需要实现哪些内容: 读取每个小文件内容的WholeFileRecordReader 定义输入小文件的规格描述WholeFileInputFormat 用来合并小文件的Mapper实现WholeSmallfilesMapper 输出合并后的文件Reducer实现IdentityReducer 配置运行将多个小文件合并成一个大文件 接下来,详细描述上面的几点内容。 WholeFileRecordReader类 输入的键值对类型,对小文件,每个文件对应一个InputSplit,我们读取这个InputSplit实际上就是具有一个Block的整个文件的内容,将整个文件的内容读取到BytesWritable,也就是一个字节数组。 01 package org.shirdrn.kodz.inaction.hadoop.smallfiles.whole; 02 03 import java.io.IOException; 04 05 import org.apache.hadoop.fs.FSDataInputStream; 06 import org.apache.hadoop.fs.FileSystem; 07 import org.apache.hadoop.fs.Path; 08 import org.apache.hadoop.io.BytesWritable; 09 import org.apache.hadoop.io.IOUtils; 10 import org.apache.hadoop.io.NullWritable; 11 import org.apache.hadoop.mapreduce.InputSplit; 12 import org.apache.hadoop.mapreduce.JobContext; 13 import org.apache.hadoop.mapreduce.RecordReader; 14 import org.apache.hadoop.mapreduce.TaskAttemptContext; 15 import org.apache.hadoop.mapreduce.lib.input.FileSplit; 16 17 public class WholeFileRecordReader extends RecordReader<NullWritable, BytesWritable> { 18 19 private FileSplit fileSplit; 20 private JobContext jobContext; 21 private NullWritable currentKey = NullWritable.get(); 22 private BytesWritable currentValue; 23 private boolean finishConverting = false; 24 25 @Override 26 public NullWritable getCurrentKey() throws IOException, InterruptedException { 27 return currentKey; 28 } 29 30 @Override 31 public BytesWritable getCurrentValue() throws IOException, InterruptedException { 32 return currentValue; 33 } 34 35 @Override 36 public void initialize(InputSplit split, TaskAttemptContext context) throwsIOException, InterruptedException { 37 this.fileSplit = (FileSplit) split; 38 this.jobContext = context; 39 context.getConfiguration().set("map.input.file", fileSplit.getPath().getName()); 40 } 41 42 @Override 43 public boolean nextKeyValue() throws IOException, InterruptedException { 44 if (!finishConverting) { 45 currentValue = new BytesWritable(); 46 int len = (int) fileSplit.getLength(); 47 byte[] content = new byte[len]; 48 Path file = fileSplit.getPath(); 49 FileSystem fs = file.getFileSystem(jobContext.getConfiguration()); 50 FSDataInputStream in = null; 51 try { 52 in = fs.open(file); 53 IOUtils.readFully(in, content, 0, len); 54 currentValue.set(content, 0, len); 55 } finally { 56 if (in != null) { 57 IOUtils.closeStream(in); 58 } 59 } 60 finishConverting = true; 61 return true; 62 } 63 return false; 64 } 65 66 @Override 67 public float getProgress() throws IOException { 68 float progress = 0; 69 if (finishConverting) { 70 progress = 1; 71 } 72 return progress; 73 } 74 75 @Override 76 public void close() throws IOException { 77 // TODO Auto-generated method stub 78 79 } 80 } 实现RecordReader接口,最核心的就是处理好迭代多行文本的内容的逻辑,每次迭代通过调用nextKeyValue()方法来判断是否还有可读的文本行,直接设置当前的Key和Value,分别在方法getCurrentKey()和getCurrentValue()中返回对应的值。 另外,我们设置了”map.input.file”的值是文件名称,以便在Map任务中取出并将文件名称作为键写入到输出。 WholeFileInputFormat类 01 package org.shirdrn.kodz.inaction.hadoop.smallfiles.whole; 02 03 import java.io.IOException; 04 05 import org.apache.hadoop.io.BytesWritable; 06 import org.apache.hadoop.io.NullWritable; 07 import org.apache.hadoop.mapreduce.InputSplit; 08 import org.apache.hadoop.mapreduce.RecordReader; 09 import org.apache.hadoop.mapreduce.TaskAttemptContext; 10 import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; 11 12 public class WholeFileInputFormat extends FileInputFormat<NullWritable, BytesWritable> { 13 14 @Override 15 public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException { 16 RecordReader<NullWritable, BytesWritable> recordReader = newWholeFileRecordReader(); 17 recordReader.initialize(split, context); 18 return recordReader; 19 } 20 } 这个类实现比较简单,继承自FileInputFormat后需要实现createRecordReader()方法,返回用来读文件记录的RecordReader,直接使用前面实现的WholeFileRecordReader创建一个实例,然后调用initialize()方法进行初始化。 WholeSmallfilesMapper 01 package org.shirdrn.kodz.inaction.hadoop.smallfiles.whole; 02 03 import java.io.IOException; 04 05 import org.apache.hadoop.io.BytesWritable; 06 import org.apache.hadoop.io.NullWritable; 07 import org.apache.hadoop.io.Text; 08 import org.apache.hadoop.mapreduce.Mapper; 09 10 public class WholeSmallfilesMapper extends Mapper<NullWritable, BytesWritable, Text, BytesWritable> { 11 12 private Text file = new Text(); 13 14 @Override 15 protected void map(NullWritable key, BytesWritable value, Context context) throwsIOException, InterruptedException { 16 String fileName = context.getConfiguration().get("map.input.file"); 17 file.set(fileName); 18 context.write(file, value); 19 } 20 } IdentityReducer类 01 package org.shirdrn.kodz.inaction.hadoop.smallfiles; 02 03 import java.io.IOException; 04 05 import org.apache.hadoop.mapreduce.Reducer; 06 07 public class IdentityReducer<Text, BytesWritable> extends Reducer<Text, BytesWritable, Text, BytesWritable> { 08 09 @Override 10 protected void reduce(Text key, Iterable<BytesWritable> values, Context context)throws IOException, InterruptedException { 11 for (BytesWritable value : values) { 12 context.write(key, value); 13 } 14 } 15 } 这个是Reduce任务的实现,只是将Map任务的输出原样写入到HDFS中。 WholeCombinedSmallfiles 01 package org.shirdrn.kodz.inaction.hadoop.smallfiles.whole; 02 03 import java.io.IOException; 04 05 import org.apache.hadoop.conf.Configuration; 06 import org.apache.hadoop.fs.Path; 07 import org.apache.hadoop.io.BytesWritable; 08 import org.apache.hadoop.io.Text; 09 import org.apache.hadoop.mapreduce.Job; 10 import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; 11 import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; 12 import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat; 13 import org.apache.hadoop.util.GenericOptionsParser; 14 import org.shirdrn.kodz.inaction.hadoop.smallfiles.IdentityReducer; 15 16 public class WholeCombinedSmallfiles { 17 18 public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { 19 20 Configuration conf = new Configuration(); 21 String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs(); 22 if (otherArgs.length != 2) { 23 System.err.println("Usage: conbinesmallfiles <in> <out>"); 24 System.exit(2); 25 } 26 27 Job job = new Job(conf, "combine smallfiles"); 28 29 job.setJarByClass(WholeCombinedSmallfiles.class); 30 job.setMapperClass(WholeSmallfilesMapper.class); 31 job.setReducerClass(IdentityReducer.class); 32 33 job.setMapOutputKeyClass(Text.class); 34 job.setMapOutputValueClass(BytesWritable.class); 35 job.setOutputKeyClass(Text.class); 36 job.setOutputValueClass(BytesWritable.class); 37 38 job.setInputFormatClass(WholeFileInputFormat.class); 39 job.setOutputFormatClass(SequenceFileOutputFormat.class); 40 41 job.setNumReduceTasks(5); 42 43 FileInputFormat.addInputPath(job, new Path(otherArgs[0])); 44 FileOutputFormat.setOutputPath(job, new Path(otherArgs[1])); 45 46 int exitFlag = job.waitForCompletion(true) ? 0 : 1; 47 System.exit(exitFlag); 48 } 49 50 } 这是是程序的入口,主要是对MapReduce任务进行配置,只需要设置好对应的配置即可。我们设置了5个Reduce任务,最终会有5个输出结果文件。 这里,我们的Reduce任务执行的输出格式为SequenceFileOutputFormat定义的,就是SequenceFile,二进制文件。 运行程序 准备工作 1 jar -cvf combine-smallfiles.jar -C ./ org/shirdrn/kodz/inaction/hadoop/smallfiles 2 xiaoxiang@ubuntu3:~$ cd /opt/stone/cloud/hadoop-1.0.3 3 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ bin/hadoop fs -mkdir/user/xiaoxiang/datasets/smallfiles 4 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ bin/hadoop fs -copyFromLocal /opt/stone/cloud/dataset/smallfiles/* /user/xiaoxiang/datasets/smallfiles 运行MapReduce程序 001 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ bin/hadoop jar combine-smallfiles.jar org.shirdrn.kodz.inaction.hadoop.smallfiles.whole.WholeCombinedSmallfiles /user/xiaoxiang/datasets/smallfiles /user/xiaoxiang/output/smallfiles/whole 002 13/03/23 14:09:24 INFO input.FileInputFormat: Total input paths to process : 117 003 13/03/23 14:09:24 INFO mapred.JobClient: Running job: job_201303111631_0016 004 13/03/23 14:09:25 INFO mapred.JobClient: map 0% reduce 0% 005 13/03/23 14:09:40 INFO mapred.JobClient: map 1% reduce 0% 006 13/03/23 14:09:46 INFO mapred.JobClient: map 3% reduce 0% 007 13/03/23 14:09:52 INFO mapred.JobClient: map 5% reduce 0% 008 13/03/23 14:09:58 INFO mapred.JobClient: map 6% reduce 0% 009 13/03/23 14:10:04 INFO mapred.JobClient: map 8% reduce 0% 010 13/03/23 14:10:10 INFO mapred.JobClient: map 10% reduce 0% 011 13/03/23 14:10:13 INFO mapred.JobClient: map 10% reduce 1% 012 13/03/23 14:10:16 INFO mapred.JobClient: map 11% reduce 1% 013 13/03/23 14:10:22 INFO mapred.JobClient: map 13% reduce 1% 014 13/03/23 14:10:28 INFO mapred.JobClient: map 15% reduce 1% 015 13/03/23 14:10:34 INFO mapred.JobClient: map 17% reduce 1% 016 13/03/23 14:10:40 INFO mapred.JobClient: map 18% reduce 2% 017 13/03/23 14:10:46 INFO mapred.JobClient: map 20% reduce 2% 018 13/03/23 14:10:52 INFO mapred.JobClient: map 22% reduce 2% 019 13/03/23 14:10:58 INFO mapred.JobClient: map 23% reduce 2% 020 13/03/23 14:11:04 INFO mapred.JobClient: map 25% reduce 3% 021 13/03/23 14:11:10 INFO mapred.JobClient: map 27% reduce 3% 022 13/03/23 14:11:16 INFO mapred.JobClient: map 29% reduce 3% 023 13/03/23 14:11:22 INFO mapred.JobClient: map 30% reduce 3% 024 13/03/23 14:11:28 INFO mapred.JobClient: map 32% reduce 3% 025 13/03/23 14:11:34 INFO mapred.JobClient: map 34% reduce 4% 026 13/03/23 14:11:40 INFO mapred.JobClient: map 35% reduce 4% 027 13/03/23 14:11:46 INFO mapred.JobClient: map 37% reduce 4% 028 13/03/23 14:11:52 INFO mapred.JobClient: map 39% reduce 4% 029 13/03/23 14:11:58 INFO mapred.JobClient: map 41% reduce 5% 030 13/03/23 14:12:04 INFO mapred.JobClient: map 42% reduce 5% 031 13/03/23 14:12:10 INFO mapred.JobClient: map 44% reduce 5% 032 13/03/23 14:12:16 INFO mapred.JobClient: map 46% reduce 5% 033 13/03/23 14:12:22 INFO mapred.JobClient: map 47% reduce 5% 034 13/03/23 14:12:25 INFO mapred.JobClient: map 47% reduce 6% 035 13/03/23 14:12:28 INFO mapred.JobClient: map 49% reduce 6% 036 13/03/23 14:12:34 INFO mapred.JobClient: map 51% reduce 6% 037 13/03/23 14:12:40 INFO mapred.JobClient: map 52% reduce 6% 038 13/03/23 14:12:46 INFO mapred.JobClient: map 54% reduce 7% 039 13/03/23 14:12:52 INFO mapred.JobClient: map 56% reduce 7% 040 13/03/23 14:12:58 INFO mapred.JobClient: map 58% reduce 7% 041 13/03/23 14:13:04 INFO mapred.JobClient: map 59% reduce 7% 042 13/03/23 14:13:10 INFO mapred.JobClient: map 61% reduce 7% 043 13/03/23 14:13:13 INFO mapred.JobClient: map 61% reduce 8% 044 13/03/23 14:13:16 INFO mapred.JobClient: map 63% reduce 8% 045 13/03/23 14:13:22 INFO mapred.JobClient: map 64% reduce 8% 046 13/03/23 14:13:28 INFO mapred.JobClient: map 66% reduce 8% 047 13/03/23 14:13:34 INFO mapred.JobClient: map 68% reduce 8% 048 13/03/23 14:13:40 INFO mapred.JobClient: map 70% reduce 9% 049 13/03/23 14:13:46 INFO mapred.JobClient: map 71% reduce 9% 050 13/03/23 14:13:52 INFO mapred.JobClient: map 73% reduce 9% 051 13/03/23 14:13:58 INFO mapred.JobClient: map 75% reduce 9% 052 13/03/23 14:14:04 INFO mapred.JobClient: map 76% reduce 9% 053 13/03/23 14:14:10 INFO mapred.JobClient: map 78% reduce 10% 054 13/03/23 14:14:16 INFO mapred.JobClient: map 80% reduce 10% 055 13/03/23 14:14:22 INFO mapred.JobClient: map 82% reduce 10% 056 13/03/23 14:14:28 INFO mapred.JobClient: map 83% reduce 10% 057 13/03/23 14:14:34 INFO mapred.JobClient: map 85% reduce 10% 058 13/03/23 14:14:37 INFO mapred.JobClient: map 85% reduce 11% 059 13/03/23 14:14:40 INFO mapred.JobClient: map 87% reduce 11% 060 13/03/23 14:14:46 INFO mapred.JobClient: map 88% reduce 11% 061 13/03/23 14:14:52 INFO mapred.JobClient: map 90% reduce 11% 062 13/03/23 14:14:58 INFO mapred.JobClient: map 92% reduce 12% 063 13/03/23 14:15:04 INFO mapred.JobClient: map 94% reduce 12% 064 13/03/23 14:15:10 INFO mapred.JobClient: map 95% reduce 12% 065 13/03/23 14:15:16 INFO mapred.JobClient: map 97% reduce 12% 066 13/03/23 14:15:22 INFO mapred.JobClient: map 99% reduce 12% 067 13/03/23 14:15:28 INFO mapred.JobClient: map 100% reduce 13% 068 13/03/23 14:15:37 INFO mapred.JobClient: map 100% reduce 26% 069 13/03/23 14:15:40 INFO mapred.JobClient: map 100% reduce 39% 070 13/03/23 14:15:49 INFO mapred.JobClient: map 100% reduce 59% 071 13/03/23 14:15:52 INFO mapred.JobClient: map 100% reduce 79% 072 13/03/23 14:15:58 INFO mapred.JobClient: map 100% reduce 100% 073 13/03/23 14:16:03 INFO mapred.JobClient: Job complete: job_201303111631_0016 074 13/03/23 14:16:03 INFO mapred.JobClient: Counters: 29 075 13/03/23 14:16:03 INFO mapred.JobClient: Job Counters 076 13/03/23 14:16:03 INFO mapred.JobClient: Launched reduce tasks=5 077 13/03/23 14:16:03 INFO mapred.JobClient: SLOTS_MILLIS_MAPS=491322 078 13/03/23 14:16:03 INFO mapred.JobClient: Total time spent by all reduces waiting after reserving slots (ms)=0 079 13/03/23 14:16:03 INFO mapred.JobClient: Total time spent by all maps waiting after reserving slots (ms)=0 080 13/03/23 14:16:03 INFO mapred.JobClient: Launched map tasks=117 081 13/03/23 14:16:03 INFO mapred.JobClient: Data-local map tasks=117 082 13/03/23 14:16:03 INFO mapred.JobClient: SLOTS_MILLIS_REDUCES=719836 083 13/03/23 14:16:03 INFO mapred.JobClient: File Output Format Counters 084 13/03/23 14:16:03 INFO mapred.JobClient: Bytes Written=147035685 085 13/03/23 14:16:03 INFO mapred.JobClient: FileSystemCounters 086 13/03/23 14:16:03 INFO mapred.JobClient: FILE_BYTES_READ=147032689 087 13/03/23 14:16:03 INFO mapred.JobClient: HDFS_BYTES_READ=147045529 088 13/03/23 14:16:03 INFO mapred.JobClient: FILE_BYTES_WRITTEN=296787727 089 13/03/23 14:16:03 INFO mapred.JobClient: HDFS_BYTES_WRITTEN=147035685 090 13/03/23 14:16:03 INFO mapred.JobClient: File Input Format Counters 091 13/03/23 14:16:03 INFO mapred.JobClient: Bytes Read=147029851 092 13/03/23 14:16:03 INFO mapred.JobClient: Map-Reduce Framework 093 13/03/23 14:16:03 INFO mapred.JobClient: Map output materialized bytes=147036169 094 13/03/23 14:16:03 INFO mapred.JobClient: Map input records=117 095 13/03/23 14:16:03 INFO mapred.JobClient: Reduce shuffle bytes=145779618 096 13/03/23 14:16:03 INFO mapred.JobClient: Spilled Records=234 097 13/03/23 14:16:03 INFO mapred.JobClient: Map output bytes=147032074 098 13/03/23 14:16:03 INFO mapred.JobClient: CPU time spent (ms)=79550 099 13/03/23 14:16:03 INFO mapred.JobClient: Total committed heap usage (bytes)=19630391296 100 13/03/23 14:16:03 INFO mapred.JobClient: Combine input records=0 101 13/03/23 14:16:03 INFO mapred.JobClient: SPLIT_RAW_BYTES=15678 102 13/03/23 14:16:03 INFO mapred.JobClient: Reduce input records=117 103 13/03/23 14:16:03 INFO mapred.JobClient: Reduce input groups=117 104 13/03/23 14:16:03 INFO mapred.JobClient: Combine output records=0 105 13/03/23 14:16:03 INFO mapred.JobClient: Physical memory (bytes) snapshot=20658409472 106 13/03/23 14:16:03 INFO mapred.JobClient: Reduce output records=117 107 13/03/23 14:16:03 INFO mapred.JobClient: Virtual memory (bytes) snapshot=65064620032 108 13/03/23 14:16:03 INFO mapred.JobClient: Map output records=117 验证程序运行结果 01 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ bin/hadoop fs -ls/user/xiaoxiang/output/smallfiles/whole 02 Found 7 items 03 -rw-r--r-- 3 xiaoxiang supergroup 0 2013-03-23 14:15 /user/xiaoxiang/output/smallfiles/whole/_SUCCESS 04 drwxr-xr-x - xiaoxiang supergroup 0 2013-03-23 14:09 /user/xiaoxiang/output/smallfiles/whole/_logs 05 -rw-r--r-- 3 xiaoxiang supergroup 30161482 2013-03-23 14:15 /user/xiaoxiang/output/smallfiles/whole/part-r-00000 06 -rw-r--r-- 3 xiaoxiang supergroup 30160646 2013-03-23 14:15 /user/xiaoxiang/output/smallfiles/whole/part-r-00001 07 -rw-r--r-- 3 xiaoxiang supergroup 27647901 2013-03-23 14:15 /user/xiaoxiang/output/smallfiles/whole/part-r-00002 08 -rw-r--r-- 3 xiaoxiang supergroup 30161567 2013-03-23 14:15 /user/xiaoxiang/output/smallfiles/whole/part-r-00003 09 -rw-r--r-- 3 xiaoxiang supergroup 28904089 2013-03-23 14:15 /user/xiaoxiang/output/smallfiles/whole/part-r-00004 10 11 xiaoxiang@ubuntu3:/opt/stone/cloud/hadoop-1.0.3$ bin/hadoop fs -text /user/xiaoxiang/output/smallfiles/whole/part-r-00000 | cut -d" " -f 1 12 data_50000_000 53 13 data_50000_005 4c 14 data_50000_014 47 15 data_50000_019 47 16 data_50000_023 50 17 data_50000_028 54 18 data_50000_032 45 19 data_50000_037 55 20 data_50000_041 4e 21 data_50000_046 4d 22 data_50000_050 4c 23 data_50000_055 55 24 data_50000_064 54 25 data_50000_069 42 26 data_50000_073 48 27 data_50000_078 54 28 data_50000_082 42 29 data_50000_087 53 30 data_50000_091 43 31 data_50000_096 41 32 data_50000_203 4d 33 data_50000_208 49 34 data_50000_212 48 35 data_50000_230 46 可以看到,Reducer阶段生成了5个文件,他们都是将小文件合并后的得到的大文件,如果需要对这些文件进行其他处理,可以再实现满足实际处理的Mapper,将输入路径指定的前面Reducer的输出路径即可。这样一来,对于大量小文件的处理,转换成了数个大文件的处理,就能够充分利用Hadoop MapReduce计算集群的优势。
我们已经知道,IoHandler是开发网络应用程序的时候,与实际业务逻辑相关的组件,即属于Mina核心框架之外的应用层组件。从Mina 官方文档上,我们几乎没有看到对IoProcessor的说明,实际上IoProcessor对实际使用Mina框架的开发人员透明,无需你去了解它的实现逻辑,它在Mina中用来处理实际的I/O操作。 我们分析的思路是,先分别对IoHandler与IoProcessor进行单独分析,然后再阐述它们之间的不同以及联系。 IoHandler 当我们通过IoSession执行相关操作的时候,如写数据,这些事件会触发Mina框架抽象的IoService实例,从而调用Mina框架底层的相关组件进行处理。这时,配置的IoHandler就被用来处理Mina所触发的相关事件,处理这些事件的操作被抽象出来。 实际上,IoHandler的继承层次非常简单,也说明了基于Mina框架开发实际网络应用程序,对业务逻辑的处理也还是相对比较容易的。看一下 IoHandler的继承层次,如图所示: IoHandler接口所定义的操作,一共定义了7个处理事件的操作,如下所示: 1 public interface IoHandler { 2 void sessionCreated(IoSession session) throws Exception; 3 void sessionClosed(IoSession session) throws Exception; 4 void sessionIdle(IoSession session, IdleStatus status) throws Exception; 5 void exceptionCaught(IoSession session, Throwable cause) throws Exception; 6 void messageReceived(IoSession session, Object message) throws Exception; 7 void messageSent(IoSession session, Object message) throws Exception; 8 } 因为IoHandler是一个接口,所以如果使用该接口我们就必须实现所有的方法,MIna通过使用IoHandlerAdapter来默认实现 IoHandler接口,并在IoHandlerAdapter中全部给出空实现,如果我们要开发自己的IoHandler,可以继承自IoHandlerAdapter,根据需要选择重写指定的处理Mina事件的方法,而对于你不感兴趣的方法就默认不给予实现(默认使用 IoHandlerAdapter的空实现)。 那么,Mina调用IoHandler的时机是什么呢?又是如何调用的呢? 其实,根据Mina的架构,我们知道,在客户端主动发起I/O操作请求以后,会等待Mina触发相应的事件,在经过一组IoFilter之后,在 IoFilter链的最后一个IoFilter被调用将要结束的时候,会调用我们注册的IoHandler实现,经过处理来满足实际业务逻辑需要。我们可以在DefaultIoFilterChain中看到一个内部IoFilter实现类TailFilter,在该类里调用了 IoHandler封装的逻辑,代码如下所示: 01 private static class TailFilter extends IoFilterAdapter { 02 @Override 03 public void sessionCreated(NextFilter nextFilter, IoSession session) throwsException { 04 try { 05 session.getHandler().sessionCreated(session); 06 } finally { 07 // Notify the related future. 08 ConnectFuture future = (ConnectFuture) session.removeAttribute(SESSION_CREATED_FUTURE); 09 if (future != null) { 10 future.setSession(session); 11 } 12 } 13 } 14 15 @Override 16 public void sessionOpened(NextFilter nextFilter, IoSession session) throwsException { 17 session.getHandler().sessionOpened(session); 18 } 19 20 @Override 21 public void sessionClosed(NextFilter nextFilter, IoSession session) throwsException { 22 AbstractIoSession s = (AbstractIoSession) session; 23 try { 24 s.getHandler().sessionClosed(session); 25 } finally { 26 try { 27 s.getWriteRequestQueue().dispose(session); 28 } finally { 29 try { 30 s.getAttributeMap().dispose(session); 31 } finally { 32 try { 33 // Remove all filters. 34 session.getFilterChain().clear(); 35 } finally { 36 if (s.getConfig().isUseReadOperation()) { 37 s.offerClosedReadFuture(); 38 } 39 } 40 } 41 } 42 } 43 } 44 45 @Override 46 public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception { 47 session.getHandler().sessionIdle(session, status); 48 } 49 50 @Override 51 public void exceptionCaught(NextFilter nextFilter, IoSession session, Throwable cause) throws Exception { 52 AbstractIoSession s = (AbstractIoSession) session; 53 try { 54 s.getHandler().exceptionCaught(s, cause); 55 } finally { 56 if (s.getConfig().isUseReadOperation()) { 57 s.offerFailedReadFuture(cause); 58 } 59 } 60 } 61 62 @Override 63 public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception { 64 AbstractIoSession s = (AbstractIoSession) session; 65 if (!(message instanceof IoBuffer)) { 66 s.increaseReadMessages(System.currentTimeMillis()); 67 } else if (!((IoBuffer) message).hasRemaining()) { 68 s.increaseReadMessages(System.currentTimeMillis()); 69 } 70 71 try { 72 session.getHandler().messageReceived(s, message); 73 } finally { 74 if (s.getConfig().isUseReadOperation()) { 75 s.offerReadFuture(message); 76 } 77 } 78 } 79 80 @Override 81 public void messageSent(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception { 82 session.getHandler().messageSent(session, writeRequest.getMessage()); 83 } 84 85 @Override 86 public void filterWrite(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception { 87 nextFilter.filterWrite(session, writeRequest); 88 } 89 90 @Override 91 public void filterClose(NextFilter nextFilter, IoSession session) throws Exception { 92 nextFilter.filterClose(session); 93 } 94 } 在上面的代码中,正好调用了IoHandler接口定义的7个处理事件的方法。如果你还想知道IoFilterChain实例是在何时被调用的, 可以跟踪Mina的源码。 * IoProcessor 基于网络的端到端的通信,Mina通过一个IoSession对象(任何获取到一个IoSession实例的持有者都可以)来间接执行 I/O操作,如发送数据、读取数据等。在Mina内部,当一个IoSession调用对应的方法,则直接触发IoProcessor来对指定的事件进行处理,它基于Ractor模式来简化网络传输的实现(事实上,Java NIO就是基于Reactor模式实现,属于同步非阻塞IO模式)。 我们看一下IoProcessor相关类的继承关系,如图所示: 看到上面AbstractPollingConnectionlessIoAcceptor,我们知道,它同时也是IoService的实现,用于网络通信中的服务端,处理接收请求。可见,对于基于UDP/IP的传输,IoAcceptor和IoProcessor的处理是实现在一起的,可能实际处理的逻辑本身比较简单,放到一起能够更好地表达它们之间的紧密联系。 下面我们看一下IoProcessor接口的定义,如下所示: 01 public interface IoProcessor<S extends IoSession> { 02 boolean isDisposing(); 03 boolean isDisposed(); 04 void dispose(); 05 void add(S session); 06 void flush(S session); 07 void write(S session, WriteRequest writeRequest); 08 void updateTrafficControl(S session); 09 void remove(S session); 10 } 我们根据上面接口总结一下,一个IoProcessor实际处理了如下内容: 添加IoSession实例,主要是使用IoProcessor内部的一个IoSession队列newSessions来存放。 Flush指定IoSession实例到IoProcessor内部的flushingSessions队列。 Write一个IoSession实例对应的WriteRequest,主要是将一个WriteRequest加入到 IoSession实例所持有的WriteRequestQueue writeRequestQueue队列。至于加入到队列的请求何时处理,其实我们可以参考IoProcessor的实现AbstractPollingIoProcessor 类,其内部有一个org.apache.mina.core.polling.AbstractPollingIoProcessor.Processor 线程类,这个线程会在调用一个IoProcessor的方法public final void add(S session)的时候被启动(实际,在调用dispose()、add(S session)、remove(S session)这三个方法的时候,都会尝试着去启动一个Processor线程,如果没有启动则会启动这个线程),然后反复循环检测并处理队列中的写请求。 当IoProcessor关闭与一个IoSession实例实例相关的连接,则会将这个IoSession实例从 removingSessions队列中移除。 控制处理事件的通信量,主要是控制读写:如果没有注册读操作(SelectionKey.OP_READ),则注册一个,否则当一个 读操作已经就绪,则进行读数据的处理;如果没有注册写操作(SelectionKey.OP_WRITE),则注册一个,否则当一个写操 作已经就绪,则进行写数据的处理。 释放所有与IoProcessor有关的资源。 总结 经过上面的对比,我们已经能够知道IoHandler与IoProcessor的本质区别。 IoHandler是在IoFilterChain执行中最后一个IoFilter中被调用,比如,经过IoFilterChain进行 codec、logging等等操作之后,已经将通信层的数据转化成我们需要的业务对象数据,这时就可以调用我们定义的IoHandler实 现来进行处理。 IoProcessor是与实际的Socket或Channel相关的操作紧密相关的,也就是说,它是支撑Mina进行处理底层实际I/O请 求的处理器。
通过Mina官 网文档,我们可以看到,有如下几个状态: Connected : the session has been created and is available Idle : the session hasn’t processed any request for at least a period of time (this period is configurable)Closing : the session is being closed (the remaining messages are being flushed, cleaning up is not terminated) Idle for read : no read has actually been made for a period of time Idle for write : no write has actually been made for a period of time Idle for both : no read nor write for a period of time Closed : The session is now closed, nothing else can be done to revive it. 对应的状态迁移图,如图所示: 通过上面的状态图,我们可以看出,是哪个事件的发生使得IoSession进入哪个状态,比较直观明了。下面,我们看一下IoSession对应的设计,类继承关系如下所示: 对于IoSession接口类,我在上图把具有不同类型功能的操作进行了分类,说明如下: 一个IoSession实例可以访问/持有哪些数据:前半部分以get开头的方法,能够返回对应的数据对象。 一个IoSession实例可以检测哪些状态数据:中间部分以is开头的一些方法。 一个IoSession实例可以执行哪些方法调用:后半部分以动词开头命名的方法。 一个IoSession实例还可以获取通信过程相关的统计信息,如读取字节数/消息数、写入字节数/消息数,等等,在上面类图中省略 了这些方法。 可见,IoSession在Mina框架中的位置是相当重要的。 根据上面的类图,我们分析一下NioSocketSession类的源代码。 AbstractIoSession实现了IoSession接口中定义的大多数方法,我们关注读和写两个重要的方法,因为他们最终也被NioSocketSession类所继承。 先看读数据请求方法read,如下所示: 01 public final ReadFuture read() { 02 if (!getConfig().isUseReadOperation()) { 03 throw new IllegalStateException("useReadOperation is not enabled."); 04 } 05 06 Queue<ReadFuture> readyReadFutures = getReadyReadFutures(); // 获取到read请求就绪队列 07 ReadFuture future; 08 synchronized (readyReadFutures) { // 这个对就绪队列是共享的,对于读请求之间需要进行同步 09 future = readyReadFutures.poll(); // 出队 10 if (future != null) { // 如果队列中有就绪的read请求 11 if (future.isClosed()) { // 如果与该IoSession相关的ReadFuture已经关闭(读取完成) 12 readyReadFutures.offer(future); // 还要将这个ReadFuture放入到队列,等待该IoSession下次可能的读请求 13 } 14 } else { 15 future = new DefaultReadFuture(this); // 如果是与该IoSession相关的第一次读请求,目前读就绪队列肯定没有一个ReadFuture实例,则需要创建一个 16 getWaitingReadFutures().offer(future); // 将新创建的ReadFuture实例放入到等待读队列 17 } 18 } 19 20 return future; // 返回一个ReadFuture实例,无论是第一次发出读请求,还是上一次读请求已经完成,对于本次读请求都将返回这个ReadFuture实例 21 } 再看一下,写数据请求方法write,如下所示: 01 public WriteFuture write(Object message, SocketAddress remoteAddress) { 02 if (message == null) { 03 throw new IllegalArgumentException("Trying to write a null message : not allowed"); 04 } 05 06 if (!getTransportMetadata().isConnectionless() && (remoteAddress != null)) { 07 throw new UnsupportedOperationException(); 08 } 09 10 if (isClosing() || !isConnected()) { // 如果该次会话正在关闭,或者就没有连接过,则封装一个异常返回一个WriteFuture对象 11 WriteFuture future = new DefaultWriteFuture(this); 12 WriteRequest request = new DefaultWriteRequest(message, future, remoteAddress); 13 WriteException writeException = new WriteToClosedSessionException(request); 14 future.setException(writeException); 15 return future; 16 } 17 18 FileChannel openedFileChannel = null; 19 20 try { 21 if ((message instanceof IoBuffer) && !((IoBuffer) message).hasRemaining()) {// 没有写任何数据 22 throw new IllegalArgumentException("message is empty. Forgot to call flip()?"); 23 } else if (message instanceof FileChannel) { 24 FileChannel fileChannel = (FileChannel) message; 25 message = new DefaultFileRegion(fileChannel, 0, fileChannel.size()); // 如果是FileChannel,则创建一个DefaultFileRegion对象,用来被Mina操纵 26 } else if (message instanceof File) { 27 File file = (File) message; 28 openedFileChannel = new FileInputStream(file).getChannel(); 29 message = new FilenameFileRegion(file, openedFileChannel, 0, openedFileChannel.size()); // 如果是File,则创建FilenameFileRegion 30 } 31 } catch (IOException e) { 32 ExceptionMonitor.getInstance().exceptionCaught(e); 33 return DefaultWriteFuture.newNotWrittenFuture(this, e); 34 } 35 36 // 可以写message了,做写前准备 37 WriteFuture writeFuture = new DefaultWriteFuture(this); 38 WriteRequest writeRequest = new DefaultWriteRequest(message, writeFuture, remoteAddress); 39 40 // Then, get the chain and inject the WriteRequest into it 41 IoFilterChain filterChain = getFilterChain(); // 获取到与该IoSession相关的IoFilterChain,方法getFilterChain实现可以看NioSocketSession类中的实现:filterChain = new DefaultIoFilterChain(this); 42 filterChain.fireFilterWrite(writeRequest); // 触发写事件,将WriteRequest注入到IoFilter实例链,执行注册的IoFilter的逻辑 43 44 // 不关心FileChannel的操作,不进行处理,直接关闭掉 45 if (openedFileChannel != null) { 46 final FileChannel finalChannel = openedFileChannel; 47 writeFuture.addListener(new IoFutureListener<WriteFuture>() { 48 public void operationComplete(WriteFuture future) { 49 try { 50 finalChannel.close(); 51 } catch (IOException e) { 52 ExceptionMonitor.getInstance().exceptionCaught(e); 53 } 54 } 55 }); 56 } 57 58 return writeFuture; // 返回WriteFuture,等待写操作异步完成 59 } 再看,NioSession类中增加了一个返回IoProcessor实例的抽象方法,而这个IoProcessor是在创建一个IoSession实例(例如,可以实例化一个NioSocketSession)的时候,由外部传到IoSession内部。我们知道,IoProcessor是Mina框架底层真正用来处理实际I/O操作的处理器,通过一个IoSession实例获取一个IoProcessor,可以方便地响应作用于IoSession的I/O读写请求,从而由这个IoProcessor直接去处理。 根据Mina框架架构设计,IoService->IoFilter Chain->IoHandler,我们知道在IoFilter Chain的一端(头部)之前会调用处理实际的I/O操作请求,也就是IoProcessor需要处理的逻辑,那么可以想象到,IoProcessor被调用的位置,可以查看org.apache.mina.core.filterchain.DefaultIoFilterChain类的源代码,其中定义了一个内部类,源码如下所示: 01 private class HeadFilter extends IoFilterAdapter { 02 @SuppressWarnings("unchecked") 03 @Override 04 public void filterWrite(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception { 05 06 AbstractIoSession s = (AbstractIoSession) session; 07 08 // Maintain counters. 09 if (writeRequest.getMessage() instanceof IoBuffer) { 10 IoBuffer buffer = (IoBuffer) writeRequest.getMessage(); 11 // I/O processor implementation will call buffer.reset() 12 // it after the write operation is finished, because 13 // the buffer will be specified with messageSent event. 14 buffer.mark(); 15 int remaining = buffer.remaining(); 16 17 if (remaining == 0) { 18 // Zero-sized buffer means the internal message 19 // delimiter. 20 s.increaseScheduledWriteMessages(); 21 } else { 22 s.increaseScheduledWriteBytes(remaining); 23 } 24 } else { 25 s.increaseScheduledWriteMessages(); 26 } 27 28 WriteRequestQueue writeRequestQueue = s.getWriteRequestQueue(); 29 30 if (!s.isWriteSuspended()) { 31 if (writeRequestQueue.size() == 0) { 32 // We can write directly the message 33 s.getProcessor().write(s, writeRequest); 34 } else { 35 s.getWriteRequestQueue().offer(s, writeRequest); 36 s.getProcessor().flush(s); 37 } 38 } else { 39 s.getWriteRequestQueue().offer(s, writeRequest); 40 } 41 } 42 43 @SuppressWarnings("unchecked") 44 @Override 45 public void filterClose(NextFilter nextFilter, IoSession session) throws Exception { 46 ((AbstractIoSession) session).getProcessor().remove(session); 47 } 48 } 最后,我们看一下NioSocketSession实例被创建的时机。其实很容易想到,当一次网络通信开始的时候,也就是客户端连接到服务器端的时候,服务器端首先进行accept,这时候一次会话才被启动,也就是在这个是被创建,拿Mina中的NioSocketAcceptor类来看,创建NioSocketSession的代码,如下所示: 01 protected NioSession accept(IoProcessor<NioSession> processor, ServerSocketChannel handle) throws Exception { 02 03 SelectionKey key = handle.keyFor(selector); 04 05 if ((key == null) || (!key.isValid()) || (!key.isAcceptable())) { 06 return null; 07 } 08 09 // accept the connection from the client 10 SocketChannel ch = handle.accept(); 11 12 if (ch == null) { 13 return null; 14 } 15 16 return new NioSocketSession(this, processor, ch); // 创建NioSocketSession实例 17 } 通过上面的分析,我们可知,IoSession在基于Mina进行网络通信的过程中,对于网络通信相关操作的请求都是基于一个IoSession实例来进行的,所以说,IoSession在Mina中是一个很重要的结构。
Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方(Provider)和服务消费方(Consumer)两个角色。关于注册中心、协议支持、服务监控等内容,详见后面描述。 总体架构 Dubbo的总体架构,如图所示: Dubbo框架设计一共划分了10个层,而最上面的Service层是留给实际想要使用Dubbo开发分布式服务的开发者实现业务逻辑的接口层。图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口, 位于中轴线上的为双方都用到的接口。 下面,结合Dubbo官方文档,我们分别理解一下框架分层架构中,各个层次的设计要点: 服务接口层(Service):该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。 配置层(Config):对外配置接口,以ServiceConfig和ReferenceConfig为中心,可以直接new配置类,也可以通过spring解析配置生成配置类。 服务代理层(Proxy):服务接口透明代理,生成服务的客户端Stub和服务器端Skeleton,以ServiceProxy为中心,扩展接口为ProxyFactory。 服务注册层(Registry):封装服务地址的注册与发现,以服务URL为中心,扩展接口为RegistryFactory、Registry和RegistryService。可能没有服务注册中心,此时服务提供方直接暴露服务。 集群层(Cluster):封装多个提供者的路由及负载均衡,并桥接注册中心,以Invoker为中心,扩展接口为Cluster、Directory、Router和LoadBalance。将多个服务提供方组合为一个服务提供方,实现对服务消费方来透明,只需要与一个服务提供方进行交互。 监控层(Monitor):RPC调用次数和调用时间监控,以Statistics为中心,扩展接口为MonitorFactory、Monitor和MonitorService。 远程调用层(Protocol):封将RPC调用,以Invocation和Result为中心,扩展接口为Protocol、Invoker和Exporter。Protocol是服务域,它是Invoker暴露和引用的主功能入口,它负责Invoker的生命周期管理。Invoker是实体域,它是Dubbo的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起invoke调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。 信息交换层(Exchange):封装请求响应模式,同步转异步,以Request和Response为中心,扩展接口为Exchanger、ExchangeChannel、ExchangeClient和ExchangeServer。 网络传输层(Transport):抽象mina和netty为统一接口,以Message为中心,扩展接口为Channel、Transporter、Client、Server和Codec。 数据序列化层(Serialize):可复用的一些工具,扩展接口为Serialization、 ObjectInput、ObjectOutput和ThreadPool。 从上图可以看出,Dubbo对于服务提供方和服务消费方,从框架的10层中分别提供了各自需要关心和扩展的接口,构建整个服务生态系统(服务提供方和服务消费方本身就是一个以服务为中心的)。 根据官方提供的,对于上述各层之间关系的描述,如下所示: 在RPC中,Protocol是核心层,也就是只要有Protocol + Invoker + Exporter就可以完成非透明的RPC调用,然后在Invoker的主过程上Filter拦截点。 图中的Consumer和Provider是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用Client和Server的原因是Dubbo在很多场景下都使用Provider、Consumer、Registry、Monitor划分逻辑拓普节点,保持统一概念。 而Cluster是外围概念,所以Cluster的目的是将多个Invoker伪装成一个Invoker,这样其它人只要关注Protocol层Invoker即可,加上Cluster或者去掉Cluster对其它层都不会造成影响,因为只有一个提供者时,是不需要Cluster的。 Proxy层封装了所有接口的透明化代理,而在其它层都以Invoker为中心,只有到了暴露给用户使用时,才用Proxy将Invoker转成接口,或将接口实现转成Invoker,也就是去掉Proxy层RPC是可以Run的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。 而Remoting实现是Dubbo协议的实现,如果你选择RMI协议,整个Remoting都不会用上,Remoting内部再划为Transport传输层和Exchange信息交换层,Transport层只负责单向消息传输,是对Mina、Netty、Grizzly的抽象,它也可以扩展UDP传输,而Exchange层是在传输层之上封装了Request-Response语义。 Registry和Monitor实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。 从上面的架构图中,我们可以了解到,Dubbo作为一个分布式服务框架,主要具有如下几个核心的要点: 服务定义 服务是围绕服务提供方和服务消费方的,服务提供方实现服务,而服务消费方调用服务。 服务注册 对于服务提供方,它需要发布服务,而且由于应用系统的复杂性,服务的数量、类型也不断膨胀;对于服务消费方,它最关心如何获取到它所需要的服务,而面对复杂的应用系统,需要管理大量的服务调用。而且,对于服务提供方和服务消费方来说,他们还有可能兼具这两种角色,即既需要提供服务,有需要消费服务。 通过将服务统一管理起来,可以有效地优化内部应用对服务发布/使用的流程和管理。服务注册中心可以通过特定协议来完成服务对外的统一。Dubbo提供的注册中心有如下几种类型可供选择: Multicast注册中心 Zookeeper注册中心 Redis注册中心 Simple注册中心 服务监控 无论是服务提供方,还是服务消费方,他们都需要对服务调用的实际状态进行有效的监控,从而改进服务质量。 远程通信与信息交换 远程通信需要指定通信双方所约定的协议,在保证通信双方理解协议语义的基础上,还要保证高效、稳定的消息传输。Dubbo继承了当前主流的网络通信框架,主要包括如下几个: Mina Netty Grizzly 服务调用 下面从Dubbo官网直接拿来,看一下基于RPC层,服务提供方和服务消费方之间的调用关系,如图所示: 上图中,蓝色的表示与业务有交互,绿色的表示只对Dubbo内部交互。上述图所描述的调用流程如下: 服务提供方发布服务到服务注册中心; 服务消费方从服务注册中心订阅服务; 服务消费方调用已经注册的可用服务 接着,将上面抽象的调用流程图展开,详细如图所示: 注册/注销服务服务的注册与注销,是对服务提供方角色而言,那么注册服务与注销服务的时序图,如图所示:服务订阅/取消为了满足应用系统的需求,服务消费方的可能需要从服务注册中心订阅指定的有服务提供方发布的服务,在得到通知可以使用服务时,就可以直接调用服务。反过来,如果不需要某一个服务了,可以取消该服务。下面看一下对应的时序图,如图所示: 协议支持 Dubbo支持多种协议,如下所示: Dubbo协议 Hessian协议 HTTP协议 RMI协议 WebService协议 Thrift协议 Memcached协议 Redis协议 在通信过程中,不同的服务等级一般对应着不同的服务质量,那么选择合适的协议便是一件非常重要的事情。你可以根据你应用的创建来选择。例如,使用RMI协议,一般会受到防火墙的限制,所以对于外部与内部进行通信的场景,就不要使用RMI协议,而是基于HTTP协议或者Hessian协议。 参考补充 Dubbo以包结构来组织各个模块,各个模块及其关系,如图所示: 可以通过Dubbo的代码(使用Maven管理)组织,与上面的模块进行比较。简单说明各个包的情况: dubbo-common 公共逻辑模块,包括Util类和通用模型。 dubbo-remoting 远程通讯模块,相当于Dubbo协议的实现,如果RPC用RMI协议则不需要使用此包。 dubbo-rpc 远程调用模块,抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理。 dubbo-cluster 集群模块,将多个服务提供方伪装为一个提供方,包括:负载均衡、容错、路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。 dubbo-registry 注册中心模块,基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。 dubbo-monitor 监控模块,统计服务调用次数,调用时间的,调用链跟踪的服务。 dubbo-config 配置模块,是Dubbo对外的API,用户通过Config使用Dubbo,隐藏Dubbo所有细节。 dubbo-container 容器模块,是一个Standalone的容器,以简单的Main加载Spring启动,因为服务通常不需要Tomcat/JBoss等Web容器的特性,没必要用Web容器去加载服务。
Hadoop Streaming提供了一个便于进行MapReduce编程的工具包,使用它可以基于一些可执行命令、脚本语言或其他编程语言来实现Mapper和 Reducer,从而充分利用Hadoop并行计算框架的优势和能力,来处理大数据。需要注意的是,Streaming方式是基于Unix系统的标准输入输出来进行MapReduce Job的运行,它区别与Pipes的地方主要是通信协议,Pipes使用的是Socket通信,是对使用C++语言来实现MapReduce Job并通过Socket通信来与Hadopp平台通信,完成Job的执行。任何支持标准输入输出特性的编程语言都可以使用Streaming方式来实现MapReduce Job,基本原理就是输入从Unix系统标准输入,输出使用Unix系统的标准输出。 Hadoop是使用Java语言编写的,所以最直接的方式的就是使用Java语言来实现Mapper和Reducer,然后配置MapReduce Job,提交到集群计算环境来完成计算。但是很多开发者可能对Java并不熟悉,而是对一些具有脚本特性的语言,如C++、Shell、Python、 Ruby、PHP、Perl有实际开发经验,Hadoop Streaming为这一类开发者提供了使用Hadoop集群来进行处理数据的工具,即工具包hadoop-streaming-.jar。 Hadoop Streaming使用了Unix的标准输入输出作为Hadoop和其他编程语言的开发接口,因此在其他的编程语言所写的程序中,只需要将标准输入作为程 序的输入,将标准输出作为程序的输出就可以了。在标准的输入输出中,Key和Value是以Tab作为分隔符,并且在Reducer的标准输入中,Hadoop框架保证了输入的数据是经过了按Key排序的。 如何使用Hadoop Streaming工具呢?我们可以查看该工具的使用方法,通过命令行来获取,如下所示: 01 xiaoxiang@ubuntu3:~/hadoop$ bin/hadoop jar ./contrib/streaming/hadoop-streaming-1.0.3.jar -info 02 Usage: $HADOOP_HOME/bin/hadoop jar $HADOOP_HOME/hadoop-streaming.jar [options] 03 Options: 04 -input <path> DFS input file(s) for the Map step 05 -output <path> DFS output directory for the Reduce step 06 -mapper <cmd|JavaClassName> The streaming command to run 07 -combiner <cmd|JavaClassName> The streaming command to run 08 -reducer <cmd|JavaClassName> The streaming command to run 09 -file <file> File/dir to be shipped in the Job jar file 10 -inputformat TextInputFormat(default)|SequenceFileAsTextInputFormat|JavaClassName Optional. 11 -outputformat TextOutputFormat(default)|JavaClassName Optional. 12 -partitioner JavaClassName Optional. 13 -numReduceTasks <num> Optional. 14 -inputreader <spec> Optional. 15 -cmdenv <n>=<v> Optional. Pass env.var to streaming commands 16 -mapdebug <path> Optional. To run this script when a map task fails 17 -reducedebug <path> Optional. To run this script when a reduce task fails 18 -io <identifier> Optional. 19 -verbose 20 21 Generic options supported are 22 -conf <configuration file> specify an application configuration file 23 -D <property=value> use value for given property 24 -fs <local|namenode:port> specify a namenode 25 -jt <local|jobtracker:port> specify a job tracker 26 -files <comma separated list of files> specify comma separated files to be copied to the map reduce cluster 27 -libjars <comma separated list of jars> specify comma separated jar files to include in the classpath. 28 -archives <comma separated list of archives> specify comma separated archives to be unarchived on the compute machines. 29 30 The general command line syntax is 31 bin/hadoop command [genericOptions] [commandOptions] 32 33 34 In -input: globbing on <path> is supported and can have multiple -input 35 Default Map input format: a line is a record in UTF-8 36 the key part ends at first TAB, the rest of the line is the value 37 Custom input format: -inputformat package.MyInputFormat 38 Map output format, reduce input/output format: 39 Format defined by what the mapper command outputs. Line-oriented 40 41 The files named in the -file argument[s] end up in the 42 working directory when the mapper and reducer are run. 43 The location of this working directory is unspecified. 44 45 To set the number of reduce tasks (num. of output files): 46 -D mapred.reduce.tasks=10 47 To skip the sort/combine/shuffle/sort/reduce step: 48 Use -numReduceTasks 0 49 A Task's Map output then becomes a 'side-effect output' rather than a reduce input 50 This speeds up processing, This also feels more like "in-place" processing 51 because the input filename and the map input order are preserved 52 This equivalent -reducer NONE 53 54 To speed up the last maps: 55 -D mapred.map.tasks.speculative.execution=true 56 To speed up the last reduces: 57 -D mapred.reduce.tasks.speculative.execution=true 58 To name the job (appears in the JobTracker Web UI): 59 -D mapred.job.name='My Job' 60 To change the local temp directory: 61 -D dfs.data.dir=/tmp/dfs 62 -D stream.tmpdir=/tmp/streaming 63 Additional local temp directories with -cluster local: 64 -D mapred.local.dir=/tmp/local 65 -D mapred.system.dir=/tmp/system 66 -D mapred.temp.dir=/tmp/temp 67 To treat tasks with non-zero exit status as SUCCEDED: 68 -D stream.non.zero.exit.is.failure=false 69 Use a custom hadoopStreaming build along a standard hadoop install: 70 $HADOOP_HOME/bin/hadoop jar /path/my-hadoop-streaming.jar [...]\ 71 [...] -D stream.shipped.hadoopstreaming=/path/my-hadoop-streaming.jar 72 For more details about jobconf parameters see: 73 74 http://wiki.apache.org/hadoop/JobConfFile 75 76 To set an environement variable in a streaming command: 77 -cmdenv EXAMPLE_DIR=/home/example/dictionaries/ 78 79 Shortcut: 80 setenv HSTREAMING "$HADOOP_HOME/bin/hadoop jar $HADOOP_HOME/hadoop-streaming.jar" 81 82 Example: $HSTREAMING -mapper "/usr/local/bin/perl5 filter.pl" 83 -file /local/filter.pl -input "/logs/0604*/*" [...] 84 Ships a script, invokes the non-shipped perl interpreter 85 Shipped files go to the working directory so filter.pl is found by perl 86 Input files are all the daily logs for days in month 2006-04 面,我们分别选择几个可以使用Hadoop Streaming工具来进行计算的例子,比如对单词词频进行统计计算,即WordCount功能。 首先,我们准备测试使用的数据集,如下所示: 1 xiaoxiang@ubuntu3:~/hadoop$ bin/hadoop fs -lsr /user/xiaoxiang/dataset/join/ 2 -rw-r--r-- 3 xiaoxiang supergroup 391103029 2013-03-26 12:19 /user/xiaoxiang/dataset/join/irc_basic_info.ds 3 -rw-r--r-- 3 xiaoxiang supergroup 11577164 2013-03-26 12:20 /user/xiaoxiang/dataset/join/irc_net_block.ds 4 -rw-r--r-- 3 xiaoxiang supergroup 8335235 2013-03-26 12:20 /user/xiaoxiang/dataset/join/irc_org_info.ds 一共有3个数据文件,大概将近400M大小。 下面,选择Python语言来实现MapReduce Job的运行。 使用Python实现Mapper,代码文件为word_count_mapper.py,代码如下所示: 1 #!/usr/bin/env python 2 3 import sys 4 5 for line in sys.stdin: 6 line = line.strip() 7 words = filter(lambda word: word, line.split()) 8 for word in words: 9 print '%s\t%s' % (word, 1) 使用Python实现Reducer,代码文件为word_count_reducer.py,代码如下所示: 01 #!/usr/bin/env python 02 03 import sys 04 from operator import itemgetter 05 06 wc_dict = {} 07 08 for line in sys.stdin: 09 line = line.strip() 10 word, count = line.split() 11 try: 12 count = int(count) 13 wc_dict[word] = wc_dict.get(word, 0) + count 14 except ValueError: 15 pass 16 17 sorted_dict = sorted(wc_dict.items(), key=itemgetter(0)) 18 for word, count in sorted_dict: 19 print '%s\t%s' % (word, count) 运行Python实现的MapReduce程序,如下所示: 01 xiaoxiang@ubuntu3:~/hadoop$ bin/hadoop jar ./contrib/streaming/hadoop-streaming-1.0.3.jar -input /user/xiaoxiang/dataset/join/ -output /user/xiaoxiang/output/streaming/python -mapper word_count_mapper.py -reducer word_count_reducer.py -numReduceTasks 2 -file *.py 02 packageJobJar: [word_count_mapper.py, word_count_reducer.py, /opt/stone/cloud/storage/tmp/hadoop-xiaoxiang/hadoop-unjar4066863202997744310/] [] /tmp/streamjob2336302975421423718.jar tmpDir=null 03 13/04/18 17:50:17 INFO util.NativeCodeLoader: Loaded the native-hadoop library 04 13/04/18 17:50:17 WARN snappy.LoadSnappy: Snappy native library not loaded 05 13/04/18 17:50:17 INFO mapred.FileInputFormat: Total input paths to process : 3 06 13/04/18 17:50:17 INFO streaming.StreamJob: getLocalDirs(): [/opt/stone/cloud/storage/mapred/local] 07 13/04/18 17:50:17 INFO streaming.StreamJob: Running job: job_201303302227_0047 08 13/04/18 17:50:17 INFO streaming.StreamJob: To kill this job, run: 09 13/04/18 17:50:17 INFO streaming.StreamJob: /opt/stone/cloud/hadoop-1.0.3/libexec/../bin/hadoop job -Dmapred.job.tracker=hdfs://ubuntu3:9001/ -killjob_201303302227_0047 10 13/04/18 17:50:17 INFO streaming.StreamJob: Tracking URL:http://ubuntu3:50030/jobdetails.jsp?jobid=job_201303302227_0047 11 13/04/18 17:50:18 INFO streaming.StreamJob: map 0% reduce 0% 12 13/04/18 17:50:33 INFO streaming.StreamJob: map 7% reduce 0% 13 13/04/18 17:50:36 INFO streaming.StreamJob: map 11% reduce 0% 14 13/04/18 17:50:39 INFO streaming.StreamJob: map 15% reduce 0% 15 13/04/18 17:50:42 INFO streaming.StreamJob: map 19% reduce 0% 16 13/04/18 17:50:45 INFO streaming.StreamJob: map 23% reduce 0% 17 13/04/18 17:50:48 INFO streaming.StreamJob: map 25% reduce 0% 18 13/04/18 17:51:09 INFO streaming.StreamJob: map 32% reduce 2% 19 13/04/18 17:51:12 INFO streaming.StreamJob: map 36% reduce 4% 20 13/04/18 17:51:15 INFO streaming.StreamJob: map 40% reduce 8% 21 13/04/18 17:51:18 INFO streaming.StreamJob: map 44% reduce 8% 22 13/04/18 17:51:21 INFO streaming.StreamJob: map 47% reduce 8% 23 13/04/18 17:51:24 INFO streaming.StreamJob: map 50% reduce 8% 24 13/04/18 17:51:45 INFO streaming.StreamJob: map 54% reduce 10% 25 13/04/18 17:51:48 INFO streaming.StreamJob: map 60% reduce 15% 26 13/04/18 17:51:51 INFO streaming.StreamJob: map 65% reduce 17% 27 13/04/18 17:51:55 INFO streaming.StreamJob: map 66% reduce 17% 28 13/04/18 17:51:58 INFO streaming.StreamJob: map 68% reduce 17% 29 13/04/18 17:52:01 INFO streaming.StreamJob: map 72% reduce 17% 30 13/04/18 17:52:04 INFO streaming.StreamJob: map 75% reduce 17% 31 13/04/18 17:52:19 INFO streaming.StreamJob: map 75% reduce 19% 32 13/04/18 17:52:22 INFO streaming.StreamJob: map 87% reduce 21% 33 13/04/18 17:52:25 INFO streaming.StreamJob: map 100% reduce 23% 34 13/04/18 17:52:28 INFO streaming.StreamJob: map 100% reduce 27% 35 13/04/18 17:52:31 INFO streaming.StreamJob: map 100% reduce 29% 36 13/04/18 17:52:37 INFO streaming.StreamJob: map 100% reduce 49% 37 13/04/18 17:52:40 INFO streaming.StreamJob: map 100% reduce 69% 38 13/04/18 17:52:43 INFO streaming.StreamJob: map 100% reduce 72% 39 13/04/18 17:52:46 INFO streaming.StreamJob: map 100% reduce 74% 40 13/04/18 17:52:49 INFO streaming.StreamJob: map 100% reduce 76% 41 13/04/18 17:52:52 INFO streaming.StreamJob: map 100% reduce 78% 42 13/04/18 17:52:55 INFO streaming.StreamJob: map 100% reduce 79% 43 13/04/18 17:52:58 INFO streaming.StreamJob: map 100% reduce 81% 44 13/04/18 17:53:01 INFO streaming.StreamJob: map 100% reduce 84% 45 13/04/18 17:53:04 INFO streaming.StreamJob: map 100% reduce 87% 46 13/04/18 17:53:07 INFO streaming.StreamJob: map 100% reduce 90% 47 13/04/18 17:53:10 INFO streaming.StreamJob: map 100% reduce 93% 48 13/04/18 17:53:13 INFO streaming.StreamJob: map 100% reduce 96% 49 13/04/18 17:53:16 INFO streaming.StreamJob: map 100% reduce 98% 50 13/04/18 17:53:19 INFO streaming.StreamJob: map 100% reduce 99% 51 13/04/18 17:53:22 INFO streaming.StreamJob: map 100% reduce 100% 52 13/04/18 17:54:10 INFO streaming.StreamJob: Job complete: job_201303302227_0047 53 13/04/18 17:54:10 INFO streaming.StreamJob: Output: /user/xiaoxiang/output/streaming/python 验证结果,如下所示: 01 xiaoxiang@ubuntu3:~/hadoop$ bin/hadoop fs -lsr /user/xiaoxiang/output/streaming/python 02 -rw-r--r-- 3 xiaoxiang supergroup 0 2013-04-18 17:54 /user/xiaoxiang/output/streaming/python/_SUCCESS 03 drwxr-xr-x - xiaoxiang supergroup 0 2013-04-18 17:50 /user/xiaoxiang/output/streaming/python/_logs 04 drwxr-xr-x - xiaoxiang supergroup 0 2013-04-18 17:50 /user/xiaoxiang/output/streaming/python/_logs/history 05 -rw-r--r-- 3 xiaoxiang supergroup 37646 2013-04-18 17:50 /user/xiaoxiang/output/streaming/python/_logs/history/job_201303302227_0047_1366278617842_xiaoxiang_streamjob2336302975421423718.jar 06 -rw-r--r-- 3 xiaoxiang supergroup 21656 2013-04-18 17:50 /user/xiaoxiang/output/streaming/python/_logs/history/job_201303302227_0047_conf.xml 07 -rw-r--r-- 3 xiaoxiang supergroup 91367389 2013-04-18 17:52 /user/xiaoxiang/output/streaming/python/part-00000 08 -rw-r--r-- 3 xiaoxiang supergroup 91268074 2013-04-18 17:52 /user/xiaoxiang/output/streaming/python/part-00001 09 xiaoxiang@ubuntu3:~/hadoop$ bin/hadoop fs -cat/user/xiaoxiang/output/streaming/python/part-00000 | head -5 10 ! 2 11 # 36 12 #039 1 13 #1059) 1 14 #1098 1 相关问题 在使用Python实现MapReduce时,总是执行失败? 可以查看TaskTracker结点运行日志,可以看到,总是找不到对应的Python脚本文件,错误示例如下: 01 xiaoxiang@ubuntu1:/opt/stone/cloud/storage/logs/hadoop/userlogs/job_201303302227_0045/attempt_201303302227_0045_m_000001_0$cat stderr 02 java.io.IOException: Cannot run program"/user/xiaoxiang/streaming/python/word_count_mapper.py": java.io.IOException: error=2, No such file or directory 03 at java.lang.ProcessBuilder.start(ProcessBuilder.java:460) 04 at org.apache.hadoop.streaming.PipeMapRed.configure(PipeMapRed.java:214) 05 at org.apache.hadoop.streaming.PipeMapper.configure(PipeMapper.java:66) 06 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 07 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) 08 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) 09 at java.lang.reflect.Method.invoke(Method.java:597) 10 at org.apache.hadoop.util.ReflectionUtils.setJobConf(ReflectionUtils.java:88) 11 at org.apache.hadoop.util.ReflectionUtils.setConf(ReflectionUtils.java:64) 12 at org.apache.hadoop.util.ReflectionUtils.newInstance(ReflectionUtils.java:117) 13 at org.apache.hadoop.mapred.MapRunner.configure(MapRunner.java:34) 14 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 15 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) 16 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) 17 at java.lang.reflect.Method.invoke(Method.java:597) 18 at org.apache.hadoop.util.ReflectionUtils.setJobConf(ReflectionUtils.java:88) 19 at org.apache.hadoop.util.ReflectionUtils.setConf(ReflectionUtils.java:64) 20 at org.apache.hadoop.util.ReflectionUtils.newInstance(ReflectionUtils.java:117) 21 at org.apache.hadoop.mapred.MapTask.runOldMapper(MapTask.java:432) 22 at org.apache.hadoop.mapred.MapTask.run(MapTask.java:372) 23 at org.apache.hadoop.mapred.Child$4.run(Child.java:255) 24 at java.security.AccessController.doPrivileged(Native Method) 25 at javax.security.auth.Subject.doAs(Subject.java:396) 26 at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1121) 27 at org.apache.hadoop.mapred.Child.main(Child.java:249) 28 Caused by: java.io.IOException: java.io.IOException: error=2, No such file or directory 29 at java.lang.UNIXProcess.<init>(UNIXProcess.java:148) 30 at java.lang.ProcessImpl.start(ProcessImpl.java:65) 31 at java.lang.ProcessBuilder.start(ProcessBuilder.java:453) 32 ... 24 more 可以使用Streaming的-file选项指定脚本文件加入到Job的Jar文件中,即使用上面运行的命令行中指定的“-file *.py”, 而实现的2个Python脚本文件就在当前运行Job的结点当前目录下。
使用Dubbo进行远程调用实现服务交互,它支持多种协议,如Hessian、HTTP、RMI、Memcached、Redis、Thrift等等。由于Dubbo将这些协议的实现进行了封装了,无论是服务端(开发服务)还是客户端(调用服务),都不需要关心协议的细节,只需要在配置中指定使用的协议即可,从而保证了服务提供方与服务消费方之间的透明。 另外,如果我们使用Dubbo的服务注册中心组件,这样服务提供方将服务发布到注册的中心,只是将服务的名称暴露给外部,而服务消费方只需要知道注册中心和服务提供方提供的服务名称,就能够透明地调用服务,后面我们会看到具体提供服务和消费服务的配置内容,使得双方之间交互的透明化。 示例场景 我们给出一个示例的应用场景: 服务方提供一个搜索服务,对服务方来说,它基于SolrCloud构建了搜索服务,包含两个集群,ZooKeeper集群和Solr集群,然后在前端通过Nginx来进行反向代理,达到负载均衡的目的。 服务消费方就是调用服务进行查询,给出查询条件(满足Solr的REST-like接口)。 应用设计 基于上面的示例场景,我们打算使用ZooKeeper集群作为服务注册中心。注册中心会暴露给服务提供方和服务消费方,所以注册服务的时候,服务先提供方只需要提供Nginx的地址给注册中心,但是注册中心并不会把这个地址暴露给服务消费方,如图所示: 我们先定义一下,通信双方需要使用的接口,如下所示: 01 package org.shirdrn.platform.dubbo.service.rpc.api; 02 03 public interface SolrSearchService { 04 05 String search(String collection, String q, ResponseType type, int start, introws); 06 07 public enum ResponseType { 08 JSON, 09 XML 10 } 11 } 基于上图中的设计,下面我们分别详细说明Provider和Consumer的设计及实现。 Provider服务设计 Provider所发布的服务组件,包含了一个SolrCloud集群,在SolrCloud集群前端又加了一个反向代理层,使用Nginx来均衡负载。Provider的搜索服务系统,设计如下图所示: 上图中,实际Nginx中将请求直接转发内部的Web Servers上,在这个过程中,使用ZooKeeper来进行协调:从多个分片(Shard)服务器上并行搜索,最后合并结果。我们看一下Nginx配置的内容片段: 01 user nginx; 02 worker_processes 4; 03 04 error_log /var/log/nginx/error.log warn; 05 pid /var/run/nginx.pid; 06 07 08 events { 09 worker_connections 1024; 10 } 11 12 13 http { 14 include /etc/nginx/mime.types; 15 default_type application/octet-stream; 16 17 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 '$status $body_bytes_sent "$http_referer" ' 19 '"$http_user_agent" "$http_x_forwarded_for"'; 20 21 access_log /var/log/nginx/access.log main; 22 23 sendfile on; 24 #tcp_nopush on; 25 26 keepalive_timeout 65; 27 28 #gzip on; 29 30 upstream master { 31 server slave1:8888 weight=1; 32 server slave4:8888 weight=1; 33 server slave6:8888 weight=1; 34 } 35 36 server { 37 listen 80; 38 server_name master; 39 location / { 40 root /usr/share/nginx/html/solr-cloud; 41 index index.html index.htm; 42 proxy_pass http://master; 43 include /home/hadoop/servers/nginx/conf/proxy.conf; 44 } 45 } 46 } 一共配置了3台Solr服务器,因为SolrCloud集群中每一个节点都可以接收搜索请求,然后由整个集群去并行搜索。最后,我们要通过Dubbo服务框架来基于已有的系统来开发搜索服务,并通过Dubbo的注册中心来发布服务。 首先需要实现服务接口,实现代码如下所示: 01 package org.shirdrn.platform.dubbo.service.rpc.server; 02 03 import java.io.IOException; 04 import java.util.HashMap; 05 import java.util.Map; 06 07 import org.apache.commons.logging.Log; 08 import org.apache.commons.logging.LogFactory; 09 import org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService; 10 import org.shirdrn.platform.dubbo.service.rpc.utils.QueryPostClient; 11 import org.springframework.context.support.ClassPathXmlApplicationContext; 12 13 public class SolrSearchServer implements SolrSearchService { 14 15 private static final Log LOG = LogFactory.getLog(SolrSearchServer.class); 16 private String baseUrl; 17 private final QueryPostClient postClient; 18 private static final Map<ResponseType, FormatHandler> handlers = newHashMap<ResponseType, FormatHandler>(0); 19 static { 20 handlers.put(ResponseType.XML, new FormatHandler() { 21 public String format() { 22 return "&wt=xml"; 23 } 24 }); 25 handlers.put(ResponseType.JSON, new FormatHandler() { 26 public String format() { 27 return "&wt=json"; 28 } 29 }); 30 } 31 32 public SolrSearchServer() { 33 super(); 34 postClient = QueryPostClient.newIndexingClient(null); 35 } 36 37 public void setBaseUrl(String baseUrl) { 38 this.baseUrl = baseUrl; 39 } 40 41 public String search(String collection, String q, ResponseType type, 42 int start, int rows) { 43 StringBuffer url = new StringBuffer(); 44 url.append(baseUrl).append(collection).append("/select?").append(q); 45 url.append("&start=").append(start).append("&rows=").append(rows); 46 url.append(handlers.get(type).format()); 47 LOG.info("[REQ] " + url.toString()); 48 return postClient.request(url.toString()); 49 } 50 51 interface FormatHandler { 52 String format(); 53 } 54 55 public static void main(String[] args) throws IOException { 56 String config = SolrSearchServer.class.getPackage().getName().replace('.','/') + "/search-provider.xml"; 57 ClassPathXmlApplicationContext context = newClassPathXmlApplicationContext(config); 58 context.start(); 59 System.in.read(); 60 } 61 62 } 对应的Dubbo配置文件为search-provider.xml,内容如下所示: 01 <?xml version="1.0" encoding="UTF-8"?> 02 03 <beans xmlns="http://www.springframework.org/schema/beans" 04 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" 05 xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.5.xsd 06 http://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"> 07 08 <dubbo:application name="search-provider" /> 09 <dubbo:registry address="zookeeper://slave1:2188?backup=slave3:2188,slave4:2188"/> 10 <dubbo:protocol name="dubbo" port="20880" /> 11 <bean id="searchService"class="org.shirdrn.platform.dubbo.service.rpc.server.SolrSearchServer"> 12 <property name="baseUrl" value="http://nginx-lbserver/solr-cloud/" /> 13 </bean> 14 <dubbo:serviceinterface="org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService"ref="searchService" /> 15 16 </beans> 上面,Dubbo服务注册中心指定ZooKeeper的地址:zookeeper://slave1:2188?backup=slave3:2188,slave4:2188,使用Dubbo协议。配置服务接口的时候,可以按照Spring的Bean的配置方式来配置,注入需要的内容,我们这里指定了搜索集群的Nginx反向代理地址http://nginx-lbserver/solr-cloud/。 Consumer调用服务设计 这个就比较简单了,拷贝服务接口,同时要配置一下Dubbo的配置文件,写个简单的客户端调用就可以实现。客户端实现的Java代码如下所示: 01 package org.shirdrn.platform.dubbo.service.rpc.client; 02 03 import java.util.concurrent.Callable; 04 import java.util.concurrent.Future; 05 06 import org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService; 07 import org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService.ResponseType; 08 import org.springframework.beans.BeansException; 09 import org.springframework.context.support.AbstractXmlApplicationContext; 10 import org.springframework.context.support.ClassPathXmlApplicationContext; 11 12 import com.alibaba.dubbo.rpc.RpcContext; 13 14 public class SearchConsumer { 15 16 private final String collection; 17 private AbstractXmlApplicationContext context; 18 private SolrSearchService searchService; 19 20 public SearchConsumer(String collection, Callable<AbstractXmlApplicationContext> call) { 21 super(); 22 this.collection = collection; 23 try { 24 context = call.call(); 25 context.start(); 26 searchService = (SolrSearchService) context.getBean("searchService"); 27 } catch (BeansException e) { 28 e.printStackTrace(); 29 } catch (Exception e) { 30 e.printStackTrace(); 31 } 32 } 33 34 public Future<String> asyncCall(final String q, final ResponseType type, final intstart, final int rows) { 35 Future<String> future = RpcContext.getContext().asyncCall(new Callable<String>() { 36 public String call() throws Exception { 37 return search(q, type, start, rows); 38 } 39 }); 40 return future; 41 } 42 43 public String syncCall(final String q, final ResponseType type, final int start,final int rows) { 44 return search(q, type, start, rows); 45 } 46 47 private String search(final String q, final ResponseType type, final int start,final int rows) { 48 return searchService.search(collection, q, type, start, rows); 49 } 50 51 public static void main(String[] args) throws Exception { 52 final String collection = "tinycollection"; 53 final String beanXML = "search-consumer.xml"; 54 final String config = SearchConsumer.class.getPackage().getName().replace('.','/') + "/" + beanXML; 55 SearchConsumer consumer = new SearchConsumer(collection, newCallable<AbstractXmlApplicationContext>() { 56 public AbstractXmlApplicationContext call() throws Exception { 57 final AbstractXmlApplicationContext context = newClassPathXmlApplicationContext(config); 58 return context; 59 } 60 }); 61 62 String q = "q=上海&fl=*&fq=building_type:1"; 63 int start = 0; 64 int rows = 10; 65 ResponseType type = ResponseType.XML; 66 for (int k = 0; k < 10; k++) { 67 for (int i = 0; i < 10; i++) { 68 start = 1 * 10 * i; 69 if(i % 2 == 0) { 70 type = ResponseType.XML; 71 } else { 72 type = ResponseType.JSON; 73 } 74 // String result = consumer.syncCall(q, type, start, rows); 75 // System.out.println(result); 76 Future<String> future = consumer.asyncCall(q, type, start, rows); 77 // System.out.println(future.get()); 78 } 79 } 80 } 81 } 查询的时候,需要提供查询字符串,符合Solr语法,例如“q=上海&fl=*&fq=building_type:1”。配置文件,我们使用search-consumer.xml,内容如下所示: 01 <?xml version="1.0" encoding="UTF-8"?> 02 03 <beans xmlns="http://www.springframework.org/schema/beans" 04 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" 05 xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.5.xsd 06 http://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"> 07 08 <dubbo:application name="search-consumer" /> 09 <dubbo:registry address="zookeeper://slave1:2188?backup=slave3:2188,slave4:2188"/> 10 <dubbo:reference id="searchService"interface="org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService" /> 11 12 </beans> 运行说明 首先保证服务注册中心的ZooKeeper集群正常运行,然后启动SolrSearchServer,启动的时候直接将服务注册到ZooKeeper集群存储中,可以通过ZooKeeper的客户端脚本来查看注册的服务数据。一切正常以后,可以启动运行客户端SearchConsumer,调用SolrSearchServer所实现的远程搜索服务。
Dubbo基于Hessian实现了自己Hessian协议,可以直接通过配置的Dubbo内置的其他协议,在服务消费方进行远程调用,也就是说,服务调用方需要使用Java语言来基于Dubbo调用提供方服务,限制了服务调用方。同时,使用Dubbo的Hessian协议实现提供方服务,而调用方可以使用标准的Hessian接口来调用,原生的Hessian协议已经支持多语言客户端调用,支持语言如下所示: Java:http://hessian.caucho.com/#Java Flash/Flex:http://hessian.caucho.com/#FlashFlex Python:http://hessian.caucho.com/#Python C++:http://hessian.caucho.com/#C C#:http://hessian.caucho.com/#NETC D:http://hessian.caucho.com/#D Erlang:http://hessian.caucho.com/#Erlang PHP:http://hessian.caucho.com/#PHP Ruby:http://hessian.caucho.com/#Ruby Objective-C:http://hessian.caucho.com/#ObjectiveC 下面,我们的思路是,先基于Dubbo封装的Hessian协议,实现提供方服务和消费方调用服务,双方必须都使用Dubbo来开发;然后,基于Dubbo封装的Hessian协议实现提供方服务,然后服务消费方使用标准的Hessian接口来进行远程调用,分别使用Java和Python语言来实现。而且,我们实现的提供方服务通过Tomcat发布到服务注册中心。 首先,使用Java语言定义一个搜索服务的接口,代码如下所示: 1 package org.shirdrn.platform.dubbo.service.rpc.api; 2 3 public interface SolrSearchService { 4 String search(String collection, String q, String type, int start, int rows); 5 } 上面接口提供了搜索远程调用功能。 基于Dubbo的Hessian协议实现提供方服务 提供方实现基于Dubbo封装的Hessian协议,实现接口SolrSearchService,实现代码如下所示: 01 package org.shirdrn.platform.dubbo.service.rpc.server; 02 03 import java.io.IOException; 04 import java.util.HashMap; 05 import java.util.Map; 06 07 import org.apache.commons.logging.Log; 08 import org.apache.commons.logging.LogFactory; 09 import org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService; 10 import org.shirdrn.platform.dubbo.service.rpc.utils.QueryPostClient; 11 import org.springframework.context.support.ClassPathXmlApplicationContext; 12 13 public class SolrSearchServer implements SolrSearchService { 14 15 private static final Log LOG = LogFactory.getLog(SolrSearchServer.class); 16 private String baseUrl; 17 private final QueryPostClient postClient; 18 private static final Map<String, FormatHandler> handlers = new HashMap<String, FormatHandler>(0); 19 static { 20 handlers.put("xml", new FormatHandler() { 21 public String format() { 22 return "&wt=xml"; 23 } 24 }); 25 handlers.put("json", new FormatHandler() { 26 public String format() { 27 return "&wt=json"; 28 } 29 }); 30 } 31 32 public SolrSearchServer() { 33 super(); 34 postClient = QueryPostClient.newIndexingClient(null); 35 } 36 37 public void setBaseUrl(String baseUrl) { 38 this.baseUrl = baseUrl; 39 } 40 41 public String search(String collection, String q, String type, int start, introws) { 42 StringBuffer url = new StringBuffer(); 43 url.append(baseUrl).append(collection).append("/select?").append(q); 44 url.append("&start=").append(start).append("&rows=").append(rows); 45 url.append(handlers.get(type.toLowerCase()).format()); 46 LOG.info("[REQ] " + url.toString()); 47 return postClient.request(url.toString()); 48 } 49 50 interface FormatHandler { 51 String format(); 52 } 53 } 因为考虑到后面要使用标准Hessian接口来调用,这里接口方法参数全部使用内置标准类型。然后,我们使用Dubbo的配置文件进行配置,文件search-provider.xml的内容如下所示: 01 <?xml version="1.0" encoding="UTF-8"?> 02 03 <beans xmlns="http://www.springframework.org/schema/beans" 04 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" 05 xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.5.xsd 06 http://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"> 07 08 <dubbo:application name="search-provider" /> 09 <dubbo:registry 10 address="zookeeper://slave1:2188?backup=slave3:2188,slave4:2188" /> 11 <dubbo:protocol name="hessian" port="8080" server="servlet" /> 12 <bean id="searchService" 13 class="org.shirdrn.platform.dubbo.service.rpc.server.SolrSearchServer"> 14 <property name="baseUrl" value="http://nginx-lbserver/solr-cloud/" /> 15 </bean> 16 <dubbo:service 17 interface="org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService" 18 ref="searchService" path="http_dubbo/search" /> 19 20 </beans> 因为使用Tomcat发布提供方服务,所以我们需要实现Spring的org.springframework.web.context.ContextLoader来初始化应用上下文(基于Spring的IoC容器来管理服务对象)。实现类SearchContextLoader代码如下所示: 01 package org.shirdrn.platform.dubbo.context; 02 03 import javax.servlet.ServletContextEvent; 04 import javax.servlet.ServletContextListener; 05 06 import org.shirdrn.platform.dubbo.service.rpc.server.SolrSearchServer; 07 import org.springframework.context.support.ClassPathXmlApplicationContext; 08 import org.springframework.web.context.ContextLoader; 09 10 public class SearchContextLoader extends ContextLoader implementsServletContextListener { 11 12 @Override 13 public void contextDestroyed(ServletContextEvent arg0) { 14 // TODO Auto-generated method stub 15 16 } 17 18 @Override 19 public void contextInitialized(ServletContextEvent arg0) { 20 String config = arg0.getServletContext().getInitParameter("contextConfigLocation"); 21 ClassPathXmlApplicationContext context = newClassPathXmlApplicationContext(config); 22 context.start(); 23 } 24 25 } 最后,配置Web应用部署描述符文件,web.xml内容如下所示: 01 <?xml version="1.0" encoding="UTF-8"?> 02 <web-app id="WebApp_ID" version="2.4" 03 xmlns="http://java.sun.com/xml/ns/j2ee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 04 xsi:schemaLocation="http://java.sun.com/xml/ns/j2eehttp://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> 05 <display-name>http_dubbo</display-name> 06 07 <listener> 08 <listener-class>org.shirdrn.platform.dubbo.context.SearchContextLoader</listener-class> 09 </listener> 10 <context-param> 11 <param-name>contextConfigLocation</param-name> 12 <param-value>classpath:search-provider.xml</param-value> 13 </context-param> 14 15 <servlet> 16 <servlet-name>search</servlet-name> 17 <servlet-class>com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class> 18 <init-param> 19 <param-name>home-class</param-name> 20 <param-value>org.shirdrn.platform.dubbo.service.rpc.server.SolrSearchServer</param-value> 21 </init-param> 22 <init-param> 23 <param-name>home-api</param-name> 24 <param-value>org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService</param-value> 25 </init-param> 26 <load-on-startup>1</load-on-startup> 27 </servlet> 28 <servlet-mapping> 29 <servlet-name>search</servlet-name> 30 <url-pattern>/search</url-pattern> 31 </servlet-mapping> 32 33 <welcome-file-list> 34 <welcome-file>index.html</welcome-file> 35 <welcome-file>index.htm</welcome-file> 36 <welcome-file>index.jsp</welcome-file> 37 <welcome-file>default.html</welcome-file> 38 <welcome-file>default.htm</welcome-file> 39 <welcome-file>default.jsp</welcome-file> 40 </welcome-file-list> 41 </web-app> 启动Tomcat以后,就可以将提供方服务发布到服务注册中心,这里服务注册中心我们使用的是ZooKeeper集群,可以参考上面Dubbo配置文件search-provider.xml的配置内容。 下面,我们通过两种方式来调用已经注册到服务注册中心的服务。 基于Dubbo的Hessian协议远程调用 服务消费方,通过Dubbo配置文件来指定注册到注册中心的服务,配置文件search-consumer.xml的内容,如下所示: 01 <?xml version="1.0" encoding="UTF-8"?> 02 03 <beans xmlns="http://www.springframework.org/schema/beans" 04 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" 05 xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.5.xsd 06 http://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"> 07 08 <dubbo:application name="search-consumer" /> 09 <dubbo:registry 10 address="zookeeper://slave1:2188?backup=slave3:2188,slave4:2188" /> 11 <dubbo:reference id="searchService" 12 interface="org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService" /> 13 14 </beans> 然后,使用Java实现远程调用,实现代码如下所示: 01 package org.shirdrn.platform.dubbo.service.rpc.client; 02 03 import java.util.concurrent.Callable; 04 import java.util.concurrent.Future; 05 06 import org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService; 07 import org.springframework.beans.BeansException; 08 import org.springframework.context.support.AbstractXmlApplicationContext; 09 import org.springframework.context.support.ClassPathXmlApplicationContext; 10 11 import com.alibaba.dubbo.rpc.RpcContext; 12 13 public class SearchConsumer { 14 15 private final String collection; 16 private AbstractXmlApplicationContext context; 17 private SolrSearchService searchService; 18 19 public SearchConsumer(String collection, Callable<AbstractXmlApplicationContext> call) { 20 super(); 21 this.collection = collection; 22 try { 23 context = call.call(); 24 context.start(); 25 searchService = (SolrSearchService) context.getBean("searchService"); 26 } catch (BeansException e) { 27 e.printStackTrace(); 28 } catch (Exception e) { 29 e.printStackTrace(); 30 } 31 } 32 33 public Future<String> asyncCall(final String q, final String type, final intstart, final int rows) { 34 Future<String> future = RpcContext.getContext().asyncCall(new Callable<String>() { 35 public String call() throws Exception { 36 return search(q, type, start, rows); 37 } 38 }); 39 return future; 40 } 41 42 public String syncCall(final String q, final String type, final int start, finalint rows) { 43 return search(q, type, start, rows); 44 } 45 46 private String search(final String q, final String type, final int start, finalint rows) { 47 return searchService.search(collection, q, type, start, rows); 48 } 49 50 public static void main(String[] args) throws Exception { 51 final String collection = "tinycollection"; 52 final String beanXML = "search-consumer.xml"; 53 final String config = SearchConsumer.class.getPackage().getName().replace('.','/') + "/" + beanXML; 54 SearchConsumer consumer = new SearchConsumer(collection, newCallable<AbstractXmlApplicationContext>() { 55 public AbstractXmlApplicationContext call() throws Exception { 56 final AbstractXmlApplicationContext context = newClassPathXmlApplicationContext(config); 57 return context; 58 } 59 }); 60 61 String q = "q=上海&fl=*&fq=building_type:1"; 62 int start = 0; 63 int rows = 10; 64 String type = "xml"; 65 for (int k = 0; k < 10; k++) { 66 for (int i = 0; i < 10; i++) { 67 start = 1 * 10 * i; 68 if (i % 2 == 0) { 69 type = "xml"; 70 } else { 71 type = "json"; 72 } 73 String result = consumer.syncCall(q, type, start, rows); 74 System.out.println(result); 75 // Future<String> future = consumer.asyncCall(q, type, start, 76 // rows); 77 // System.out.println(future.get()); 78 } 79 } 80 } 81 82 } 执行该调用实现,可以远程调用提供方发布的服务。 这种方式限制了服务调用方也必须使用Dubbo来开发调用的代码,也就是限制了编程的语言,而无论是对于内部还是外部,各个团队之间必然存在语言的多样性,如果限制了编程语言,那么开发的服务也只能在内部使用。 基于标准Hessian协议接口的远程调用 下面,使用标准Hessian接口来实现远程调用,这时就不需要关心服务提供方的所使用的开发语言,因为最终是通过HTTP的方式来访问。我们需要下载Hessian对应语言的调用实现库,才能更方便地编程。 使用Java语言实现远程调用 使用Java语言实现,代码如下所示: 01 package org.shirdrn.rpc.hessian; 02 03 import org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService; 04 05 import com.caucho.hessian.client.HessianProxyFactory; 06 07 public class HessianConsumer { 08 09 public static void main(String[] args) throws Throwable { 10 11 String serviceUrl = "http://10.95.3.74:8080/http_dubbo/search"; 12 HessianProxyFactory factory = new HessianProxyFactory(); 13 14 SolrSearchService searchService = (SolrSearchService) factory.create(SolrSearchService.class, serviceUrl); 15 16 String q = "q=上海&fl=*&fq=building_type:1"; 17 String collection = "tinycollection"; 18 int start = 0; 19 int rows = 10; 20 String type = "xml"; 21 String result = searchService.search(collection, q, type, start, rows); 22 System.out.println(result); 23 } 24 } 我们只需要知道提供服务暴露的URL和服务接口即可,这里URL为http://10.95.3.74:8080/http_dubbo/search,接口为org.shirdrn.platform.dubbo.service.rpc.api.SolrSearchService。运行上面程序,可以调用提供方发布的服务。 使用Python语言实现远程调用 使用Python客户端来进行远程调用,我们可以从https://github.com/bgilmore/mustaine下载,然后安装Hessian的代理客户端Python实现库: 1 git clone https://github.com/bgilmore/mustaine.git 2 cd mustaine 3 sudo python setup.py install 然后就可以使用了,使用Python进行远程调用的实现代码如下所示: 01 #!/usr/bin/python 02 03 # coding=utf-8 04 from mustaine.client import HessianProxy 05 06 serviceUrl = 'http://10.95.3.74:8080/http_dubbo/search' 07 q = 'q=*:*&fl=*&fq=building_type:1' 08 start = 0 09 rows = 10 10 resType = 'xml' 11 collection = 'tinycollection' 12 13 if __name__ == '__main__': 14 proxy = HessianProxy(serviceUrl) 15 result = proxy.search(collection, q, resType, start, rows) 16 print result 运行上面程序,就可以看到远程调用的结果。
HAProxy是一款免费、快速并且可靠的一种代理解决方案,支持高可用性、负载均衡特性,同时适用于做基于TCP和HTTP的应用的代理。对于一些负载较大的Web站点,使用HAProxy特别合适。HAProxy能够支撑数以万计的并发连接。它的配置简单,能够很容易整合大我们现有的应用架构之中。 下面,我们在CentOS 6.4上进行安装配置HAProxy。 安装配置 按照如下步骤进行安装: 1 wget http://haproxy.1wt.eu/download/1.4/src/haproxy-1.4.24.tar.gz 2 tar xvzf haproxy-1.4.24.tar.gz 3 cd haproxy-1.4.24 4 make TARGET=linux26 5 make install 默认安装,HAProxy对应的配置文件的存放路径为/etc/haproxy/haproxy.cfg。 我们看一下,默认安装的配置文件内容,如下所示: 01 #--------------------------------------------------------------------- 02 # Example configuration for a possible web application. See the 03 # full configuration options online. 04 # 05 # http://haproxy.1wt.eu/download/1.4/doc/configuration.txt 06 # 07 #--------------------------------------------------------------------- 08 09 #--------------------------------------------------------------------- 10 # Global settings 11 #--------------------------------------------------------------------- 12 global 13 # to have these messages end up in /var/log/haproxy.log you will 14 # need to: 15 # 16 # 1) configure syslog to accept network log events. This is done 17 # by adding the '-r' option to the SYSLOGD_OPTIONS in 18 # /etc/sysconfig/syslog 19 # 20 # 2) configure local2 events to go to the /var/log/haproxy.log 21 # file. A line like the following can be added to 22 # /etc/sysconfig/syslog 23 # 24 # local2.* /var/log/haproxy.log 25 # 26 log 127.0.0.1 local2 27 28 chroot /var/lib/haproxy 29 pidfile /var/run/haproxy.pid 30 maxconn 4000 31 user haproxy 32 group haproxy 33 daemon 34 35 # turn on stats unix socket 36 stats socket /var/lib/haproxy/stats 37 38 #--------------------------------------------------------------------- 39 # common defaults that all the 'listen' and 'backend' sections will 40 # use if not designated in their block 41 #--------------------------------------------------------------------- 42 defaults 43 mode http 44 log global 45 option httplog 46 option dontlognull 47 option http-server-close 48 option forwardfor except 127.0.0.0/8 49 option redispatch 50 retries 3 51 timeout http-request 10s 52 timeout queue 1m 53 timeout connect 10s 54 timeout client 1m 55 timeout server 1m 56 timeout http-keep-alive 10s 57 timeout check 10s 58 maxconn 3000 59 60 #--------------------------------------------------------------------- 61 # main frontend which proxys to the backends 62 #--------------------------------------------------------------------- 63 frontend main *:5000 64 acl url_static path_beg -i /static /images /javascript /stylesheets 65 acl url_static path_end -i .jpg .gif .png .css .js 66 67 use_backend static if url_static 68 default_backend app 69 70 #--------------------------------------------------------------------- 71 # static backend for serving up images, stylesheets and such 72 #--------------------------------------------------------------------- 73 backend static 74 balance roundrobin 75 server static 127.0.0.1:4331 check 76 77 #--------------------------------------------------------------------- 78 # round robin balancing between the various backends 79 #--------------------------------------------------------------------- 80 backend app 81 balance roundrobin 82 server app1 127.0.0.1:5001 check 83 server app2 127.0.0.1:5002 check 84 server app3 127.0.0.1:5003 check 85 server app4 127.0.0.1:5004 check 我们对上面配置文件的内容,适当的扩展,做简单的解释: global段 global段用于配置进程级的参数。官网文档基于参数的功能,将global配置参数分为3组: 进程管理和安全 性能调优 调试 具体内容可以参考文档详细介绍。 defaults段 defaults段主要是代理配置的默认配置段,设置默认参数,这些默认的配置可以在后面配置的其他段中使用。如果其他段中想修改默认的配置参数,只需要覆盖defaults段中的出现配置项内容。 关于defaults段可以配置的参数,可以参考官网文档的详细介绍。 frontend段 frontend段主要配置前端监听的Socket相关的属性,也就是接收请求链接的虚拟节点。这里除了配置这些静态的属性,还可以根据一定的规则,将请求重定向到配置的backend上,backend可能配置的是一个服务器,也可能是一组服务器(集群)。 backend段 backend段主要是配置的实际服务器的信息,通过frontend配置的重定向请求,转发到backend配置的服务器上。 listen段 listen段是将frontend和backend这两段整合在一起,直接将请求从代理转发到实际的后端服务器上。 启动HAProxy代理 启动非常简单,执行如下命令即可: 1 sudo haproxy -f /etc/haproxy/haproxy.cfg 我们简单修改一下配置文件内容,配置一个用来均衡后端SolrCloud搜索集群服务器,如下所示: 01 #--------------------------------------------------------------------- 02 # Global settings 03 #--------------------------------------------------------------------- 04 global 05 log 127.0.0.1 local2 06 07 chroot /var/lib/haproxy 08 pidfile /var/run/haproxy.pid 09 maxconn 4000 10 user haproxy 11 group haproxy 12 daemon 13 14 # turn on stats unix socket 15 stats socket /var/lib/haproxy/stats 16 17 #--------------------------------------------------------------------- 18 # common defaults that all the 'listen' and 'backend' sections will 19 # use if not designated in their block 20 #--------------------------------------------------------------------- 21 defaults 22 mode http 23 log global 24 option httplog 25 option dontlognull 26 option http-server-close 27 option forwardfor except 127.0.0.0/8 28 option redispatch 29 retries 3 30 timeout http-request 10s 31 timeout queue 1m 32 timeout connect 10s 33 timeout client 1m 34 timeout server 1m 35 timeout http-keep-alive 10s 36 timeout check 10s 37 maxconn 3000 38 39 #--------------------------------------------------------------------- 40 # main frontend which proxys to the backends 41 #--------------------------------------------------------------------- 42 frontend haproxy-lbserver 43 bind 0.0.0.0:80 44 acl url_solr_path path_beg /solr-cloud 45 acl url_static path_beg -i /static /images /javascript /stylesheets 46 acl url_static path_end -i .jpg .gif .png .css .js 47 48 use_backend static if url_static 49 use_backend solr-cloud if url_solr_path 50 default_backend static 51 52 #--------------------------------------------------------------------- 53 # static backend for serving up images, stylesheets and such 54 #--------------------------------------------------------------------- 55 backend static 56 balance roundrobin 57 server static 127.0.0.1:4331 check 58 59 #--------------------------------------------------------------------- 60 # round robin balancing between the various backends 61 #--------------------------------------------------------------------- 62 backend solr-cloud 63 balance roundrobin 64 server solr-1 slave1:8888 check 65 server solr-2 slave2:8888 check 66 server solr-3 slave3:8888 check 67 server solr-4 slave4:8888 check frontend的名称为haproxy-lbserver,实际上映射为具体服务IP地址,绑定到80端口,然后请求Path设置为/solr-cloud,也就是前端接收到类似以“http://haproxy-lbserver/solr-cloud”开始的链接,后面可以加上具体的其他请求参数。 在frontend中使用use_backend指令指定了一个转发至的backend,名称为solr-cloud,可以在use_backend指令后面使用过滤条件指令if来指定转发的backend名称。 backend中指定了实际集群服务器的配置,对其进行负载均衡,一共指定了4台Solr搜索服务器,使用roundrobin负载均衡策略。 我们将默认配置文件拷贝到目录/home/hadoop/shiyanjun/haproxy-1.4.24/conf下面,然后启动haproxy: 1 sudo haproxy -f /home/hadoop/shiyanjun/haproxy-1.4.24/conf/haproxy.cfg 启动成功以后,可以访问类似如下的请求链接: 1 http://haproxy-lbserver/solr-cloud/mycollection/select?q=北京&fl=*&fq=building_type:1&start=0&rows=10 HAProxy会将请求转发至backend端的集群服务器上去,执行实际的请求处理。 HAProxy的官网文档相当详细,推荐参考官网文档,了解对应的配置选项和使用方法。
当我们实现了一个Hadoop MapReduce Job以后,而这个Job可能又依赖很多外部的jar文件,在Hadoop集群上运行时,有时会出现找不到具体Class的异常。出现这种问题,基本上就是在Hadoop Job执行过程中,没有从执行的上下文中找到对应的jar文件(实际是unjar的目录,目录里面是对应的Class文件)。所以,我们自然而然想到,正确配置好对应的classpath,MapReduce Job运行时就能够找到。 有两种方式可以更好地实现,一种是设置HADOOP_CLASSPATH,将Job所依赖的jar文件加载到HADOOP_CLASSPATH,这种配置只针对该Job生效,Job结束之后HADOOP_CLASSPATH会被清理;另一种方式是,直接在构建代码的时候,将依赖jar文件与Job代码打成一个jar文件,这种方式可能会使得最终的jar文件比较大,但是结合一些代码构建工具,如Maven,可以在依赖控制方面保持一个Job一个依赖的构建配置,便于管理。下面,我们分别说明这两种方式。 设置HADOOP_CLASSPATH 比如,我们有一个使用HBase的应用,操作HBase数据库中表,肯定需要ZooKeeper,所以对应的jar文件的位置都要设置正确,让运行时Job能够检索并加载。 Hadoop实现里面,有个辅助工具类org.apache.hadoop.util.GenericOptionsParser,能够帮助我们加载对应的文件到classpath中,操作比较容易一些。 下面我们是我们实现的一个例子,程序执行入口的类,代码如下所示: 01 package org.shirdrn.kodz.inaction.hbase.job.importing; 02 03 import java.io.IOException; 04 import java.net.URISyntaxException; 05 06 import org.apache.hadoop.conf.Configuration; 07 import org.apache.hadoop.fs.Path; 08 import org.apache.hadoop.hbase.HBaseConfiguration; 09 import org.apache.hadoop.hbase.client.Put; 10 import org.apache.hadoop.hbase.io.ImmutableBytesWritable; 11 import org.apache.hadoop.hbase.mapreduce.TableOutputFormat; 12 import org.apache.hadoop.mapreduce.Job; 13 import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; 14 import org.apache.hadoop.util.GenericOptionsParser; 15 16 /** 17 * Table DDL: create 't_sub_domains', 'cf_basic', 'cf_status' 18 * <pre> 19 * cf_basic:domain cf_basic:len 20 * cf_status:status cf_status:live 21 * </pre> 22 * 23 * @author shirdrn 24 */ 25 public class DataImporter { 26 27 public static void main(String[] args) 28 throws IOException, InterruptedException, ClassNotFoundException, URISyntaxException { 29 30 Configuration conf = HBaseConfiguration.create(); 31 String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs(); 32 33 assert(otherArgs.length == 2); 34 35 if(otherArgs.length < 2) { 36 System.err.println("Usage: \n" + 37 " ImportDataDriver -libjars <jar1>[,<jar2>...[,<jarN>]] <tableName> <input>"); 38 System.exit(1); 39 } 40 String tableName = otherArgs[0].trim(); 41 String input = otherArgs[1].trim(); 42 43 // set table columns 44 conf.set("table.cf.family", "cf_basic"); 45 conf.set("table.cf.qualifier.fqdn", "domain"); 46 conf.set("table.cf.qualifier.timestamp", "create_at"); 47 48 Job job = new Job(conf, "Import into HBase table"); 49 job.setJarByClass(DataImporter.class); 50 job.setMapperClass(ImportFileLinesMapper.class); 51 job.setOutputFormatClass(TableOutputFormat.class); 52 53 job.getConfiguration().set(TableOutputFormat.OUTPUT_TABLE, tableName); 54 job.setOutputKeyClass(ImmutableBytesWritable.class); 55 job.setOutputValueClass(Put.class); 56 57 job.setNumReduceTasks(0); 58 59 FileInputFormat.addInputPath(job, new Path(input)); 60 61 int exitCode = job.waitForCompletion(true) ? 0 : 1; 62 System.exit(exitCode); 63 } 64 65 } 可以看到,我们可以通过-libjars选项来指定该Job运行所依赖的第三方jar文件,具体使用方法,说明如下: 第一步:设置环境变量 我们修改.bashrc文件,增加如下配置内容: 1 export HADOOP_HOME=/opt/stone/cloud/hadoop-1.0.3 2 export PATH=$PATH:$HADOOP_HOME/bin 3 export HBASE_HOME=/opt/stone/cloud/hbase-0.94.1 4 export PATH=$PATH:$HBASE_HOME/bin 5 export ZK_HOME=/opt/stone/cloud/zookeeper-3.4.3 不要忘记要使当前的配置生效: 1 . .bashrc 2 或 3 source .bashrc 这样就可以方便地引用外部的jar文件了。 第二步:确定Job依赖的jar文件列表 上面提到,我们要使用HBase,需要HBase和ZooKeeper的相关jar文件,用到的文件如下所示: 1 HADOOP_CLASSPATH=$HBASE_HOME/hbase-0.94.1.jar:$ZK_HOME/zookeeper-3.4.3.jar ./bin/hadoop jar import-into-hbase.jar 设置当前Job执行的HADOOP_CLASSPATH变量,只对当前Job有效,所以没有必要在.bashrc中进行配置。 第三步:运行开发的Job 运行我们开发的Job,通过命令行输入HADOOP_CLASSPATH变量,以及使用-libjars选项指定当前这个Job依赖的第三方jar文件,启动命令行如下所示: 1 xiaoxiang@ubuntu3:~/hadoop$ HADOOP_CLASSPATH=$HBASE_HOME/hbase-0.94.1.jar:$ZK_HOME/zookeeper-3.4.3.jar ./bin/hadoop jar import-into-hbase.jar org.shirdrn.kodz.inaction.hbase.job.importing.ImportDataDriver -libjars $HBASE_HOME/hbase-0.94.1.jar,$HBASE_HOME/lib/protobuf-java-2.4.0a.jar,$ZK_HOME/zookeeper-3.4.3.jar t_sub_domains /user/xiaoxiang/datasets/domains/ 需要注意的是,环境变量中内容使用冒号分隔,而-libjars选项中的内容使用逗号分隔。 这样,我们就能够正确运行开发的Job了。 下面看看我们开发的Job运行的结果: 001 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:zookeeper.version=3.4.3-1240972, built on 02/06/2012 10:48 GMT 002 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:host.name=ubuntu3 003 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:java.version=1.6.0_30 004 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:java.vendor=Sun Microsystems Inc. 005 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:java.home=/usr/java/jdk1.6.0_30/jre 006 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:java.class.path=/opt/stone/cloud/hadoop-1.0.3/libexec/../conf:/usr/java/jdk1.6.0_30/lib/tools.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/..:/opt/stone/cloud/hadoop-1.0.3/libexec/../hadoop-core-1.0.3.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/asm-3.2.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/aspectjrt-1.6.5.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/aspectjtools-1.6.5.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-beanutils-1.7.0.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-beanutils-core-1.8.0.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-cli-1.2.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-codec-1.4.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-collections-3.2.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-configuration-1.6.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-daemon-1.0.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-digester-1.8.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-el-1.0.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-httpclient-3.0.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-io-2.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-lang-2.4.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-logging-1.1.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-logging-api-1.0.4.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-math-2.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/commons-net-1.4.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/core-3.1.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/hadoop-capacity-scheduler-1.0.3.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/hadoop-datajoin-1.0.3.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/hadoop-fairscheduler-1.0.3.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/hadoop-thriftfs-1.0.3.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/hsqldb-1.8.0.10.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jackson-core-asl-1.8.8.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jackson-mapper-asl-1.8.8.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jasper-compiler-5.5.12.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jasper-runtime-5.5.12.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jdeb-0.8.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jersey-core-1.8.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jersey-json-1.8.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jersey-server-1.8.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jets3t-0.6.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jetty-6.1.26.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jetty-util-6.1.26.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jsch-0.1.42.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/junit-4.5.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/kfs-0.2.2.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/log4j-1.2.15.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/mockito-all-1.8.5.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/oro-2.0.8.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/protobuf-java-2.4.0a.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/servlet-api-2.5-20081211.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/slf4j-api-1.4.3.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/slf4j-log4j12-1.4.3.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/xmlenc-0.52.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jsp-2.1/jsp-2.1.jar:/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/jsp-2.1/jsp-api-2.1.jar:/opt/stone/cloud/hbase-0.94.1/hbase-0.94.1.jar:/opt/stone/cloud/zookeeper-3.4.3/zookeeper-3.4.3.jar 007 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:java.library.path=/opt/stone/cloud/hadoop-1.0.3/libexec/../lib/native/Linux-amd64-64 008 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:java.io.tmpdir=/tmp 009 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:java.compiler=<NA> 010 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:os.name=Linux 011 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:os.arch=amd64 012 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:os.version=3.0.0-12-server 013 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:user.name=xiaoxiang 014 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:user.home=/home/xiaoxiang 015 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Client environment:user.dir=/opt/stone/cloud/hadoop-1.0.3 016 13/04/10 22:03:32 INFO zookeeper.ZooKeeper: Initiating client connection, connectString=ubuntu3:2222 sessionTimeout=180000 watcher=hconnection 017 13/04/10 22:03:32 INFO zookeeper.ClientCnxn: Opening socket connection to server /172.0.8.252:2222 018 13/04/10 22:03:32 INFO zookeeper.RecoverableZooKeeper: The identifier of this process is 17561@ubuntu3 019 13/04/10 22:03:32 WARN client.ZooKeeperSaslClient: SecurityException: java.lang.SecurityException: Unable to locate a login configuration occurred when trying to find JAAS configuration. 020 13/04/10 22:03:32 INFO client.ZooKeeperSaslClient: Client will not SASL-authenticate because the default JAAS configuration section 'Client' could not be found. If you are not using SASL, you may ignore this. On the other hand, if you expected SASL to work, please fix your JAAS configuration. 021 13/04/10 22:03:32 INFO zookeeper.ClientCnxn: Socket connection established to ubuntu3/172.0.8.252:2222, initiating session 022 13/04/10 22:03:32 INFO zookeeper.ClientCnxn: Session establishment complete on server ubuntu3/172.0.8.252:2222, sessionid = 0x13decd0f3960042, negotiated timeout = 180000 023 13/04/10 22:03:32 INFO mapreduce.TableOutputFormat: Created table instance for t_sub_domains 024 13/04/10 22:03:32 INFO input.FileInputFormat: Total input paths to process : 1 025 13/04/10 22:03:32 INFO util.NativeCodeLoader: Loaded the native-hadoop library 026 13/04/10 22:03:32 WARN snappy.LoadSnappy: Snappy native library not loaded 027 13/04/10 22:03:32 INFO mapred.JobClient: Running job: job_201303302227_0034 028 13/04/10 22:03:33 INFO mapred.JobClient: map 0% reduce 0% 029 13/04/10 22:03:50 INFO mapred.JobClient: map 2% reduce 0% 030 13/04/10 22:03:53 INFO mapred.JobClient: map 3% reduce 0% 031 13/04/10 22:03:56 INFO mapred.JobClient: map 4% reduce 0% 032 13/04/10 22:03:59 INFO mapred.JobClient: map 6% reduce 0% 033 13/04/10 22:04:03 INFO mapred.JobClient: map 7% reduce 0% 034 13/04/10 22:04:06 INFO mapred.JobClient: map 8% reduce 0% 035 13/04/10 22:04:09 INFO mapred.JobClient: map 10% reduce 0% 036 13/04/10 22:04:15 INFO mapred.JobClient: map 12% reduce 0% 037 13/04/10 22:04:18 INFO mapred.JobClient: map 13% reduce 0% 038 13/04/10 22:04:21 INFO mapred.JobClient: map 14% reduce 0% 039 13/04/10 22:04:24 INFO mapred.JobClient: map 15% reduce 0% 040 13/04/10 22:04:27 INFO mapred.JobClient: map 17% reduce 0% 041 13/04/10 22:04:33 INFO mapred.JobClient: map 18% reduce 0% 042 13/04/10 22:04:36 INFO mapred.JobClient: map 19% reduce 0% 043 13/04/10 22:04:39 INFO mapred.JobClient: map 20% reduce 0% 044 13/04/10 22:04:42 INFO mapred.JobClient: map 21% reduce 0% 045 13/04/10 22:04:45 INFO mapred.JobClient: map 23% reduce 0% 046 13/04/10 22:04:48 INFO mapred.JobClient: map 24% reduce 0% 047 13/04/10 22:04:51 INFO mapred.JobClient: map 25% reduce 0% 048 13/04/10 22:04:54 INFO mapred.JobClient: map 27% reduce 0% 049 13/04/10 22:04:57 INFO mapred.JobClient: map 28% reduce 0% 050 13/04/10 22:05:00 INFO mapred.JobClient: map 29% reduce 0% 051 13/04/10 22:05:03 INFO mapred.JobClient: map 31% reduce 0% 052 13/04/10 22:05:06 INFO mapred.JobClient: map 32% reduce 0% 053 13/04/10 22:05:09 INFO mapred.JobClient: map 33% reduce 0% 054 13/04/10 22:05:12 INFO mapred.JobClient: map 34% reduce 0% 055 13/04/10 22:05:15 INFO mapred.JobClient: map 35% reduce 0% 056 13/04/10 22:05:18 INFO mapred.JobClient: map 37% reduce 0% 057 13/04/10 22:05:21 INFO mapred.JobClient: map 38% reduce 0% 058 13/04/10 22:05:24 INFO mapred.JobClient: map 39% reduce 0% 059 13/04/10 22:05:27 INFO mapred.JobClient: map 41% reduce 0% 060 13/04/10 22:05:30 INFO mapred.JobClient: map 42% reduce 0% 061 13/04/10 22:05:33 INFO mapred.JobClient: map 43% reduce 0% 062 13/04/10 22:05:36 INFO mapred.JobClient: map 44% reduce 0% 063 13/04/10 22:05:39 INFO mapred.JobClient: map 46% reduce 0% 064 13/04/10 22:05:42 INFO mapred.JobClient: map 47% reduce 0% 065 13/04/10 22:05:45 INFO mapred.JobClient: map 48% reduce 0% 066 13/04/10 22:05:48 INFO mapred.JobClient: map 50% reduce 0% 067 13/04/10 22:05:54 INFO mapred.JobClient: map 52% reduce 0% 068 13/04/10 22:05:57 INFO mapred.JobClient: map 53% reduce 0% 069 13/04/10 22:06:00 INFO mapred.JobClient: map 54% reduce 0% 070 13/04/10 22:06:03 INFO mapred.JobClient: map 55% reduce 0% 071 13/04/10 22:06:06 INFO mapred.JobClient: map 57% reduce 0% 072 13/04/10 22:06:12 INFO mapred.JobClient: map 59% reduce 0% 073 13/04/10 22:06:15 INFO mapred.JobClient: map 60% reduce 0% 074 13/04/10 22:06:18 INFO mapred.JobClient: map 61% reduce 0% 075 13/04/10 22:06:21 INFO mapred.JobClient: map 62% reduce 0% 076 13/04/10 22:06:24 INFO mapred.JobClient: map 63% reduce 0% 077 13/04/10 22:06:27 INFO mapred.JobClient: map 64% reduce 0% 078 13/04/10 22:06:30 INFO mapred.JobClient: map 66% reduce 0% 079 13/04/10 22:06:33 INFO mapred.JobClient: map 67% reduce 0% 080 13/04/10 22:06:36 INFO mapred.JobClient: map 68% reduce 0% 081 13/04/10 22:06:42 INFO mapred.JobClient: map 69% reduce 0% 082 13/04/10 22:06:45 INFO mapred.JobClient: map 70% reduce 0% 083 13/04/10 22:06:48 INFO mapred.JobClient: map 71% reduce 0% 084 13/04/10 22:06:51 INFO mapred.JobClient: map 73% reduce 0% 085 13/04/10 22:06:54 INFO mapred.JobClient: map 74% reduce 0% 086 13/04/10 22:06:57 INFO mapred.JobClient: map 75% reduce 0% 087 13/04/10 22:07:00 INFO mapred.JobClient: map 77% reduce 0% 088 13/04/10 22:07:03 INFO mapred.JobClient: map 78% reduce 0% 089 13/04/10 22:07:12 INFO mapred.JobClient: map 79% reduce 0% 090 13/04/10 22:07:18 INFO mapred.JobClient: map 80% reduce 0% 091 13/04/10 22:07:24 INFO mapred.JobClient: map 81% reduce 0% 092 13/04/10 22:07:30 INFO mapred.JobClient: map 82% reduce 0% 093 13/04/10 22:07:36 INFO mapred.JobClient: map 83% reduce 0% 094 13/04/10 22:07:48 INFO mapred.JobClient: map 84% reduce 0% 095 13/04/10 22:07:51 INFO mapred.JobClient: map 85% reduce 0% 096 13/04/10 22:07:59 INFO mapred.JobClient: map 86% reduce 0% 097 13/04/10 22:08:05 INFO mapred.JobClient: map 87% reduce 0% 098 13/04/10 22:08:11 INFO mapred.JobClient: map 88% reduce 0% 099 13/04/10 22:08:17 INFO mapred.JobClient: map 89% reduce 0% 100 13/04/10 22:08:23 INFO mapred.JobClient: map 90% reduce 0% 101 13/04/10 22:08:29 INFO mapred.JobClient: map 91% reduce 0% 102 13/04/10 22:08:35 INFO mapred.JobClient: map 92% reduce 0% 103 13/04/10 22:08:41 INFO mapred.JobClient: map 93% reduce 0% 104 13/04/10 22:08:47 INFO mapred.JobClient: map 94% reduce 0% 105 13/04/10 22:08:53 INFO mapred.JobClient: map 95% reduce 0% 106 13/04/10 22:08:59 INFO mapred.JobClient: map 96% reduce 0% 107 13/04/10 22:09:05 INFO mapred.JobClient: map 97% reduce 0% 108 13/04/10 22:09:11 INFO mapred.JobClient: map 98% reduce 0% 109 13/04/10 22:09:17 INFO mapred.JobClient: map 99% reduce 0% 110 13/04/10 22:09:23 INFO mapred.JobClient: map 100% reduce 0% 111 13/04/10 22:09:31 INFO mapred.JobClient: Job complete: job_201303302227_0034 112 13/04/10 22:09:31 INFO mapred.JobClient: Counters: 18 113 13/04/10 22:09:31 INFO mapred.JobClient: Job Counters 114 13/04/10 22:09:31 INFO mapred.JobClient: SLOTS_MILLIS_MAPS=550605 115 13/04/10 22:09:31 INFO mapred.JobClient: Total time spent by all reduces waiting after reserving slots (ms)=0 116 13/04/10 22:09:31 INFO mapred.JobClient: Total time spent by all maps waiting after reserving slots (ms)=0 117 13/04/10 22:09:31 INFO mapred.JobClient: Launched map tasks=2 118 13/04/10 22:09:31 INFO mapred.JobClient: Data-local map tasks=2 119 13/04/10 22:09:31 INFO mapred.JobClient: SLOTS_MILLIS_REDUCES=0 120 13/04/10 22:09:31 INFO mapred.JobClient: File Output Format Counters 121 13/04/10 22:09:31 INFO mapred.JobClient: Bytes Written=0 122 13/04/10 22:09:31 INFO mapred.JobClient: FileSystemCounters 123 13/04/10 22:09:31 INFO mapred.JobClient: HDFS_BYTES_READ=104394990 124 13/04/10 22:09:31 INFO mapred.JobClient: FILE_BYTES_WRITTEN=64078 125 13/04/10 22:09:31 INFO mapred.JobClient: File Input Format Counters 126 13/04/10 22:09:31 INFO mapred.JobClient: Bytes Read=104394710 127 13/04/10 22:09:31 INFO mapred.JobClient: Map-Reduce Framework 128 13/04/10 22:09:31 INFO mapred.JobClient: Map input records=4995670 129 13/04/10 22:09:31 INFO mapred.JobClient: Physical memory (bytes) snapshot=279134208 130 13/04/10 22:09:31 INFO mapred.JobClient: Spilled Records=0 131 13/04/10 22:09:31 INFO mapred.JobClient: CPU time spent (ms)=129130 132 13/04/10 22:09:31 INFO mapred.JobClient: Total committed heap usage (bytes)=202833920 133 13/04/10 22:09:31 INFO mapred.JobClient: Virtual memory (bytes) snapshot=1170251776 134 13/04/10 22:09:31 INFO mapred.JobClient: Map output records=4995670 135 13/04/10 22:09:31 INFO mapred.JobClient: SPLIT_RAW_BYTES=280 可以看到,除了加载Hadoop对应的HADOOP_HOME变量指定的路径下,lib*目录下的jar文件以外,还加载了我们设置的-libjars选项中指定的第三方jar文件,供Job运行时使用。 将Job代码和依赖jar文件打包 我比较喜欢这种方式,因为这样做首先利用饿Maven的很多优点,如管理依赖、自动构建。另外,对于其他想要使用该Job的开发人员或部署人员,无需关系更多的配置,只要按照Maven的构建规则去构建,就可以生成最终的部署文件,从而也就减少了在执行Job的时候,出现各种常见的问题(如CLASSPATH设置有问题等)。 使用如下的Maven构建插件配置,执行mvn package命令,就可以完成这些任务: 01 <build> 02 <plugins> 03 <plugin> 04 <artifactId>maven-assembly-plugin</artifactId> 05 <configuration> 06 <archive> 07 <manifest> 08 <mainClass>org.shirdrn.solr.cloud.index.hadoop.SolrCloudIndexer</mainClass> 09 </manifest> 10 </archive> 11 <descriptorRefs> 12 <descriptorRef>jar-with-dependencies</descriptorRef> 13 </descriptorRefs> 14 </configuration> 15 <executions> 16 <execution> 17 <id>make-assembly</id> 18 <phase>package</phase> 19 <goals> 20 <goal>single</goal> 21 </goals> 22 </execution> 23 </executions> 24 </plugin> 25 </plugins> 26 </build> 最后生成的jar文件在target目录下面,例如名称类似solr-platform-2.0-jar-with-dependencies.jar,然后可以直接拷贝这个文件到指定的目录,提交到Hadoop计算集群运行。
实现一个Web服务的过程,大概有3个基本的过程: Web服务提供者设计并开发Web服务 Web服务提供者发布Web服务 Web服务请求者调用Web服务 下面,我通过一个例子,来实现上述过程: 假设一个Web服务提供者提供一个对域名进行探测解析的服务,给定一个域名,可以给出改域名解析后对应的IP地址列表。Web服务提供者设计并开发这个Web服务,然后将服务发布出去,并可以让Web服务请求者进行调用。 开发Web服务 服务接口文件DetectionService.java代码如下所示: 1 package org.shirdrn.server.webservices.jaxws; 2 3 public interface DetectService { 4 DetectedResult detect(String domain); 5 } 对应的实现类DomainDetectionService.java,如下所示: 01 package org.shirdrn.server.webservices.jaxws; 02 03 import java.net.InetAddress; 04 import java.net.UnknownHostException; 05 import java.util.Date; 06 07 import javax.jws.WebMethod; 08 import javax.jws.WebService; 09 import javax.jws.soap.SOAPBinding; 10 11 import org.xbill.DNS.Address; 12 13 @WebService(serviceName = "DomainDetector",portName = "DomainDetectorPort", 14 targetNamespace = "http://ws.shirdrn.org/jaxws/WSDomainDetector") 15 @SOAPBinding(style=SOAPBinding.Style.DOCUMENT,use=SOAPBinding.Use.LITERAL, 16 parameterStyle=SOAPBinding.ParameterStyle.WRAPPED) 17 public class DomainDetectService implements DetectService { 18 19 @WebMethod 20 public DetectedResult detect(String domain) { 21 DetectedResult result = new DetectedResult(); 22 result.setStartTime(new Date()); 23 try { 24 InetAddress[] addresses = Address.getAllByName(domain); 25 for(InetAddress addr : addresses) { 26 result.getIpAddresses().add(addr.getHostAddress()); 27 } 28 } catch (UnknownHostException e) { 29 e.printStackTrace(); 30 } finally { 31 result.setFinishTime(new Date()); 32 result.setTimeTaken(result.getFinishTime().getTime() - result.getStartTime().getTime()); 33 } 34 return result; 35 } 36 37 } 上面用到一个DtectedResult类,如下所示: 01 package org.shirdrn.server.webservices.jaxws; 02 03 import java.util.ArrayList; 04 import java.util.Date; 05 import java.util.List; 06 07 public class DetectedResult { 08 09 private Date startTime; 10 private Date finishTime; 11 private long timeTaken; 12 private List<String> ipAddresses = new ArrayList<String>(); 13 14 public Date getStartTime() { 15 return startTime; 16 } 17 18 public void setStartTime(Date startTime) { 19 this.startTime = startTime; 20 } 21 22 public Date getFinishTime() { 23 return finishTime; 24 } 25 26 public void setFinishTime(Date finishTime) { 27 this.finishTime = finishTime; 28 } 29 30 public long getTimeTaken() { 31 return timeTaken; 32 } 33 34 public void setTimeTaken(long timeTaken) { 35 this.timeTaken = timeTaken; 36 } 37 38 public List<String> getIpAddresses() { 39 return ipAddresses; 40 } 41 42 public void setIpAddresses(List<String> ipAddresses) { 43 this.ipAddresses = ipAddresses; 44 } 45 46 } 生成Web Service描述文件(WSDL),如下所示: 1 shirdrn@SYJ:~$ cd ~/programs/eclipse-java-juno/workspace/self_practice/bin 2 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/bin$ wsgen -cp . org.shirdrn.server.webservices.jaxws.DomainDetectService -wsdl 3 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/bin$ ls 4 com DomainDetector_schema1.xsd DomainDetector.wsdl log4j.properties main org server.xml test 5 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/bin$ cp DomainDetect* /home/shirdrn/programs/eclipse-java-juno/workspace/self_practice/src/org/shirdrn/server/webservices/jaxws/wsdl 经过上面步骤,生成了两个文件:DomainDetect_schema1.xsd和DomainDetect.wsdl,内容分别如下所示: DomainDetector_schema1.xsd内容 01 <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 02 <xs:schema version="1.0"targetNamespace="http://ws.shirdrn.org/jaxws/WSDomainDetector" 03 xmlns:tns="http://ws.shirdrn.org/jaxws/WSDomainDetector"xmlns:xs="http://www.w3.org/2001/XMLSchema"> 04 05 <xs:element name="detect" type="tns:detect" /> 06 07 <xs:element name="detectResponse" type="tns:detectResponse" /> 08 09 <xs:complexType name="detect"> 10 <xs:sequence> 11 <xs:element name="arg0" type="xs:string" minOccurs="0" /> 12 </xs:sequence> 13 </xs:complexType> 14 15 <xs:complexType name="detectResponse"> 16 <xs:sequence> 17 <xs:element name="return" type="tns:detectedResult" minOccurs="0"/> 18 </xs:sequence> 19 </xs:complexType> 20 21 <xs:complexType name="detectedResult"> 22 <xs:sequence> 23 <xs:element name="finishTime" type="xs:dateTime" minOccurs="0" /> 24 <xs:element name="ipAddresses" type="xs:string" nillable="true" 25 minOccurs="0" maxOccurs="unbounded" /> 26 <xs:element name="startTime" type="xs:dateTime" minOccurs="0" /> 27 <xs:element name="timeTaken" type="xs:long" /> 28 </xs:sequence> 29 </xs:complexType> 30 </xs:schema> DomainDetector.wsdl内容 01 <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 02 <!-- Generated by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS 03 RI 2.1.6 in JDK 6. --> 04 <definitions targetNamespace="http://ws.shirdrn.org/jaxws/WSDomainDetector" 05 name="DomainDetector" xmlns="http://schemas.xmlsoap.org/wsdl/"xmlns:tns="http://ws.shirdrn.org/jaxws/WSDomainDetector" 06 xmlns:xsd="http://www.w3.org/2001/XMLSchema"xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"> 07 <types> 08 <xsd:schema> 09 <xsd:importnamespace="http://ws.shirdrn.org/jaxws/WSDomainDetector" 10 schemaLocation="DomainDetector_schema1.xsd" /> 11 </xsd:schema> 12 </types> 13 <message name="detect"> 14 <part name="parameters" element="tns:detect" /> 15 </message> 16 <message name="detectResponse"> 17 <part name="parameters" element="tns:detectResponse" /> 18 </message> 19 <portType name="DomainDetectService"> 20 <operation name="detect"> 21 <input message="tns:detect" /> 22 <output message="tns:detectResponse" /> 23 </operation> 24 </portType> 25 <binding name="DomainDetectorPortBinding" type="tns:DomainDetectService"> 26 <soap:binding transport="http://schemas.xmlsoap.org/soap/http" 27 style="document" /> 28 <operation name="detect"> 29 <soap:operation soapAction="" /> 30 <input> 31 <soap:body use="literal" /> 32 </input> 33 <output> 34 <soap:body use="literal" /> 35 </output> 36 </operation> 37 </binding> 38 <service name="DomainDetector"> 39 <port name="DomainDetectorPort" binding="tns:DomainDetectorPortBinding"> 40 <soap:address location="REPLACE_WITH_ACTUAL_URL" /> 41 </port> 42 </service> 43 </definitions> 可以看到,在wsdl文件中通过import引用了前面的schema文件,这个schema文件用于定义类型。 发布Web服务 我们可以发布我们上面开发的Web服务,代码如下所示: 01 package org.shirdrn.server.webservices.jaxws; 02 03 import javax.xml.ws.Endpoint; 04 05 public class PublishingServer { 06 07 08 public static void main(String[] args) { 09 String address ="http://172.0.8.212:9033/ws/services/DomainDetectService"; 10 DetectService domainDetectService = new DomainDetectService(); 11 12 Endpoint.publish(address, domainDetectService); 13 } 14 15 } 可以查看本地运行的Web服务进程: 1 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/src/org/shirdrn/server/webservices/jaxws$ sudo netstat -nap | grep 9033 2 tcp6 0 0 172.0.8.212:9033 :::* LISTEN 8302/java 3 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/src/org/shirdrn/server/webservices/jaxws$ jps 4 8302 PublishingServer 5 8358 Jps 可见,服务发布成功。 也可以在浏览器中打开链接:http://172.0.8.212:9033/ws/services/DomainDetectService?wsdl,就能看到发布的WSDL,内容如下所示: 01 <!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.1.6 in JDK 6. --> 02 <!-- Generated by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.1.6 in JDK 6. --> 03 <definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 04 xmlns:tns="http://ws.shirdrn.org/jaxws/WSDomainDetector"xmlns:xsd="http://www.w3.org/2001/XMLSchema" 05 xmlns="http://schemas.xmlsoap.org/wsdl/"targetNamespace="http://ws.shirdrn.org/jaxws/WSDomainDetector" 06 name="DomainDetector"> 07 <types> 08 <xsd:schema> 09 <xsd:import namespace="http://ws.shirdrn.org/jaxws/WSDomainDetector" 10 schemaLocation="http://172.0.8.212:9033/ws/services/DomainDetectService?xsd=1" /> 11 </xsd:schema> 12 </types> 13 <message name="detect"> 14 <part name="parameters" element="tns:detect" /> 15 </message> 16 <message name="detectResponse"> 17 <part name="parameters" element="tns:detectResponse" /> 18 </message> 19 <portType name="DomainDetectService"> 20 <operation name="detect"> 21 <input message="tns:detect" /> 22 <output message="tns:detectResponse" /> 23 </operation> 24 </portType> 25 <binding name="DomainDetectorPortBinding" type="tns:DomainDetectService"> 26 <soap:binding transport="http://schemas.xmlsoap.org/soap/http" 27 style="document" /> 28 <operation name="detect"> 29 <soap:operation soapAction="" /> 30 <input> 31 <soap:body use="literal" /> 32 </input> 33 <output> 34 <soap:body use="literal" /> 35 </output> 36 </operation> 37 </binding> 38 <service name="DomainDetector"> 39 <port name="DomainDetectorPort" binding="tns:DomainDetectorPortBinding"> 40 <soap:address 41 location="http://172.0.8.212:9033/ws/services/DomainDetectService" /> 42 </port> 43 </service> 44 </definitions> 调用Web服务 首先要根据服务提供者发布的Web服务,生成服务请求者客户端代码,如下所示: 01 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/bin$ wsimport -keep -p org.shirdrn.client.webservices.jaxwshttp://172.0.8.212:9033/ws/services/DomainDetectService?wsdl 02 parsing WSDL... 03 04 generating code... 05 06 compiling code... 07 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/bin$ cd org/shirdrn/client/webservices/jaxws/ 08 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/bin/org/shirdrn/client/webservices/jaxws$ cp *.java /home/shirdrn/programs/eclipse-java-juno/workspace/self_practice/src/org/shirdrn/client/webservices/jaxws 09 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/bin/org/shirdrn/client/webservices/jaxws$ cd ~/programs/eclipse-java-juno/workspace/self_practice/src/org/shirdrn/client/webservices/jaxws/ 10 shirdrn@SYJ:~/programs/eclipse-java-juno/workspace/self_practice/src/org/shirdrn/client/webservices/jaxws$ ls 11 DetectedResult.java DetectResponse.java DomainDetector.java ObjectFactory.java 12 Detect.java DomainDetectService.java package-info.java 上面自动生成了客户端代码的骨架,如下几个文件: DetectedResult.java DetectResponse.java DomainDetector.java ObjectFactory.java Detect.java DomainDetectService.java package-info.java 基于这些代码,就可以实现服务请求者对服务提供者发布服务的调用。我们实现了一个简单的调用,代码如下所示: 01 package org.shirdrn.client.webservices.jaxws; 02 03 public class DomainDetectClient { 04 05 public static void main(String[] args) { 06 DomainDetector detector = new DomainDetector(); 07 String domain = "baidu.com"; 08 DetectedResult result = detector.getDomainDetectorPort().detect(domain); 09 System.out.println(result.getIpAddresses()); 10 } 11 12 } 调用后可以返回对域名进行解析后得到的IP地址列表,结果如下所示: 1 [220.181.111.86, 123.125.114.144, 220.181.111.85] 到此为止,我们完成了一个简单的Web Service的开发、发布、调用的过程。
我们以Sun HotSpot VM来进行分析,首先应该知道,如果我们没有指定任何GC策略的时候,JVM默认使用的GC策略。Java虚拟机是按照分代的方式来回收垃圾空间,我们应该知道,垃圾回收主要是针对堆(Heap)内存进行分代回收,将对内存可以分成新生代(Young Generation)、年老代(Tenured Generation)和永久代(Permanent Generation)三个部分。 分代GC 分代GC包括如下三代: 新生代(Young Generation) 新生代有划分为Eden、From Survivor和To Survivor三个部分,他们对应的内存空间的大小比例为8:1:1,也就是,为对象分配内存的时候,首先使用Eden空间,经过GC后,没有被回收的会首先进入From Survivor区域,任何时候,都会保持一个Survivorq区域(From Survivor或To Survivor)完全空闲,也就是说新生代的内存利用率最大为90%。From Survivor和To Survivor两个区域会根据GC的实际情况,进行互换,将From Survivor区域中的对象全部复制到To Survivor区域中,或者反过来,将To Survivor区域中的对象全部复制到From Survivor区域中。 年老代(Tenured Generation) GC过程中,当某些对象经过多次GC都没有被回收,可能会进入到年老代。或者,当新生代没有足够的空间来为对象分配内存时,可能会直接在年老代进行分配。 永久代(Permanent Generation) 永久代实际上对应着虚拟机运行时数据区的“方法区”,这里主要存放类信息、静态变量、常量等数据。一般情况下,永久代中对应的对象的GC效率非常低,因为这里的的大部分对象在运行都不要进行GC,它们会一直被利用,直到JVM退出。 分代GC算法选择 新生代 通常情况下会有大量的对象需要分配内存,而且他们的生命周期很短,所以新生代的GC吞吐量很高,大部分对象都要被回收,从而,剩下的活跃对象比较少,所以新生代适合使用复制算法来进行GC,这样保证复制的数据的量较小,效率最好。 年老代 很多对象经过多次GC以后,经过Eden Space,多次经过From Survivor和To Survivor之后才会进入年老代,而且对象在年老代的存活时间比较长,如果进行使用复制算法来进行GC,需要移动大量的对象,导致效率很低。所以,年老代适合使用标记-清除算法(或者标记-清除-整理),需要被GC的对象很少,那么标记的对象就很少,在对标记的对象进行回收,效率就会很高。 默认GC策略 我们看一下,默认情况下,JVM针对上述不同的分代区域,使用哪些GC策略,如表所示: 运行模式 新生代垃GC策略 年老代GC策略 Client Serial GC Serial Old GC Server Parallel Scavenge GC Serial Old GC(PS MarkSweep) 平时我们运行Java程序,没有指定任何选项的时候,默认根据上面的GC策略搭配进行GC。 GC策略搭配 在进行JVM调优的过程中,并非任何一种新生代GC策略都可以和另一种年老代GC策略进行配合工作,所以,我们应该知道,哪些种组合可以有效地进行GC,而且应该在什么样的应用场景下选择哪一种组合,如下表所示: 新生代GC策略 年老代GC策略 说明 组合1 Serial Serial Old Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。 组合2 Serial CMS+Serial Old CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。 组合3 ParNew CMS 使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。 如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。 组合4 ParNew Serial Old 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。 组合5 Parallel Scavenge Serial Old Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。 组合6 Parallel Scavenge Parallel Old Parallel Old是Serial Old的并行版本
决策树是以实例为基础的归纳学习算法。 它从一组无次序、无规则的元组中推理出决策树表示形式的分类规则。它采用自顶向下的递归方式,在决策树的内部结点进行属性值的比较,并根据不同的属性值从该结点向下分支,叶结点是要学习划分的类。从根到叶结点的一条路径就对应着一条合取规则,整个决策树就对应着一组析取表达式规则。 一棵决策树由以下3类结点构成: 根结点 内部结点(决策结点) 叶结点 其中,根结点和内部结点都对应着我们要进行分类的属性集中的一个属性,而叶结点是分类中的类标签的集合。如果一棵决策树构建起来,其分类精度满足我们的实际需要,我们就可以使用它来进行分类新的数据集。 这棵决策树就是我们根据已有的训练数据集训练出来的分类模型,可以通过使用测试数据集来对分类模型进行验证,经过调整模型直到达到我们所期望的分类精度,然后就可以使用该模型来预测实际应用中的新数据,对新的数据进行分类。 通过上面描述,我们已经能够感觉出,在构建决策树的过程中,如果选择其中的内部结点(决策结点),才能够使我们的决策树得到较高的分类精度,这是难点。其中,ID3算法主要是给出了通过信息增益的方式来进行决策结点的选择。 首先,看一下如何计算信息熵。熵是不确定性的度量指标,假如事件A的全概率划分是(A1,A2,…,An),每部分发生的概率是(p1,p2,…,pn),那么信息熵通过如下公式计算: 1 Info(A) = Entropy(p1,p2,...,pn) = -p1 * log2(p1) -p2 * log2(p2) - ... -pn * log2(pn) 我们以一个很典型被引用过多次的训练数据集D为例,来说明ID3算法如何计算信息增益并选择决策结点,训练集如图所示: 上面的训练集有4个属性,即属性集合A={OUTLOOK, TEMPERATURE, HUMIDITY, WINDY};而类标签有2个,即类标签集合C={Yes, No},分别表示适合户外运动和不适合户外运动,其实是一个二分类问题。 数据集D包含14个训练样本,其中属于类别“Yes”的有9个,属于类别“No”的有5个,则计算其信息熵: 1 Info(D) = -9/14 * log2(9/14) - 5/14 * log2(5/14) = 0.940 下面对属性集中每个属性分别计算信息熵,如下所示: OUTLOOK属性 OUTLOOK属性中,有3个取值:Sunny、Overcast和Rainy,样本分布情况如下: 类别为Yes时,Sunny有2个样本;类别为No时,Sunny有3个样本。 类别为Yes时,Overcast有4个样本;类别为No时,Overcast有0个样本。 类别为Yes时,Rainy有3个样本;类别为No时,Rainy有2个样本。 从而可以计算OUTLOOK属性的信息熵: 1 Info(OUTLOOK) = 5/14 * [- 2/5 * log2(2/5) – 3/5 * log2(3/5)] + 4/14 * [ - 4/4 * log2(4/4) - 0/4 * log2(0/4)] + 5/14 * [ - 3/5 * log2(3/5) – 2/5 * log2(2/5)] = 0.694 TEMPERATURE属性 TEMPERATURE属性中,有3个取值:Hot、Mild和Cool,样本分布情况如下: 类别为Yes时,Hot有2个样本;类别为No时,Hot有2个样本。 类别为Yes时,Mild有4个样本;类别为No时,Mild有2个样本。 类别为Yes时,Cool有3个样本;类别为No时,Cool有1个样本。 1 Info(TEMPERATURE) = 4/14 * [- 2/4 * log2(2/4) – 2/4 * log2(2/4)] + 6/14 * [ - 4/6 * log2(4/6) - 2/6 * log2(2/6)] + 4/14 * [ - 3/4 * log2(3/4) – 1/4 * log2(1/4)] = 0.911 HUMIDITY属性 TEMPERATURE属性中,有2个取值:High和Normal,样本分布情况如下: 类别为Yes时,High有3个样本;类别为No时,High有4个样本。 类别为Yes时,Normal有6个样本;类别为No时,Normal有1个样本。 1 Info(HUMIDITY) = 7/14 * [- 3/7 * log2(3/7) – 4/7 * log2(4/7)] + 7/14 * [ - 6/7 * log2(6/7) - 1/7 * log2(1/7)] = 0.789 WINDY属性 WINDY属性中,有2个取值:True和False,样本分布情况如下: 类别为Yes时,True有3个样本;类别为No时,True有3个样本。 类别为Yes时,False有6个样本;类别为No时,False有2个样本。 1 Info(WINDY) = 6/14 * [- 3/6 * log2(3/6) – 3/6 * log2(3/6)] + 8/14 * [ - 6/8 * log2(6/8) - 2/8 * log2(2/8)] = 0.892 根据上面的数据,我们可以计算选择第一个根结点所依赖的信息增益值,计算如下所示: 1 Gain(OUTLOOK) = Info(D) - Info(OUTLOOK) = 0.940 - 0.694 = 0.246 2 Gain(TEMPERATURE) = Info(D) - Info(TEMPERATURE) = 0.940 - 0.911 = 0.029 3 Gain(HUMIDITY) = Info(D) - Info(HUMIDITY) = 0.940 - 0.789 = 0.151 4 Gain(WINDY) = Info(D) - Info(WINDY) = 0.940 - 0.892 = 0.048 根据上面对各个属性的信息增益值进行比较,选出信息增益值最大的属性: 1 Max(Gain(OUTLOOK), Gain(TEMPERATURE), Gain(HUMIDITY), Gain(WINDY)) = Gain(OUTLOOK) 所以,第一个根结点我们选择属性OUTLOOK。 继续执行上述信息熵和信息增益的计算,最终能够选出其他的决策结点,从而建立一棵决策树,这就是我们训练出来的分类模型。基于此模型,可以使用一组测试数据及进行模型的验证,最后能够对新数据进行预测。 ID3算法的优点是:算法的理论清晰,方法简单,学习能力较强。 ID3算法的缺点是:只对比较小的数据集有效,且对噪声比较敏感,当训练数据集加大时,决策树可能会随之改变。
选择使用Solr,对数据库中数据进行索引,可以单独写程序将数据库中的数据导出并建立索引,这个过程可能对于数据处理的控制更灵活一些,但是却可能带来很大的工作量。选择使用Solr的DIH组件,可以很方便的对数据库表中数据进行索引,下面基于MySQL数据库实现建立索引。 首先,需要设计你的schema,最主要的工作是,将数据库表中字段映射为Lucene索引(Solr直接使用Lucene的索引格式和数据)的Field,从而将数据表中的一条记录映射为Lucene中的Document,然后进行索引。另外,在schema.xml配置文件中,还需要指定各个字段在索引数据中的属性信息(如是否索引、是否存储、是否分词、排序规则等),以及Field所使用的分析器、过滤器等。在schema.xml文件进行配置,下面是配置实例: 01 <?xml version="1.0" ?> 02 <schema name="example core zero" version="1.1"> 03 <types> 04 <fieldtype name="int" class="solr.IntField" omitNorms="true" /> 05 <fieldtype name="string" class="solr.TextField" sortMissingLast="true"omitNorms="true"> 06 <analyzer type="index"> 07 <charFilter class="solr.MappingCharFilterFactory" mapping="mapping-ISOLatin1Accent.txt" /> 08 <tokenizer class="solr.KeywordTokenizerFactory" /> 09 <filter class="solr.LowerCaseFilterFactory" /> 10 </analyzer> 11 <analyzer type="query"> 12 <tokenizer class="solr.KeywordTokenizerFactory" /> 13 <filter class="solr.LowerCaseFilterFactory" /> 14 </analyzer> 15 </fieldtype> 16 <fieldtype name="long" class="solr.LongField" omitNorms="true" /> 17 <fieldtype name="date" class="solr.TrieDateField" sortMissingLast="true"omitNorms="true" /> 18 <fieldtype name="text" class="solr.TextField" sortMissingLast="true"omitNorms="true"> 19 <analyzer type="index"> 20 <tokenizer class="solr.StandardTokenizerFactory" /> 21 </analyzer> 22 <analyzer type="query"> 23 <tokenizer class="solr.StandardTokenizerFactory" /> 24 </analyzer> 25 </fieldtype> 26 </types> 27 28 <fields> 29 <field name="id" type="int" indexed="true" stored="true" multiValued="false"required="true" /> 30 <field name="domain" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 31 <field name="alex_rank" type="int" indexed="true" stored="true"multiValued="false" required="false" /> 32 <field name="server_port" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 33 <field name="cert_validity_notBefore" type="string" indexed="true"stored="true" multiValued="false" required="false" /> 34 <field name="cert_validity_notAfter_yyyyMMdd" type="string" indexed="true"stored="true" multiValued="false" required="false" /> 35 <field name="cert_issuer_brand" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 36 <field name="cert_validation" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 37 <field name="cert_isMultiDomain" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 38 <field name="cert_issuer_brand_isXRelated" type="string" indexed="true"stored="true" multiValued="false" required="false" /> 39 <field name="cert_subject_C" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 40 <field name="cert_isWildcard" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 41 <field name="cert_notAfter" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 42 <field name="special_ssl" type="int" indexed="true" stored="true"multiValued="false" required="false" /> 43 <field name="competitor_logo" type="string" indexed="true" stored="true"multiValued="false" required="false" /> 44 <field name="segment" type="text" indexed="true" stored="false"multiValued="true" required="false" /> 45 </fields> 46 47 <!-- field to use to determine and enforce document uniqueness. --> 48 <uniqueKey>id</uniqueKey> 49 50 <!-- field for the QueryParser to use when an explicit fieldname is absent --> 51 <defaultSearchField>domain</defaultSearchField> 52 53 <!-- SolrQueryParser configuration: defaultOperator="AND|OR" --> 54 <solrQueryParser defaultOperator="OR" /> 55 </schema> 定义好上面的内容,就应该考虑从数据库表中如何查询出记录,然后通过处理进行索引。通常,对于已经存在的基于数据库的系统或应用,很可能需要对某些字段的值进行一些处理,然后再进行索引,使用Solr定义的一些组件,在一定程度上可以满足需要。比如,数据表中时间字段包含到毫秒,实际我们只需要到日期,所以进行索引之前要把时间字符串做截断处理,等等。 通过使用Solr的DIH(Data Import Handler)组件,可以很容易地进行配置,就能实现将数据库的数据进行索引,而且还提供了一些方便的操作,如全量索引、增量索引等功能。 下面,在solrconfig.xml中配置DIH对应的requestHandler,实际身上是暴露一个REST接口来实现数据库数据导出并索引,配置如下: 1 <requestHandler name="/dataimport"class="org.apache.solr.handler.dataimport.DataImportHandler"> 2 <lst name="defaults"> 3 <str name="config">data-config.xml</str> 4 </lst> 5 </requestHandler> 上面的请求接口为“/dataimport”,可以通过如下类似的URL执行数据导入处理: 1 http://172.0.8.212:8080/seaarch-server/core0/dataimport?command=full-import 上面DIH对应的requestHandler配置中,配置文件data-config.xml定义了数据库的基本配置,以及导出数据的映射规则,即导出数据库表中对应哪些字段的值,以及对特定字段的值做如何处理,下面是一个示例: 01 <dataConfig> 02 <dataSource name="jdbc" driver="com.mysql.jdbc.Driver"url="jdbc:mysql://172.0.8.249:5606/marketing_db_saved?zeroDateTimeBehavior=convertToNull" user="developer" password="sedept@shiyanjun.cn" /> 03 <document name="mkt_data"> 04 <entity name="marketing_data" pk="id" query="select * from marketing_data where id between ${dataimporter.request.offset} and ${dataimporter.request.offset}+1000000" deltaQuery="select * from marketing_data where updated_at &gt; '${dih.last_index_time}'" transformer="RegexTransformer"> 05 <field column="id" name="id" /> 06 <field column="domain" name="domain" /> 07 <field column="alex_rank" name="alex_rank" /> 08 <field column="server_port" name="server_port" /> 09 <field column="cert_validity_notBefore" name="cert_validity_notBefore" /> 10 <field column="cert_validity_notAfter" /> 11 <field column="cert_validity_notAfter_yyyyMMdd" regex="(.*?)\s+.*"name="cert_validity_notAfter_yyyyMMdd" sourceColName="cert_validity_notAfter" /> 12 <field column="cert_issuer_brand" name="cert_issuer_brand" /> 13 <field column="cert_validation" name="cert_validation" /> 14 <field column="cert_isMultiDomain" name="cert_isMultiDomain" /> 15 <field column="cert_issuer_brand_isXRelated"name="cert_issuer_brand_isXRelated" /> 16 <field column="cert_isWildcard" name="cert_isWildcard" /> 17 <field column="cert_notAfter" name="cert_notAfter" /> 18 <field column="special_ssl" name="special_ssl" /> 19 <field column="competitor_logo" name="competitor_logo" /> 20 <field column="segment" name="segment" /> 21 </entity> 22 </document> 23 </dataConfig> 我们说明一下上面配置中的一些关键点: 全量索引 下面的SQL语句是与全量索引相关的: 1 SELECT * from marketing_data WHERE id between ${dataimporter.request.offset} AND${dataimporter.request.offset}+1000000 从表marketing_data中查询,表主键为id,查询条件为id between ${dataimporter.request.offset} and ${dataimporter.request.offset}+1000000,也就是id属于一个区间,而不是直接SELECT全表。Solr的DIH暴露了请求中传递的变量 ${dataimporter.request.offset},也就是在请求的requestHandler中可以附带附加属性条件,例如,下面请求URL中的offset=5000000参数: 1 http://172.0.8.212:8080/seaarch-server/core0/dataimport?command=full-import&offset=5000000 另外,还有一个参数是很重要的,它决定着是否清除已经存在的索引数据,默认为clean=true,如果不想删除以前的索引数据,一定要在请求的URL中指定该属性为false,请求URL如下: 1 http://172.0.8.212:8080/seaarch-server/core0/dataimport?command=full-import&offset=5000000&clean=false 另外,索引完成后一半需要执行commit操作,将内存中索引数据持久化到文件系统,防止改变丢失,所以需要在请求的URL中增加commit=true,例如: 1 http://172.0.8.212:8080/seaarch-server/core0/dataimport?command=full-import&offset=5000000&clean=false&commit=true 对于数据表中数据量很大的应用场景,通过这种方式,可以实现每次请求处理一批数据,避免对整个表造成过大的压力,影响正常线上业务操作数据库。 增量索引 上面requestHandler的配置中,属性配置内容: 1 deltaQuery="SELECT * from marketing_data WHERE updated_at &gt;'${dih.last_index_time}' 表示请求中指定的命令为增量索引方式,只需要通过指定请求的命令为delta-import即可,对应的请求URL为: 1 http://172.0.8.212:8080/search-server/core0/dataimport?command=delta-import 字段updated_at是数据表中时间戳的字段,where条件的含义是,如果上次索引时间点之后,表中记录的时间戳发生变化(即发生在上次索引之后),则对这些最近更新的记录进行索引(因为数据库表字段到LuceneField的映射中,使用的表主键id,如果是新增记录,则添加索引,就增加Document,如果只是更新表中记录,则对索引中已经存在的id相同的Document的数据进行更新)。 不过,上面两种方式还是需要手动干预处理批次,或者写个附加脚本进行成批索引。 还有一种更好的方式,可以直接在配置文件中进行配置,那就是属性文件dataimport.properties,它能够记录很多有用的状态,以及配置很多有用的选项。然而,这个功能在Solr 3.x版本中不能使用,如果使用的是Solr 4.x,可以使用。下面看一个示例配置: 01 #Tue Jul 21 12:10:50 CEST 2010 02 metadataObject.last_index_time=2010-09-20 11\:12\:47 03 last_index_time=2010-09-20 11\:12\:47 04 05 06 ################################################# 07 # # 08 # dataimport scheduler properties # 09 # # 10 ################################################# 11 12 # to sync or not to sync 13 # 1 - active; anything else - inactive 14 syncEnabled=1 15 16 # which cores to schedule 17 # in a multi-core environment you can decide which cores you want syncronized 18 # leave empty or comment it out if using single-core deployment 19 syncCores=coreHr,coreEn 20 21 # solr server name or IP address 22 # [defaults to localhost if empty] 23 server=localhost 24 25 # solr server port 26 # [defaults to 80 if empty] 27 port=8080 28 29 # application name/context 30 # [defaults to current ServletContextListener's context (app) name] 31 webapp=solrTest_WEB 32 33 # URL params [mandatory] 34 # remainder of URL 35 params=/search?qt=/dataimport&command=delta-import&clean=false&commit=true 36 37 # schedule interval 38 # number of minutes between two runs 39 # [defaults to 30 if empty] 40 interval=10 上面配置中,最后一个选项interval=10可以指定程序自动根据配置的时间间隔进行索引,而且上面的配置适合增量索引(可以从params=/search?qt=/dataimport&command=delta-import&clean=false&commit=true中的command=delta-import看出),因为它只需要根据对比上次索引时间和数据表中的更新时间戳字段来判断哪些数据需要进行索引或者更新索引数据。如果是实时性较强的应用,这个间隔自然可以设置短一些,保证基于搜索的应用的查询能够更接近实时,不过要根据自己应用的实际的需要去选择。
C4.5是机器学习算法中的另一个分类决策树算法,它是基于ID3算法进行改进后的一种重要算法,相比于ID3算法,改进有如下几个要点: 用信息增益率来选择属性。ID3选择属性用的是子树的信息增益,这里可以用很多方法来定义信息,ID3使用的是熵(entropy, 熵是一种不纯度度量准则),也就是熵的变化值,而C4.5用的是信息增益率。 在决策树构造过程中进行剪枝,因为某些具有很少元素的结点可能会使构造的决策树过适应(Overfitting),如果不考虑这些结点可能会更好。 对非离散数据也能处理。 能够对不完整数据进行处理。 首先,说明一下如何计算信息增益率。 熟悉了ID3算法后,已经知道如何计算信息增益,计算公式如下所示(来自Wikipedia): 或者,用另一个更加直观容易理解的公式计算: 按照类标签对训练数据集D的属性集A进行划分,得到信息熵: 按照属性集A中每个属性进行划分,得到一组信息熵: 计算信息增益 然后计算信息增益,即前者对后者做差,得到属性集合A一组信息增益: 这样,信息增益就计算出来了。 计算信息增益率 下面看,计算信息增益率的公式,如下所示(来自Wikipedia): 其中,IG表示信息增益,按照前面我们描述的过程来计算。而IV是我们现在需要计算的,它是一个用来考虑分裂信息的度量,分裂信息用来衡量属性分 裂数据的广度和均匀程序,计算公式如下所示(来自Wikipedia): 简化一下,看下面这个公式更加直观: 其中,V表示属性集合A中的一个属性的全部取值。 我们以一个很典型被引用过多次的训练数据集D为例,来说明C4.5算法如何计算信息增益并选择决策结点。 上面的训练集有4个属性,即属性集合A={OUTLOOK, TEMPERATURE, HUMIDITY, WINDY};而类标签有2个,即类标签集合C={Yes, No},分别表示适合户外运动和不适合户外运动,其实是一个二分类问题。 我们已经计算过信息增益,这里直接列出来,如下所示: 数据集D包含14个训练样本,其中属于类别“Yes”的有9个,属于类别“No”的有5个,则计算其信息熵: 1 Info(D) = -9/14 * log2(9/14) - 5/14 * log2(5/14) = 0.940 下面对属性集中每个属性分别计算信息熵,如下所示: 1 Info(OUTLOOK) = 5/14 * [- 2/5 * log2(2/5) – 3/5 * log2(3/5)] + 4/14 * [ - 4/4 * log2(4/4) - 0/4 * log2(0/4)] + 5/14 * [ - 3/5 * log2(3/5) – 2/5 * log2(2/5)] = 0.694 2 Info(TEMPERATURE) = 4/14 * [- 2/4 * log2(2/4) – 2/4 * log2(2/4)] + 6/14 * [ - 4/6 * log2(4/6) - 2/6 * log2(2/6)] + 4/14 * [ - 3/4 * log2(3/4) – 1/4 * log2(1/4)] = 0.911 3 Info(HUMIDITY) = 7/14 * [- 3/7 * log2(3/7) – 4/7 * log2(4/7)] + 7/14 * [ - 6/7 * log2(6/7) - 1/7 * log2(1/7)] = 0.789 4 Info(WINDY) = 6/14 * [- 3/6 * log2(3/6) – 3/6 * log2(3/6)] + 8/14 * [ - 6/8 * log2(6/8) - 2/8 * log2(2/8)] = 0.892 根据上面的数据,我们可以计算选择第一个根结点所依赖的信息增益值,计算如下所示: 1 Gain(OUTLOOK) = Info(D) - Info(OUTLOOK) = 0.940 - 0.694 = 0.246 2 Gain(TEMPERATURE) = Info(D) - Info(TEMPERATURE) = 0.940 - 0.911 = 0.029 3 Gain(HUMIDITY) = Info(D) - Info(HUMIDITY) = 0.940 - 0.789 = 0.151 4 Gain(WINDY) = Info(D) - Info(WINDY) = 0.940 - 0.892 = 0.048 接下来,我们计算分裂信息度量H(V): OUTLOOK属性 属性OUTLOOK有3个取值,其中Sunny有5个样本、Rainy有5个样本、Overcast有4个样本,则 1 H(OUTLOOK) = - 5/14 * log2(5/14) - 5/14 * log2(5/14) - 4/14 * log2(4/14) = 1.577406282852345 TEMPERATURE属性 属性TEMPERATURE有3个取值,其中Hot有4个样本、Mild有6个样本、Cool有4个样本,则 1 H(TEMPERATURE) = - 4/14 * log2(4/14) - 6/14 * log2(6/14) - 4/14 * log2(4/14) = 1.5566567074628228 HUMIDITY属性 属性HUMIDITY有2个取值,其中Normal有7个样本、High有7个样本,则 1 H(HUMIDITY) = - 7/14 * log2(7/14) - 7/14 * log2(7/14) = 1.0 WINDY属性 属性WINDY有2个取值,其中True有6个样本、False有8个样本,则 1 H(WINDY) = - 6/14 * log2(6/14) - 8/14 * log2(8/14) = 0.9852281360342516 根据上面计算结果,我们可以计算信息增益率,如下所示: 1 IGR(OUTLOOK) = Gain(OUTLOOK) / H(OUTLOOK) = 0.246/1.577406282852345 = 0.15595221261270145 2 IGR(TEMPERATURE) = Gain(TEMPERATURE) / H(TEMPERATURE) = 0.029 / 1.5566567074628228 = 0.018629669509642094 3 IGR(HUMIDITY) = Gain(HUMIDITY) / H(HUMIDITY) = 0.151/1.0 = 0.151 4 IGR(WINDY) = Gain(WINDY) / H(WINDY) = 0.048/0.9852281360342516 = 0.048719680492692784 根据计算得到的信息增益率进行选择属性集中的属性作为决策树结点,对该结点进行分裂。 C4.5算法的优点是:产生的分类规则易于理解,准确率较高。 C4.5算法的缺点是:在构造树的过程中,需要对数据集进行多次的顺序扫描和排序,因而导致算法的低效。
实现MySQL表数据全量索引和增量索引,基于Solr DIH组件实现起来比较简单,只需要重复使用Solr的DIH(Data Import Handler)组件,对data-config.xml进行简单的修改即可。Solr DIH组件的实现类为org.apache.solr.handler.dataimport.DataImportHandler,在Solr的solrconfig.xml中配置两个handler,配置分别说明如下。 全量索引 solrconfig.xml配置如下: 1 <requestHandler name="/dataimport" 2 class="org.apache.solr.handler.dataimport.DataImportHandler"> 3 <lst name="defaults"> 4 <str name="config">data-config.xml</str> 5 </lst> 6 </requestHandler> 上面这个是针对全量索引的,主要是配置data-config.xml文件,示例如下所示: 01 <dataConfig> 02 <dataSource name="jdbc" driver="com.mysql.jdbc.Driver" 03 url="jdbc:mysql://172.0.8.249:5606/marketing_db_saved?zeroDateTimeBehavior=convertToNull" 04 user="developer" password="sedept@shiyanjun.cn"/> 05 <document name="mkt_data"> 06 <entity name="marketing_data" pk="id" 07 query="select * from marketing_data limit ${dataimporter.request.length} offset ${dataimporter.request.offset}" 08 transformer="RegexTransformer"> 09 <field column="id" name="id" /> 10 <field column="domain" name="domain" /> 11 <field column="alex_rank" name="alex_rank" /> 12 <field column="server_port" name="server_port" /> 13 <field column="cert_validity_notBefore" name="cert_validity_notBefore" /> 14 <field column="cert_validity_notAfter" /> 15 <field column="cert_validity_notAfter_yyyyMMdd" regex="(.*?)\s+.*"name="cert_validity_notAfter_yyyyMMdd" sourceColName="cert_validity_notAfter"/> 16 <field column="cert_issuer_brand" name="cert_issuer_brand" /> 17 <field column="cert_validation" name="cert_validation" /> 18 <field column="cert_isMultiDomain" name="cert_isMultiDomain" /> 19 <field column="cert_issuer_brand_isXRelated"name="cert_issuer_brand_isXRelated" /> 20 <field column="cert_isWildcard" name="cert_isWildcard" /> 21 <field column="cert_notAfter" name="cert_notAfter" /> 22 <field column="special_ssl" name="special_ssl" /> 23 <field column="competitor_logo" name="competitor_logo" /> 24 <field column="segment" name="segment" /> 25 </entity> 26 </document> 27 </dataConfig> 上面主要是通过内置变量 “${dataimporter.request.length}”和 “${dataimporter.request.offset}”来设置一个批次索引的数据表记录数,请求的URL示例如下: 1 http://172.0.8.212:8080/search-server/core0/dataimport?command=full-import&commit=true&clean=false&offset=1000000&length=100000 上面表示,对数据表中id范围为[10000000, 1100000]的记录进行索引,因为数据表可能达到千万记录数,而且线上有业务在操作数据库,所以要选择分批进行索引。 增量索引 solrconfig.xml配置如下: 01 <requestHandler name="/deltaimport"class="org.apache.solr.handler.dataimport.DataImportHandler"> 02 <lst name="defaults"> 03 <str name="config">delta-data-config.xml</str> 04 </lst> 05 </requestHandler> 06 上面定义请求的接口为“deltaimport”,对应的配置文件delta- data-config.xml内容如下所示: 07 <dataConfig> 08 <dataSource name="jdbc" driver="com.mysql.jdbc.Driver" 09 url="jdbc:mysql://172.0.8.249:5606/marketing_db_saved?zeroDateTimeBehavior=convertToNull" 10 user="developer" password="sedept@shiyanjun.cn"/> 11 <document name="mkt_data"> 12 <entity name="marketing_data" pk="id" 13 query="select * from marketing_data" 14 deltaImportQuery="select * from marketing_data where id='${dih.delta.id}'" 15 deltaQuery="select id from marketing_data where updated_at &gt; '${dih.last_index_time}'" 16 transformer="RegexTransformer"> 17 <field column="id" name="id" /> 18 <field column="domain" name="domain" /> 19 <field column="alex_rank" name="alex_rank" /> 20 <field column="server_port" name="server_port" /> 21 <field column="cert_validity_notBefore" name="cert_validity_notBefore" /> 22 <field column="cert_validity_notAfter" /> 23 <field column="cert_validity_notAfter_yyyyMMdd" regex="(.*?)\s+.*"name="cert_validity_notAfter_yyyyMMdd" sourceColName="cert_validity_notAfter"/> 24 <field column="cert_issuer_brand" name="cert_issuer_brand" /> 25 <field column="cert_validation" name="cert_validation" /> 26 <field column="cert_isMultiDomain" name="cert_isMultiDomain" /> 27 <field column="cert_issuer_brand_isXRelated"name="cert_issuer_brand_isXRelated" /> 28 <field column="cert_isWildcard" name="cert_isWildcard" /> 29 <field column="cert_notAfter" name="cert_notAfter" /> 30 <field column="special_ssl" name="special_ssl" /> 31 <field column="competitor_logo" name="competitor_logo" /> 32 <field column="segment" name="segment" /> 33 </entity> 34 </document> 35 </dataConfig> 上面主要是通过内置变量“${dih.delta.id}”和 “${dih.last_index_time}”来记录本次索引的id和最后索引时间。这里,会保存在deltaimport.properties文件中,示例如下: 1 #Sat Feb 16 17:48:49 CST 2013 2 last_index_time=2013-02-16 17\:48\:48 3 marketing_data.last_index_time=2013-02-16 17\:48\:48 请求增量索引的接口为“deltaimport”,可以写一个定时脚本,例如每隔一天执行一次 (一天索引一次,只索引数据表中更新的记录),请求URL示例如下: 1 172.0.8.212:8080/search-server/core0/dataimport?command=delta-import 有关“query”,“deltaImportQuery”, “deltaQuery”的解释,引用官网说明,如下所示: The query gives the data needed to populate fields of the Solr document in full-import The deltaImportQuery gives the data needed to populate fields when running a delta-import The deltaQuery gives the primary keys of the current entity which have changes since the last index time 还有更灵活的索引方式,以后再讲。
ZooKeeper是一个分布式开源框架,提供了协调分布式应用的基本服务,它向外部应用暴露一组通用服务——分布式同步(Distributed Synchronization)、命名服务(Naming Service)、集群维护(Group Maintenance)等,简化分布式应用协调及其管理的难度,提供高性能的分布式服务。ZooKeeper本身可以以Standalone模式安装运行,不过它的长处在于通过分布式ZooKeeper集群(一个Leader,多个Follower),基于一定的策略来保证ZooKeeper集群的稳定性和可用性,从而实现分布式应用的可靠性。 有关ZooKeeper的介绍,网上很多,也可以参考文章后面,我整理的一些相关链接。 ZooKeeper的安装配置还算比较容易的,下面,我们简单说明一下ZooKeeper的配置。 ZooKeeper Standalone模式 从Apache网站上(zookeeper.apache.org)下载ZooKeeper软件包,我选择了3.3.4版本的(zookeeper-3.3.4.tar.gz),在一台Linux机器上安装非常容易,只需要解压缩后,简单配置一下即可以启动ZooKeeper服务器进程。 将zookeeper-3.3.4/conf目录下面的 zoo_sample.cfg修改为zoo.cfg,配置文件内容如下所示: 1 tickTime=2000 2 dataDir=/home/hadoop/storage/zookeeper 3 clientPort=2181 4 initLimit=5 5 syncLimit=2 上面各个配置参数的含义也非常简单,引用如下所示: tickTime —— the basic time unit in milliseconds used by ZooKeeper. It is used to do heartbeats and the minimum session timeout will be twice the tickTime. dataDir —— the location to store the in-memory database snapshots and, unless specified otherwise, the transaction log of updates to the database. clientPort —— the port to listen for client connections 下面启动ZooKeeper服务器进程: 1 cd zookeeper-3.3.4/ 2 bin/zkServer.sh start 通过jps命令可以查看ZooKeeper服务器进程,名称为QuorumPeerMain。 在客户端连接ZooKeeper服务器,执行如下命令: 1 bin/zkCli.sh -server dynamic:2181 上面dynamic是我的主机名,如果在本机执行,则执行如下命令即可: 1 bin/zkCli.sh 客户端连接信息如下所示: 01 shirdrn@master:~/installation/zookeeper-3.3.4$ bin/zkCli.sh -server dynamic:2181 02 Connecting to dynamic:2181 03 2013-10-28 21:30:06,178 - INFO [main:Environment@97] - Client environment:zookeeper.version=3.3.3-1203054, built on 11/17/2011 05:47 GMT 04 2013-10-28 21:30:06,188 - INFO [main:Environment@97] - Client environment:host.name=master 05 2013-10-28 21:30:06,191 - INFO [main:Environment@97] - Client environment:java.version=1.6.0_30 06 2013-10-28 21:30:06,194 - INFO [main:Environment@97] - Client environment:java.vendor=Sun Microsystems Inc. 07 2013-10-28 21:30:06,200 - INFO [main:Environment@97] - Client environment:java.home=/home/hadoop/installation/jdk1.6.0_30/jre 08 2013-10-28 21:30:06,203 - INFO [main:Environment@97] - Client environment:java.class.path=/home/hadoop/installation/zookeeper-3.3.4/bin/../build/classes:/home/hadoop/installation/zookeeper-3.3.4/bin/../build/lib/*.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../zookeeper-3.3.4.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/log4j-1.2.15.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/jline-0.9.94.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-lang-2.4.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-collections-3.2.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-cli-1.1.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/apache-rat-tasks-0.6.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/apache-rat-core-0.6.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../src/java/lib/*.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../conf:/home/hadoop/installation/jdk1.6.0_30/lib/*.jar:/home/hadoop/installation/jdk1.6.0_30/jre/lib/*.jar 09 2013-10-28 21:30:06,206 - INFO [main:Environment@97] - Client environment:java.library.path=/home/hadoop/installation/jdk1.6.0_30/jre/lib/i386/client:/home/hadoop/installation/jdk1.6.0_30/jre/lib/i386:/home/hadoop/installation/jdk1.6.0_30/jre/../lib/i386:/usr/java/packages/lib/i386:/lib:/usr/lib 10 2013-10-28 21:30:06,213 - INFO [main:Environment@97] - Client environment:java.io.tmpdir=/tmp 11 2013-10-28 21:30:06,216 - INFO [main:Environment@97] - Client environment:java.compiler=<NA> 12 2013-10-28 21:30:06,235 - INFO [main:Environment@97] - Client environment:os.name=Linux 13 2013-10-28 21:30:06,244 - INFO [main:Environment@97] - Client environment:os.arch=i386 14 2013-10-28 21:30:06,246 - INFO [main:Environment@97] - Client environment:os.version=3.0.0-14-generic 15 2013-10-28 21:30:06,251 - INFO [main:Environment@97] - Client environment:user.name=hadoop 16 2013-10-28 21:30:06,254 - INFO [main:Environment@97] - Client environment:user.home=/home/hadoop 17 2013-10-28 21:30:06,255 - INFO [main:Environment@97] - Client environment:user.dir=/home/hadoop/installation/zookeeper-3.3.4 18 2013-10-28 21:30:06,264 - INFO [main:ZooKeeper@379] - Initiating client connection, connectString=dynamic:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@bf32c 19 2013-10-28 21:30:06,339 - INFO [main-SendThread():ClientCnxn$SendThread@1061] - Opening socket connection to server dynamic/192.168.0.107:2181 20 Welcome to ZooKeeper! 21 2013-10-28 21:30:06,397 - INFO [main-SendThread(dynamic:2181):ClientCnxn$SendThread@950] - Socket connection established to dynamic/192.168.0.107:2181, initiating session 22 JLine support is enabled 23 2013-10-28 21:30:06,492 - INFO [main-SendThread(dynamic:2181):ClientCnxn$SendThread@739] - Session establishment complete on server dynamic/192.168.0.107:2181, sessionid = 0x134b9b714f9000c, negotiated timeout = 30000 24 25 WATCHER:: 26 27 WatchedEvent state:SyncConnected type:None path:null 28 [zk: dynamic:2181(CONNECTED) 0] 接着,可以使用help查看Zookeeper客户端可以使用的基本操作命令。 ZooKeeper Distributed模式 ZooKeeper分布式模式安装(ZooKeeper集群)也比较容易,这里说明一下基本要点。 首先要明确的是,ZooKeeper集群是一个独立的分布式协调服务集群,“独立”的含义就是说,如果想使用ZooKeeper实现分布式应用的协调与管理,简化协调与管理,任何分布式应用都可以使用,这就要归功于Zookeeper的数据模型(Data Model)和层次命名空间(Hierarchical Namespace)结构,详细可以参考http://zookeeper.apache.org/doc/trunk/zookeeperOver.html。在设计你的分布式应用协调服务时,首要的就是考虑如何组织层次命名空间。 下面说明分布式模式的安装配置,过程如下所示:第一步:主机名称到IP地址映射配置 ZooKeeper集群中具有两个关键的角色:Leader和Follower。集群中所有的结点作为一个整体对分布式应用提供服务,集群中每个结点之间都互相连接,所以,在配置的ZooKeeper集群的时候,每一个结点的host到IP地址的映射都要配置上集群中其它结点的映射信息。 例如,我的ZooKeeper集群中每个结点的配置,以zk-01为例,/etc/hosts内容如下所示: 1 192.168.0.179 zk-01 2 192.168.0.178 zk-02 3 192.168.0.177 zk-03 ZooKeeper采用一种称为Leader election的选举算法。在整个集群运行过程中,只有一个Leader,其他的都是Follower,如果ZooKeeper集群在运行过程中Leader出了问题,系统会采用该算法重新选出一个Leader。因此,各个结点之间要能够保证互相连接,必须配置上述映射。 ZooKeeper集群启动的时候,会首先选出一个Leader,在Leader election过程中,某一个满足选举算的结点就能成为Leader。整个集群的架构可以参考http://zookeeper.apache.org/doc/trunk/zookeeperOver.html#sc_designGoals。第二步:修改ZooKeeper配置文件 在其中一台机器(zk-01)上,解压缩zookeeper-3.3.4.tar.gz,修改配置文件conf/zoo.cfg,内容如下所示: 1 tickTime=2000 2 dataDir=/home/hadoop/storage/zookeeper 3 clientPort=2181 4 initLimit=5 5 syncLimit=2 6 server.1=zk-01:2888:3888 7 server.2=zk-02:2888:3888 8 server.3=zk-03:2888:3888 上述配置内容说明,可以参考http://zookeeper.apache.org/doc/trunk/zookeeperStarted.html#sc_RunningReplicatedZooKeeper。第三步:远程复制分发安装文件 上面已经在一台机器zk-01上配置完成ZooKeeper,现在可以将该配置好的安装文件远程拷贝到集群中的各个结点对应的目录下: 1 cd /home/hadoop/installation/ 2 scp -r zookeeper-3.3.4/ shirdrn@zk-02:/home/hadoop/installation/ 3 scp -r zookeeper-3.3.4/ shirdrn@zk-03:/home/hadoop/installation/ 第四步:设置myid 在我们配置的dataDir指定的目录下面,创建一个myid文件,里面内容为一个数字,用来标识当前主机,conf/zoo.cfg文件中配置的server.X中X为什么数字,则myid文件中就输入这个数字,例如: 1 shirdrn@zk-01:~/installation/zookeeper-3.3.4$ echo "1" > /home/hadoop/storage/zookeeper/myid 2 shirdrn@zk-02:~/installation/zookeeper-3.3.4$ echo "2" > /home/hadoop/storage/zookeeper/myid 3 shirdrn@zk-03:~/installation/zookeeper-3.3.4$ echo "3" > /home/hadoop/storage/zookeeper/myid 按照上述进行配置即可。第五步:启动ZooKeeper集群 在ZooKeeper集群的每个结点上,执行启动ZooKeeper服务的脚本,如下所示: 1 shirdrn@zk-01:~/installation/zookeeper-3.3.4$ bin/zkServer.sh start 2 shirdrn@zk-02:~/installation/zookeeper-3.3.4$ bin/zkServer.sh start 3 shirdrn@zk-03:~/installation/zookeeper-3.3.4$ bin/zkServer.sh start 以结点zk-01为例,日志如下所示: 001 shirdrn@zk-01:~/installation/zookeeper-3.3.4$ tail -500f zookeeper.out 002 2013-10-28 06:51:19,117 - INFO [main:QuorumPeerConfig@90] - Reading configuration from: /home/hadoop/installation/zookeeper-3.3.4/bin/../conf/zoo.cfg 003 2013-10-28 06:51:19,133 - INFO [main:QuorumPeerConfig@310] - Defaulting to majority quorums 004 2013-10-28 06:51:19,167 - INFO [main:QuorumPeerMain@119] - Starting quorum peer 005 2013-10-28 06:51:19,227 - INFO [main:NIOServerCnxn$Factory@143] - binding to port 0.0.0.0/0.0.0.0:2181 006 2013-10-28 06:51:19,277 - INFO [main:QuorumPeer@819] - tickTime set to 2000 007 2013-10-28 06:51:19,278 - INFO [main:QuorumPeer@830] - minSessionTimeout set to -1 008 2013-10-28 06:51:19,279 - INFO [main:QuorumPeer@841] - maxSessionTimeout set to -1 009 2013-10-28 06:51:19,281 - INFO [main:QuorumPeer@856] - initLimit set to 5 010 2013-10-28 06:51:19,347 - INFO [Thread-1:QuorumCnxManager$Listener@473] - My election bind port: 3888 011 2013-10-28 06:51:19,393 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumPeer@621] - LOOKING 012 2013-10-28 06:51:19,396 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:FastLeaderElection@663] - New election. My id = 1, Proposed zxid = 0 013 2013-10-28 06:51:19,400 - INFO [WorkerReceiver Thread:FastLeaderElection@496] - Notification: 1 (n.leader), 0 (n.zxid), 1 (n.round), LOOKING (n.state), 1 (n.sid), LOOKING (my state) 014 2013-10-28 06:51:19,416 - WARN [WorkerSender Thread:QuorumCnxManager@384] - Cannotopen channel to 2 at election address zk-02/192.168.0.178:3888 015 java.net.ConnectException: Connection refused 016 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 017 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 018 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 019 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 020 at org.apache.zookeeper.server.quorum.QuorumCnxManager.toSend(QuorumCnxManager.java:340) 021 at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.process(FastLeaderElection.java:360) 022 at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.run(FastLeaderElection.java:333) 023 at java.lang.Thread.run(Thread.java:662) 024 2013-10-28 06:51:19,420 - WARN [WorkerSender Thread:QuorumCnxManager@384] - Cannotopen channel to 3 at election address zk-03/192.168.0.177:3888 025 java.net.ConnectException: Connection refused 026 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 027 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 028 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 029 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 030 at org.apache.zookeeper.server.quorum.QuorumCnxManager.toSend(QuorumCnxManager.java:340) 031 at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.process(FastLeaderElection.java:360) 032 at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.run(FastLeaderElection.java:333) 033 at java.lang.Thread.run(Thread.java:662) 034 2013-10-28 06:51:19,612 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 2 at election address zk-02/192.168.0.178:3888 035 java.net.ConnectException: Connection refused 036 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 037 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 038 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 039 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 040 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 041 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 042 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 043 2013-10-28 06:51:19,615 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 3 at election address zk-03/192.168.0.177:3888 044 java.net.ConnectException: Connection refused 045 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 046 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 047 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 048 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 049 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 050 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 051 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 052 2013-10-28 06:51:19,616 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:FastLeaderElection@697] - Notification time out: 400 053 2013-10-28 06:51:20,019 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 2 at election address zk-02/192.168.0.178:3888 054 java.net.ConnectException: Connection refused 055 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 056 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 057 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 058 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 059 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 060 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 061 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 062 2013-10-28 06:51:20,021 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 3 at election address zk-03/192.168.0.177:3888 063 java.net.ConnectException: Connection refused 064 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 065 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 066 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 067 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 068 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 069 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 070 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 071 2013-10-28 06:51:20,022 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:FastLeaderElection@697] - Notification time out: 800 072 2013-10-28 06:51:20,825 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 2 at election address zk-02/192.168.0.178:3888 073 java.net.ConnectException: Connection refused 074 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 075 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 076 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 077 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 078 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 079 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 080 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 081 2013-10-28 06:51:20,827 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 3 at election address zk-03/192.168.0.177:3888 082 java.net.ConnectException: Connection refused 083 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 084 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 085 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 086 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 087 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 088 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 089 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 090 2013-10-28 06:51:20,828 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:FastLeaderElection@697] - Notification time out: 1600 091 2013-10-28 06:51:22,435 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 2 at election address zk-02/192.168.0.178:3888 092 java.net.ConnectException: Connection refused 093 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 094 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 095 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 096 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 097 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 098 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 099 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 100 2013-10-28 06:51:22,439 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 3 at election address zk-03/192.168.0.177:3888 101 java.net.ConnectException: Connection refused 102 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 103 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 104 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 105 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 106 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 107 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 108 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 109 2013-10-28 06:51:22,441 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:FastLeaderElection@697] - Notification time out: 3200 110 2013-10-28 06:51:22,945 - INFO [WorkerReceiver Thread:FastLeaderElection@496] - Notification: 2 (n.leader), 0 (n.zxid), 1 (n.round), LOOKING (n.state), 2 (n.sid), LOOKING (my state) 111 2013-10-28 06:51:22,946 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:FastLeaderElection@721] - Updating proposal 112 2013-10-28 06:51:22,949 - WARN [WorkerSender Thread:QuorumCnxManager@384] - Cannotopen channel to 3 at election address zk-03/192.168.0.177:3888 113 java.net.ConnectException: Connection refused 114 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 115 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:567) 116 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:100) 117 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 118 at org.apache.zookeeper.server.quorum.QuorumCnxManager.toSend(QuorumCnxManager.java:340) 119 at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.process(FastLeaderElection.java:360) 120 at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.run(FastLeaderElection.java:333) 121 at java.lang.Thread.run(Thread.java:662) 122 2013-10-28 06:51:22,951 - INFO [WorkerReceiver Thread:FastLeaderElection@496] - Notification: 2 (n.leader), 0 (n.zxid), 1 (n.round), LOOKING (n.state), 1 (n.sid), LOOKING (my state) 123 2013-10-28 06:51:23,156 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumPeer@643] - FOLLOWING 124 2013-10-28 06:51:23,170 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Learner@80] - TCP NoDelay set to: true 125 2013-10-28 06:51:23,206 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:zookeeper.version=3.3.3-1203054, built on 11/17/2011 05:47 GMT 126 2013-10-28 06:51:23,207 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:host.name=zk-01 127 2013-10-28 06:51:23,207 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:java.version=1.6.0_30 128 2013-10-28 06:51:23,208 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:java.vendor=Sun Microsystems Inc. 129 2013-10-28 06:51:23,208 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:java.home=/home/hadoop/installation/jdk1.6.0_30/jre 130 2013-10-28 06:51:23,209 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:java.class.path=/home/hadoop/installation/zookeeper-3.3.4/bin/../build/classes:/home/hadoop/installation/zookeeper-3.3.4/bin/../build/lib/*.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../zookeeper-3.3.4.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/log4j-1.2.15.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/jline-0.9.94.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-lang-2.4.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-collections-3.2.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-cli-1.1.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/apache-rat-tasks-0.6.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/apache-rat-core-0.6.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../src/java/lib/*.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../conf:/home/hadoop/installation/jdk1.6.0_30/lib/*.jar:/home/hadoop/installation/jdk1.6.0_30/jre/lib/*.jar 131 2013-10-28 06:51:23,210 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:java.library.path=/home/hadoop/installation/jdk1.6.0_30/jre/lib/i386/client:/home/hadoop/installation/jdk1.6.0_30/jre/lib/i386:/home/hadoop/installation/jdk1.6.0_30/jre/../lib/i386:/usr/java/packages/lib/i386:/lib:/usr/lib 132 2013-10-28 06:51:23,210 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:java.io.tmpdir=/tmp 133 2013-10-28 06:51:23,212 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:java.compiler=<NA> 134 2013-10-28 06:51:23,212 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:os.name=Linux 135 2013-10-28 06:51:23,212 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:os.arch=i386 136 2013-10-28 06:51:23,213 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:os.version=3.0.0-14-generic 137 2013-10-28 06:51:23,213 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:user.name=hadoop 138 2013-10-28 06:51:23,214 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:user.home=/home/hadoop 139 2013-10-28 06:51:23,214 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Environment@97] - Server environment:user.dir=/home/hadoop/installation/zookeeper-3.3.4 140 2013-10-28 06:51:23,223 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:ZooKeeperServer@151] - Created server with tickTime 2000 minSessionTimeout 4000 maxSessionTimeout 40000 datadir /home/hadoop/storage/zookeeper/version-2 snapdir /home/hadoop/storage/zookeeper/version-2 141 2013-10-28 06:51:23,339 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Learner@294] - Getting a snapshot from leader 142 2013-10-28 06:51:23,358 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:Learner@325] - Setting leader epoch 1 143 2013-10-28 06:51:23,358 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:FileTxnSnapLog@254] - Snapshotting: 0 144 2013-10-28 06:51:25,511 - INFO [WorkerReceiver Thread:FastLeaderElection@496] - Notification: 3 (n.leader), 0 (n.zxid), 1 (n.round), LOOKING (n.state), 3 (n.sid), FOLLOWING (my state) 145 2013-10-28 06:51:42,584 - INFO [WorkerReceiver Thread:FastLeaderElection@496] - Notification: 3 (n.leader), 0 (n.zxid), 2 (n.round), LOOKING (n.state), 3 (n.sid), FOLLOWING (my state) 我启动的顺序是zk-01>zk-02>zk-03,由于ZooKeeper集群启动的时候,每个结点都试图去连接集群中的其它结点,先启动的肯定连不上后面还没启动的,所以上面日志前面部分的异常是可以忽略的。通过后面部分可以看到,集群在选出一个Leader后,最后稳定了。 其他结点可能也出现类似问题,属于正常。第六步:安装验证 可以通过ZooKeeper的脚本来查看启动状态,包括集群中各个结点的角色(或是Leader,或是Follower),如下所示,是在ZooKeeper集群中的每个结点上查询的结果: 01 shirdrn@zk-01:~/installation/zookeeper-3.3.4$ bin/zkServer.sh status 02 JMX enabled by default 03 Using config: /home/hadoop/installation/zookeeper-3.3.4/bin/../conf/zoo.cfg 04 Mode: follower 05 06 shirdrn@zk-02:~/installation/zookeeper-3.3.4$ bin/zkServer.sh status 07 JMX enabled by default 08 Using config: /home/hadoop/installation/zookeeper-3.3.4/bin/../conf/zoo.cfg 09 Mode: leader 10 11 shirdrn@zk-03:~/installation/zookeeper-3.3.4$ bin/zkServer.sh status 12 JMX enabled by default 13 Using config: /home/hadoop/installation/zookeeper-3.3.4/bin/../conf/zoo.cfg 14 Mode: follower 通过上面状态查询结果可见,zk-02是集群的Leader,其余的两个结点是Follower。 另外,可以通过客户端脚本,连接到ZooKeeper集群上。对于客户端来说,ZooKeeper是一个整体(ensemble),连接到ZooKeeper集群实际上感觉在独享整个集群的服务,所以,你可以在任何一个结点上建立到服务集群的连接,例如: 01 shirdrn@zk-03:~/installation/zookeeper-3.3.4$ bin/zkCli.sh -server zk-01:2181 02 Connecting to zk-01:2181 03 2013-10-28 07:14:21,068 - INFO [main:Environment@97] - Client environment:zookeeper.version=3.3.3-1203054, built on 11/17/2011 05:47 GMT 04 2013-10-28 07:14:21,080 - INFO [main:Environment@97] - Client environment:host.name=zk-03 05 2013-10-28 07:14:21,085 - INFO [main:Environment@97] - Client environment:java.version=1.6.0_30 06 2013-10-28 07:14:21,089 - INFO [main:Environment@97] - Client environment:java.vendor=Sun Microsystems Inc. 07 2013-10-28 07:14:21,095 - INFO [main:Environment@97] - Client environment:java.home=/home/hadoop/installation/jdk1.6.0_30/jre 08 2013-10-28 07:14:21,104 - INFO [main:Environment@97] - Client environment:java.class.path=/home/hadoop/installation/zookeeper-3.3.4/bin/../build/classes:/home/hadoop/installation/zookeeper-3.3.4/bin/../build/lib/*.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../zookeeper-3.3.4.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/log4j-1.2.15.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/jline-0.9.94.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-lang-2.4.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-collections-3.2.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/commons-cli-1.1.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/apache-rat-tasks-0.6.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../lib/apache-rat-core-0.6.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../src/java/lib/*.jar:/home/hadoop/installation/zookeeper-3.3.4/bin/../conf:/home/hadoop/installation/jdk1.6.0_30/lib/*.jar:/home/hadoop/installation/jdk1.6.0_30/jre/lib/*.jar 09 2013-10-28 07:14:21,111 - INFO [main:Environment@97] - Client environment:java.library.path=/home/hadoop/installation/jdk1.6.0_30/jre/lib/i386/client:/home/hadoop/installation/jdk1.6.0_30/jre/lib/i386:/home/hadoop/installation/jdk1.6.0_30/jre/../lib/i386:/usr/java/packages/lib/i386:/lib:/usr/lib 10 2013-10-28 07:14:21,116 - INFO [main:Environment@97] - Client environment:java.io.tmpdir=/tmp 11 2013-10-28 07:14:21,124 - INFO [main:Environment@97] - Client environment:java.compiler=<NA> 12 2013-10-28 07:14:21,169 - INFO [main:Environment@97] - Client environment:os.name=Linux 13 2013-10-28 07:14:21,175 - INFO [main:Environment@97] - Client environment:os.arch=i386 14 2013-10-28 07:14:21,177 - INFO [main:Environment@97] - Client environment:os.version=3.0.0-14-generic 15 2013-10-28 07:14:21,185 - INFO [main:Environment@97] - Client environment:user.name=hadoop 16 2013-10-28 07:14:21,188 - INFO [main:Environment@97] - Client environment:user.home=/home/hadoop 17 2013-10-28 07:14:21,190 - INFO [main:Environment@97] - Client environment:user.dir=/home/hadoop/installation/zookeeper-3.3.4 18 2013-10-28 07:14:21,197 - INFO [main:ZooKeeper@379] - Initiating client connection, connectString=zk-01:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@bf32c 19 2013-10-28 07:14:21,305 - INFO [main-SendThread():ClientCnxn$SendThread@1061] - Opening socket connection to server zk-01/192.168.0.179:2181 20 Welcome to ZooKeeper! 21 2013-10-28 07:14:21,376 - INFO [main-SendThread(zk-01:2181):ClientCnxn$SendThread@950] - Socket connection established to zk-01/192.168.0.179:2181, initiating session 22 JLine support is enabled 23 [zk: zk-01:2181(CONNECTING) 0] 2013-10-28 07:14:21,872 - INFO [main-SendThread(zk-01:2181):ClientCnxn$SendThread@739] - Session establishment complete on server zk-01/192.168.0.179:2181, sessionid = 0x134bdcd6b730000, negotiated timeout = 30000 24 25 WATCHER:: 26 27 WatchedEvent state:SyncConnected type:None path:null 28 29 [zk: zk-01:2181(CONNECTED) 0] ls / 30 [zookeeper] 当前根路径为/zookeeper。 总结说明 主机名与IP地址映射配置问题 启动ZooKeeper集群时,如果ZooKeeper集群中zk-01结点的日志出现如下错误: 01 java.net.SocketTimeoutException 02 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:109) 03 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 04 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 05 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 06 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 07 2013-10-28 06:37:46,026 - INFO [QuorumPeer:/0:0:0:0:0:0:0:0:2181:FastLeaderElection@697] - Notification time out: 6400 08 2013-10-28 06:37:57,431 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 2 at election address zk-02/202.106.199.35:3888 09 java.net.SocketTimeoutException 10 at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:109) 11 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:371) 12 at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectAll(QuorumCnxManager.java:404) 13 at org.apache.zookeeper.server.quorum.FastLeaderElection.lookForLeader(FastLeaderElection.java:688) 14 at org.apache.zookeeper.server.quorum.QuorumPeer.run(QuorumPeer.java:622) 15 2013-10-28 06:38:02,442 - WARN [QuorumPeer:/0:0:0:0:0:0:0:0:2181:QuorumCnxManager@384] - Cannot open channel to 3 at election address zk-03/202.106.199.35:3888 很显然,zk-01在启动时连接集群中其他结点(zk-02、zk-03)时,主机名映射的IP与我们实际配置的不一致,所以集群中各个结点之间无法建立链路,整个ZooKeeper集群启动是失败的。 上面错误日志中zk-02/202.106.199.35:3888实际应该是zk-02/202.192.168.0.178:3888就对了,但是在进行域名解析的时候映射有问题,修改每个结点的/etc/hosts文件,将ZooKeeper集群中所有结点主机名到IP地址的映射配置上。 参考链接 下面是我整理搜集的有关ZooKeeper相关内容的网址,可以学习参考。
我们通过学习借鉴,哪些项目或应用都使用了ZooKeeper,可以了解我们的应用使用ZooKeeper是否能真正地带来价值,当然,有些项目可能也未必非常适合使用ZooKeeper,我们要批判地学习、借鉴和吸收。 下面是一些使用了ZooKeeper实现的案例: HDFS HA(QJM) Hadoop 2.x之前的版本,HDFS集群中Namenode是整个集群的中央元数据存储和服务节点,它存在SPOF的问题。在2.x版本中,提出了各种HA方案,避免Namenode的SPOF问题,其中基于QJM(Quorum Journal Manager)的方案可以解决这个问题:使用QJM的方案中,HDFS集群中存在两类节点,一类是Namenode节点(包括Active状态的Namenode,和Standby状态的Namenode),另一类是JournalNode,进行容错。当Active状态的Namenode元数据发生改变时,通过JournalNode进程(ZooKeeper集群中)来监视这种变化,然后同步到Standby状态的Namenode节点(实际上同步的是EditLog镜像文件内容的变更)。 当Active状态的节点发生故障后,Standby节点的Namenode自动切换,并接管HDFS集群中Active状态Namenode的服务,用来向客户端提供元数据服务。 Solr Solr是一个开源的分布式搜索引擎,支持索引的分片和复制,可以根据需要来线性增加节点,扩展集群。Solr使用ZooKeeper主要实现如下功能: 配置文件的管理:每个Collection都有对应的配置文件,多个分片共享配置文件(schema.xml、solrconfig.xml) Collection管理:一个Solr集群可以有多个逻辑上独立的Collection,每个Collection又包括多个分片和副本 集群节点管理:Solr集群中有哪些活跃的节点可以使用,每个节点上都有Collection的分片(Shard) Leader分片选举:一个Collection的多个分片可以设置冗余的副本,一个分片的多个副本中只有一个Leader可以进提供服务,如果某个存储Leader分片的节点宕机,Solr基于ZooKeeper来重新选出一个Leader分片,持续提供服务 HBase HBase是一个基于Hadoop平台的开源NoSQL数据库,它使用ZooKeeper主要实现如下功能: Master选举:HBase基于Master-Slave模式架构,可以有多个HMaster,使用ZooKeeper实现了SPOF下Master的选举 租约管理:客户端与RegionServer交互时,会生成租约,该租约对象具有有效期 表元数据管理:HBase中包括用户表及其两个特殊的表:-ROOT-表和.META.表(例如,管理-ROOT-表中location信息,一个-ROOT-表只有一个Region,它保存了RegionServer的地址信息。) 协调RegionServer节点:数据变更会通过ZooKeeper同步复制到其他节点 Lily Lily是一个分布式数据管理平台,它基于Hadoop、HBase、Solr、ZooKeeper实现。使用ZooKeeper来注册Lily Node,从而管理集群节点的状态信息。 Dubbo Dubbo是阿里巴巴开源的分布式服务框架,它可以选择使用ZooKeeper作为服务注册中心。Dubbo服务基于Provider-Consumer模型,在服务发布的时候可以选择使用ZooKeeper作为注册中心来管理注册的服务,包括Provider发布的服务和Consumer订阅的服务。 Katta Katta实现了Lucene的分布式索引,能够提供数据的实时访问。Katta使用ZooKeeper实现了集群节点的管理,Master的选举,以及Lucene索引的管理。 Strom Storm流式计算框架使用ZooKeeper来协调整个计算集群,Storm计算集群存在Nimbus和Supervisor两类节点。Nimbus负责分配任务(Topology),将任务信息写入ZooKeeper存储,然后Supervisor从ZooKeeper中读取任务信息。另外,Nimbus也监控集群中的计算任务节点,Supervisor也会发送心跳信息(包括状态信息)到ZooKeeper中,使得Nimbus可以实现状态的监控,任何计算节点出现故障,只要重新启动之后,继续从ZooKeeper中获取数据即可继续执行计算任务。 BookKeeper BookKeeper是Apache ZooKeeper项目的一个子项目。它是一个用来可靠地记录流数据的系统,主要用于存储WAL(Write Ahead Log)。 我们知道,Hadoop Namenode用来存储HDSF集群的元数据,其中存在一个用于写就花数据的EditLog文件,和一个存在于内存中的FsImage镜像,每当客户端与HDFS集群交互时,对于集群中数据的变更都会记录在Namenode的EditLog文件中,然后再将该变更同步到内存的FsImage镜像上。 在BookKeeper中,服务节点(多个)称为Bookie,日志流(Log Stream)称为Ledger,每个日志单元(如一条记录)被称为Ledger条目。一组服务节点Bookie主要存储Ledger,Ledger的类型非常复杂多样,那么可能某一个Bookie节点可能发生故障,然而只要我们的BookKeeper系统的多个服务节点Bookie存储中存在正确可用的节点,整个系统就可以正常对外提供服务,BookKeeper的元数据存储在ZooKeeper中(使用ZooKeeper存储的只是元数据,实际日志流数据存储在Bookie中)。 Hadoop HDFS HA基于BookKeeper系统,可以实现HDFS Namenode的高可用性,这种方案称为BJM(BookKeeper Journal Manager),HDFS HA的另一种方案叫做QJM(Quorum Journal Manager)。可以参考相关文档,在后面会给出参考连接。 HedWig HedWig是基于ZooKeeper和BookKeeper的发布-订阅系统。在HedWig系统中,使用ZooKeeper来持久化系统的元数据,使用BookKeeper来存储实际的消息数据。 其他方案 还有其他一些开源方案,都使用了ZooKeeper,如下所示: Kafka Flume Accumulo Mesos
我们知道,HBase是一个基于列的NoSQL数据库,它可以实现的数据的灵活存储。它本身是一个大表,在一些应用中,通过设计RowKey,可以实现对海量数据的快速存储和访问。但是,对于复杂的查询统计类需求,如果直接基于HBase API来实现,性能非常差,或者,可以通过实现MapReduce程序来进行查询分析,这也继承了MapReduce所具备的延迟性。 实现Impala与HBase整合,我们能够获得的好处有如下几个: 可以使用我们熟悉的SQL,像操作传统关系型数据库一样,很容易给出复杂查询、统计分析的SQL设计 Impala查询统计分析,比原生的MapReduce以及Hive的执行速度快很多 Impala与HBase整合,需要将HBase的RowKey和列映射到Impala的Table字段中。Impala使用Hive的Metastore来存储元数据信息,与Hive类似,在于HBase进行整合时,也是通过外部表(EXTERNAL)的方式来实现。 准备工作 首先,我们需要做如下准备工作: 安装配置Hadoop集群(http://www.cloudera.com/content/cloudera-content/cloudera-docs/CDH4/latest/CDH4-Installation-Guide/cdh4ig_topic_4_4.html) 安装配置HBase集群(http://www.cloudera.com/content/cloudera-content/cloudera-docs/CDH4/latest/CDH4-Installation-Guide/cdh4ig_topic_20.html) 安装配置Hive(http://www.cloudera.com/content/cloudera-content/cloudera-docs/CDH4/latest/CDH4-Installation-Guide/cdh4ig_topic_18.html) 安装配置Impala(http://www.cloudera.com/content/cloudera-content/cloudera-docs/Impala/latest/Installing-and-Using-Impala/ciiu_noncm_installation.html?scroll=noncm_installation) 涉及到相关系统的安装配置,可以参考相关文档和资料。 下面,我们通过一个示例表test_info来说明,Impala与HBase整合的步骤: 整合过程 在HBase中创建表 首先,我们使用HBase Shell创建一个表,如下所示: 1 create 'test_info', 'info' 表名为test_info,只有一个名称为info的列簇(Column Family),我们计划该列簇中存在4个列,分别为info:user_id、info:user_type、info:gender、info:birthday。 在Hive中创建外部表 创建外部表,对应的DDL如下所示: 1 CREATE EXTERNAL TABLE sho.test_info( 2 user_id string, 3 user_type tinyint, 4 gender string, 5 birthday string) 6 ROW FORMAT SERDE 'org.apache.hadoop.hive.hbase.HBaseSerDe' 7 STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler' 8 WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key, info:user_type, info:gender, info:birthday") 9 TBLPROPERTIES("hbase.table.name" = "test_info"); 上面DDL语句中,在WITH SERDEPROPERTIES选项中指定Hive外部表字段到HBase列的映射,其中“:key”对应于HBase中的RowKey,名称为“user_id”,其余的就是列簇info中的列名。最后在TBLPROPERTIES中指定了HBase中要进行映射的表名。 在Impala中同步元数据 Impala共享Hive的Metastore,这时需要同步元数据,可以通过在Impala Shell中执行同步命令: 1 INVALIDATE METADATA; 然后,就可以查看到映射HBase中表的结构: 1 DESC test_info; 表结构如图所示: 通过上面三步,我们就完成了Impala和HBase的整合配置。 验证整合 下面,我们通过实践来验证上述的配置是否生效。 我们模拟客户端插入数据到HBase表中,可以使用HBase API或者HBase Thrift来实现,这里我们使用了HBase Thrift接口来进行操作,详见文章 HBase Thrift客户端Java API实践。 然后,我们就可以通过Impala Shell进行查询分析。基于上面创建整合的示例表,插入20000000(2000万)记录,我们做一个统计分析的示例,SQL语句如下所示: 1 SELECT user_type, COUNT(user_id) AS cnt FROM test_info WHERE gender='M' GROUP BYuser_type ORDER BY cnt DESC LIMIT 10; 运行结果信息,如下图所示: 上述程序运行所在Hadoop集群共有3个Datanode,执行上述统计SQL共用时88.13s。我的Hadoop集群配置比较低,2个节点是双核CPU,另一个是4核,内存足够,大概10G左右,而且还有好多程序在共享这些节点,如数据库服务器、SOLR集群等。如果提高配置,做一些优化,针对20000000(2000万)条记录做统计分析,应该可以在5s以内出来结果。由于测试数据是我们随机生成的,gender取值为’M’和’F’,user_type的值为1到10,经过统计分组后,数据分布还算均匀。
k-means(k-均值)算法是一种基于距离的聚类算法,它用质心(Centroid)到属于该质心的点距离这个度量来实现聚类,通常可以用于N维空间中对象。下面,我们以二维空间为例,概要地总结一下k-means聚类算法的一些要点: 除了随机选择的初始质心,后续迭代质心是根据给定的待聚类的集合S中点计算均值得到的,所以质心一般不是S中的点,但是标识的是一簇点的中心。 基本k-means算法,开始需要随机选择指定的k个质心,因为初始k个质心是随机选择的,所以每次执行k-means聚类的结果可能都不相同。如果初始随机选择的质心位置不好,可能造成k-means聚类的结果非常不理想。 计算质心:假设k-means聚类过程中,得到某一个簇的集合Ci={p(x1,y1), p(x2,y2), …,p(xn,yn)},则簇Ci的质心,质心x坐标为(x1+x2+ …+xn)/n,质心y坐标为(y1+y2+ …+yn)/n。 k-means算法的终止条件:质心在每一轮迭代中会发生变化,然后需要重新将非质心点指派给最近的质心而形成新的簇,如果只有很少的一部分点在迭代过程中,还在改变簇(如,更新一次质心,有些点从一个簇移动到另一个簇),那么满足这样一个收敛条件,可以提前结束迭代过程。 k-means算法的框架是:首先随机选择k个初始质心点,然后执行聚类处理迭代,不断更新质心,直到满足算法收敛条件。由于该算法收敛于局部最优,所以多次执行聚类算法,通过比较,选择聚类效果最好的结果作为最终的结果。 k-means算法聚类完成后,没有离群点,所有的点都会被指派到对应的簇中。 由于k-means算法比较简单,对于算法的实现过程,我们概要地描述如下: 随机选择k个初始质心; 如果没有满足聚类算法终止条件,则继续执行步骤3,否则转步骤5; 计算每个非质心点p到k个质心的欧几里德距离,将p指派给距离最近的质心; 根据上一步的k个质心及其对应的非质心点集,重新计算新的质心点,然后转步骤2; 输出聚类结果,算法可以执行多次,使用散点图比较不同的聚类结果。 下面,我们详细说明上述步骤: 随机选择初始质心 由于随机选择初始质心,每次执行聚类选择的初始质心都不相同,这也导致k-means算法聚类后,没有确定的结果,或者说,可能两次聚类的结果完全不同。该过程的实现,比较简单,只要随机选择给定待聚类点集合中的点即可,初始质心是实际存在的点,代码如下所示: 01 @Override 02 public TreeSet<Centroid> select(int k, List<Point2D> points) { 03 TreeSet<Centroid> centroids = Sets.newTreeSet(); 04 Set<Point2D> selectedPoints = Sets.newHashSet(); 05 while(selectedPoints.size() < k) { // 先随机选择k个点 06 int index = random.nextInt(points.size()); 07 Point2D p = points.get(index); 08 selectedPoints.add(p); 09 } 10 11 Iterator<Point2D> iter = selectedPoints.iterator(); 12 int id = 0; 13 while(iter.hasNext()) { // 构造Centroid质心对象,分配一个id作为簇的唯一标识 14 centroids.add(new Centroid(id++, iter.next())); 15 } 16 return centroids; 17 } 有一些方法,可以在这一步中,解决初始质心选择的随机性,可以将选择初始质心作为选择策略的设计,根据需要选择不同的策略,比如,可以这样设计策略接口: 1 public interface SelectInitialCentroidsPolicy { 2 3 TreeSet<Centroid> select(int k, List<Point2D> points); 4 } 我们这里只给了简单地随机选择策略,也是基本k-means算法最基础的策略。其他方法,可以查阅相关资料。 计算欧几里德距离,指派点到质心所在簇 计算每个非质心点到全部k个质心点的距离,将该非质心点指派给距离最小的质心点所在的簇。如果输出的数据量比较大,可以将数据集合进行分割,基于多线程去并行处理,最后再合并结果。我们的实现思路是:每个线程都共享k个质心的集合,然后将非质心点均匀分发到多个线程的队列中,然后每个线程从队列取出非质心点,计算非质心点到k个质心的距离,并计算出距离最短的质心,将该非质心点指派给该质心所在的簇。实现代码如下所示: 01 Task task = q.poll(); 02 Point2D p1 = task.point; 03 04 // assign points to a nearest centroid 05 Distance minDistance = null; 06 for(Centroid centroid : task.centroids) { // 计算一个非质心点,到k个质心的距离,并计算距离最短的 07 double distance = MetricUtils.euclideanDistance(p1, centroid); 08 if(minDistance != null) { 09 if(distance < minDistance.distance) { 10 minDistance = new Distance(p1, centroid, distance); 11 } 12 } else { 13 minDistance = new Distance(p1, centroid, distance); 14 } 15 } 16 LOG.debug("Assign Point2D[" + p1 + "] to Centroid[" + minDistance.centroid + "]"); 17 18 Multiset<Point2D> pointsBelongingToCentroid = localClusteredPoints.get(minDistance.centroid); 19 if(pointsBelongingToCentroid == null) { 20 pointsBelongingToCentroid = HashMultiset.create(); 21 localClusteredPoints.put(minDistance.centroid, pointsBelongingToCentroid); // localClusteredPoints是局部的,key为质心,value为属于该质心的非质心点的集合 22 } 23 pointsBelongingToCentroid.add(p1); 这样,经过一轮的迭代计算,每个线程都处理完,得到一个局部的指派的簇的集合,然后对每个局部集合进行合并,得到一个全局的、质心到属于该质心的点的簇的集合,作为下一次迭代的输入,也比较容易处理。 迭代终止条件计算 这一步应该算是k-means算法聚类过程中比较核心的步骤。我们考虑了如下3个终止条件: 比较相邻的2轮迭代结果,在2轮过程中移动的非质心点的个数,设置移动非质心点占比全部点数的最小比例值,如果达到则算法终止 为了防止k-means聚类过程长时间不收敛,设置最大迭代次数,如果达到最大迭代次数还没有达到上述条件,则也终止计算 如果相邻2次迭代过程,质心没有发生变化,则算法终止,这是最强的终止约束条件。能够满足这种条件,几乎是不可能的,除非两次迭代过程中没有非质心点重新指派给到另一个不同的质心。 我们计算k-means聚类的核心代码框架,如下所示: 01 @Override 02 public void clustering() { 03 // start centroid calculators 04 for (int i = 0; i < parallism; i++) { // 启动parallism个线程计算距离并指派簇 05 CentroidCalculator calculator = new CentroidCalculator(calculatorQueueSize); 06 calculators.add(calculator); 07 executorService.execute(calculator); 08 LOG.info("Centroid calculator started: " + calculator); 09 } 10 11 // sort by centroid id ASC 12 TreeSet<Centroid> centroids = selectInitialCentroidsPolicy.select(k, allPoints);// 随机选择初始质心 13 LOG.info("Initial selected centroids: " + centroids); 14 15 // 下面进入迭代过程 16 int iterations = 0; 17 boolean stopped = false; 18 CentroidSetWithClusteringPoints lastClusteringResult = null; // 上一轮聚类结果 19 CentroidSetWithClusteringPoints currentClusteringResult = null; // 当前轮聚类结果 20 int totalPointCount = allPoints.size(); 21 float currentClusterMovingPointRate = 1.0f; 22 try { 23 // enter clustering iteration procedure 24 while(currentClusterMovingPointRate > maxMovingPointRate 25 && !stopped 26 && iterations < maxIterations) { // 3个终止条件约束 27 LOG.info("Start iterate: #" + (++iterations)); 28 29 currentClusteringResult = computeCentroids(centroids); // 每一轮重新计算质心点 30 LOG.info("Re-computed centroids: " + centroids); 31 32 // compute centroid convergence status 33 int numMovingPoints = 0; 34 if(lastClusteringResult == null) { 35 numMovingPoints = totalPointCount; 36 } else { 37 // compare 2 iterations' result for centroid computation 38 numMovingPoints = analyzeMovingPoints(lastClusteringResult.clusteringPoints, currentClusteringResult.clusteringPoints); // 分析两轮聚类结果:在簇之间移动的非质心点的集合 39 40 // check iteration stop condition 41 boolean isIdentical = (currentClusteringResult.centroids.size() == 42 Multisets.intersection(HashMultiset.create(lastClusteringResult.centroids), HashMultiset.create(currentClusteringResult.centroids)).size()); // 检测终止最强约束条件:两轮迭代是否没有非质心点发生重新指派,即质心完全没变 43 if(iterations > 1 && isIdentical) { 44 stopped = true; 45 } 46 } 47 lastClusteringResult = currentClusteringResult; 48 centroids = currentClusteringResult.centroids; 49 currentClusterMovingPointRate = (float) numMovingPoints / totalPointCount; // 计算非质心点移动比例 50 51 LOG.info("Clustering meta: k=" + k + 52 ", numMovingPoints=" + numMovingPoints + 53 ", totalPointCount=" + totalPointCount + 54 ", stopped=" + stopped + 55 ", currentClusterMovingPointRate=" + currentClusterMovingPointRate ); 56 57 // reset some structures 58 reset(); 59 for(CentroidCalculator calculator : calculators) { 60 calculator.reset(); 61 } 62 63 LOG.info("Finish iterate: #" + iterations); 64 } 65 } finally { 66 // notify all calculators to exit normally 67 clusteringCompletedFinally = true; 68 69 LOG.info("Shutdown executor service: " + executorService); 70 executorService.shutdown(); 71 72 // process final clustering result 73 LOG.info("Final clustering result: "); 74 Iterator<Entry<Centroid, Multiset<Point2D>>> iter = currentClusteringResult.clusteringPoints.entrySet().iterator(); 75 while(iter.hasNext()) { // 达到终止条件后,处理最终的结果 76 Entry<Centroid, Multiset<Point2D>> entry = iter.next(); 77 int id = entry.getKey().getId(); 78 Set<ClusterPoint<Point2D>> set = Sets.newHashSet(); 79 for(Point2D p : entry.getValue()) { 80 set.add(new ClusterPoint2D(p, id)); 81 } 82 clusteredPoints.put(id, set); 83 id++; 84 } 85 centroidSet = currentClusteringResult.clusteringPoints.keySet(); 86 } 87 } 下面,我们讨论一下,如何根据两次聚类迭代结果,计算在簇之间移动的点的个数。如果把两轮聚类迭代结果中的k个簇分别从整体上来比较,得出在前后两轮迭代结果中在簇之间移动的非质心点的个数,可能比较麻烦,也容易陷入混乱的计算逻辑中。 我们可以这么思考:假设a、b两轮迭代结束,a轮中生成k个簇的集合Ca={C(a1),C(a2), …,C(ak)},b轮中生成k个簇的集合Cb={C(b1),C(b2), …,C(bk)},我们假设生成的簇是有编号的,而且,a轮生成的簇C(ai),在b轮重新计算质心后生成的新簇为C(bi),这样一一对应起来,分别计算在簇C(ai)与簇C(bi)之间移动的点的个数,首先计算簇C(ai)与簇C(bi)的交集S: 1 S = C(ai) ∩ C(bi) 然后,分别计算簇C(ai)、簇C(bi)与S的差集Dai、Dbi: 1 Dai = Ca - S = Ca - (C(ai) ∩ C(bi) ) 2 Dbi = Cb - S = Cb - (C(ai) ∩ C(bi) ) 这样,差集Dai和Dbi中的点都是在两轮聚类中移动的非质心点,由于一个簇中的点可能移动到另一个簇中,如某非质心点p,从C(ai)移动到C(bj),其中i不等于j,那么在计算差集Dai与Dbi时,发现C(ai)中少了点p,点p被放入差集Dai;在计算簇C(aj)与簇C(bj)时,发现C(bj)中多了一个点p,则点p又被放入差集Dbj。可见,点p被放入到两个差集Dai和Dbj中,所以我们需要对最终得到的k个差集先做并计算: 1 D = Σ(Dai ∪ dbi), i=1,2, ...k 然后再对集合D做一个去重操作,得到的点的集合就是两轮迭代过程中,在簇之间移动的点的集合。 我们基于上述计算思路实现的代码,对应上面代码中的analyzeMovingPoints方法,代码实现如下所示: 01 private int analyzeMovingPoints(TreeMap<Centroid, Multiset<Point2D>> lastClusteringPoints, 02 TreeMap<Centroid, Multiset<Point2D>> currentClusteringPoints) { 03 // Map<current, Map<last, intersected point count>> 04 Set<Point2D> movingPoints = Sets.newHashSet(); // 用来收集移动的点,使用Set集合类去重 05 Iterator<Entry<Centroid, Multiset<Point2D>>> lastIter = lastClusteringPoints.entrySet().iterator(); 06 Iterator<Entry<Centroid, Multiset<Point2D>>> currentIter = currentClusteringPoints.entrySet().iterator(); 07 while(lastIter.hasNext() && currentIter.hasNext()) { 08 Entry<Centroid, Multiset<Point2D>> last = lastIter.next(); 09 Entry<Centroid, Multiset<Point2D>> current = currentIter.next(); 10 Multiset<Point2D> intersection = Multisets.intersection(last.getValue(), current.getValue()); // 计算交集S = C(ai) ∩ C(bi) 11 movingPoints.addAll(Multisets.difference(last.getValue(), intersection)); // 计算差集Dai = Ca - S = Ca - (C(ai) ∩ C(bi) ) 12 movingPoints.addAll(Multisets.difference(current.getValue(), intersection));// 计算差集Dbi = Cb - S = Cb - (C(ai) ∩ C(bi) ) 13 } 14 return movingPoints.size(); 15 } 通过上面的计算逻辑,就能够计算出两轮聚类过程中,在簇之间移动的点的集合和个数。 聚类效果 每次执行k-means聚类,得到的结果都不相同,我们可以执行两次,取k=10,看一下聚类结果的散点图,如下图所示: 图中,标号为9999的点为质心点,上面两图对比可以看出,聚类结果中簇的形状是不同的,其中红色值满足迭代停止条件的质心的坐标位置。下面,我们选择不同的k值:5、10、20、50,分别执行k-means聚类,然后对比聚类结果,如下图所示: 总结 通过上面的实现,我们知道基本k-means聚类算法的实现过程比较简单,很容易实现。另外,该聚类算法适用于处理具有中心的球形簇,而且运行相当有效。但是,该聚类算法的结果受随机选择的质心的影响,每次计算都得到不同的结果,而且当待聚数据的具有不同的尺寸,或者密度非常不均匀,聚类结果非常的差。为了解决k-means聚类随机算法选择初始质心的问题,会有很多处理方法,可以查阅相关资料,其中bisecting k-means算法(二分k-均值)就是基于基本k-means得到的一种变体,能够比较好地处理,不受随机选择初始质心的影响,后续我们会实现并详细讨论。
DBSCAN(Density-Based Spatial Clustering of Applications with Noise)聚类算法,它是一种基于高密度连通区域的、基于密度的聚类算法,能够将具有足够高密度的区域划分为簇,并在具有噪声的数据中发现任意形状的簇。我们总结一下DBSCAN聚类算法原理的基本要点: DBSCAN算法需要选择一种距离度量,对于待聚类的数据集中,任意两个点之间的距离,反映了点之间的密度,说明了点与点是否能够聚到同一类中。由于DBSCAN算法对高维数据定义密度很困难,所以对于二维空间中的点,可以使用欧几里德距离来进行度量。 DBSCAN算法需要用户输入2个参数:一个参数是半径(Eps),表示以给定点P为中心的圆形邻域的范围;另一个参数是以点P为中心的邻域内最少点的数量(MinPts)。如果满足:以点P为中心、半径为Eps的邻域内的点的个数不少于MinPts,则称点P为核心点。 DBSCAN聚类使用到一个k-距离的概念,k-距离是指:给定数据集P={p(i); i=0,1,…n},对于任意点P(i),计算点P(i)到集合D的子集S={p(1), p(2), …, p(i-1), p(i+1), …, p(n)}中所有点之间的距离,距离按照从小到大的顺序排序,假设排序后的距离集合为D={d(1), d(2), …, d(k-1), d(k), d(k+1), …,d(n)},则d(k)就被称为k-距离。也就是说,k-距离是点p(i)到所有点(除了p(i)点)之间距离第k近的距离。对待聚类集合中每个点p(i)都计算k-距离,最后得到所有点的k-距离集合E={e(1), e(2), …, e(n)}。 根据经验计算半径Eps:根据得到的所有点的k-距离集合E,对集合E进行升序排序后得到k-距离集合E’,需要拟合一条排序后的E’集合中k-距离的变化曲线图,然后绘出曲线,通过观察,将急剧发生变化的位置所对应的k-距离的值,确定为半径Eps的值。 根据经验计算最少点的数量MinPts:确定MinPts的大小,实际上也是确定k-距离中k的值,DBSCAN算法取k=4,则MinPts=4。 另外,如果觉得经验值聚类的结果不满意,可以适当调整Eps和MinPts的值,经过多次迭代计算对比,选择最合适的参数值。可以看出,如果MinPts不变,Eps取得值过大,会导致大多数点都聚到同一个簇中,Eps过小,会导致一个簇的分裂;如果Eps不变,MinPts的值取得过大,会导致同一个簇中点被标记为离群点,MinPts过小,会导致发现大量的核心点。 我们需要知道的是,DBSCAN算法,需要输入2个参数,这两个参数的计算都来自经验知识。半径Eps的计算依赖于计算k-距离,DBSCAN取k=4,也就是设置MinPts=4,然后需要根据k-距离曲线,根据经验观察找到合适的半径Eps的值,下面的算法实现过程中,我们会详细说明。 对于算法的实现,首先我们概要地描述一下实现的过程: 解析样本数据文件 计算每个点与其他所有点之间的欧几里德距离 计算每个点的k-距离值,并对所有点的k-距离集合进行升序排序,输出的排序后的k-距离值 将所有点的k-距离值,在Excel中用散点图显示k-距离变化趋势 根据散点图确定半径Eps的值 根据给定MinPts=4,以及半径Eps的值,计算所有核心点,并建立核心点与到核心点距离小于半径Eps的点的映射 根据得到的核心点集合,以及半径Eps的值,计算能够连通的核心点,并得到离群点 将能够连通的每一组核心点,以及到核心点距离小于半径Eps的点,都放到一起,形成一个簇 选择不同的半径Eps,使用DBSCAN算法聚类得到的一组簇及其离群点,使用散点图对比聚类效果 然后,再详细描述聚类过程的具体实现。 计算欧几里德距离 我们使用的样本数据,来自一组经纬度坐标数据,数据文件格式类似如下所示: 01 116.389463 39.87194 02 116.389463 39.874577 03 116.312984 39.887419 04 116.382798 39.853576 05 116.496648 39.872999 06 116.436246 39.911165 07 116.622074 40.061037 08 116.599267 40.062485 09 116.441824 39.940168 10 116.599267 40.062485 11 116.402096 39.942057 12 116.37319 39.93428 13 116.327812 39.899396 14 116.374739 39.898751 15 116.287195 39.959335 16 116.513574 39.878222 17 116.474355 39.962825 18 116.400651 40.008559 19 ... ... 我们需要做的首先就是,解析样本数据文件,将其转换成我们需要的表示形式,我们定义了Point2D类,代码如下所示: 01 package org.shirdrn.dm.clustering.common; 02 03 public class Point2D { 04 05 protected final Double x; 06 protected final Double y; 07 08 public Point2D(Double x, Double y) { 09 super(); 10 this.x = x; 11 this.y = y; 12 } 13 14 @Override 15 public int hashCode() { 16 return 31 * x.hashCode() + 31 * y.hashCode(); 17 } 18 19 @Override 20 public boolean equals(Object obj) { 21 Point2D other = (Point2D) obj; 22 return this.x.doubleValue() == other.x.doubleValue() && this.y.doubleValue() == other.y.doubleValue(); 23 } 24 25 public Double getX() { 26 return x; 27 } 28 29 public Double getY() { 30 return y; 31 } 32 33 @Override 34 public String toString() { 35 return "(" + x + ", " + y + ")"; 36 } 37 38 } 我们可以将解析后的点的对象放到一个List<Point2D> allPoints集合里面,以便后续使用时迭代集合。在计算两点之间的欧几里德距离时,需要迭代前面生成的Point2D的集合,计算欧几里德距离,实现方法如下所示: 1 public static double euclideanDistance(Point2D p1, Point2D p2) { 2 double sum = 0.0; 3 double diffX = p1.getX() - p2.getX(); 4 double diffY = p1.getY() - p2.getY(); 5 sum += diffX * diffX + diffY * diffY; 6 return Math.sqrt(sum); 7 } 如果需要,可以将计算的所有点之间的距离缓存,因为计算k-距离需要多次访问点的欧几里德距离,比如,可以使用Guava库中的Cache工具: 1 Cache<Set<Point2D>, Double> distanceCache = 2 CacheBuilder.newBuilder().maximumSize(Integer.MAX_VALUE).build(); 上面代码中,设置缓存容纳足够多(Integer.MAX_VALUE)的对象,将计算出的全部的欧几里德距离放在内存中,便于后续迭代时重用。 计算k-距离 每个点都要计算k-距离,在计算一个点的k-距离的时候,首先要计算该点到其他所有点的欧几里德距离,按照距离升序排序后,选择第k小的距离作为k-距离的值,实现代码如下所示: 01 Task task = q.poll(); // 从队列q中取出一个Task,就是计算一个点的k-距离的任务 02 KPoint2D p1 = (KPoint2D) task.p; 03 final TreeSet<Double> sortedDistances = Sets.newTreeSet(new Comparator<Double>() { // 创建一个降序排序TreeSet 04 05 @Override 06 public int compare(Double o1, Double o2) { 07 double diff = o1 - o2; 08 if(diff > 0) { 09 return -1; 10 } 11 if(diff < 0) { 12 return 1; 13 } 14 return 0; 15 } 16 17 }); 18 for (int i = 0; i < allPoints.size(); i++) { // 计算点p1与allPoints集合中每个点的k-距离 19 if(task.pos != i) { // 点p1与它自己的欧几里德距离没必要计算 20 KPoint2D p2 = (KPoint2D) allPoints.get(i); 21 Set<Point2D> set = Sets.newHashSet((Point2D) p1, (Point2D) p2); 22 Double distance = distanceCache.getIfPresent(set); // 从缓存中取出欧几里德距离(可能不存在) 23 if(distance == null) { 24 distance = MetricUtils.euclideanDistance(p1, p2); 25 distanceCache.put(set, distance); // 不存在则加入缓存中 26 } 27 if(!sortedDistances.contains(distance)) { 28 sortedDistances.add(distance); 29 } 30 if(sortedDistances.size() > k) { // TreeSet中只最多保留k个欧几里德距离值 31 Iterator<Double> iter = sortedDistances.iterator(); 32 iter.next(); 33 // remove (k+1)th minimum distance 34 iter.remove(); // 将k+1个距离值中最大的删除,剩余k个是最小的 35 } 36 } 37 } 38 39 // collect k-distance 40 p1.kDistance = sortedDistances.iterator().next(); // 此时,TreeSet中最大的,就是第k最小的距离 上述代码中,KPoint2D类是Point2D的子类,不过比基类Point2D多了一个k-距离的属性,代码如下所示: 01 private class KPoint2D extends Point2D { 02 03 private Double kDistance = 0.0; 04 05 public KPoint2D(Double x, Double y) { 06 super(x, y); 07 } 08 09 @Override 10 public int hashCode() { 11 return super.hashCode(); 12 } 13 14 @Override 15 public boolean equals(Object obj) { 16 return super.equals(obj); 17 } 18 19 } 代码比较容易,可以查看相关注释信息。 绘制k-距离曲线,寻找半径Eps x轴坐标点我们直接使用递增的自然数序列,每个点对应一个自然数,y轴就是所有点的k-距离的大小,我们在代码中可以进行处理,实现如下: 1 for(int i=0; i<allPoints.size(); i++) { 2 KPoint2D kp = (KPoint2D) allPoints.get(i); 3 System.out.println(i + "\t" + kp.kDistance); 4 } 最终生成的数据,输出格式类似如下: 01 0 6.795895820371257E-4 02 1 8.305064719800753E-4 03 2 8.692537028985709E-4 04 3 8.81717074805001E-4 05 4 9.38043175973106E-4 06 5 0.0010181414440047032 07 6 0.0011109837982601679 08 7 0.0011109837982601679 09 8 0.0011414013316968501 10 9 0.0011533646431092647 11 10 0.0011540277293025107 12 11 0.0011712783614491256 13 12 0.001171973122556046 14 13 0.001171973122556046 15 14 0.0012320292204251713 16 15 0.0012371273176228466 17 ... ... 我们把输出数据复制到Excel表格中,使用上述数据生成散点图,基于x坐标取了4个不同的范围,观察曲线的变化情况,0~2600、0~2000、2001~2630、0~2500各个x坐标范围内的点,对应的散点图分别如下所示: 通过上图可以看出: 左上图(0~2600):由于x=2500之后店的k-距离变化太快(可以忽略),导致前面点的k-距离的变化趋势无法观察出来。 右上图(0~2000):去掉尾部的一些点的k-距离,可以看出曲线的变化趋势。 左下图(2001~2630):x坐标轴后半部分的距离的变化趋势。 右下图(0~2500):去掉尾部一些k-距离点,展示大部分k-距离点的变化趋势。 综合上面4个图,可以选择得到半径Eps的范围大致在0.002~0.006之间,已知MinPts=4,具体我们可以选择下面3个k-距离的值作为半径Eps: 1 0.0025094814205335555 2 0.004417483559674606 3 0.006147849217403014 通过下一步进行聚类,看一下使用选择的Eps的这几个值进行聚类的效果对比。另外,对半径Eps=8也进行聚类,主要是为了看一下半径变化对聚类效果的影响。 计算核心点 聚类过程需要知道半径Eps,半径已知,首先需要计算有哪些核心点,给定点,如果以该点为中心半径为Eps的邻域内的其他点的数量大于等于MinPts,则该点就为核心点。计算核心点的实现代码如下所示: 01 Point2D p1 = taskQueue.poll(); 02 if(p1 != null) { 03 ++processedPoints; 04 Set<Point2D> set = Sets.newHashSet(); 05 Iterator<Point2D> iter = epsEstimator.allPointIterator(); 06 while(iter.hasNext()) { 07 Point2D p2 = iter.next(); 08 if(!p2.equals(p1)) { 09 double distance = epsEstimator.getDistance(Sets.newHashSet(p1, p2)); 10 // collect a point belonging to the point p1 11 if(distance <= eps) { // 收集每个点与其邻域内的点之间距离小于等于Eps的点 12 set.add(p2); 13 } 14 } 15 } 16 // decide whether p1 is core point 17 if(set.size() >= minPts) { // 计算收集到的邻域内的点的个数,小于等于MinPts,则加入到映射表Map<Point2D, Set<Point2D>> corePointWithNeighbours中 18 corePointWithNeighbours.put(p1, set); 19 LOG.debug("Decide core point: point" + p1 + ", set=" + set); 20 } else { 21 // here, perhaps a point was wrongly put outliers set 22 // afterwards we should remedy outliers set 23 if(!outliers.contains(p1)) { // 这里,会把一些点错误地加入到离群点集合outliers中,后面会进行修正 24 outliers.add(p1); 25 } 26 } 27 } 那些被错误放入离群点集合的点,需要在计算核心点完成之后,遍历离群点集合,与核心点集合(及其对应的映射点集合)进行比对,代码如下所示: 01 // process outliers 02 Iterator<Point2D> iter = outliers.iterator(); 03 while(iter.hasNext()) { 04 Point2D np = iter.next(); 05 if(corePointWithNeighbours.containsKey(np)) { 06 iter.remove(); 07 } else { 08 for(Set<Point2D> set : corePointWithNeighbours.values()) { 09 if(set.contains(np)) { 10 iter.remove(); 11 break; 12 } 13 } 14 } 15 } 这样,有些非离群点就从离群点集合中被排除了。 连通核心点生成簇 核心点能够连通(有些书籍中称为:“密度可达”),它们构成的以Eps长度为半径的圆形邻域相互连接或重叠,这些连通的核心点及其所处的邻域内的全部点构成一个簇。假设MinPts=4,则连通的核心点示例,如下图所示: 假设MinPts=4,上图中存在两个簇,每个簇都是通过核心点连通在一起的,每个簇是由连通的核心点及其核心点半径Eps圆形邻域内的点组成的,在这两个簇所覆盖的范围外部的点,都是离群点(Outliers)。 计算连通的核心点的思路是,基于广度遍历与深度遍历集合的方式:从核心点集合S中取出一个点p,计算点p与S集合中每个点(除了p点)是否连通,可能会得到一个连通核心点的集合C1,然后从集合S中删除点p和C1集合中的点,得到核心点集合S1;再从S1中取出一个点p1,计算p1与核心点集合S1集中每个点(除了p1点)是否连通,可能得到一个连通核心点集合C2,再从集合S1中删除点p1和C2集合中所有点,得到核心点集合S2,……最后得到p、p1、p2、……,以及C1、C2、……就构成一个簇的核心点。最终将核心点集合S中的点都遍历完成,得到所有的簇。具体代码实现,如下所示: 01 // join connected core points 02 LOG.info("Joining connected points ..."); 03 Set<Point2D> corePoints = Sets.newHashSet(corePointWithNeighbours.keySet()); 04 while(true) { 05 Set<Point2D> set = Sets.newHashSet(); 06 Iterator<Point2D> iter = corePoints.iterator(); 07 if(iter.hasNext()) { 08 Point2D p = iter.next(); 09 iter.remove(); 10 Set<Point2D> connectedPoints = joinConnectedCorePoints(p, corePoints); 11 set.addAll(connectedPoints); 12 while(!connectedPoints.isEmpty()) { 13 connectedPoints = joinConnectedCorePoints(connectedPoints, corePoints); 14 set.addAll(connectedPoints); 15 } 16 clusteredPoints.put(p, set); 17 } else { 18 break; 19 } 20 } 上面调用了重载的两个方法joinConnectedCorePoints,分别根据参数不同计算连通的核心点集合:计算核心点集合中一个点,与该核心点集合中其它的所有核心点是否连通,调用如下方法: 01 private Set<Point2D> joinConnectedCorePoints(Point2D p1, Set<Point2D> leftCorePoints) { 02 Set<Point2D> set = Sets.newHashSet(); 03 for(Point2D p2 : leftCorePoints) { 04 double distance = epsEstimator.getDistance(Sets.newHashSet(p1, p2)); 05 if(distance <= eps) { 06 // join 2 core points to the same cluster 07 set.add(p2); 08 } 09 } 10 // remove connected points 11 leftCorePoints.removeAll(set); // 删除已经确定为与p1连通的核心点 12 return set; 13 } 还有一个方法是,上面第一个参数变为一个集合,它调用上面的方法处理每一个点,方法代码实现如下所示: 1 private Set<Point2D> joinConnectedCorePoints(Set<Point2D> connectedPoints, Set<Point2D> leftCorePoints) { 2 Set<Point2D> set = Sets.newHashSet(); 3 for(Point2D p1 : connectedPoints) { 4 set.addAll(joinConnectedCorePoints(p1, leftCorePoints)); 5 } 6 return set; 7 } 最后,集合clusteredPoints存储的就是聚类后的核心点的信息,再根据核心点到其邻域内半径小于等于Eps的点的集合的映射,就能将所有的点生成聚类了。 聚类效果比较 选择不同的半径Eps进行聚类分析, 聚类的结果也不相同,有些情况下差异很大,有些情况差异较小。比如,在MinPts=4的情况下,如何绘制k-距离曲线,已经在前面详细说明了处理过程,我们根据k-距离趋势增长曲线,选择了一组半径Eps的值,执行DBSCAN聚类算法,下面我们比较在MinPts=4和MinPts=8的情况下,再分别选择不同的半径Eps,执行DBSCAN聚类算法生成簇,对分布情况进行对比。 参数:MinPts=4 选择半径Eps的值分别为如下3个观察值: 1 0.0025094814205335555 2 0.004417483559674606 3 0.006147849217403014 最终得到的聚类效果图在下面可以看到,其中,左侧为没有离群点的情况各个簇的分布情况,右侧是将离群点全部加入到图上显示,图中图例中的9999表示离群点,其他的都是聚类生成的簇,如下图所示: 通过上图可以看出,半径Eps选择的较小时,会产生大量的离群点,其实我们想一下,半径小了,自然落在某个点的邻域内的点减少的可能性就增加了,导致很多点可能就无法成为核心点,自然也就无法成为某个簇的点,而且很生成的簇包含的点的数量都比较少,某些本来很接近的点应该可以成为同一个簇,但是被分裂了。 当半径比较大一些时,生成的一个簇包含了比较多的点,而且这个簇的形状中间可能出现一些“空洞”,因为点之间的距离大一些也能满足成为核心点的要求,所以这些点聚到一簇中可能确实比较合理。 参数:MinPts=8 选择半径Eps的值分别为如下3个观察值: 1 0.004900098978598581 2 0.009566439044911 3 0.013621050253196359 使用我们实现的DBSCAN聚类算法进行分析处理,得到的聚类结果,如下图所示: 总结 因为DBSCAN聚类算法,是基于密度的聚类算法,所以对于密度分别不均,各个簇的密度变化较大时,可能会导致一些问题:比如半径Eps较大时,本来不属于同一个的簇的点被聚到一个簇中;比如半径Eps比较小时,会出现大量比较小的簇(即每个簇中含有的点不较少,但是这些点组成的小簇密度确实很大),同时出现了大量的点不满足成为核心点的要求,MinPts越大越容易出现这种情况。 DBSCAN聚类算法的思想非常明确易懂,虽然需要用户输入2个参数,但是正是输入参数的灵活性,我们可以根据自己实际应用的需要,适当调整参数值,在特定应用场景下进行聚类分析,得到合适的簇划分。通过上面选择不同参数进行聚类的结果对比,如果希望局部比较密集的点都能够生成簇,那么在固定MinPts的值的条件下,半径Eps通过调整变小,可能达到这一需要,产生的簇的数量比较多,但是同时也产生了大量分散的离群点,实际上应该可以进行二次聚类,将离群点也通过合适的方式归到指定的簇中;如果希望生成全局比较大的簇,可以适当调整半径Eps变大,生成的簇的数量较少,离群点的数量也相对较少。
我们基于Hadoop 1.2.1源码分析MapReduce V1的处理流程。在MapReduce程序运行的过程中,JobTracker端会在内存中维护一些与Job/Task运行相关的信息,了解这些内容对分析MapReduce程序执行流程的源码会非常有帮助。 在编写MapReduce程序时,我们是以Job为单位进行编程处理,一个应用程序可能由一组Job组成,而MapReduce框架给我们暴露的只是一些Map和Reduce的函数接口,在运行期它会构建对应MapTask和ReduceTask,所以我们知道一个Job是由一个或多个MapTask,以及0个或1个ReduceTask组成。而对于MapTask,它是根据输入的数据文件的的逻辑分片(InputSplit)而定的,通常有多少个分片就会有多少个MapTask;而对于ReduceTask,它会根据我们编写的MapReduce程序配置的个数来运行。 有了这些信息,我们能够预想到,在Job运行过程中,无非也需要维护与这些Job/Task相关的一些状态信息,通过一定的调度策略来管理Job/Task的运行。这里,我们主要关注JobTracker端的一些非常有用的数据结构:JobTracker、JobInProgress、TaskInProgress,来熟悉各种数据结构的定义及作用。 数据结构总体抽象 MapReduce框架就为了运行Job,所以我们基于Job的抽象来对JobTracker端的相关对象进行抽象,总体上理解它们之间的关系,如下图所示: 在JobTracker端,通过维护JobInProgress的信息来跟踪Job的运行生命周期,那么,JobTracker端肯定有一个用来维护所有Job状态的JobInProgress对象集合。而Job又是由Task组成,所以自然而然JobInProgress中应该有对Task运行状态的维护,Task的状态在JobTracker端通过TaskInProgress来抽象。一个Task可能运行失败,所以可能经过多次运行才能成功,而每一次运行会对应一个TaskAttempID,那么一个TaskInProgress又可能对应着多个TaskAttempID。 TaskInProgress数据结构 TaskAttemptID结构 一个TaskInProgress结构中包含了3个TaskAttemptID类型的数据,如下图所示: TaskSplitMetaInfo结构 JobTracker会创建每一个Task需要运行Split的信息,包含了该Split所在的位置信息、起始偏移量、总输入字节数,结构图如下所示: 其他结构 结构图 说明 1 TreeSet<TaskAttemptID> tasks 一个TaskInProgress包含的TaskAttemptID的集合。 一个Task(MapTask/ReduceTask)可能包含多个TaskAttemptID在TaskTracker上运行,比如一个 ReduceTask在TaskTracker上运行,同时可能存在一个推测执行的ReduceTask,他们对应了2个不同的 TaskAttemptID。 1 TreeMap<TaskAttemptID, String> activeTasks 维护了当前运行的Task,该Task运行在哪个TaskTracker上。 1 TreeMap<TaskAttemptID, String> cleanupTasks 记录了某个cleanup task在哪个TaskTracker上。 1 TreeSet<TaskAttemptID> tasksReportedClosed 满足如下3种条件的Task会被加入到该数据结构中: this.failed) || ((job.getStatus().getRunState() != JobStatus.RUNNING && (job.getStatus().getRunState() != JobStatus.PREP)) isComplete() && !(isMapTask() && !jobSetup && !jobCleanup && isComplete(taskid)) isCommitPending(taskid) && !shouldCommit(taskid) 该数据结构用来辅助判断,是否一个Task已经完成(成功/失败),需要被TaskTracker终止掉,这个需要JobTracker发送KillTaskAction指令,通知TaskTracker终止该Task运行。 1 TreeMap<TaskAttemptID, Boolean> tasksToKill 记录了某个Task是否需要被Kill掉。 1 TreeSet<String> machinesWhereFailed 记录了执行Task失败的TaskTracker的host信息。 JobInProgress数据结构 JobID结构 JobID的结构,如下图所示: 上图中jtIdentifier的值为job,它是组成一个Job的ID字符串的前缀,唯一标识一个Job的完整ID的组成,如下所示: 1 job_<JobTracker启动时间字符串>_<序号> 例如,一个Job的ID字符串为job_200912121733_0002 。 JobProfile结构 JobProfile描述了一个Job的基本信息,它的结构,如下图所示: 通过上图可以看出,JobProfile包含了一个Job的如下信息: 标识名称 类型 说明 jobid JobID 唯一标识一个Job的ID,例如:job_200912121733_0002 user String 提交的该Job的所属用户名称,例如:shirdrn url String 在Web UI页面上查看该Job信息的链接,例如:http://jobtracker.hadoopcluster.com:8080/jobdetails.jsp?jobid=job_200912121733_0002 name String 提交Job的用户为该Job设置的名称字符串,例如:ChainUserEventsJob jobFile String 该Job所对应的配置文件,例如:/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/job.xml queueName String 提交的该Job所在的队列的名称,例如:default 组成Job的Task的信息 一个Job可能包含多个Task(MapTask/ReduceTask),每个Task在JobTracker端使用TaskInProgress结构来跟踪Task的信息,一个Job由下面4组结构来表示这些信息,如下图所示: 上图中出现了4种类型的Task,我们需要明白每种Task的作用是什么。一个Job在调度时,需要分解为上述4种类型的Task,基于类型来说明,一个Job对应的这4种类型的Task的运行顺序为:setup task、MapTask、ReduceTask、cleanup task,其中setup task和cleanup task运行也需要申请slot来运行,map setup运行需要占用Map Slot,而reduce setup运行需要占用Reduce Slot,对于cleanup task也是类似的。 这里说明一下cleanup task和setup task的作用。其实在JobTracker端来看,setup task、cleanup task都与MapTask、ReduceTask使用相同的TaskInProgress数据结构来维护状态。setup task主要是在一个Job开始运行之前,初始化一些状态信息,由于存在MapTask和ReduceTask两种计算型Task,那么对应就存在map setup task和reduce setup task两种setup task。cleanup task主要是在一个Job运行结束后,负责清理在TaskTracker上运行的Task生成的临时数据,更新TaskTracker端维护的相关对象的状态信息,等等,类似地也存在map cleanup task和reduce cleanup task两种cleanup task。 JobStatus结构 JobStatus结构定义一个Job的当前状态信息,如下图所示: 除了定义Job运行状态信息,还包含了其他信息,如下表所示: 标识名称 类型 说明 jobid JobID 唯一标识一个Job的ID,例如:job_200912121733_0002 mapProgress float MapTask运行进度百分比 reduceProgress float ReduceTask运行进度百分比 cleanupProgress float cleanup task运行进度百分比 setupProgress float setup task运行进度百分比 runState int Job运行状态:RUNNING = 1;SUCCEEDED = 2;FAILED = 3;PREP = 4;KILLED = 5; user String 提交的该Job的所属用户名称,例如:shirdrn priority JobPriority Job优先级信息,是一个枚举类型,包含如下优先级:VERY_HIGH, HIGH, NORMAL, LOW, VERY_LOW schedulingInfo String Job调度信息 jobACLs Map 该Job设置的ACL(访问控制列表)列表信息 Counters结构 Counters包含了一组计数器,用来跟踪一个Job运行的信息,结构如下图所示: 每个Job都包含一组Counter计数器,如下表所示: 标识名称 类型 NUM_FAILED_MAPS 失败的MapTask数量 NUM_FAILED_REDUCES 失败的ReduceTask数量 TOTAL_LAUNCHED_MAPS 所有启动的 MapTask数量 TOTAL_LAUNCHED_REDUCES 所有启动的 ReduceTask数量 OTHER_LOCAL_MAPS 其他Local MapTask数量 DATA_LOCAL_MAPS DATA_LOCAL的MapTask数量 NODEGROUP_LOCAL_MAPS NODEGROUP_LOCAL的MapTask数量 RACK_LOCAL_MAPS RACK_LOCAL的MapTask数量 SLOTS_MILLIS_MAPS 被占用的Map slot的“Slot个数 * (结束时间 – 开始时间)” SLOTS_MILLIS_REDUCES 被占用的Reduce slot:“Slot个数 * (结束时间 – 开始时间)” FALLOW_SLOTS_MILLIS_MAPS 空闲Map Slot:“(当前时间 – 开始时间) * Slot个数” FALLOW_SLOTS_MILLIS_REDUCES 空闲Reduce Slot:“(当前时间 – 开始时间) * Slot个数” 其他结构 JobInProgress中使用了大量的集合来维护Job/Task相关的状态信息,具体内容如下表所示: 结构图 说明 1 Map<Node, List<TaskInProgress>> nonRunningMapCache JobTracker端维护了某个Node上,没有运行的MapTask列表的信息。 在调度MapTask之前,需要计算某个MapTask将要运行在哪些Node上,这里维护了某个Node所对应的没有运行的MapTask的列表信息。 1 Map<Node, Set<TaskInProgress>> runningMapCache JobTracker端维护了某个Node上,当前正在运行的MapTask列表的信息。 1 List<TaskInProgress> nonLocalMaps JobTracker端维护的、非Local,并且还没有运行的MapTask的列表。 1 SortedSet<TaskInProgress> failedMaps JobTracker端维护了失败的MapTask的信息,在该集合中的TaskInProgress基于失败次数降序排序。 当某个MapTask失败以后,就会被放到该集合中,后续重新调度MapTask运行时,会检索该集合。 1 Set<TaskInProgress> nonLocalRunningMaps JobTracker端维护的、 非Local、正在运行的MapTask保存在该数据结构中。 1 Set<TaskInProgress> nonRunningReduces JobTracker端维护的、没有运行的ReduceTask的列表。 1 List<TaskAttemptID> mapCleanupTasks 为MapTask运行的cleanup task列表。 1 List<TaskAttemptID> reduceCleanupTasks 为ReduceTask运行的cleanup task列表。 1 List<TaskCompletionEvent> taskCompletionEvents TaskCompletionEvent是用来跟踪Task完成事件的数据结构,该列表结构保存了TaskCompletionEvent。 当一个Task的状态为TaskStatus.State.SUCCEEDED/TaskStatus.State.FAILED/TaskStatus.State.KILLED的时候,会创建一个对应的TaskCompletionEvent对象,根据该对象来更新JobTracker端维护的Task的状态信息。 1 Map<TaskAttemptID, Integer> mapTaskIdToFetchFailuresMap TaskTracker发送心跳的时候,会将TaskTracker的状态信息发送给JobTracker,状态信息通过TaskTrackerStatus表示,该对象中包含了Task的报告信息TaskStatus。如果是在运行ReduceTask时,抓取MapTask输出的结果失败时,会在根据Task报告信息,更新JobTracker端维护的mapTaskIdToFetchFailuresMap,记录了Task抓取MapTask输出失败的次数计数信息。 1 Map<TaskType, Long> firstTaskLaunchTimes TaskType枚举类型定义如下: 1 public enum TaskType { 2 MAP, REDUCE, JOB_SETUP, JOB_CLEANUP, TASK_CLEANUP 3 } 该firstTaskLaunchTimes数据结构保存了某个TaskType类型第一次运行的时间戳信息。 1 Map<TaskTracker, FallowSlotInfo> trackersReservedForMaps FallowSlotInfo包含了空闲的slot信息,主要九个包含了空闲的slot的个数信息。 该数据结构维护了在某个TaskTracker上为MapTask运行所预留的空闲slot的信息。 1 Map<TaskTracker, FallowSlotInfo> trackersReservedForReduces 该数据结构维护了在某个TaskTracker上为ReduceTask运行所预留的空闲slot的信息。 JobTracker数据结构 JobTracker通过在内存中维护有关Job、Task相关的所有信息,来跟踪他们运行、交互过程中所发生的数据交换,等等,如下表所示: 结构图 说明 1 List<ServicePlugin> plugins 通过ServicePlugin接口,可以基于任意的RPC协议暴露DataNode或NameNode的功能。 通过配置项mapreduce.jobtracker.plugins可以设置ServicePlugin,JobTracker启动的时候会加载初始化配置的ServicePlugin。 1 List<JobInProgressListener> jobInProgressListeners JobTracker维护了一组JobInProgressListener监听器,在JobTracker运行过程中,发生某些事件会触发注册的JobInProgressListener的执行。比如,JobClient提交一个Job,JobTracker端会触发对应的JobInProgressListener调用jobAdded()初始化该Job;比如,Job执行过程中状态发生变更,会触发JobInProgressListener调用jobUpdated()执行;比如,Job运行完成,会触发obInProgressListener调用jobRemoved()执行。 JobTracker初始化时会创建TaskScheduler,而启动TaskScheduler的时候,会把TaskScheduler所维护的JobInProgressListener添加到jobInProgressListeners列表中。 1 Map<JobID, JobInProgress> jobs JobTracker维护一个JobID->JobInProgress映射的列表,JobID标识一个提交的Job,JobInProgress是JobTracker端维护的Job的所有信息的数据结构。在如下情况下,会检索/操作该jobs数据结构: JobClient提交Job的时候,会创建JobInProgress,并加入到jobs集合中 JobClient远程调用Kill掉指定Job的时候,会根据JobID从jobs中获取JobInProgress信息,并Kill掉该Job,更新状态信息 JobClient查询当前运行的所有Job信息时,会检索jobs列表 在JobTracker端检索一个Job所维护的Task信息时,会根据JobInProgress所维护的数据结构获取到对应的Task的信息TaskInProgress Job运行状态不为RUNNING,并且也不为PREP,并且完成时间早于当前时间,会将Job从jobs列表删除 JobTracker解析接收到的TaskTracker发送的心跳的过程中,会检索并更新jobs列表中的Job信息,找到可以分配给该TaskTracker的属于满足条件的Job所包含的Task 1 TreeMap<String, ArrayList<JobInProgress>> userToJobsMap 用来跟踪某个用户提交的需要运行的Job集合的数据结构。 当Job完成(success/failure/killed)后,会在JobTracker内存中保存一些Job,这些Job属于哪些用户的。默认情况下会保存MAX_COMPLETE_USER_JOBS_IN_MEMORY=100个用户的已完成的Job,当超过该值时,会清理掉最早的用户以及对应的完成的Job信息。 可以通过配置项mapred.jobtracker.completeuserjobs.maximum来设置该值。 1 Map<String, Set<JobID>> trackerToJobsToCleanup 用来跟踪某个TaskTracker上运行的Job集合的数据结构。 当一个Job已经运行完成,TaskTracker需要知道哪些运行在该节点上的Job已经完成,并等待通知进行清理,这时会在JobTracker端检索该Map,取出该TaskTracker对应的需要进行清理的Job的集合。 另外,还有一种情况,当JobTracker一段时间内没有收到TaskTracker发送的心跳报告,这时会将该TaskTracker对应的Job集合从trackerToJobsToCleanup中删除,后续会重新调度这些运行在该有问题的TaskTracker上的Task(这些Task属于某些Job,JobTracker分配任务的单位是Task)。 1 Map<String, Set<TaskAttemptID>> trackerToTasksToCleanup 用来跟踪某个TaskTracker上运行的Task集合的数据结构。 当Job运行完成(成功或者失败)后,一个TaskTracker需要知道属于该Job的哪些Task运行在该TaskTracker上,需要对这些Task进行清理。JobTracker端会查询出这类Task,并通过心跳的响应,向对应的TaskTracker发送KillTaskAction指令,通知TaskTracker清理这些Task运行时生成的临时文件等。 1 Map<TaskAttemptID, TaskInProgress> taskidToTIPMap TaskAttemptID用来标识一个MapTask或一个ReduceTask,通过该数据结构可以根据TaskAttemptID获取到MapTask/ReduceTask的运行信息,也就是TaskInProgress对象。 当需要检索MapTask/ReduceTask,或者对JobTracker端所维护的该Task的状态信息进行更新的时候,需要通过该数据结构获取到。 1 TreeMap<TaskAttemptID, String> taskidToTrackerMap 维护TaskAttemptID到TaskTracker的映射关系,可以通过一个Task的ID获取到该Task运行在哪个TaskTracker上。 1 Map<String, Set<TaskTracker>> hostnameToTaskTracker 一台主机上,可能运行着多个TaskTracker进程,该数据结构用来维护host到TaskTracker集合的映射关系。如果一个host被加入了黑名单,则该host上面的所有TaskTracker都无法接收任务。 1 TreeMap<String, Set<TaskAttemptID>> trackerToTaskMap 某个TaskTracker上都运行着哪些Task,通过该数据结构来维护这种映射关系。 1 TreeMap<String, Set<TaskAttemptID>> trackerToMarkedTasksMap 在某个TaskTracker上都运行完成了哪些Task,通过该数据结构来维护这种映射关系。 1 Map<String, HeartbeatResponse> trackerToHeartbeatResponseMap TaskTracker会周期性地向JobTracker发送心跳报告,最近一次发送的心跳报告,JobTracker会给其一个响应,最后的这个响应的数据保存在该数据结构中。 1 Map<String, Node> hostnameToNodeMap JobTracker维护了一个网络拓扑结构(NetworkTopology),组成该拓扑结构的是一个一个的Node,每个Node都包含了网络位置信息、继承关系信息、名称等。 每个TaskTracker都是整个Hadoop集群的一个节点,通过该数据结构维护了TaskTracker在集群拓扑结构中相关信息。 比如,根据给定TaskTracker ID,从hostnameToNodeMap中检索出其对应的Node信息,在调度一个Job的MapTask运行时(MapTask运行具有Locality特性),可以基于local、rack-local、off-switch的顺序优先选择前面的Node运行该MapTask。 附录 这里给出文中(文字/图片上)一些缩写词对应的完整名称,如下表所示: 简写词 完整名称 JIP JobInProgress TIP TaskInProgress TAID TaskAttemptID TTID TaskTracker ID TT HOST TaskTracker Host Name TCE TaskCompletionEvent JIPL JobInProgressListener
我们基于Hadoop 1.2.1源码分析MapReduce V1的处理流程。MapReduce V1实现中,主要存在3个主要的分布式进程(角色):JobClient、JobTracker和TaskTracker,我们主要是以这三个角色的实际处理活动为主线,并结合源码,分析实际处理流程。上一篇我们分析了Job提交过程中JobClient端的处理流程(详见文章 MapReduce V1:Job提交流程之JobClient端分析),这里我们继续详细分析Job提交在JobTracker端的具体流程。通过阅读源码可以发现,这部分的处理逻辑还是有点复杂,经过梳理,更加细化清晰的流程,如下图所示: 上图中主要分为两大部分:一部分是JobClient基于RPC调用提交Job到JobTracker后,在JobTracker端触发TaskScheduler所注册的一系列Listener进行Job信息初始化;另一部分是JobTracker端监听Job队列的线程,监听到Job状态发生变更触发一系列Listener更新状态。我们从这两个方面展开分析: JobTracker接收Job提交 JobTracker接收到JobClient提交的Job,在JobTracker端具体执行流程,描述如下: JobClient基于JobSubmissionProtocol协议远程调用JobTracker的submitJob方法提交Job JobTracker接收提交的Job,创建一个JobInProgress对象,将其放入内部维护的Map<JobID, JobInProgress> jobs队列中 触发JobQueueJobInProgressListener 执行JobQueueJobInProgressListener的jobAdded方法,创建JobSchedulingInfo对象,并放入到JobQueueJobInProgressListener内部维护的Map<JobSchedulingInfo, JobInProgress> jobQueue队列中 触发EagerTaskInitializationListener 执行EagerTaskInitializationListener的jobAdded方法,将JobInProgress对象加入到List<JobInProgress> jobInitQueue队列中 在JobTracker端使用TaskScheduler进行Job/Task的调度,可以通过mapred.jobtracker.taskScheduler配置所使用的TaskScheduler实现类,默认使用的实现类JobQueueTaskScheduler,如下所示: 1 // Create the scheduler 2 Class<? extends TaskScheduler> schedulerClass 3 = conf.getClass("mapred.jobtracker.taskScheduler", JobQueueTaskScheduler.class, TaskScheduler.class); 4 taskScheduler = (TaskScheduler) ReflectionUtils.newInstance(schedulerClass, conf); 如果想要使用其他的TaskScheduler实现,可以在mapred-site.xml中配置mapred.jobtracker.taskScheduler的属性值,覆盖默认的调度策略即可。 在JobQueueTaskScheduler实现类中,注册了2个JobInProgressListener,JobInProgressListener是用来监听由JobClient端提交后在JobTracker端Job(在JobTracker端维护的JobInProgress)生命周期变化,并触发相应事件(jobAdded/jobUpdated/jobRemoved)的,如下所示: 01 protected JobQueueJobInProgressListener jobQueueJobInProgressListener; 02 protected EagerTaskInitializationListener eagerTaskInitializationListener; 03 private float padFraction; 04 05 public JobQueueTaskScheduler() { 06 this.jobQueueJobInProgressListener = new JobQueueJobInProgressListener(); 07 } 08 09 @Override 10 public synchronized void start() throws IOException { 11 super.start(); 12 taskTrackerManager.addJobInProgressListener(jobQueueJobInProgressListener); // taskTrackerManager是JobTracker的引用 13 eagerTaskInitializationListener.setTaskTrackerManager(taskTrackerManager); 14 eagerTaskInitializationListener.start(); 15 taskTrackerManager.addJobInProgressListener(eagerTaskInitializationListener); 16 } JobTracker维护一个List<JobInProgressListener> jobInProgressListeners队列,在TaskScheduler(默认JobQueueTaskScheduler )启动的时候向JobTracker注册。在JobClient提交Job后,在JobTracker段创建一个对应的JobInProgress对象,并将其放入到jobs队列后,触发这一组JobInProgressListener的jobAdded方法。 JobTracker管理Job提交 JobTracker接收到提交的Job后,需要对提交的Job进行初始化操作,具体流程如下所示: EagerTaskInitializationListener.JobInitManager线程监控EagerTaskInitializationListener内部的List<JobInProgress> jobInitQueue队列 加载一个EagerTaskInitializationListener.InitJob线程去初始化Job 在EagerTaskInitializationListener.InitJob线程中,调用JobTracker的initJob方法初始化Job 调用JobInProgress的initTasks方法初始化该Job对应的Tasks 从HDFS读取该Job对应的splits信息,创建MapTask和ReduceTask(在JobTracker端维护的Task实际上是TaskInProgress) Job状态变更,触发JobQueueJobInProgressListener 如果Job优先级(Priority)/开始时间发生变更,则对Map<JobSchedulingInfo, JobInProgress> jobQueue队列进行重新排序;如果Job完成,则将Job从jobQueue队列中移除 Job状态变更,触发EagerTaskInitializationListener 如果Job优先级(Priority)/开始时间发生变更,则对List<JobInProgress> jobInitQueue队列进行重新排序 下面,我们分析的Job初始化,以及Task初始化,都是在JobTracker端执行的工作,主要是为了管理Job和Task的运行,创建了对应的数据结构,Job对应JobInProgress,Task对应TaskInProgress。我们分析说明如下: Job初始化 JobTracker接收到JobClient提交的Job,在放到JobTracker的Map<JobID, JobInProgress> jobs队列后,触发2个JobInProgressListener执行jobAdded方法,首先会放到EagerTaskInitializationListener的List<JobInProgress> jobInitQueue队列中。在EagerTaskInitializationListener内部,有一个内部线程类JobInitManager在监控jobInitQueue队列,如果有新的JobInProgress对象加入到队列,则取出并启动一个新的初始化线程InitJob去初始化该Job,代码如下所示: 01 class JobInitManager implements Runnable { 02 03 public void run() { 04 JobInProgress job = null; 05 while (true) { 06 try { 07 synchronized (jobInitQueue) { 08 while (jobInitQueue.isEmpty()) { 09 jobInitQueue.wait(); 10 } 11 job = jobInitQueue.remove(0); // 取出JobInProgress 12 } 13 threadPool.execute(new InitJob(job)); // 创建一个InitJob线程去初始化该JobInProgress 14 } catch (InterruptedException t) { 15 LOG.info("JobInitManagerThread interrupted."); 16 break; 17 } 18 } 19 LOG.info("Shutting down thread pool"); 20 threadPool.shutdownNow(); 21 } 22 } 然后,在InitJob线程中,调用JobTracker的initJob方法初始化Job,如下所示: 01 class InitJob implements Runnable { 02 03 private JobInProgress job; 04 05 public InitJob(JobInProgress job) { 06 this.job = job; 07 } 08 09 public void run() { 10 ttm.initJob(job); // TaskTrackerManager ttm,调用JobTracker的initJob方法初始化 11 } 12 } JobTracker中的initJob方法的主要逻辑,如下所示: 01 JobStatus prevStatus = (JobStatus)job.getStatus().clone(); 02 LOG.info("Initializing " + job.getJobID()); 03 job.initTasks(); // 调用JobInProgress的initTasks方法初始化Task 04 // Inform the listeners if the job state has changed 05 // Note : that the job will be in PREP state. 06 JobStatus newStatus = (JobStatus)job.getStatus().clone(); 07 if (prevStatus.getRunState() != newStatus.getRunState()) { 08 JobStatusChangeEvent event = 09 new JobStatusChangeEvent(job, EventType.RUN_STATE_CHANGED, prevStatus, 10 newStatus); 11 synchronized (JobTracker.this) { 12 updateJobInProgressListeners(event); // 更新Job相关队列的状态 13 } 14 } 实际上,在JobTracker中的initJob方法中最核心的逻辑,就是初始化组成该Job的MapTask和ReduceTask,它们在JobTracker端都抽象为TaskInProgress。 初始化Task 在JobClient提交Job的过程中,已经将该Job所对应的资源复制到HDFS,在JobTracker端需要读取这些信息来创建MapTask和ReduceTask。我们回顾一下:默认情况下,split和对应的元数据存储路径分别为/tmp/hadoop/mapred/staging/${user}/.staging/${jobid}/job.split和/tmp/hadoop/mapred/staging/${user}/.staging/${jobid}/job.splitmetainfo,在创建MapTask和ReduceTask只需要split的元数据信息即可,我们看一下job.splitmetainfo文件存储的数据格式如下图所示: 上图中,META_SPLIT_FILE_HEADER的值为META-SPL,版本version的值为1,numSplits的值根据实际Job输入split大小计算的到,SplitMetaInfo包括的信息为split所存放的节点位置个数、所有的节点位置信息、split在文件中的起始偏移量、split数据的长度。有了这些描述信息,JobTracker就可以知道一个Job需要创建几个MapTask,实现代码如下所示: 1 TaskSplitMetaInfo[] splits = createSplits(jobId); 2 ... 3 numMapTasks = splits.length; 4 ... 5 maps = new TaskInProgress[numMapTasks]; // MapTask在JobTracker的表示为TaskInProgress 6 for(int i=0; i < numMapTasks; ++i) { 7 inputLength += splits[i].getInputDataLength(); 8 maps[i] = new TaskInProgress(jobId, jobFile, splits[i], jobtracker, conf, this, i, numSlotsPerMap); 9 } 而ReduceTask的个数,根据用户在配置Job时指定的Reduce的个数,创建ReduceTask的代码,如下所示: 1 // 2 // Create reduce tasks 3 // 4 this.reduces = new TaskInProgress[numReduceTasks]; 5 for (int i = 0; i < numReduceTasks; i++) { 6 reduces[i] = new TaskInProgress(jobId, jobFile, numMapTasks, i, jobtracker, conf,this, numSlotsPerReduce); 7 nonRunningReduces.add(reduces[i]); 8 } 除了创建MapTask和ReduceTask之外,还会创建setup和cleanup task,每个Job的MapTask和ReduceTask各对应一个,即共计2个setup task和2个cleanup task。setup task用来初始化MapTask/ReduceTask,而cleanup task用来清理MapTask/ReduceTask。创建setup和cleanup task,代码如下所示: 01 // create cleanup two cleanup tips, one map and one reduce. 02 cleanup = new TaskInProgress[2]; // cleanup task,map对应一个,reduce对应一个 03 04 // cleanup map tip. This map doesn't use any splits. Just assign an empty split. 05 TaskSplitMetaInfo emptySplit = JobSplit.EMPTY_TASK_SPLIT; 06 cleanup[0] = new TaskInProgress(jobId, jobFile, emptySplit, jobtracker, conf, this, numMapTasks, 1); 07 cleanup[0].setJobCleanupTask(); 08 09 // cleanup reduce tip. 10 cleanup[1] = new TaskInProgress(jobId, jobFile, numMapTasks, numReduceTasks, jobtracker, conf, this, 1); 11 cleanup[1].setJobCleanupTask(); 12 13 // create two setup tips, one map and one reduce. 14 setup = new TaskInProgress[2]; // setup task,map对应一个,reduce对应一个 15 16 // setup map tip. This map doesn't use any split. Just assign an empty split. 17 setup[0] = new TaskInProgress(jobId, jobFile, emptySplit, jobtracker, conf, this, numMapTasks + 1, 1); 18 setup[0].setJobSetupTask(); 19 20 // setup reduce tip. 21 setup[1] = new TaskInProgress(jobId, jobFile, numMapTasks, numReduceTasks + 1, jobtracker, conf, this, 1); 22 setup[1].setJobSetupTask(); 一个Job在JobInProgress中进行初始化Task,这里初始化Task使得该Job满足被调度的要求,比如,知道一个Job有哪些Task组成,每个Task对应哪个split等等。在初始化完成后,置一个Task初始化完成标志,如下所示: 01 synchronized(jobInitKillStatus){ 02 jobInitKillStatus.initDone = true; 03 04 // set this before the throw to make sure cleanup works properly 05 tasksInited = true; // 置Task初始化完成标志 06 07 if(jobInitKillStatus.killed) { 08 throw new KillInterruptedException("Job " + jobId + " killed in init"); 09 } 10 } 在置tasksInited = true;后,该JobInProgress就可以被TaskScheduler进行调度了,调度时,是以Task(MapTask/ReduceTask)为单位分派给TaskTracker。而对于哪些TaskTracker可以运行Task,需要通过TaskTracker向JobTracker周期性发送的心跳得到TaskTracker的健康状况信息、节点资源信息等来确定,是否该TaskTracker可以运行一个Job的一个或多个Task。
我们基于Hadoop 1.2.1源码分析MapReduce V1的处理流程。MapReduce V1实现中,主要存在3个主要的分布式进程(角色):JobClient、JobTracker和TaskTracker,我们主要是以这三个角色的实际处理活动为主线,并结合源码,分析实际处理流程。下图是《Hadoop权威指南》一书给出的MapReduce V1处理Job的抽象流程图: 如上图,我们展开阴影部分的处理逻辑,详细分析Job提交在JobClient端的具体流程。在编写好MapReduce程序以后,需要将Job提交给JobTracker,那么我们就需要了解在提交Job的过程中,在JobClient端都做了哪些工作,或者说执行了哪些处理。在JobClient端提交Job的处理流程,如下图所示: 上图所描述的Job的提交流程,说明如下所示: 在MR程序中创建一个Job实例,设置Job状态 创建一个JobClient实例,准备将创建的Job实例提交到JobTracker 在创建JobClient的过程中,首先必须保证建立到JobTracker的RPC连接 基于JobSubmissionProtocol协议远程调用JobTracker获取一个新的Job ID 根据MR程序中配置的Job,在HDFS上创建Job相关目录,并将配置的tmpfiles、tmpjars、tmparchives,以及Job对应jar文件等资源复制到HDFS 根据Job配置的InputFormat,计算该Job输入的Split信息和元数据(SplitMetaInfo)信息,以及计算出map和reduce的个数,最后将这些信息连通Job配置写入到HDFS(保证JobTracker能够读取) 通过JobClient基于JobSubmissionProtocol协议方法submitJob提交Job到JobTracker MR程序创建Job 下面的MR程序示例代码,已经很熟悉了: 01 public static void main(String[] args) throws Exception { 02 Configuration conf = new Configuration(); 03 String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs(); 04 if (otherArgs.length != 2) { 05 System.err.println("Usage: wordcount <in> <out>"); 06 System.exit(2); 07 } 08 Job job = new Job(conf, "word count"); 09 job.setJarByClass(WordCount.class); 10 job.setMapperClass(TokenizerMapper.class); 11 job.setCombinerClass(IntSumReducer.class); 12 job.setReducerClass(IntSumReducer.class); 13 job.setOutputKeyClass(Text.class); 14 job.setOutputValueClass(IntWritable.class); 15 FileInputFormat.addInputPath(job, new Path(otherArgs[0])); 16 FileOutputFormat.setOutputPath(job, new Path(otherArgs[1])); 17 System.exit(job.waitForCompletion(true) ? 0 : 1); 18 } 在MR程序中,首先创建一个Job,并进行配置,然后通过调用Job的waitForCompletion方法将Job提交到MapReduce集群。这个过程中,Job存在两种状态:Job.JobState.DEFINE和Job.JobState.RUNNING,创建一个Job后,该Job的状态为Job.JobState.DEFINE,Job内部通过JobClient基于org.apache.hadoop.mapred.JobSubmissionProtocol协议提交给JobTracker,然后该Job的状态变为Job.JobState.RUNNING。 Job提交目录submitJobDir 通过如下代码可以看到,Job提交目录是如何创建的: 1 JobConf jobCopy = job; 2 Path jobStagingArea = JobSubmissionFiles.getStagingDir(JobClient.this, jobCopy); // 获取到StagingArea目录 3 JobID jobId = jobSubmitClient.getNewJobId(); 4 Path submitJobDir = new Path(jobStagingArea, jobId.toString()); 获取StagingArea目录,JobClient需要通过JobSubmissionProtocol协议的远程方法getStagingAreaDir从JobTracker端获取到,我们看一下JobTracker端的getStagingAreaDirInternal方法,如下所示: 1 private String getStagingAreaDirInternal(String user) throws IOException { 2 final Path stagingRootDir = newPath(conf.get("mapreduce.jobtracker.staging.root.dir", "/tmp/hadoop/mapred/staging")); 3 final FileSystem fs = stagingRootDir.getFileSystem(conf); 4 return fs.makeQualified(new Path(stagingRootDir, user+"/.staging")).toString(); 5 } 最终获取到的StagingArea目录为${mapreduce.jobtracker.staging.root.dir}/${user}/.staging/,例如,如果使用默认的mapreduce.jobtracker.staging.root.dir值,用户为shirdrn,则StagingArea目录/tmp/hadoop/mapred/staging/shirdrn/.staging/。通过Path submitJobDir = new Path(jobStagingArea, jobId.toString());可以得到submitJobDir,假如一个job的ID为job_200912121733_0002,则submitJobDir的值为/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/ 拷贝资源文件 在配置Job的时候,可以指定tmpfiles、tmpjars、tmparchives,JobClient会将对应的资源文件拷贝到指定的目录中,对应目录如下代码所示: 1 Path filesDir = JobSubmissionFiles.getJobDistCacheFiles(submitJobDir); 2 Path archivesDir = JobSubmissionFiles.getJobDistCacheArchives(submitJobDir); 3 Path libjarsDir = JobSubmissionFiles.getJobDistCacheLibjars(submitJobDir); 4 ... 5 Path submitJarFile = JobSubmissionFiles.getJobJar(submitJobDir); 6 job.setJar(submitJarFile.toString()); 7 fs.copyFromLocalFile(originalJarFile, submitJarFile); 上面已经知道Job提交目录,可以分别得到对应的资源所在目录: tmpfiles目录:/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/files tmpjars目录:/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/libjars tmparchives目录:/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/archives Job Jar文件:/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/job.jar 然后,就可以将对应的资源文件拷贝到对应的目录中。 计算并存储Split数据 根据Job配置中设置的InputFormat,计算该Job的数据数据文件是如何进行分片的,代码如下所示: 1 Configuration conf = job.getConfiguration(); 2 InputFormat<?, ?> input = ReflectionUtils.newInstance(job.getInputFormatClass(), conf); 3 List<InputSplit> splits = input.getSplits(job); 实际上就是调用InputFormat的getSplits方法,如果不适用Hadoop自带的FileInputFormat的默认getSplits方法实现,可以自定义实现,重写该默认实现逻辑来定义数据数据文件分片的规则。 计算出输入文件的分片信息,然后需要将这些分片数据写入到HDFS供JobTracker查询初始化MapTask,写入分片数据的实现代码: 1 T[] array = (T[]) splits.toArray(new InputSplit[splits.size()]); 2 // sort the splits into order based on size, so that the biggest 3 // go first 4 Arrays.sort(array, new SplitComparator()); // 根据InputSplit的长度做了一个逆序排序 5 JobSplitWriter.createSplitFiles(jobSubmitDir, conf, jobSubmitDir.getFileSystem(conf), array); // 将split及其元数据信息写入HDFS 接着调用JobSplitWriter.createSplitFiles方法存储Split信息,并创建元数据信息,并保存元数据信息。存储Split信息,代码实现如下所示: 01 SerializationFactory factory = new SerializationFactory(conf); 02 int i = 0; 03 long offset = out.getPos(); 04 for(T split: array) { 05 long prevCount = out.getPos(); 06 Text.writeString(out, split.getClass().getName()); 07 Serializer<T> serializer = factory.getSerializer((Class<T>) split.getClass()); 08 serializer.open(out); 09 serializer.serialize(split); // 将split序列化写入到HDFS文件中 10 long currCount = out.getPos(); 11 String[] locations = split.getLocations(); 12 final int max_loc = conf.getInt(MAX_SPLIT_LOCATIONS, 10); 13 if (locations.length > max_loc) { 14 LOG.warn("Max block location exceeded for split: "+ split + " splitsize: " + locations.length + " maxsize: " + max_loc); 15 locations = Arrays.copyOf(locations, max_loc); 16 } 17 info[i++] = new JobSplit.SplitMetaInfo(locations, offset, split.getLength()); // 创建SplitMetaInfo实例 18 offset += currCount - prevCount; 19 } 我们先看一下FileSplit包含的分片内容,如下所示: 1 private Path file; 2 private long start; 3 private long length; 4 private String[] hosts; 在序列化保存FileSplit到HDFS,可以通过查看FileSplit的write方法,如下所示: 1 @Override 2 public void write(DataOutput out) throws IOException { 3 Text.writeString(out, file.toString()); 4 out.writeLong(start); 5 out.writeLong(length); 6 } 需要注意的是,这里面并没有将FileSplit的hosts信息保存,而是存储到了SplitMetaInfo中new JobSplit.SplitMetaInfo(locations, offset, split.getLength())。 下面是保存SplitMetaInfo信息的实现: 01 private static void writeJobSplitMetaInfo(FileSystem fs, Path filename, 02 FsPermission p, int splitMetaInfoVersion, 03 JobSplit.SplitMetaInfo[] allSplitMetaInfo) throws IOException { 04 // write the splits meta-info to a file for the job tracker 05 FSDataOutputStream out = FileSystem.create(fs, filename, p); 06 out.write(JobSplit.META_SPLIT_FILE_HEADER); // 写入META头信息:META_SPLIT_FILE_HEADER = "META-SPL".getBytes("UTF-8"); 07 WritableUtils.writeVInt(out, splitMetaInfoVersion); // META版本信息:1 08 WritableUtils.writeVInt(out, allSplitMetaInfo.length); // META对象的数量:每个InputSplit对应一个SplitMetaInfo 09 for (JobSplit.SplitMetaInfo splitMetaInfo : allSplitMetaInfo) { 10 splitMetaInfo.write(out); // 每个都进行存储 11 } 12 out.close(); 13 } 看一下SplitMetaInfo存储时包含的数据信息: 1 public void write(DataOutput out) throws IOException { 2 WritableUtils.writeVInt(out, locations.length); // location个数 3 for (int i = 0; i < locations.length; i++) { 4 Text.writeString(out, locations[i]); // 写入每一个location位置信息 5 } 6 WritableUtils.writeVLong(out, startOffset); // 偏移量 7 WritableUtils.writeVLong(out, inputDataLength); // 数据长度 8 } 最后,我们看一下这些数据保存的目录和文件情况。前面已经知道Job提交目录,下面看split存储的文件是如何构建的: 1 FSDataOutputStream out = createFile(fs, JobSubmissionFiles.getJobSplitFile(jobSubmitDir), conf); 2 SplitMetaInfo[] info = writeNewSplits(conf, splits, out); 那么split保存的文件为:/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/job.split。 同样,split元数据信息文件构建如下所示: 1 writeJobSplitMetaInfo(fs,JobSubmissionFiles.getJobSplitMetaFile(jobSubmitDir), 2 new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION), splitVersion, info); split元数据信息文件为:/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/job.splitmetainfo。 保存Job配置数据 在提交Job到JobTracker之前,还需要保存Job的配置信息,这些配置数据根据用户在MR程序中配置,覆盖默认的配置值,最后保存到XML文件(job.xml)到HDFS,供JobTracker查询。如下代码,创建submitJobFile文件并写入job配置数据: 01 ... 02 Path submitJobFile = JobSubmissionFiles.getJobConfPath(submitJobDir); 03 jobCopy.set("mapreduce.job.dir", submitJobDir.toString()); 04 ... 05 // Write job file to JobTracker's fs 06 FSDataOutputStream out = FileSystem.create(fs, submitJobFile, newFsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION)); 07 ... 08 try { 09 jobCopy.writeXml(out); 10 } finally { 11 out.close(); 12 } 前面已经知道Job提交目录,我们很容易就能得到job.xml文件的存储路径:/tmp/hadoop/mapred/staging/shirdrn/.staging/job_200912121733_0002/job.xml。 最后,所有的数据都已经准备完成,JobClient就可以基于JobSubmissionProtocol协议方法submitJob,提交Job到JobTracker运行。
Akka基于Actor模型,提供了一个用于构建可扩展的(Scalable)、弹性的(Resilient)、快速响应的(Responsive)应用程序的平台。本文基本上是基于Akka的官方文档(版本是2.3.12),通过自己的理解,来阐述Akka提供的一些组件或概念,另外总结了Akka的一些使用场景。 Actor 维基百科这样定义Actor模型: 在计算科学领域,Actor模型是一个并行计算(Concurrent Computation)模型,它把actor作为并行计算的基本元素来对待:为响应一个接收到的消息,一个actor能够自己做出一些决策,如创建更多的actor,或发送更多的消息,或者确定如何去响应接收到的下一个消息。 Actor是Akka中最核心的概念,它是一个封装了状态和行为的对象,Actor之间可以通过交换消息的方式进行通信,每个Actor都有自己的收件箱(Mailbox)。 通过Actor能够简化锁及线程管理,可以非常容易地开发出正确地并发程序和并行系统,Actor具有如下特性: 提供了一种高级抽象,能够简化在并发(Concurrency)/并行(Parallelism)应用场景下的编程开发 提供了异步非阻塞的、高性能的事件驱动编程模型 超级轻量级事件处理(每GB堆内存几百万Actor) 实现一个Actor,可以继承特质akka.actor.Actor,实现一个receive方法,应该在receive方法中定义一系列的case语句,基于标准Scala的模式匹配方法,来实现每一种消息的处理逻辑。 我们先看一下Akka中特质Actor的定义: 01 trait Actor { 02 03 import Actor._ 04 05 type Receive = Actor.Receive 06 07 implicit val context: ActorContext = { 08 val contextStack = ActorCell.contextStack.get 09 if ((contextStack.isEmpty) || (contextStack.head eq null)) 10 throw ActorInitializationException( 11 s"You cannot create an instance of [${getClass.getName}] explicitly using the constructor (new). " + 12 "You have to use one of the 'actorOf' factory methods to create a new actor. See the documentation.") 13 val c = contextStack.head 14 ActorCell.contextStack.set(null :: contextStack) 15 c 16 } 17 18 implicit final val self = context.self //MUST BE A VAL, TRUST ME 19 20 final def sender(): ActorRef = context.sender() 21 22 def receive: Actor.Receive // 这个是在子类中一定要实现的抽象方法 23 24 protected[akka] def aroundReceive(receive: Actor.Receive, msg: Any): Unit =receive.applyOrElse(msg, unhandled) 25 26 protected[akka] def aroundPreStart(): Unit = preStart() 27 28 protected[akka] def aroundPostStop(): Unit = postStop() 29 30 protected[akka] def aroundPreRestart(reason: Throwable, message: Option[Any]): Unit= preRestart(reason, message) 31 32 protected[akka] def aroundPostRestart(reason: Throwable): Unit = postRestart(reason) 33 34 def supervisorStrategy: SupervisorStrategy = SupervisorStrategy.defaultStrategy 35 36 @throws(classOf[Exception]) // when changing this you MUST also change UntypedActorDocTest 37 def preStart(): Unit = () // 启动Actor之前需要执行的操作,默认为空实现,可以重写该方法 38 39 @throws(classOf[Exception]) // when changing this you MUST also change UntypedActorDocTest 40 def postStop(): Unit = () // 终止Actor之前需要执行的操作,默认为空实现,可以重写该方法 41 42 @throws(classOf[Exception]) // when changing this you MUST also change UntypedActorDocTest 43 def preRestart(reason: Throwable, message: Option[Any]): Unit = { // 重启Actor之前需要执行的操作,默认终止该Actor所监督的所有子Actor,然后调用postStop()方法,可以重写该方法 44 context.children foreach { child ⇒ 45 context.unwatch(child) 46 context.stop(child) 47 } 48 postStop() 49 } 50 51 @throws(classOf[Exception]) // when changing this you MUST also change UntypedActorDocTest 52 def postRestart(reason: Throwable): Unit = { // 重启Actor之前需要执行的操作,默认执行preStart()的实现逻辑,可以重写该方法 53 preStart() 54 } 55 56 def unhandled(message: Any): Unit = { 57 message match { 58 case Terminated(dead) ⇒ throw new DeathPactException(dead) 59 case _ ⇒ context.system.eventStream.publish(UnhandledMessage(message, sender(), self)) 60 } 61 } 62 } 上面特质中提供了几个Hook,具体说明可以看代码中注释,我们可以在继承该特质时重写Hook方法,实现自己的处理逻辑。 一个Actor是有生命周期(Lifecycle)的,如下图所示: 通过上图我们可以看到,一除了/system路径下面的Actor外,一个Actor初始时路径为空,调用ActorSystem的actorOf方法创建一个Actor实例,返回一个引用ActorRef,它包括一个UID和一个Path,标识了一个Actor,可以通过该引用向该Actor实例发送消息。 ActorSystem 在Akka中,一个ActorSystem是一个重量级的结构,他需要分配多个线程,所以在实际应用中,按照逻辑划分的每个应用对应一个ActorSystem实例。 一个ActorSystem是具有分层结构(Hierarchical Structure)的:一个Actor能够管理(Oversee)某个特定的函数,他可能希望将一个task分解为更小的多个子task,这样它就需要创建多个子Actor(Child Actors),并监督这些子Actor处理任务的进度等详细情况,实际上这个Actor创建了一个Supervisor来监督管理子Actor执行拆分后的多个子task,如果一个子Actor执行子task失败,那么就要向Supervisor发送一个消息说明处理子task失败。需要知道的是,一个Actor能且仅能有一个Supervisor,就是创建它的那个Actor。基于被监控任务的性质和失败的性质,一个Supervisor可以选择执行如下操作选择: 重新开始(Resume)一个子Actor,保持它内部的状态 重启一个子Actor,清除它内部的状态 终止一个子Actor 扩大失败的影响,从而使这个子Actor失败 将一个Actor以一个监督层次结构视图来看是非常重要的,因为它诠释了上面第4种操作选择的存在性,而且对前3种操作选择也有影响:重新开始(Resume)一个Actor,则该Actor的所有子Actor都继续工作;重启一个Actor,则该Actor的所有子Actor都被重新启动;终止一个Actor,则该Actor的所有子Actor都被终止。另外,一个Actor的preRestart方法的默认行为是终止所有子Actor,如果我们不想这样,可以在继承Actor的实现中重写preRestart方法的逻辑。 一个ActorSystem在创建过程中,至少启动3个Actor,如下图所示: 上图是一个类似树状层次结构,ActorSystem的Top-Level层次结构,与Actor关联起来,称为Actor路径(Actor Path),不同的路径代表了不同的监督范围(Supervision Scope)。下面说明ActorSystem的监督范围: “/”路径:通过根路径可以搜索到所有的Actor “/user”路径:用户创建的Top-Level Actor在该路径下面,通过调用ActorSystem.actorOf来实现Actor的创建 “/system”路径:系统创建的Top-Level Actor在该路径下面 “/deadLetters”路径:消息被发送到已经终止,或者不存在的Actor,这些Actor都在该路径下面 “/temp”路径:被系统临时创建的Actor在该路径下面 “/remote”路径:改路径下存在的Actor,它们的Supervisor都是远程Actor的引用 TypedActor TypedActor是Akka基于Active对象(Active Object)设计模式的一个实现,关于Active对象模式,可以看维基百科的定义: Active对象模式解耦了在一个对象上执行方法和调用方法的逻辑,执行方法和调用方法分别在各自的线程执行上下文中。该模式的目标是通过使用异步方法调用和一个调度器来处理请求,从而实现并行计算处理,该模式由6个元素组成: 一个Proxy对象,提供一个面向客户端的接口和一组公共的方法 一个接口,定义了请求一个Active对象上的方法的集合 一个来自客户端请求的列表 一个调度器,确定下一次处理哪一个请求 Active对象上方法的实现 一个回掉或者变量,供客户端接收请求被处理后的结果 通过前面对Actor的了解,我们知道Actor更适用于在Akka的Actor系统之间来实现并行计算处理,而TypedActor适用于桥接Actor系统和非Actor系统。TypedActor是基于JDK的Proxy来实现的,与Actor不同的是,Actor一次处理一个消息,而TypedActor一次处理一个调用(Call)。关于更多关于TypedActor,可以查看Akka文档。 Cluster Akka Cluster提供了一个容错(Fault-Tolerant)、去中心化(Decentralized)、基于P2P的集群服务,而且不会出现单点故障(SPOF, Single Point Of Failure)。Akka基于Gossip实现集群服务,而且支持服务自动失败检测。 关于Gossip协议的说明,维基百科说明如下所示: Gossip协议是点对点(Computer-to-Computer)通信协议的一种,它受社交网络中的流言传播的特点所启发。现在分布式系统常常使用Gossip协议来解决其他方式所无法解决的问题,或者是由于底层网络的超大特殊结构,或者是因为Gossip方案是解决这类问题最有效的一种方式。 一个Akka集群由一组成员节点组成,每个成员节点通过hostname:port:uid来唯一标识,并且每个成员节点之间是解耦合的(Decoupled)。一个Akka应用程序是一个分布式应用程序,它具有一个Actor的集合S,而每个节点上可以启动这个Akka应用S的集合的的一部分Actor,而不必是全集S。如果一个新的成员节点需要加入到Akka集群,只需要在集群中任意一个成员节点上执行Join命令即可。 Akka集群中各个成员节点之间的状态关系,如下图所示: Akka集群中任何一个成员节点都有可能成为集群的Leader,这是基于Gossip收敛(Convergence)过程得到的确定性结果,没有经过选举的过程。Leader只是一种角色,在各轮Gossip收敛过程中Leader是不断变化的。Leader的职责是使成员节点进入/离开集群。 一个成员节点开始于joining状态,一旦所有其节点都看到了该新加入Akka集群的节点,则Leader会设置这个节点的状态为up。 如果一个节点安全离开Akka集群,可预期地它的状态会变为leaving状态,当Leader看到该节点为leaving状态,会将其状态修改为exiting,然后当所有节点看到该节点状态为exiting,则Leader将该节点移除,状态修改为removed状态。 如果一个节点处于unreachable状态,基于Gossip协议Leader是无法执行任何操作收敛(Convergence)到该节点的,所以unreachable状态的节点的状态是必须被改变的,它必须变成reachable状态或者down状态。如果该节点想再次加入到Akka集群,它必须需要重新启动,并且重新加入集群(经由joining状态)。 Remoting Akka Remoting的设计目标是基于P2P风格的网络通信,所以它存在如下限制: 不支持NAT(Network Address Translation) 不支持负载均衡器(Load Balancers) Akka提供了种方式来使用Remoting功能: 通过调用actorSelection方法搜索一个actor,该方法输入的参数的模式为:akka.<protocol>://<actor system>@<hostname>:<port>/<actor path> 通过actorOf方法创建一个actor 下面看一下Remoting系统中故障恢复模型(Failure Recovery Model),如下图所示: 上图中,连接到一个远程系统的过程中,包括上面4种状态:在进行任何通信之前,系统处于Idle状态;当第一次一个消息尝试向远程系统发送,或者当远程系统连接过来,这时系统状态变为Active;当两个系统通信失败,连接丢失,这时系统变为Gated状态;当系统通信过程中,由于参与通信的系统的状态不一致导致系统无法恢复,这时远程系统变为Quarantined状态,只有重新启动系统才能恢复,重启后系统变为Active状态。 Persistence Akka的持久性能够使得有状态的Actor实例保存它的内部状态,在Actor重启后能够更快的进行恢复。需要强调的是,持久化的仅仅是Actor的内部状态,而不是Actor当前的状态,Actor内部状态的变化会被一追加的方式存到到指定的存储中,一旦追加完成存储状态,这些数据就不会被更新。有状态的Actor通过重放(Replay)持久化的状态来快速恢复,重建内部状态。 Akka Persistence的架构有如下几个要点: PersistentActor 它是一个持久的、有状态的Actor,能够将持久化消息到一个日志系统中。当一个PersistentActor重启的时候,它能够重放记录到日志系统中的消息,从而基于这些消息来恢复一个Actor的内部状态。 PersistentView 持久化视图是一个持久的有状态的Actor,它接收被记录到一个PersistentActor中的消息,但是它本身并不记录消息到日志系统,而是通过复制一个PersistentActor的消息流,来更新自己内部状态。 AtLeastOnceDelivery 提供了一个消息至少传递一次(At-Lease-Once)的语义,在发送者和接收者所在的JVM崩溃的时候,将消息传递到目的地。 Journal 一个日志系统存储发送给一个PersistentActor的消息序列,可以在应用程序中控制是否一个PersistentActor将消息序列记录到日志中。日志系统是支持插件式的,默认情况下,消息被记录到本地文件系统中。 Akka Camel Akka提供了一个模块,能够与Apache Camel整合。Apache Camel是一个实现了EIP(Enterprise Integration Patterns)的整合框架,支持通过各种各样的协议进行消息交换。所以Akka的Actor可以通过Scala或Java API与其它系统进行通信,协议比如HTTP、SOAP、TCP、FTP、SMTP、JMS。 Akka适用场景 Akka适用场景非常广泛,这里根据一些已有的使用案例来总结一下,Akka能够在哪些应用场景下投入生产环境: 事务处理(Transaction Processing) 在线游戏系统、金融/银行系统、交易系统、投注系统、社交媒体系统、电信服务系统。 后端服务(Service Backend) 任何行业的任何类型的应用都可以使用,比如提供REST、SOAP等风格的服务,类似于一个服务总线,Akka支持纵向&横向扩展,以及容错/高可用(HA)的特性。 并行计算(Concurrency/Parallelism) 任何具有并发/并行计算需求的行业,基于JVM的应用都可以使用,如使用编程语言Scala、Java、Groovy、JRuby开发。 仿真 Master/Slave架构风格的计算系统、计算网格系统、MapReduce系统。 通信Hub(Communications Hub) 电信系统、Web媒体系统、手机媒体系统。 复杂事件流处理(Complex Event Stream Processing) Akka本身提供的Actor就适合处理基于事件驱动的应用,所以可以更加容易处理具有复杂事件流的应用。 其它特性 Akka还支持很多其它特性,如下所示: 支持Future,可以同步或异步地获取发送消息的结果 支持基于事件的Dispatcher,将多个Actor与一个线程池绑定 支持消息路由,可以提供不同的消息路由策略,如Akka支持如下策略:RoundRobinRoutingLogic、RandomRoutingLogic、SmallestMailboxRoutingLogic、BroadcastRoutingLogic、ScatterGatherFirstCompletedRoutingLogic、TailChoppingRoutingLogic、ConsistentHashingRoutingLogic 支持FSM,提供基于事件的状态转移
JStorm是由Alibaba开源的实时计算系统,它使用Java重写了Apache Storm(使用Clojure+Java混编),而且在原来的基础上做了很多改进的地方。使用Java重写,对于使用Java的开发人员来说,可以通过阅读源码来了解JStorm内部的原理和实现,而且可以根据运行错误日志来排查错误。 下面通过安装配置,以及简单使用的验证,来说明JStorm宏观上与Apache Storm的不同之处。 安装配置JStorm Server 首先,要保证JDK成功安装配置,然后在一个节点上下载、安装、配置JStorm。例如,我在hadoop1节点上,下载并解压缩: 1 wget http://42.121.19.155/jstorm/jstorm-0.9.6.2.zip 2 unzip jstorm-0.9.6.2.zip 3 cd jstorm-0.9.6.2 修改配置文件conf/storm.yaml,内容修改如下: 01 ########### These MUST be filled in for a storm configuration 02 storm.zookeeper.servers: 03 - "10.10.4.128" 04 - "10.10.4.129" 05 - "10.10.4.130" 06 07 storm.zookeeper.root: "/jstorm" 08 09 # %JSTORM_HOME% is the jstorm home directory 10 storm.local.dir: "/tmp/jstorm/data" 11 12 java.library.path: "/usr/local/lib:/opt/local/lib:/usr/lib" 13 14 supervisor.slots.ports: 15 - 6800 16 - 6801 17 - 6802 18 - 6803 要保证ZooKeeper集群已经成功启动,并在ZooKeeper中创建/jstorm,执行如下命令: 1 ssh zookeeper@10.10.4.128 2 /usr/local/zookeeper/bin/zkCli.sh 然后创建/jstorm,执行如下命令: 1 create /jstorm "" 配置环境变量JSTORM_HOME,修改~/.bashrc文件,增加如下内容: 1 export JSTORM_HOME=/home/kaolatj/jstorm-0.9.6.2 2 export PATH=$PATH:$JSTORM_HOME/bin 使环境变量生效: 1 source ~/.bashrc 配置完上面内容后,需要创建~/.jstorm目录,并将配置好的storm.yaml文件拷贝到该目录下: 1 mkdir ~/.jstorm 2 cp -f $JSTORM_HOME/conf/storm.yaml ~/.jstorm 最好在每个节点都执行上述配置,尤其是在提交Topology的时候,如果没有这个就会报错的。 最后,要将JStorm安装文件拷贝到集群其他从节点上,我这里有2个从节点hadoop2和hadoop3,执行如下命令: 1 scp -r /home/kaolatj/jstorm-0.9.6.2 kaolatj@hadoop2:~/ 2 scp -r /home/kaolatj/jstorm-0.9.6.2 kaolatj@hadoop3:~/ 同样,在从节点上配置好环境变量JSTORM_HOME。 安装JStorm UI 安装JStorm UI,可以安装在任何一个节点上,只要保证JStorm UI的安装包(WAR文件)的配置文件和JStorm集群相同即可。JStorm UI运行在Web容器之中,可以使用Tomcat。我这里,直接在Nimbus节点上安装Jstorm UI。 首先,安装Tomcat Web容器: 1 wget http://apache.fayea.com/tomcat/tomcat-7/v7.0.57/bin/apache-tomcat-7.0.57.zip 2 unzip apache-tomcat-7.0.57.zip 3 cd apache-tomcat-7.0.57 4 chmod +x bin/*.sh 然后,将jstorm-ui-0.9.6.2.war软件包拷贝到Tomcat的webapps目录下,jstorm-ui-0.9.6.2.war直接在解压缩的jstorm-0.9.6.2.zip包中,拷贝即可: 1 cp ~/jstorm-0.9.6.2/jstorm-ui-0.9.6.2.war webapps/ 2 mv ROOT ROOT.old 3 ln -s jstorm-ui-0.9.6.2 ROOT 在启动Tomcat之前,要保证配置文件$JSTORM_HOME/conf/storm.yaml拷贝到目录~/.jstorm下面。 最后,启动Tomcat,并查看日志: 1 bin/catalina.sh start 2 3 tail -100f logs/catalina.out JStorm UI安装完成后,可以通过访问http://10.10.4.125:8080即可看到Web UI界面。 另外,可以直接通过源码进行构建,将对应的配置配好的文件$JSTORM_HOME/conf/storm.yaml直接打包到WAR文件里面,然后就可以直接发布到Web容器中(如Tomcat),这样可以不用将$JSTORM_HOME/conf/storm.yaml拷贝到目录~/.jstorm下面。 验证JStorm 我这里写了一个相对比较复杂的JStorm程序,原来是基于apache-storm-0.9.2-incubating构建的应用,现在迁移到JStorm计算平台,保留了Apache Storm中一些工具包,像storm-kafka,同时还用到Kafka,在Storm UI上DAG图如下所示: 参考Maven依赖配置如下: 01 <properties> 02 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 03 <jstorm.version>0.9.6.2-snapshot</jstorm.version> 04 </properties> 05 06 <dependencies> 07 <dependency> 08 <groupId>com.alibaba.jstorm</groupId> 09 <artifactId>jstorm-server</artifactId> 10 <version>${jstorm.version}</version> 11 <scope>provided</scope> 12 </dependency> 13 <dependency> 14 <groupId>com.alibaba.jstorm</groupId> 15 <artifactId>jstorm-client</artifactId> 16 <version>${jstorm.version}</version> 17 <scope>provided</scope> 18 </dependency> 19 <dependency> 20 <groupId>com.alibaba.jstorm</groupId> 21 <artifactId>jstorm-client-extension</artifactId> 22 <version>${jstorm.version}</version> 23 <scope>provided</scope> 24 </dependency> 25 26 <dependency> 27 <groupId>org.apache.storm</groupId> 28 <artifactId>storm-kafka</artifactId> 29 <version>0.9.3-rc1</version> 30 <exclusions> 31 <exclusion> 32 <groupId>log4j</groupId> 33 <artifactId>log4j</artifactId> 34 </exclusion> 35 </exclusions> 36 </dependency> 37 <dependency> 38 <groupId>org.apache.kafka</groupId> 39 <artifactId>kafka_2.9.2</artifactId> 40 <version>0.8.1.1</version> 41 <exclusions> 42 <exclusion> 43 <groupId>org.apache.zookeeper</groupId> 44 <artifactId>zookeeper</artifactId> 45 </exclusion> 46 <exclusion> 47 <groupId>log4j</groupId> 48 <artifactId>log4j</artifactId> 49 </exclusion> 50 </exclusions> 51 </dependency> 52 53 <dependency> 54 <groupId>org.apache.hadoop</groupId> 55 <artifactId>hadoop-client</artifactId> 56 <version>2.2.0</version> 57 <exclusions> 58 <exclusion> 59 <groupId>org.slf4j</groupId> 60 <artifactId>slf4j-log4j12</artifactId> 61 </exclusion> 62 </exclusions> 63 </dependency> 64 <dependency> 65 <groupId>org.apache.hadoop</groupId> 66 <artifactId>hadoop-hdfs</artifactId> 67 <version>2.2.0</version> 68 <exclusions> 69 <exclusion> 70 <groupId>org.slf4j</groupId> 71 <artifactId>slf4j-log4j12</artifactId> 72 </exclusion> 73 </exclusions> 74 </dependency> 75 <dependency> 76 <groupId>com.github.ptgoetz</groupId> 77 <artifactId>storm-hdfs</artifactId> 78 <version>0.1.3-SNAPSHOT</version> 79 </dependency> 80 <dependency> 81 <groupId>commons-configuration</groupId> 82 <artifactId>commons-configuration</artifactId> 83 <version>1.10</version> 84 </dependency> 85 <dependency> 86 <groupId>org.slf4j</groupId> 87 <artifactId>slf4j-api</artifactId> 88 <version>1.7.5</version> 89 <scope>provided</scope> 90 </dependency> 91 92 </dependencies> 提交Topology程序包到JStorm集群,执行如下命令: 1 bin/jstorm jar ~/jstorm-msg-process-0.0.1-SNAPSHOT.jar org.shirdrn.storm.msg.MsgProcessTopology MsgProcessTopology 然后,我们可以通过命令行来查看Topology列表: 1 bin/jstorm list 结果示例如下所示: 1 ClusterSummary(supervisors:[SupervisorSummary(host:hadoop2, supervisor_id:43bb2701-4a3c-4941-8605-68000c66eea5, uptime_secs:89864, num_workers:4, num_used_workers:2), SupervisorSummary(host:hadoop3, supervisor_id:0e72bc94-52d2-4695-8c29-8fbf57d89c9c, uptime_secs:96051, num_workers:4, num_used_workers:3)], nimbus_uptime_secs:226520, topologies:[TopologySummary(id:WordCountTopology-1-1420369616, name:WordCountTopology, status:ACTIVE, uptime_secs:237166, num_tasks:5, num_workers:3, error_info:Y), TopologySummary(id:MsgProcessTopology-3-1420447738, name:MsgProcessTopology, status:ACTIVE, uptime_secs:159044, num_tasks:23, num_workers:2, error_info:)], version:0.9.6.2) 接着再看一下JStorm UI首页的效果图,如图所示: 运行在JStorm集群上的MsgProcessTopology,点击上图中的Topology Name中的链接,就可以看到Topology的效果图,如下所示: 在Jstorm UI首页上,点击Supervisor节点链接,可以查看各个节点上运行的Topology及其Task的消息情况,示例如图所示: 点击Task List中的链接,还可以查看某个Task的明细信息,不再累述。 问题总结 如果原来基于Apache Storm开发的程序,理论上可以无需改动便可以运行在JStorm集群上,只不过在编译打包的时候,指定JStorm依赖: 01 <dependency> 02 <groupId>com.alibaba.jstorm</groupId> 03 <artifactId>jstorm-server</artifactId> 04 <version>${jstorm.version}</version> 05 <scope>provided</scope> 06 </dependency> 07 <dependency> 08 <groupId>com.alibaba.jstorm</groupId> 09 <artifactId>jstorm-client</artifactId> 10 <version>${jstorm.version}</version> 11 <scope>provided</scope> 12 </dependency> 13 <dependency> 14 <groupId>com.alibaba.jstorm</groupId> 15 <artifactId>jstorm-client-extension</artifactId> 16 <version>${jstorm.version}</version> 17 <scope>provided</scope> 18 </dependency> 启动Nimbus和Supervisor进程的时候,一定要在后台启动,否则可能会出现进程无缘无故挂掉的问题,可以执行命令: 1 nohup jstorm nimbus >/dev/null 2>&1 & 2 nohup jstorm supervisor >/dev/null 2>&1 & 如果忘记配置cp -f $JSTORM_HOME/conf/storm.yaml ~/.jstorm,在提交Topology到JStorm集群时,会出现如下错误: 1 [INFO 2015-01-04 17:34:50 CuratorFrameworkImpl:238 main] Starting 2 [WARN 2015-01-04 17:34:50 ClientCnxn:1102 main-SendThread(localhost:2181)] Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect 3 java.net.ConnectException: Connection refused 4 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) 5 at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:708) 6 at org.apache.zookeeper.ClientCnxnSocketNIO.doTransport(ClientCnxnSocketNIO.java:361) 7 at org.apache.zookeeper.ClientCnxn$SendThread.run(ClientCnxn.java:1081) 8 [WARN 2015-01-04 17:34:51 ClientCnxn:1102 main-SendThread(localhost:2181)] Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect 9 java.net.ConnectException: Connection refused 目前,在哪个节点上提交Topology,必须配置将 $JSTORM_HOME/conf/storm.yaml拷贝到~/.jstorm目录下,否则就会报错。 这个配置,我觉得以后JStorm可以做个优化,只要宿主机安装了JStorm程序,实际上就应该根据环境变量$JSTORM_HOME自动找到对应的配置文件$JSTORM_HOME/conf/storm.yaml。 在使用JStorm Maven依赖的时候,你可以无法在网上找到Public Repository,这时,你需要下载指定版本的JStorm源码,然后在你的开发主机上安装到本地Maven Repository,执行如下命令: 1 cd ~/jstorm-0.9.6.2 2 mvn install -DskipTests 查看JStorm运行日志,每个Topology的程序运行日志会写入自己的日志文件,便于查看和排错,如图所示:
Hue是一个开源的Apache Hadoop UI系统,最早是由Cloudera Desktop演化而来,由Cloudera贡献给开源社区,它是基于Python Web框架Django实现的。通过使用Hue我们可以在浏览器端的Web控制台上与Hadoop集群进行交互来分析处理数据,例如操作HDFS上的数据,运行MapReduce Job等等。很早以前就听说过Hue的便利与强大,一直没能亲自尝试使用,下面先通过官网给出的特性,通过翻译原文简单了解一下Hue所支持的功能特性集合: 默认基于轻量级sqlite数据库管理会话数据,用户认证和授权,可以自定义为MySQL、Postgresql,以及Oracle 基于文件浏览器(File Browser)访问HDFS 基于Hive编辑器来开发和运行Hive查询 支持基于Solr进行搜索的应用,并提供可视化的数据视图,以及仪表板(Dashboard) 支持基于Impala的应用进行交互式查询 支持Spark编辑器和仪表板(Dashboard) 支持Pig编辑器,并能够提交脚本任务 支持Oozie编辑器,可以通过仪表板提交和监控Workflow、Coordinator和Bundle 支持HBase浏览器,能够可视化数据、查询数据、修改HBase表 支持Metastore浏览器,可以访问Hive的元数据,以及HCatalog 支持Job浏览器,能够访问MapReduce Job(MR1/MR2-YARN) 支持Job设计器,能够创建MapReduce/Streaming/Java Job 支持Sqoop 2编辑器和仪表板(Dashboard) 支持ZooKeeper浏览器和编辑器 支持MySql、PostGresql、Sqlite和Oracle数据库查询编辑器 下面,我们通过实际安装来验证Hue的一些功能。 环境准备 这里,我所基于的基本环境及其配置情况,如下所示: CentOS-6.6 (Final) JDK-1.7.0_25 Maven-3.2.1 Git-1.7.1 Hue-3.7.0(branch-3.7.1) Hadoop-2.2.0 Hive-0.14 Python-2.6.6 基于上面的软件工具,要保证正确安装和配置。需要说明的是,我们通过Hue来执行Hive查询,需要启动HiveServer2服务: 1 cd /usr/local/hive 2 bin/hiveserver2 & 否则通过Hue Web控制无法执行Hive查询。 安装配置 我新建了一个hadoop用户,以hadoop用户,首先使用yum工具来安装Hue相关的依赖软件: 1 sudo yum install krb5-devel cyrus-sasl-gssapi cyrus-sasl-deve libxml2-devel libxslt-devel mysql mysql-devel openldap-devel python-devel python-simplejson sqlite-devel 然后,执行如下命令进行Hue软件包的下载构建: 1 cd /usr/local/ 2 sudo git clone https://github.com/cloudera/hue.git branch-3.7.1 3 sudo chown -R hadoop:hadoop branch-3.7.1/ 4 cd branch-3.7.1/ 5 make apps 上述过程如果没有任何问题,我们就已经安装好Hue。Hue的配置文件为/usr/local/branch-3.7.1/desktop/conf/pseudo-distributed.ini,默认的配置文件不能正常运行Hue,所以需要修改其中的内容,与我们对应的Hadoop集群配置相对应。该配置文件根据整合不同的软件,将配置分成多个段,每个段下面还有子段,便于管理配置,如下所示(省略子段名称): desktop libsaml libopenid liboauth librdbms hadoop filebrowser liboozie oozie beeswax impala pig sqoop proxy hbase search indexer jobsub jobbrowser zookeeper spark useradmin libsentry 我们很容易根据需要来配置自己需要的内容。我们修改配置文件的情况,如下表所示: Hue配置段 Hue配置项 Hue配置值 说明 desktop default_hdfs_superuser hadoop HDFS管理用户 desktop http_host 10.10.4.125 Hue Web Server所在主机/IP desktop http_port 8000 Hue Web Server服务端口 desktop server_user hadoop 运行Hue Web Server的进程用户 desktop server_group hadoop 运行Hue Web Server的进程用户组 desktop default_user yanjun Hue管理员 hadoop/hdfs_clusters fs_defaultfs hdfs://hadoop6:8020 对应core-site.xml配置项fs.defaultFS hadoop/hdfs_clusters hadoop_conf_dir /usr/local/hadoop/etc/hadoop Hadoop配置文件目录 hadoop/yarn_clusters resourcemanager_host hadoop6 对应yarn-site.xml配置项yarn.resourcemanager.hostname hadoop/yarn_clusters resourcemanager_port 8032 ResourceManager服务端口号 hadoop/yarn_clusters resourcemanager_api_url http://hadoop6:8088 对应于yarn-site.xml配置项yarn.resourcemanager.webapp.address hadoop/yarn_clusters proxy_api_url http://hadoop6:8888 对应yarn-site.xml配置项yarn.web-proxy.address hadoop/yarn_clusters history_server_api_url http://hadoo6:19888 对应mapred-site.xml配置项mapreduce.jobhistory.webapp.address beeswax hive_server_host 10.10.4.125 Hive所在节点主机名/IP beeswax hive_server_port 10000 HiveServer2服务端口号 beeswax hive_conf_dir /usr/local/hive/conf Hive配置文件目录 上面主要配置了Hadoop集群相关的内容,以及Hive(beeswax段配置的是Hive,通过HIveServer2与Hive交互)。 最后,启动Hue服务,执行如下命令: 1 cd /usr/local/branch-3.7.1/ 2 build/env/bin/supervisor & Hue功能验证 我们主要通过在Hue Web控制台上执行Hive查询,所以需要准备Hive相关的表和数据。 Hive准备 我们首先在Hive中创建一个数据库(如果没有权限则授权): 1 GRANT ALL TO USER hadoop; 2 CREATE DATABASE user_db; 这里,hadoop用户是Hive的管理用户,可以将全部权限赋给该用户。 创建示例表,建表DDL如下所示: 01 CREATE TABLE user_db.daily_user_info ( 02 device_type int, 03 version string, 04 channel string, 05 udid string) 06 PARTITIONED BY ( 07 stat_date string) 08 ROW FORMAT DELIMITED 09 FIELDS TERMINATED BY '\t' 10 STORED AS INPUTFORMAT 11 'org.apache.hadoop.mapred.TextInputFormat' 12 OUTPUTFORMAT 13 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'; 准备的数据文件格式,示例如下所示: 1 0 3.2.1 C-gbnpk b01b8178b86cebb9fddc035bb238876d 2 0 3.0.7 A-wanglouko e2b7a3d8713d51c0215c3a4affacbc95 3 0 1.2.7 H-follower 766e7b2d2eedba2996498605fa03ed33 4 0 1.2.7 A-shiry d2924e24d9dbc887c3bea5a1682204d9 5 0 1.5.1 Z-wammer f880af48ba2567de0f3f9a6bb70fa962 6 0 1.2.7 H-clouda aa051d9e2accbae74004d761ec747110 7 0 2.2.13 H-clouda 02a32fd61c60dd2c5d9ed8a826c53be4 8 0 2.5.9 B-ywsy 04cc447ad65dcea5a131d5a993268edf 各个字段之间使用TAB分隔,每个字段含义与上面表user_db.daily_user_info的字段对应,然后我们将测试数据加载到示例表的各个分区之中: 1 LOAD DATA LOCAL INPATH '/home/hadoop/u2014-12-05.log' OVERWRITE INTO TABLEuser_db.daily_user_info PARTITION (stat_date='2014-12-05'); 2 LOAD DATA LOCAL INPATH '/home/hadoop/u2014-12-06.log' OVERWRITE INTO TABLEuser_db.daily_user_info PARTITION (stat_date='2014-12-06'); 3 LOAD DATA LOCAL INPATH '/home/hadoop/u2014-12-07.log' OVERWRITE INTO TABLEuser_db.daily_user_info PARTITION (stat_date='2014-12-07'); 4 LOAD DATA LOCAL INPATH '/home/hadoop/u2014-12-08.log' OVERWRITE INTO TABLEuser_db.daily_user_info PARTITION (stat_date='2014-12-08'); 5 LOAD DATA LOCAL INPATH '/home/hadoop/u2014-12-09.log' OVERWRITE INTO TABLEuser_db.daily_user_info PARTITION (stat_date='2014-12-09'); 6 LOAD DATA LOCAL INPATH '/home/hadoop/u2014-12-10.log' OVERWRITE INTO TABLEuser_db.daily_user_info PARTITION (stat_date='2014-12-10'); 7 LOAD DATA LOCAL INPATH '/home/hadoop/u2014-12-11.log' OVERWRITE INTO TABLEuser_db.daily_user_info PARTITION (stat_date='2014-12-11'); 可以通过Hive CLI接口登录,查看表中数据: 1 SELECT COUNT(1) FROM daily_user_info; 我这里有241709545条记录作为测试数据。 Hue登录页面 Hue服务启动成功后,可以直接通过浏览器打开连接http://10.10.4.125:8000/,就可以登录。第一次打开,需要输入默认用户和口令,然后就可以登录进去,如下图所示: 首次登录,选择使用的用户即为Hue管理员用户,权限很大,可以添加用户并管理用户及其用户组的操作权限。 Hue用户首页 登录成功以后,进入Hue Web控制台首页,如下图所示: 登录成功后,首先会执行一些基本环境的配置检查工作,它与我们实际修改配置时都指定了哪些应用有关系。 Hive查询编辑器页面 用户登录成功后,选择Query Editors下面的Hive菜单项,如图所示: 在提交查询的时候,由于该查询执行时间较长,可以等待查询执行,最后结果显示在的现房的Results标签页上,也可以在执行过程中查看Hive后台执行情况。 Job浏览器页面 通过Job浏览器(Job Browser)页面http://10.10.4.125:8000/jobbrowser/,可以查看运行在Hadoop集群上各种状态的Job,包括Succeeded、Running、Failed、Killed这4种状态,如图所示: 如果想要看到Job具体执行状态信息,需要正确配置并启动Hadoop集群的JobHistoryServer和WebAppProxyServer服务,可以通过Web页面看到相关数据,我们的示例,如图所示: 如果想看某个Job对应的MapTask或者ReduceTask执行情况,可以点击对应链接进去,和通过Hadoop YARN的Job Web管理界面类似,监控起来非常方便。 用户管理和授权认证 以授权管理员用户登录成功后,可以通过点击右上角用户(我这里是yanjun),下拉列表中有“Manage Users”菜单项,在这里面可以创建新用户,并指定访问权限,如下图所示: 上面,我创建了几个用户,并指定用户所属的组(Groups,支持组管理)。实际上,我们可以将不同的Hue应用设置为不同的组,然后将新建的用户分配到该相关组,通过这种方式可以控制用户访问Hue应用的权限。上面创建并分配权限的用户可以通过设置的用户名和口令登录Hue Web管理系统,与各种Hadoop相关的应用(不仅仅限于此,如MySQL、Spark等)进行交互。 总结 通过上面的了解,以及安装配置过程所遇到的问题,做一个总结: 如果基于CentOS环境安装配置Hue,可能相对复杂一点,不一定能够很容易的配置成功。我开始基于CentOS-5.11(Final)进行配置,没有配置成功,可能是使用的Hue的版本太高(branch-3.0和branch-3.7.1我都试过),或者可能是CentOS依赖的一些软件包无法安装等问题导致的。建议最好使用较新版本的CentOS,我这里使用的是CentOS-6.6 (Final),Hue使用的branch-3.7.1源码编译,并且Python版本需要2.6+。 使用Hue,我们可能会对用户管理及其权限分配也很感兴趣,所以数据存储,可以根据需要使用我们熟悉的其他关系数据库,如MySQL等,并做好备份,以防使用Hue应用的相关用户数据丢失,造成无法访问Hadoop集群等问题。需要修改Hue的配置文件,将默认存储方式sqlite3改成我们熟悉的关系数据库,目前支持MySQL、Postgresql,以及Oracle。 如果有必要,可能结合Hadoop集群底层的访问控制机制,如Kerberos,或者Hadoop SLA,配合Hue的用户管理和授权认证功能,更好地进行访问权限的约束和控制。 根据前面我们提到的Hue特性,我们可以根据自己实际的应用场景,来选择不同的Hue应用,通过这种插件式的配置来启动应用,通过Hue与其交互,如Oozie、Pig、Spark、HBase等等。 使用更低版本的Hive,如0.12,可能在验证过程中会遇到问题,可以根据Hive的版本来选择兼容版本的Hue来安装配置。 由于本次安装配置实践,并没有使用Cloudera发行的CDH软件包,如果使用CDH可能会更加顺利一些。
HDFS是一个分布式文件系统,在HDFS上写文件的过程与我们平时使用的单机文件系统非常不同,从宏观上来看,在HDFS文件系统上创建并写一个文件,流程如下图(来自《Hadoop:The Definitive Guide》一书)所示: 具体过程描述如下: Client调用DistributedFileSystem对象的create方法,创建一个文件输出流(FSDataOutputStream)对象 通过DistributedFileSystem对象与Hadoop集群的NameNode进行一次RPC远程调用,在HDFS的Namespace中创建一个文件条目(Entry),该条目没有任何的Block 通过FSDataOutputStream对象,向DataNode写入数据,数据首先被写入FSDataOutputStream对象内部的Buffer中,然后数据被分割成一个个Packet数据包 以Packet最小单位,基于Socket连接发送到按特定算法选择的HDFS集群中一组DataNode(正常是3个,可能大于等于1)中的一个节点上,在这组DataNode组成的Pipeline上依次传输Packet 这组DataNode组成的Pipeline反方向上,发送ack,最终由Pipeline中第一个DataNode节点将Pipeline ack发送给Client 完成向文件写入数据,Client在文件输出流(FSDataOutputStream)对象上调用close方法,关闭流 调用DistributedFileSystem对象的complete方法,通知NameNode文件写入成功 下面代码使用Hadoop的API来实现向HDFS的文件写入数据,同样也包括创建一个文件和写数据两个主要过程,代码如下所示: 01 static String[] contents = new String[] { 02 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 03 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 04 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", 05 "dddddddddddddddddddddddddddddddd", 06 "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", 07 }; 08 09 public static void main(String[] args) { 10 String file = "hdfs://h1:8020/data/test/test.log"; 11 Path path = new Path(file); 12 Configuration conf = new Configuration(); 13 FileSystem fs = null; 14 FSDataOutputStream output = null; 15 try { 16 fs = path.getFileSystem(conf); 17 output = fs.create(path); // 创建文件 18 for(String line : contents) { // 写入数据 19 output.write(line.getBytes("UTF-8")); 20 output.flush(); 21 } 22 } catch (IOException e) { 23 e.printStackTrace(); 24 } finally { 25 try { 26 output.close(); 27 } catch (IOException e) { 28 e.printStackTrace(); 29 } 30 } 31 } 结合上面的示例代码,我们先从fs.create(path);开始,可以看到FileSystem的实现DistributedFileSystem中给出了最终返回FSDataOutputStream对象的抽象逻辑,代码如下所示: 1 public FSDataOutputStream create(Path f, FsPermission permission, 2 boolean overwrite, 3 int bufferSize, short replication, long blockSize, 4 Progressable progress) throws IOException { 5 6 statistics.incrementWriteOps(1); 7 return new FSDataOutputStream 8 (dfs.create(getPathName(f), permission, overwrite, true, replication, blockSize, progress, bufferSize), statistics); 9 } 上面,DFSClient dfs的create方法中创建了一个OutputStream对象,在DFSClient的create方法: 01 public OutputStream create(String src, 02 FsPermission permission, 03 boolean overwrite, 04 boolean createParent, 05 short replication, 06 long blockSize, 07 Progressable progress, 08 int buffersize 09 ) throws IOException { 10 ... ... 11 } 创建了一个DFSOutputStream对象,如下所示: 1 final DFSOutputStream result = new DFSOutputStream(src, masked, 2 overwrite, createParent, replication, blockSize, progress, buffersize, 3 conf.getInt("io.bytes.per.checksum", 512)); 下面,我们从DFSOutputStream类开始,说明其内部实现原理。 DFSOutputStream内部原理 打开一个DFSOutputStream流,Client会写数据到流内部的一个缓冲区中,然后数据被分解成多个Packet,每个Packet大小为64k字节,每个Packet又由一组chunk和这组chunk对应的checksum数据组成,默认chunk大小为512字节,每个checksum是对512字节数据计算的校验和数据。 当Client写入的字节流数据达到一个Packet的长度,这个Packet会被构建出来,然后会被放到队列dataQueue中,接着DataStreamer线程会不断地从dataQueue队列中取出Packet,发送到复制Pipeline中的第一个DataNode上,并将该Packet从dataQueue队列中移到ackQueue队列中。ResponseProcessor线程接收从Datanode发送过来的ack,如果是一个成功的ack,表示复制Pipeline中的所有Datanode都已经接收到这个Packet,ResponseProcessor线程将packet从队列ackQueue中删除。 在发送过程中,如果发生错误,所有未完成的Packet都会从ackQueue队列中移除掉,然后重新创建一个新的Pipeline,排除掉出错的那些DataNode节点,接着DataStreamer线程继续从dataQueue队列中发送Packet。 下面是DFSOutputStream的结构及其原理,如图所示: 我们从下面3个方面来描述内部流程: 创建Packet Client写数据时,会将字节流数据缓存到内部的缓冲区中,当长度满足一个Chunk大小(512B)时,便会创建一个Packet对象,然后向该Packet对象中写Chunk Checksum校验和数据,以及实际数据块Chunk Data,校验和数据是基于实际数据块计算得到的。每次满足一个Chunk大小时,都会向Packet中写上述数据内容,直到达到一个Packet对象大小(64K),就会将该Packet对象放入到dataQueue队列中,等待DataStreamer线程取出并发送到DataNode节点。 发送Packet DataStreamer线程从dataQueue队列中取出Packet对象,放到ackQueue队列中,然后向DataNode节点发送这个Packet对象所对应的数据。 接收ack 发送一个Packet数据包以后,会有一个用来接收ack的ResponseProcessor线程,如果收到成功的ack,则表示一个Packet发送成功。如果成功,则ResponseProcessor线程会将ackQueue队列中对应的Packet删除。 DFSOutputStream初始化 首先看一下,DFSOutputStream的初始化过程,构造方法如下所示: 01 DFSOutputStream(String src, FsPermission masked, boolean overwrite, 02 boolean createParent, short replication, long blockSize, Progressable progress, 03 int buffersize, int bytesPerChecksum) throws IOException { 04 this(src, blockSize, progress, bytesPerChecksum, replication); 05 06 computePacketChunkSize(writePacketSize, bytesPerChecksum); // 默认 writePacketSize=64*1024(即64K),bytesPerChecksum=512(没512个字节计算一个校验和), 07 08 try { 09 if (createParent) { // createParent为true表示,如果待创建的文件的父级目录不存在,则自动创建 10 namenode.create(src, masked, clientName, overwrite, replication, blockSize); 11 } else { 12 namenode.create(src, masked, clientName, overwrite, false, replication, blockSize); 13 } 14 } catch(RemoteException re) { 15 throw re.unwrapRemoteException(AccessControlException.class, 16 FileAlreadyExistsException.class, 17 FileNotFoundException.class, 18 NSQuotaExceededException.class, 19 DSQuotaExceededException.class); 20 } 21 streamer.start(); // 启动一个DataStreamer线程,用来将写入的字节流打包成packet,然后发送到对应的Datanode节点上 22 } 23 上面computePacketChunkSize方法计算了一个packet的相关参数,我们结合代码来查看,如下所示: 24 int chunkSize = csize + checksum.getChecksumSize(); 25 int n = DataNode.PKT_HEADER_LEN + SIZE_OF_INTEGER; 26 chunksPerPacket = Math.max((psize - n + chunkSize-1)/chunkSize, 1); 27 packetSize = n + chunkSize*chunksPerPacket; 我们用默认的参数值替换上面的参数,得到: 1 int chunkSize = 512 + 4; 2 int n = 21 + 4; 3 chunksPerPacket = Math.max((64*1024 - 25 + 516-1)/516, 1); // 127 4 packetSize = 25 + 516*127; 上面对应的参数,说明如下表所示: 参数名称 参数值 参数含义 chunkSize 512+4=516 每个chunk的字节数(数据+校验和) csize 512 每个chunk数据的字节数 psize 64*1024 每个packet的最大字节数(不包含header) DataNode.PKT_HEADER_LEN 21 每个packet的header的字节数 chunksPerPacket 127 组成每个packet的chunk的个数 packetSize 25+516*127=65557 每个packet的字节数(一个header+一组chunk) 在计算好一个packet相关的参数以后,调用create方法与Namenode进行RPC请求,请求创建文件: 1 if (createParent) { // createParent为true表示,如果待创建的文件的父级目录不存在,则自动创建 2 namenode.create(src, masked, clientName, overwrite, replication, blockSize); 3 } else { 4 namenode.create(src, masked, clientName, overwrite, false, replication, blockSize); 5 } 远程调用上面方法,会在FSNamesystem中创建对应的文件路径,并初始化与该创建的文件相关的一些信息,如租约(向Datanode节点写数据的凭据)。文件在FSNamesystem中创建成功,就要初始化并启动一个DataStreamer线程,用来向Datanode写数据,后面我们详细说明具体处理逻辑。 Packet结构与定义 Client向HDFS写数据,数据会被组装成Packet,然后发送到Datanode节点。Packet分为两类,一类是实际数据包,另一类是heatbeat包。一个Packet数据包的组成结构,如图所示: 上图中,一个Packet是由Header和Data两部分组成,其中Header部分包含了一个Packet的概要属性信息,如下表所示: 字段名称 字段类型 字段长度 字段含义 pktLen int 4 4 + dataLen + checksumLen offsetInBlock long 8 Packet在Block中偏移量 seqNo long 8 Packet序列号,在同一个Block唯一 lastPacketInBlock boolean 1 是否是一个Block的最后一个Packet dataLen int 4 dataPos – dataStart,不包含Header和Checksum的长度 Data部分是一个Packet的实际数据部分,主要包括一个4字节校验和(Checksum)与一个Chunk部分,Chunk部分最大为512字节。 在构建一个Packet的过程中,首先将字节流数据写入一个buffer缓冲区中,也就是从偏移量为25的位置(checksumStart)开始写Packet数据的Chunk Checksum部分,从偏移量为533的位置(dataStart)开始写Packet数据的Chunk Data部分,直到一个Packet创建完成为止。如果一个Packet的大小未能达到最大长度,也就是上图对应的缓冲区中,Chunk Checksum与Chunk Data之间还保留了一段未被写过的缓冲区位置,这种情况说明,已经在写一个文件的最后一个Block的最后一个Packet。在发送这个Packet之前,会检查Chunksum与Chunk Data之间的缓冲区是否为空白缓冲区(gap),如果有则将Chunk Data部分向前移动,使得Chunk Data 1与Chunk Checksum N相邻,然后才会被发送到DataNode节点。 我们看一下Packet对应的Packet类定义,定义了如下一些字段: 01 ByteBuffer buffer; // only one of buf and buffer is non-null 02 byte[] buf; 03 long seqno; // sequencenumber of buffer in block 04 long offsetInBlock; // 该packet在block中的偏移量 05 boolean lastPacketInBlock; // is this the last packet in block? 06 int numChunks; // number of chunks currently in packet 07 int maxChunks; // 一个packet中包含的chunk的个数 08 int dataStart; 09 int dataPos; 10 int checksumStart; 11 int checksumPos; Packet类有一个默认的没有参数的构造方法,它是用来做heatbeat的,如下所示: 01 Packet() { 02 this.lastPacketInBlock = false; 03 this.numChunks = 0; 04 this.offsetInBlock = 0; 05 this.seqno = HEART_BEAT_SEQNO; // 值为-1 06 07 buffer = null; 08 int packetSize = DataNode.PKT_HEADER_LEN + SIZE_OF_INTEGER; // 21+4=25 09 buf = new byte[packetSize]; 10 11 checksumStart = dataStart = packetSize; 12 checksumPos = checksumStart; 13 dataPos = dataStart; 14 maxChunks = 0; 15 } 通过代码可以看到,一个heatbeat的内容,实际上只有一个长度为25字节的header数据。通过this.seqno = HEART_BEAT_SEQNO;的值可以判断一个packet是否是heatbeat包,如果seqno为-1表示这是一个heatbeat包。 Client发送Packet数据 可以DFSClient类中看到,发送一个Packet之前,首先需要向选定的DataNode发送一个Header数据包,表明要向DataNode写数据,该Header的数据结构,如图所示: 上图显示的是Client发送Packet到第一个DataNode节点的Header数据结构,主要包括待发送的Packet所在的Block(先向NameNode分配Block ID等信息)的相关信息、Pipeline中另外2个DataNode的信息、访问令牌(Access Token)和校验和信息,Header中各个字段及其类型,详见下表: 字段名称 字段类型 字段长度 字段含义 Transfer Version short 2 Client与DataNode之间数据传输版本号,由常量DataTransferProtocol.DATA_TRANSFER_VERSION定义,值为17 OP int 4 操作类型,由常量DataTransferProtocol.OP_WRITE_BLOCK定义,值为80 blkId long 8 Block的ID值,由NameNode分配 GS long 8 时间戳(Generation Stamp),NameNode分配blkId的时候生成的时间戳 DNCnt int 4 DataNode复制Pipeline中DataNode节点的数量 Recovery Flag boolean 1 Recover标志 Client Text Client主机的名称,在使用Text进行序列化的时候,实际包含长度len与主机名称字符串ClientHost srcNode boolean 1 是否发送src node的信息,默认值为false,不发送src node的信息 nonSrcDNCnt int 4 由Client写的该Header数据,该数不包含Pipeline中第一个节点(即为DNCnt-1) DN2 DatanodeInfo DataNode信息,包括StorageID、InfoPort、IpcPort、capacity、DfsUsed、remaining、LastUpdate、XceiverCount、Location、HostName、AdminState DN3 DatanodeInfo DataNode信息,包括StorageID、InfoPort、IpcPort、capacity、DfsUsed、remaining、LastUpdate、XceiverCount、Location、HostName、AdminState Access Token Token 访问令牌信息,包括IdentifierLength、Identifier、PwdLength、Pwd、KindLength、Kind、ServiceLength、Service CheckSum Header DataChecksum 1+4 校验和Header信息,包括type、bytesPerChecksum Header数据包发送成功,Client会收到一个成功响应码(DataTransferProtocol.OP_STATUS_SUCCESS = 0),接着将Packet数据发送到Pipeline中第一个DataNode上,如下所示: 1 Packet one = null; 2 one = dataQueue.getFirst(); // regular data packet 3 ByteBuffer buf = one.getBuffer(); 4 // write out data to remote datanode 5 blockStream.write(buf.array(), buf.position(), buf.remaining()); 6 7 if (one.lastPacketInBlock) { // 如果是Block中的最后一个Packet,还要写入一个0标识该Block已经写入完成 8 blockStream.writeInt(0); // indicate end-of-block 9 } 否则,如果失败,则会与NameNode进行RPC调用,删除该Block,并把该Pipeline中第一个DataNode加入到excludedNodes列表中,代码如下所示: 01 if (!success) { 02 LOG.info("Abandoning " + block); 03 namenode.abandonBlock(block, src, clientName); 04 05 if (errorIndex < nodes.length) { 06 LOG.info("Excluding datanode " + nodes[errorIndex]); 07 excludedNodes.add(nodes[errorIndex]); 08 } 09 10 // Connection failed. Let's wait a little bit and retry 11 retry = true; 12 } DataNode端服务组件 数据最终会发送到DataNode节点上,在一个DataNode上,数据在各个组件之间流动,流程如下图所示: DataNode服务中创建一个后台线程DataXceiverServer,它是一个SocketServer,用来接收来自Client(或者DataNode Pipeline中的非最后一个DataNode节点)的写数据请求,然后在DataXceiverServer中将连接过来的Socket直接派发给一个独立的后台线程DataXceiver进行处理。所以,Client写数据时连接一个DataNode Pipeline的结构,实际流程如图所示: 每个DataNode服务中的DataXceiver后台线程接收到来自前一个节点(Client/DataNode)的Socket连接,首先读取Header数据: 01 Block block = new Block(in.readLong(), dataXceiverServer.estimateBlockSize, in.readLong()); 02 LOG.info("Receiving " + block + " src: " + remoteAddress + " dest: " + localAddress); 03 int pipelineSize = in.readInt(); // num of datanodes in entire pipeline 04 boolean isRecovery = in.readBoolean(); // is this part of recovery? 05 String client = Text.readString(in); // working on behalf of this client 06 boolean hasSrcDataNode = in.readBoolean(); // is src node info present 07 if (hasSrcDataNode) { 08 srcDataNode = new DatanodeInfo(); 09 srcDataNode.readFields(in); 10 } 11 int numTargets = in.readInt(); 12 if (numTargets < 0) { 13 throw new IOException("Mislabelled incoming datastream."); 14 } 15 DatanodeInfo targets[] = new DatanodeInfo[numTargets]; 16 for (int i = 0; i < targets.length; i++) { 17 DatanodeInfo tmp = new DatanodeInfo(); 18 tmp.readFields(in); 19 targets[i] = tmp; 20 } 21 Token<BlockTokenIdentifier> accessToken = new Token<BlockTokenIdentifier>(); 22 accessToken.readFields(in); 上面代码中,读取Header的数据,与前一个Client/DataNode写入Header字段的顺序相对应,不再累述。在完成读取Header数据后,当前DataNode会首先将Header数据再发送到Pipeline中下一个DataNode结点,当然该DataNode肯定不是Pipeline中最后一个DataNode节点。接着,该DataNode会接收来自前一个Client/DataNode节点发送的Packet数据,接收Packet数据的逻辑实际上在BlockReceiver中完成,包括将来自前一个Client/DataNode节点发送的Packet数据写入本地磁盘。在BlockReceiver中,首先会将接收到的Packet数据发送写入到Pipeline中下一个DataNode节点,然后再将接收到的数据写入到本地磁盘的Block文件中。 DataNode持久化Packet数据 在DataNode节点的BlockReceiver中进行Packet数据的持久化,一个Packet是一个Block中一个数据分组,我们首先看一下,一个Block在持久化到磁盘上的物理存储结构,如下图所示: 每个Block文件(如上图中blk_1084013198文件)都对应一个meta文件(如上图中blk_1084013198_10273532.meta文件),Block文件是一个一个Chunk的二进制数据(每个Chunk的大小是512字节),而meta文件是与每一个Chunk对应的Checksum数据,是序列化形式存储。 写文件过程中Client/DataNode与NameNode进行RPC调用 Client在HDFS文件系统中写文件过程中,会发生多次与NameNode节点进行RPC调用来完成写数据相关操作,主要是在如下时机进行RPC调用: 写文件开始时创建文件:Client调用create在NameNode节点的Namespace中创建一个标识该文件的条目 在Client连接Pipeline中第一个DataNode节点之前,Client调用addBlock分配一个Block(blkId+DataNode列表+租约) 如果与Pipeline中第一个DataNode节点连接失败,Client调用abandonBlock放弃一个已经分配的Block 一个Block已经写入到DataNode节点磁盘,Client调用fsync让NameNode持久化Block的位置信息数据 文件写完以后,Client调用complete方法通知NameNode写入文件成功 DataNode节点接收到并成功持久化一个Block的数据后,DataNode调用blockReceived方法通知NameNode已经接收到Block 具体RPC调用的详细过程,可以参考源码。
Flume NG是一个分布式、可靠、可用的系统,它能够将不同数据源的海量日志数据进行高效收集、聚合、移动,最后存储到一个中心化数据存储系统中。由原来的Flume OG到现在的Flume NG,进行了架构重构,并且现在NG版本完全不兼容原来的OG版本。经过架构重构后,Flume NG更像是一个轻量的小工具,非常简单,容易适应各种方式日志收集,并支持failover和负载均衡。 架构设计要点 Flume的架构主要有一下几个核心概念: Event:一个数据单元,带有一个可选的消息头 Flow:Event从源点到达目的点的迁移的抽象 Client:操作位于源点处的Event,将其发送到Flume Agent Agent:一个独立的Flume进程,包含组件Source、Channel、Sink Source:用来消费传递到该组件的Event Channel:中转Event的一个临时存储,保存有Source组件传递过来的Event Sink:从Channel中读取并移除Event,将Event传递到Flow Pipeline中的下一个Agent(如果有的话) Flume NG架构,如图所示: 外部系统产生日志,直接通过Flume的Agent的Source组件将事件(如日志行)发送到中间临时的channel组件,最后传递给Sink组件,HDFS Sink组件可以直接把数据存储到HDFS集群上。 一个最基本Flow的配置,格式如下: 01 # list the sources, sinks and channels for the agent 02 <Agent>.sources = <Source1> <Source2> 03 <Agent>.sinks = <Sink1> <Sink2> 04 <Agent>.channels = <Channel1> <Channel2> 05 06 # set channel for source 07 <Agent>.sources.<Source1>.channels = <Channel1> <Channel2> ... 08 <Agent>.sources.<Source2>.channels = <Channel1> <Channel2> ... 09 10 # set channel for sink 11 <Agent>.sinks.<Sink1>.channel = <Channel1> 12 <Agent>.sinks.<Sink2>.channel = <Channel2> 尖括号里面的,我们可以根据实际需求或业务来修改名称。下面详细说明: 表示配置一个Agent的名称,一个Agent肯定有一个名称。与是Agent的Source组件的名称,消费传递过来的Event。与是Agent的Channel组件的名称。与是Agent的Sink组件的名称,从Channel中消费(移除)Event。 上面配置内容中,第一组中配置Source、Sink、Channel,它们的值可以有1个或者多个;第二组中配置Source将把数据存储(Put)到哪一个Channel中,可以存储到1个或多个Channel中,同一个Source将数据存储到多个Channel中,实际上是Replication;第三组中配置Sink从哪一个Channel中取(Task)数据,一个Sink只能从一个Channel中取数据。 下面,根据官网文档,我们展示几种Flow Pipeline,各自适应于什么样的应用场景: 多个Agent顺序连接 可以将多个Agent顺序连接起来,将最初的数据源经过收集,存储到最终的存储系统中。这是最简单的情况,一般情况下,应该控制这种顺序连接的Agent的数量,因为数据流经的路径变长了,如果不考虑failover的话,出现故障将影响整个Flow上的Agent收集服务。 多个Agent的数据汇聚到同一个Agent 这种情况应用的场景比较多,比如要收集Web网站的用户行为日志,Web网站为了可用性使用的负载均衡的集群模式,每个节点都产生用户行为日志,可以为每个节点都配置一个Agent来单独收集日志数据,然后多个Agent将数据最终汇聚到一个用来存储数据存储系统,如HDFS上。 多路(Multiplexing)Agent 这种模式,有两种方式,一种是用来复制(Replication),另一种是用来分流(Multiplexing)。Replication方式,可以将最前端的数据源复制多份,分别传递到多个channel中,每个channel接收到的数据都是相同的,配置格式,如下所示: 01 # List the sources, sinks and channels for the agent 02 <Agent>.sources = <Source1> 03 <Agent>.sinks = <Sink1> <Sink2> 04 <Agent>.channels = <Channel1> <Channel2> 05 06 # set list of channels for source (separated by space) 07 <Agent>.sources.<Source1>.channels = <Channel1> <Channel2> 08 09 # set channel for sinks 10 <Agent>.sinks.<Sink1>.channel = <Channel1> 11 <Agent>.sinks.<Sink2>.channel = <Channel2> 12 13 <Agent>.sources.<Source1>.selector.type = replicating 上面指定了selector的type的值为replication,其他的配置没有指定,使用的Replication方式,Source1会将数据分别存储到Channel1和Channel2,这两个channel里面存储的数据是相同的,然后数据被传递到Sink1和Sink2。 Multiplexing方式,selector可以根据header的值来确定数据传递到哪一个channel,配置格式,如下所示: 1 # Mapping for multiplexing selector 2 <Agent>.sources.<Source1>.selector.type = multiplexing 3 <Agent>.sources.<Source1>.selector.header = <someHeader> 4 <Agent>.sources.<Source1>.selector.mapping.<Value1> = <Channel1> 5 <Agent>.sources.<Source1>.selector.mapping.<Value2> = <Channel1> <Channel2> 6 <Agent>.sources.<Source1>.selector.mapping.<Value3> = <Channel2> 7 #... 8 9 <Agent>.sources.<Source1>.selector.default = <Channel2> 上面selector的type的值为multiplexing,同时配置selector的header信息,还配置了多个selector的mapping的值,即header的值:如果header的值为Value1、Value2,数据从Source1路由到Channel1;如果header的值为Value2、Value3,数据从Source1路由到Channel2。 实现load balance功能 Load balancing Sink Processor能够实现load balance功能,上图Agent1是一个路由节点,负责将Channel暂存的Event均衡到对应的多个Sink组件上,而每个Sink组件分别连接到一个独立的Agent上,示例配置,如下所示: 1 a1.sinkgroups = g1 2 a1.sinkgroups.g1.sinks = k1 k2 k3 3 a1.sinkgroups.g1.processor.type = load_balance 4 a1.sinkgroups.g1.processor.backoff = true 5 a1.sinkgroups.g1.processor.selector = round_robin 6 a1.sinkgroups.g1.processor.selector.maxTimeOut=10000 实现failover能 Failover Sink Processor能够实现failover功能,具体流程类似load balance,但是内部处理机制与load balance完全不同:Failover Sink Processor维护一个优先级Sink组件列表,只要有一个Sink组件可用,Event就被传递到下一个组件。如果一个Sink能够成功处理Event,则会加入到一个Pool中,否则会被移出Pool并计算失败次数,设置一个惩罚因子,示例配置如下所示: 1 a1.sinkgroups = g1 2 a1.sinkgroups.g1.sinks = k1 k2 k3 3 a1.sinkgroups.g1.processor.type = failover 4 a1.sinkgroups.g1.processor.priority.k1 = 5 5 a1.sinkgroups.g1.processor.priority.k2 = 7 6 a1.sinkgroups.g1.processor.priority.k3 = 6 7 a1.sinkgroups.g1.processor.maxpenalty = 20000 基本功能 我们看一下,Flume NG都支持哪些功能(目前最新版本是1.5.0.1),了解它的功能集合,能够让我们在应用中更好地选择使用哪一种方案。说明Flume NG的功能,实际还是围绕着Agent的三个组件Source、Channel、Sink来看它能够支持哪些技术或协议。我们不再对各个组件支持的协议详细配置进行说明,通过列表的方式分别对三个组件进行概要说明: Flume Source Source类型 说明 Avro Source 支持Avro协议(实际上是Avro RPC),内置支持 Thrift Source 支持Thrift协议,内置支持 Exec Source 基于Unix的command在标准输出上生产数据 JMS Source 从JMS系统(消息、主题)中读取数据,ActiveMQ已经测试过 Spooling Directory Source 监控指定目录内数据变更 Twitter 1% firehose Source 通过API持续下载Twitter数据,试验性质 Netcat Source 监控某个端口,将流经端口的每一个文本行数据作为Event输入 Sequence Generator Source 序列生成器数据源,生产序列数据 Syslog Sources 读取syslog数据,产生Event,支持UDP和TCP两种协议 HTTP Source 基于HTTP POST或GET方式的数据源,支持JSON、BLOB表示形式 Legacy Sources 兼容老的Flume OG中Source(0.9.x版本) Flume Channel Channel类型 说明 Memory Channel Event数据存储在内存中 JDBC Channel Event数据存储在持久化存储中,当前Flume Channel内置支持Derby File Channel Event数据存储在磁盘文件中 Spillable Memory Channel Event数据存储在内存中和磁盘上,当内存队列满了,会持久化到磁盘文件(当前试验性的,不建议生产环境使用) Pseudo Transaction Channel 测试用途 Custom Channel 自定义Channel实现 Flume Sink Sink类型 说明 HDFS Sink 数据写入HDFS Logger Sink 数据写入日志文件 Avro Sink 数据被转换成Avro Event,然后发送到配置的RPC端口上 Thrift Sink 数据被转换成Thrift Event,然后发送到配置的RPC端口上 IRC Sink 数据在IRC上进行回放 File Roll Sink 存储数据到本地文件系统 Null Sink 丢弃到所有数据 HBase Sink 数据写入HBase数据库 Morphline Solr Sink 数据发送到Solr搜索服务器(集群) ElasticSearch Sink 数据发送到Elastic Search搜索服务器(集群) Kite Dataset Sink 写数据到Kite Dataset,试验性质的 Custom Sink 自定义Sink实现 另外还有Channel Selector、Sink Processor、Event Serializer、Interceptor等组件,可以参考官网提供的用户手册。 应用实践 安装Flume NG非常简单,我们使用最新的1.5.0.1版本,执行如下命令: 1 cd /usr/local 2 wget http://mirror.bit.edu.cn/apache/flume/1.5.0.1/apache-flume-1.5.0.1-bin.tar.gz 3 tar xvzf apache-flume-1.5.0.1-bin.tar.gz 4 cd apache-flume-1.5.0.1-bin 如果需要使用到Hadoop集群,保证Hadoop相关的环境变量都已经正确配置,并且Hadoop集群可用。下面,通过一些实际的配置实例,来了解Flume的使用。为了简单期间,channel我们使用Memory类型的channel。 Avro Source+Memory Channel+Logger Sink 使用apache-flume-1.5.0.1自带的例子,使用Avro Source接收外部数据源,Logger作为sink,即通过Avro RPC调用,将数据缓存在channel中,然后通过Logger打印出调用发送的数据。 配置Agent,修改配置文件conf/flume-conf.properties,内容如下: 01 # Define a memory channel called ch1 on agent1 02 agent1.channels.ch1.type = memory 03 04 # Define an Avro source called avro-source1 on agent1 and tell it 05 # to bind to 0.0.0.0:41414. Connect it to channel ch1. 06 agent1.sources.avro-source1.channels = ch1 07 agent1.sources.avro-source1.type = avro 08 agent1.sources.avro-source1.bind = 0.0.0.0 09 agent1.sources.avro-source1.port = 41414 10 11 # Define a logger sink that simply logs all events it receives 12 # and connect it to the other end of the same channel. 13 agent1.sinks.log-sink1.channel = ch1 14 agent1.sinks.log-sink1.type = logger 15 16 # Finally, now that we've defined all of our components, tell 17 # agent1 which ones we want to activate. 18 agent1.channels = ch1 19 agent1.channels.ch1.capacity = 1000 20 agent1.sources = avro-source1 21 agent1.sinks = log-sink1 首先,启动Agent进程: 1 bin/flume-ng agent -c ./conf/ -f conf/flume-conf.properties -Dflume.root.logger=DEBUG,console -n agent1 然后,启动Avro Client,发送数据: 1 bin/flume-ng avro-client -c ./conf/ -H 0.0.0.0 -p 41414 -F /usr/local/programs/logs/sync.log -Dflume.root.logger=DEBUG,console Avro Source+Memory Channel+HDFS Sink 配置Agent,修改配置文件conf/flume-conf-hdfs.properties,内容如下: 01 # Define a source, channel, sink 02 agent1.sources = avro-source1 03 agent1.channels = ch1 04 agent1.sinks = hdfs-sink 05 06 # Configure channel 07 agent1.channels.ch1.type = memory 08 agent1.channels.ch1.capacity = 1000000 09 agent1.channels.ch1.transactionCapacity = 500000 10 11 # Define an Avro source called avro-source1 on agent1 and tell it 12 # to bind to 0.0.0.0:41414. Connect it to channel ch1. 13 agent1.sources.avro-source1.channels = ch1 14 agent1.sources.avro-source1.type = avro 15 agent1.sources.avro-source1.bind = 0.0.0.0 16 agent1.sources.avro-source1.port = 41414 17 18 # Define a logger sink that simply logs all events it receives 19 # and connect it to the other end of the same channel. 20 agent1.sinks.hdfs-sink1.channel = ch1 21 agent1.sinks.hdfs-sink1.type = hdfs 22 agent1.sinks.hdfs-sink1.hdfs.path = hdfs://h1:8020/data/flume/ 23 agent1.sinks.hdfs-sink1.hdfs.filePrefix = sync_file 24 agent1.sinks.hdfs-sink1.hdfs.fileSuffix = .log 25 agent1.sinks.hdfs-sink1.hdfs.rollSize = 1048576 26 agent1.sinks.hdfs-sink1.rollInterval = 0 27 agent1.sinks.hdfs-sink1.hdfs.rollCount = 0 28 agent1.sinks.hdfs-sink1.hdfs.batchSize = 1500 29 agent1.sinks.hdfs-sink1.hdfs.round = true 30 agent1.sinks.hdfs-sink1.hdfs.roundUnit = minute 31 agent1.sinks.hdfs-sink1.hdfs.threadsPoolSize = 25 32 agent1.sinks.hdfs-sink1.hdfs.useLocalTimeStamp = true 33 agent1.sinks.hdfs-sink1.hdfs.minBlockReplicas = 1 34 agent1.sinks.hdfs-sink1.fileType = SequenceFile 35 agent1.sinks.hdfs-sink1.writeFormat = TEXT 首先,启动Agent: 1 bin/flume-ng agent -c ./conf/ -f conf/flume-conf-hdfs.properties -Dflume.root.logger=INFO,console -n agent1 然后,启动Avro Client,发送数据: 1 bin/flume-ng avro-client -c ./conf/ -H 0.0.0.0 -p 41414 -F /usr/local/programs/logs/sync.log -Dflume.root.logger=DEBUG,console 可以查看同步到HDFS上的数据: 1 hdfs dfs -ls /data/flume 结果示例,如下所示: 1 -rw-r--r-- 3 shirdrn supergroup 1377617 2014-09-16 14:35 /data/flume/sync_file.1410849320761.log 2 -rw-r--r-- 3 shirdrn supergroup 1378137 2014-09-16 14:35 /data/flume/sync_file.1410849320762.log 3 -rw-r--r-- 3 shirdrn supergroup 259148 2014-09-16 14:35 /data/flume/sync_file.1410849320763.log Spooling Directory Source+Memory Channel+HDFS Sink 配置Agent,修改配置文件flume-conf-spool.properties,内容如下: 01 # Define source, channel, sink 02 agent1.sources = spool-source1 03 agent1.channels = ch1 04 agent1.sinks = hdfs-sink1 05 06 # Configure channel 07 agent1.channels.ch1.type = memory 08 agent1.channels.ch1.capacity = 1000000 09 agent1.channels.ch1.transactionCapacity = 500000 10 11 # Define and configure an Spool directory source 12 agent1.sources.spool-source1.channels = ch1 13 agent1.sources.spool-source1.type = spooldir 14 agent1.sources.spool-source1.spoolDir = /home/shirdrn/data/ 15 agent1.sources.spool-source1.ignorePattern = event(_\d{4}\-\d{2}\-\d{2}_\d{2}_\d{2})?\.log(\.COMPLETED)? 16 agent1.sources.spool-source1.batchSize = 50 17 agent1.sources.spool-source1.inputCharset = UTF-8 18 19 # Define and configure a hdfs sink 20 agent1.sinks.hdfs-sink1.channel = ch1 21 agent1.sinks.hdfs-sink1.type = hdfs 22 agent1.sinks.hdfs-sink1.hdfs.path = hdfs://h1:8020/data/flume/ 23 agent1.sinks.hdfs-sink1.hdfs.filePrefix = event_%y-%m-%d_%H_%M_%S 24 agent1.sinks.hdfs-sink1.hdfs.fileSuffix = .log 25 agent1.sinks.hdfs-sink1.hdfs.rollSize = 1048576 26 agent1.sinks.hdfs-sink1.hdfs.rollCount = 0 27 agent1.sinks.hdfs-sink1.hdfs.batchSize = 1500 28 agent1.sinks.hdfs-sink1.hdfs.round = true 29 agent1.sinks.hdfs-sink1.hdfs.roundUnit = minute 30 agent1.sinks.hdfs-sink1.hdfs.threadsPoolSize = 25 31 agent1.sinks.hdfs-sink1.hdfs.useLocalTimeStamp = true 32 agent1.sinks.hdfs-sink1.hdfs.minBlockReplicas = 1 33 agent1.sinks.hdfs-sink1.fileType = SequenceFile 34 agent1.sinks.hdfs-sink1.writeFormat = TEXT 35 agent1.sinks.hdfs-sink1.rollInterval = 0 启动Agent进程,执行如下命令: 1 bin/flume-ng agent -c ./conf/ -f conf/flume-conf-spool.properties -Dflume.root.logger=INFO,console -n agent1 可以查看HDFS上同步过来的数据: 1 hdfs dfs -ls /data/flume 结果示例,如下所示: 01 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:52 /data/flume/event_14-09-17_10_52_00.1410922355094.log 02 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:52 /data/flume/event_14-09-17_10_52_00.1410922355095.log 03 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:52 /data/flume/event_14-09-17_10_52_00.1410922355096.log 04 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:52 /data/flume/event_14-09-17_10_52_00.1410922355097.log 05 -rw-r--r-- 3 shirdrn supergroup 1530 2014-09-17 10:53 /data/flume/event_14-09-17_10_52_00.1410922355098.log 06 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:53 /data/flume/event_14-09-17_10_53_00.1410922380386.log 07 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:53 /data/flume/event_14-09-17_10_53_00.1410922380387.log 08 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:53 /data/flume/event_14-09-17_10_53_00.1410922380388.log 09 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:53 /data/flume/event_14-09-17_10_53_00.1410922380389.log 10 -rw-r--r-- 3 shirdrn supergroup 1072265 2014-09-17 10:53 /data/flume/event_14-09-17_10_53_00.1410922380390.log Exec Source+Memory Channel+File Roll Sink 配置Agent,修改配置文件flume-conf-file.properties,内容如下: 01 # Define source, channel, sink 02 agent1.sources = tail-source1 03 agent1.channels = ch1 04 agent1.sinks = file-sink1 05 06 # Configure channel 07 agent1.channels.ch1.type = memory 08 agent1.channels.ch1.capacity = 1000000 09 agent1.channels.ch1.transactionCapacity = 500000 10 11 # Define and configure an Exec source 12 agent1.sources.tail-source1.channels = ch1 13 agent1.sources.tail-source1.type = exec 14 agent1.sources.tail-source1.command = tail -F /home/shirdrn/data/event.log 15 agent1.sources.tail-source1.shell = /bin/sh -c 16 agent1.sources.tail-source1.batchSize = 50 17 18 # Define and configure a File roll sink 19 # and connect it to the other end of the same channel. 20 agent1.sinks.file-sink1.channel = ch1 21 agent1.sinks.file-sink1.type = file_roll 22 agent1.sinks.file-sink1.batchSize = 100 23 agent1.sinks.file-sink1.serializer = TEXT 24 agent1.sinks.file-sink1.sink.directory = /home/shirdrn/sink_data 启动Agent进程,执行如下命令: 1 bin/flume-ng agent -c ./conf/ -f conf/flume-conf-file.properties -Dflume.root.logger=INFO,console -n agent1 可以查看File Roll Sink对应的本地文件系统目录/home/shirdrn/sink_data下,示例如下所示: 1 -rw-rw-r-- 1 shirdrn shirdrn 13944825 Sep 17 11:36 1410924990039-1 2 -rw-rw-r-- 1 shirdrn shirdrn 11288870 Sep 17 11:37 1410924990039-2 3 -rw-rw-r-- 1 shirdrn shirdrn 0 Sep 17 11:37 1410924990039-3 4 -rw-rw-r-- 1 shirdrn shirdrn 20517500 Sep 17 11:38 1410924990039-4 5 -rw-rw-r-- 1 shirdrn shirdrn 16343250 Sep 17 11:38 1410924990039-5 有关Flume NG更多配置及其说明,请参考官方用户手册,非常详细。
在使用Java读取一个文件系统中的一个文件时,我们会首先构造一个DataInputStream对象,然后就能够从文件中读取数据。对于存储在HDFS上的文件,也对应着类似的工具类,但是底层的实现逻辑却是非常不同的。我们先从使用DFSClient.DFSDataInputStream类来读取HDFS上一个文件的一段代码来看,如下所示: 01 package org.shirdrn.hadoop.hdfs; 02 03 import java.io.BufferedReader; 04 import java.io.IOException; 05 import java.io.InputStreamReader; 06 07 import org.apache.hadoop.conf.Configuration; 08 import org.apache.hadoop.fs.FSDataInputStream; 09 import org.apache.hadoop.fs.FileSystem; 10 import org.apache.hadoop.fs.Path; 11 12 public class HdfsFileReader { 13 14 public static void main(String[] args) { 15 String file = "hdfs://hadoop-cluster-m:8020/data/logs/basis_user_behavior/201405071237_10_10_1_73.log"; 16 Path path = new Path(file); 17 18 Configuration conf = new Configuration(); 19 FileSystem fs; 20 FSDataInputStream in; 21 BufferedReader reader = null; 22 try { 23 fs = FileSystem.get(conf); 24 in = fs.open(path); // 打开文件path,返回一个FSDataInputStream流对象 25 reader = new BufferedReader(new InputStreamReader(in)); 26 String line = null; 27 while((line = reader.readLine()) != null) { // 读取文件行内容 28 System.out.println("Record: " + line); 29 } 30 } catch (IOException e) { 31 e.printStackTrace(); 32 } finally { 33 try { 34 if(reader != null) reader.close(); 35 } catch (IOException e) { 36 e.printStackTrace(); 37 } 38 } 39 } 40 41 } 基于上面代码,我们可以看到,通过一个FileSystem对象可以打开一个Path文件,返回一个FSDataInputStream文件输入流对象,然后从该FSDataInputStream对象就能够读取出文件的内容。所以,我们从FSDataInputStream入手,详细分析从HDFS读取文件内容的过程,在实际地读取物理数据块之前,首先要获取到文件对应的Block列表元数据信息,整体流程如下图所示: 下面,详细说明整个流程: 创建FSDataInputStream流对象 从一个Path路径对象,能够获取到一个FileSystem对象,然后通过调用FileSystem的open方法打开一个文件流: 1 public FSDataInputStream open(Path f) throws IOException { 2 return open(f, getConf().getInt("io.file.buffer.size", 4096)); 3 } 由于FileSystem是抽象类,将具体的打开操作留给具体子类实现,例如FTPFileSystem、HarFileSystem、WebHdfsFileSystem等,不同的文件系统具有不同打开文件的行为,我们以DistributedFileSystem为例,open方法实现,代码如下所示: 1 public FSDataInputStream open(Path f, int bufferSize) throws IOException { 2 statistics.incrementReadOps(1); 3 return new DFSClient.DFSDataInputStream( 4 dfs.open(getPathName(f), bufferSize, verifyChecksum, statistics)); 5 } statistics对象用来收集文件系统操作的统计数据,这里使读取文件操作的计数器加1。然后创建了一个DFSClient.DFSDataInputStream对象,该对象的参数是通过DFSClient dfs客户端对象打开一个这个文件从而返回一个DFSInputStream对象,下面,我们看DFSClient的open方法实现,代码如下所示: 1 public DFSInputStream open(String src, int buffersize, boolean verifyChecksum, 2 FileSystem.Statistics stats) throws IOException { 3 checkOpen(); 4 // Get block info from namenode 5 return new DFSInputStream(src, buffersize, verifyChecksum); 6 } checkOpen方法就是检查一个标志位clientRunning,表示当前的dfs客户端对象是否已经创建并初始化,在dfs客户端创建的时候该标志就为true,表示客户端正在运行状态。我们知道,当客户端DFSClient连接到Namenode的时候,实际上是创建了一个到Namenode的RPC连接,Namenode作为Server角色,DFSClient作为Client角色,它们之间建立起Socket连接。只有显式调用DFSClient的close方法时,才会修改clientRunning的值为false,实际上真正地关闭了已经建立的RPC连接。 我们看一下创建DFSInputStream的构造方法实现: 1 DFSInputStream(String src, int buffersize, boolean verifyChecksum) throws IOException { 2 this.verifyChecksum = verifyChecksum; 3 this.buffersize = buffersize; 4 this.src = src; 5 prefetchSize = conf.getLong("dfs.read.prefetch.size", prefetchSize); 6 openInfo(); 7 } 先设置了几个与读取文件相关的参数值,这里有一个预先读取文件的Block字节数的参数prefetchSize,它的值设置如下: 1 public static final long DEFAULT_BLOCK_SIZE = DFSConfigKeys.DFS_BLOCK_SIZE_DEFAULT; 2 public static final long DFS_BLOCK_SIZE_DEFAULT = 64*1024*1024; 3 4 defaultBlockSize = conf.getLong("dfs.block.size", DEFAULT_BLOCK_SIZE); 5 private long prefetchSize = 10 * defaultBlockSize; 这个prefetchSize的值默认为10*64*1024*1024=671088640,也就是说,默认预读取一个文件的10个块,即671088640B=640M,如果想要修改这个值,设置dfs.block.size即可覆盖默认值。 然后调用了openInfo方法,从Namenode获取到该打开文件的信息,在openInfo方法中,具体实现如下所示: 01 synchronized void openInfo() throws IOException { 02 for (int retries = 3; retries > 0; retries--) { 03 if (fetchLocatedBlocks()) { // fetch block success. 如果成功获取到待读取文件对应的Block列表,则直接返回 04 return; 05 } else { 06 // Last block location unavailable. When a cluster restarts, 07 // DNs may not report immediately. At this time partial block 08 // locations will not be available with NN for getting the length. 09 // Lets retry a few times to get the length. 10 DFSClient.LOG.warn("Last block locations unavailable. " 11 + "Datanodes might not have reported blocks completely." 12 + " Will retry for " + retries + " times"); 13 waitFor(4000); 14 } 15 } 16 throw new IOException("Could not obtain the last block locations."); 17 } 上述代码中,有一个for循环用来获取Block列表。如果成功获取到待读取文件的Block列表,则直接返回,否则,最多执行3次等待重试操作(最多花费时间大于12秒)。未能成功读取文件的Block列表信息,是因为Namenode无法获取到文件对应的块列表的信息,当整个集群启动的时候,Datanode会主动向NNamenode上报对应的Block信息,只有Block Report完成之后,Namenode就能够知道组成文件的Block及其所在Datanode列表的信息。openInfo方法方法中调用了fetchLocatedBlocks方法,用来与Namenode进行RPC通信调用,实际获取对应的Block列表,实现代码如下所示: 01 private boolean fetchLocatedBlocks() throws IOException, 02 FileNotFoundException { 03 LocatedBlocks newInfo = callGetBlockLocations(namenode, src, 0, prefetchSize); 04 if (newInfo == null) { 05 throw new FileNotFoundException("File does not exist: " + src); 06 } 07 08 if (locatedBlocks != null && !locatedBlocks.isUnderConstruction() && !newInfo.isUnderConstruction()) { 09 Iterator<LocatedBlock> oldIter = locatedBlocks.getLocatedBlocks().iterator(); 10 Iterator<LocatedBlock> newIter = newInfo.getLocatedBlocks().iterator(); 11 while (oldIter.hasNext() && newIter.hasNext()) { 12 if (!oldIter.next().getBlock().equals(newIter.next().getBlock())) { 13 throw new IOException("Blocklist for " + src + " has changed!"); 14 } 15 } 16 } 17 boolean isBlkInfoUpdated = updateBlockInfo(newInfo); 18 this.locatedBlocks = newInfo; 19 this.currentNode = null; 20 return isBlkInfoUpdated; 21 } 调用callGetBlockLocations方法,实际上是根据创建RPC连接以后得到的Namenode的代理对象,调用Namenode来获取到指定文件的Block的位置信息(位于哪些Datanode节点上):namenode.getBlockLocations(src, start, length)。调用callGetBlockLocations方法返回一个LocatedBlocks对象,该对象包含了文件长度信息、List blocks列表对象,其中LocatedBlock包含了一个Block的基本信息: 1 private Block b; 2 private long offset; // offset of the first byte of the block in the file 3 private DatanodeInfo[] locs; 4 private boolean corrupt; 有了这些文件的信息(文件长度、文件包含的Block的位置等信息),DFSClient就能够执行后续读取文件数据的操作了,详细过程我们在后面分析说明。 通过Namenode获取文件信息 上面,我们提到获取一个文件的基本信息,是通过Namenode来得到的,这里详细分析Namenode是如何获取到这些文件信息的,实现方法getBlockLocations的代码,如下所示: 1 public LocatedBlocks getBlockLocations(String src, long offset, long length) throwsIOException { 2 myMetrics.incrNumGetBlockLocations(); 3 return namesystem.getBlockLocations(getClientMachine(), src, offset, length); 4 } 可以看到,Namenode又委托管理HDFS name元数据的FSNamesystem的getBlockLocations方法实现: 01 LocatedBlocks getBlockLocations(String clientMachine, String src, long offset, longlength) throws IOException { 02 LocatedBlocks blocks = getBlockLocations(src, offset, length, true, true, true); 03 if (blocks != null) { 04 //sort the blocks 05 // In some deployment cases, cluster is with separation of task tracker 06 // and datanode which means client machines will not always be recognized 07 // as known data nodes, so here we should try to get node (but not 08 // datanode only) for locality based sort. 09 Node client = host2DataNodeMap.getDatanodeByHost(clientMachine); 10 if (client == null) { 11 List<String> hosts = new ArrayList<String> (1); 12 hosts.add(clientMachine); 13 String rName = dnsToSwitchMapping.resolve(hosts).get(0); 14 if (rName != null) 15 client = new NodeBase(clientMachine, rName); 16 } 17 18 DFSUtil.StaleComparator comparator = null; 19 if (avoidStaleDataNodesForRead) { 20 comparator = new DFSUtil.StaleComparator(staleInterval); 21 } 22 // Note: the last block is also included and sorted 23 for (LocatedBlock b : blocks.getLocatedBlocks()) { 24 clusterMap.pseudoSortByDistance(client, b.getLocations()); 25 if (avoidStaleDataNodesForRead) { 26 Arrays.sort(b.getLocations(), comparator); 27 } 28 } 29 } 30 return blocks; 31 } 跟踪代码,最终会在下面的方法中实现了,如何获取到待读取文件的Block的元数据列表,以及如何取出该文件的各个Block的数据,方法实现代码,这里我做了详细的注释,可以参考,如下所示: 01 private synchronized LocatedBlocks getBlockLocationsInternal(String src, 02 long offset, 03 long length, 04 int nrBlocksToReturn, 05 boolean doAccessTime, 06 boolean needBlockToken) 07 throws IOException { 08 INodeFile inode = dir.getFileINode(src); // 获取到与待读取文件相关的inode数据 09 if (inode == null) { 10 return null; 11 } 12 if (doAccessTime && isAccessTimeSupported()) { 13 dir.setTimes(src, inode, -1, now(), false); 14 } 15 Block[] blocks = inode.getBlocks(); // 获取到文件src所包含的Block的元数据列表信息 16 if (blocks == null) { 17 return null; 18 } 19 if (blocks.length == 0) { // 获取到文件src的Block数,这里=0,该文件的Block数据还没创建,可能正在创建 20 return inode.createLocatedBlocks(new ArrayList<LocatedBlock>(blocks.length)); 21 } 22 List<LocatedBlock> results; 23 results = new ArrayList<LocatedBlock>(blocks.length); 24 25 int curBlk = 0; // 当前Block在Block[] blocks数组中的索引位置 26 long curPos = 0, blkSize = 0; // curPos表示某个block在文件中的字节偏移量,blkSize为Block的大小(字节数) 27 int nrBlocks = (blocks[0].getNumBytes() == 0) ? 0 : blocks.length; // 获取到文件src的Block数,实际上一定>0,但是第一个block大小可能为0,这种情况认为nrBlocks=0 28 for (curBlk = 0; curBlk < nrBlocks; curBlk++) { // 根据前面代码,我们知道offset=0,所以这个循环第一次进来肯定就break出去了(正常的话,blkSize>0,所以我觉得这段代码写的稍微有点晦涩) 29 blkSize = blocks[curBlk].getNumBytes(); 30 assert blkSize > 0 : "Block of size 0"; 31 if (curPos + blkSize > offset) { 32 break; 33 } 34 curPos += blkSize; 35 } 36 37 if (nrBlocks > 0 && curBlk == nrBlocks) // offset >= end of file, 到这里curBlk=0,如果从文件src的第一个Block的字节数累加计算,知道所有的Block的字节数都累加上了,总字节数仍然<=请求的offset,说明即使到了文件尾部,仍然没有达到offset的值。从前面fetchLocatedBlocks()方法中调用我们知道,offset=0,所以执行该分支表示文件src没有可用的Block数据块可读 38 return null; 39 40 long endOff = offset + length; // 41 42 do { 43 // 获取Block所在位置(Datanode节点) 44 int numNodes = blocksMap.numNodes(blocks[curBlk]); // 计算文件src中第curBlk个Block存储在哪些Datanode节点上 45 int numCorruptNodes = countNodes(blocks[curBlk]).corruptReplicas(); // 计算存储文件src中第curBlk个Block但无法读取该Block的Datanode节点数 46 int numCorruptReplicas = corruptReplicas.numCorruptReplicas(blocks[curBlk]); // 计算FSNamesystem在内存中维护的Block=>Datanode映射的列表中,无法读取该Block的Datanode节点数 47 if (numCorruptNodes != numCorruptReplicas) { 48 LOG.warn("Inconsistent number of corrupt replicas for " 49 + blocks[curBlk] + "blockMap has " + numCorruptNodes 50 + " but corrupt replicas map has " + numCorruptReplicas); 51 } 52 DatanodeDescriptor[] machineSet = null; // 下面的if...else用来获取一个Block所在的Datanode节点 53 boolean blockCorrupt = false; 54 if (inode.isUnderConstruction() && curBlk == blocks.length - 1 55 && blocksMap.numNodes(blocks[curBlk]) == 0) { // 如果文件正在创建,当前blocks[curBlk]还没有创建成功(即没有可用的Datanode可以提供该Block的服务),仍然返回待创建Block所在的Datanode节点列表。数据块是在Datanode上存储的,只要Datanode完成数据块的存储后,通过heartbeat将数据块的信息上报给Namenode后,这些信息才会存储到blocksMap中 56 // get unfinished block locations 57 INodeFileUnderConstruction cons = (INodeFileUnderConstruction) inode; 58 machineSet = cons.getTargets(); 59 blockCorrupt = false; 60 } else { // 文件已经创建完成 61 blockCorrupt = (numCorruptNodes == numNodes); // 是否当前的Block在所有Datanode节点上的副本都坏掉,无法提供服务 62 int numMachineSet = blockCorrupt ? numNodes : (numNodes - numCorruptNodes); // 如果是,则返回所有Datanode节点,否则,只返回可用的Block副本所在的Datanode节点 63 machineSet = new DatanodeDescriptor[numMachineSet]; 64 if (numMachineSet > 0) { // 获取到当前Block所有副本所在的Datanode节点列表 65 numNodes = 0; 66 for (Iterator<DatanodeDescriptor> it = blocksMap.nodeIterator(blocks[curBlk]); it.hasNext();) { 67 DatanodeDescriptor dn = it.next(); 68 boolean replicaCorrupt = corruptReplicas.isReplicaCorrupt(blocks[curBlk], dn); 69 if (blockCorrupt || (!blockCorrupt && !replicaCorrupt)) 70 machineSet[numNodes++] = dn; 71 } 72 } 73 } 74 LocatedBlock b = new LocatedBlock(blocks[curBlk], machineSet, curPos, blockCorrupt); // 创建一个包含Block的元数据对象、所在Datanode节点列表、起始索引位置(字节数)、健康状况的LocatedBlock对象 75 if (isAccessTokenEnabled && needBlockToken) { // 如果启用Block级的令牌(Token)访问,则为当前用户生成读模式的令牌信息,一同封装到返回的LocatedBlock对象中 76 b.setBlockToken(accessTokenHandler.generateToken(b.getBlock(), EnumSet.of(BlockTokenSecretManager.AccessMode.READ))); 77 } 78 79 results.add(b); // 收集待返回给读取文件的客户端需要的LocatedBlock列表 80 curPos += blocks[curBlk].getNumBytes(); 81 curBlk++; 82 } while (curPos < endOff && curBlk < blocks.length && results.size() < nrBlocksToReturn); 83 84 return inode.createLocatedBlocks(results); // 将收集的LocatedBlock列表数据封装到一个LocatedBlocks对象中返回 85 } 我们可以看一下,最后的调用inode.createLocatedBlocks(results)生成LocatedBlocks对象的实现,代码如下所示: 1 LocatedBlocks createLocatedBlocks(List<LocatedBlock> blocks) { 2 return new LocatedBlocks(computeContentSummary().getLength(), blocks, isUnderConstruction()); // 通过ContentSummary对象获取到文件的长度 3 } 客户端通过RPC调用,获取到了文件对应的Block以及所在Datanode列表的信息,然后就可以根据LocatedBlocks来进一步获取到对应的Block对应的物理数据块。 对Block列表进行排序 我们再回到FSNamesystem类,调用getBlockLocationsInternal方法的getBlockLocations方法中,在返回文件block列表LocatedBlocks之后,会对每一个Block所在的Datanode进行的一个排序,排序的基本规则有如下2点: Client到Block所在的Datanode的距离最近,这个是通过网络拓扑关系来进行计算,例如Client的网络路径为/dc1/r1/c1,那么路径为/dc1/r1/dn1的Datanode就比路径为/dc1/r2/dn2的距离小,/dc1/r1/dn1对应的Block就会排在前面 从上面一点可以推出,如果Client就是某个Datanode,恰好某个Block的Datanode列表中包括该Datanode,则该Datanode对应的Block排在前面 Block所在的Datanode列表中,如果其中某个Datanode在指定的时间内没有向Namenode发送heartbeat(默认由常量DFSConfigKeys.DFS_NAMENODE_STALE_DATANODE_INTERVAL_DEFAULT定义,默认值为30s),则该Datanode的状态即为STALE,具有该状态的Datanode对应的Block排在后面 基于上述规则排序后,Block列表返回到Client。 Client与Datanode交互更新文件Block列表 我们要回到前面分析的DFSClient.DFSInputStream.fetchLocatedBlocks()方法中,查看在调用该方法之后,是如何执行实际处理逻辑的: 01 private boolean fetchLocatedBlocks() throws IOException, 02 FileNotFoundException { 03 LocatedBlocks newInfo = callGetBlockLocations(namenode, src, 0, prefetchSize); // RPC调用向Namenode获取待读取文件对应的Block及其位置信息LocatedBlocks对象 04 if (newInfo == null) { 05 throw new FileNotFoundException("File does not exist: " + src); 06 } 07 08 if (locatedBlocks != null && !locatedBlocks.isUnderConstruction() && !newInfo.isUnderConstruction()) { // 这里面locatedBlocks!=null是和后面调用updateBlockInfo方法返回的状态有关的 09 Iterator<LocatedBlock> oldIter = locatedBlocks.getLocatedBlocks().iterator(); 10 Iterator<LocatedBlock> newIter = newInfo.getLocatedBlocks().iterator(); 11 while (oldIter.hasNext() && newIter.hasNext()) { // 检查2次获取到的LocatedBlock列表:第2次得到newInfo包含的Block列表,在第2次得到的locatedBlocks中是否发生变化,如果发生了变化,则不允许读取,抛出异常 12 if (!oldIter.next().getBlock().equals(newIter.next().getBlock())) { 13 throw new IOException("Blocklist for " + src + " has changed!"); 14 } 15 } 16 } 17 boolean isBlkInfoUpdated = updateBlockInfo(newInfo); 18 this.locatedBlocks = newInfo; 19 this.currentNode = null; 20 return isBlkInfoUpdated; 21 } 如果第一次读取该文件时,已经获取到了对应的block列表,缓存在客户端;如果客户端第二次又读取了该文件,仍然获取到一个block列表对象。在两次读取之间,可能存在原文件完全被重写的情况,所以新得到的block列表与原列表完全不同了,存在这种情况,客户端直接抛出IO异常,如果原文件对应的block列表没有变化,则更新客户端缓存的对应block列表信息。 当集群重启的时候(如果允许安全模式下读文件),或者当一个文件正在创建的时候,Datanode向Namenode进行Block Report,这个过程中可能Namenode还没有完全重建好Block到Datanode的映射关系信息,所以即使在这种情况下,仍然会返回对应的正在创建的Block所在的Datanode列表信息,可以从前面getBlockLocationsInternal方法中看到,INode的对应UnderConstruction状态为true。这时,一个Block对应的所有副本中的某些可能还在创建过程中。 上面方法中,调用updateBlockInfo来更新文件的Block元数据列表信息,对于文件的某些Block可能没有创建完成,所以Namenode所保存的关于文件的Block的的元数据信息可能没有及时更新(Datanode可能还没有完成Block的报告),代码实现如下所示: 01 private boolean updateBlockInfo(LocatedBlocks newInfo) throws IOException { 02 if (!serverSupportsHdfs200 || !newInfo.isUnderConstruction() || !(newInfo.locatedBlockCount() > 0)) { // 如果获取到的newInfo可以读取文件对应的Block信息,则返回true 03 return true; 04 } 05 06 LocatedBlock last = newInfo.get(newInfo.locatedBlockCount() - 1); // 从Namenode获取文件的最后一个Block的元数据对象LocatedBlock 07 boolean lastBlockInFile = (last.getStartOffset() + last.getBlockSize() == newInfo.getFileLength()); 08 if (!lastBlockInFile) { // 如果“文件长度 != 最后一个块起始偏移量 + 最后一个块长度”,说明文件对应Block的元数据信息还没有更新,但是仍然返回给读取文件的该客户端 09 return true; 10 } 11 // 这时,已经确定last是该文件的最后一个bolck,检查最后个block的存储位置信息 12 if (last.getLocations().length == 0) { 13 return false; 14 } 15 16 ClientDatanodeProtocol primary = null; 17 Block newBlock = null; 18 for (int i = 0; i < last.getLocations().length && newBlock == null; i++) { // 根据从Namenode获取到的LocatedBlock last中对应的Datanode列表信息,Client与Datanode建立RPC连接,获取最后一个Block的元数据 19 DatanodeInfo datanode = last.getLocations()[i]; 20 try { 21 primary = createClientDatanodeProtocolProxy(datanode, conf, last .getBlock(), last.getBlockToken(), socketTimeout, connectToDnViaHostname); 22 newBlock = primary.getBlockInfo(last.getBlock()); 23 } catch (IOException e) { 24 if (e.getMessage().startsWith( 25 "java.io.IOException: java.lang.NoSuchMethodException: " 26 + "org.apache.hadoop.hdfs.protocol" 27 + ".ClientDatanodeProtocol.getBlockInfo")) { 28 // We're talking to a server that doesn't implement HDFS-200. 29 serverSupportsHdfs200 = false; 30 } else { 31 LOG.info("Failed to get block info from " 32 + datanode.getHostName() + " probably does not have " 33 + last.getBlock(), e); 34 } 35 } finally { 36 if (primary != null) { 37 RPC.stopProxy(primary); 38 } 39 } 40 } 41 42 if (newBlock == null) { // Datanode上不存在最后一个Block对应的元数据信息,直接返回 43 if (!serverSupportsHdfs200) { 44 return true; 45 } 46 throw new IOException("Failed to get block info from any of the DN in pipeline: "+ Arrays.toString(last.getLocations())); 47 } 48 49 long newBlockSize = newBlock.getNumBytes(); 50 long delta = newBlockSize - last.getBlockSize(); 51 // 对于文件的最后一个Block,如果从Namenode获取到的元数据,与从Datanode实际获取到的元数据不同,则以Datanode获取的为准,因为可能Datanode还没有及时将Block的变化信息向Namenode汇报 52 last.getBlock().setNumBytes(newBlockSize); 53 long newlength = newInfo.getFileLength() + delta; 54 newInfo.setFileLength(newlength); // 修改文件Block和位置元数据列表信息 55 LOG.debug("DFSClient setting last block " + last + " to length " + newBlockSize + " filesize is now " + newInfo.getFileLength()); 56 return true; 57 } 我们看一下,在updateBlockInfo方法中,返回false的情况:Client向Namenode发起的RPC请求,已经获取到了组成该文件的数据块的元数据信息列表,但是,文件的最后一个数据块的存储位置信息无法获取到,说明Datanode还没有及时通过block report将数据块的存储位置信息报告给Namenode。通过在openInfo()方法中可以看到,获取文件的block列表信息有3次重试机会,也就是调用updateBlockInfo方法返回false,可以有12秒的时间,等待Datanode向Namenode汇报文件的最后一个块的位置信息,以及Namenode更新内存中保存的文件对应的数据块列表元数据信息。 我们再看一下,在updateBlockInfo方法中,返回true的情况: 文件已经创建完成,文件对应的block列表元数据信息可用 文件正在创建中,但是当前能够读取到的已经完成的最后一个块(非组成文件的最后一个block)的元数据信息可用 文件正在创建中,文件的最后一个block的元数据部分可读:从Namenode无法获取到该block对应的位置信息,这时Client会与Datanode直接进行RPC通信,获取到该文件最后一个block的位置信息 上面Client会与Datanode直接进行RPC通信,获取文件最后一个block的元数据,这时可能由于网络问题等等,无法得到文件最后一个block的元数据,所以也会返回true,也就是说,Client仍然可以读取该文件,只是无法读取到最后一个block的数据。 这样,在Client从Namenode/Datanode获取到的文件的Block列表元数据已经是可用的信息,可以根据这些信息读取到各个Block的物理数据块内容了,准确地说,应该是文件处于打开状态了,已经准备好后续进行的读操作了。
Shark(Hive on Spark)是UC Lab为Spark设计并开源的一款数据仓库系统,提供了分布式SQL查询引擎,它能够完全兼容Hive。首先,我们通过下面的图,看一下Shark与Hive的关系(http://shark.cs.berkeley.edu/img/shark-hive-integration.png):以前我们使用Hive分析HDFS中数据时,通过将HQL翻译成MapReduce作业(Job)在Hadoop集群上运行;而使用Shark可以像使用Hive一样容易,如HQL、Metastore、序列化格式、UDF等Shark都支持,不同的是Shark运行在Spark集群上执行计算,基于Spark系统所使用的RDD模型。官方文档给出的性能方面的数据是,使用Shark查询分析HDFS数据,能比Hive快30多倍,如图所示(http://shark.cs.berkeley.edu/img/perf.png): 下面,我们通过安装配置Shark来简单地体验一下。 准备软件包 jdk-7u25-linux-x64.tar.gz scala-2.10.3.tgz apache-maven-3.2.1-bin.tar.gz hadoop-1.2.1.tar.gz spark-0.9.0-incubating-bin-hadoop1.tgz hive-0.11-shark-0.9.0.tar.gz 环境变量配置 针对上述准备软件包,我们需要安装配置好JDK、Scala环境,保证Hadoop集群能够正常启动运行,同时Hive也能够执行正确地查询分析工作 01 export JAVA_HOME=/usr/java/jdk1.7.0_25/ 02 export PATH=$PATH:$JAVA_HOME/bin 03 export CLASSPATH=$JAVA_HOME/lib/*.jar:$JAVA_HOME/jre/lib/*.jar 04 05 export SCALA_HOME=/usr/scala/scala-2.10.3 06 export PATH=$PATH:$SCALA_HOME/bin 07 08 export MAVEN_HOME=/home/shirdrn/cloud/programs/apache-maven-3.2.1 09 export PATH=$PATH:$MAVEN_HOME/bin 10 11 export HADOOP_HOME=/home/shirdrn/cloud/programs/hadoop-1.2.1 12 export PATH=$PATH:$HADOOP_HOME/bin 13 export HADOOP_LOG_DIR=/home/shirdrn/cloud/storage/hadoop-1.2.1/logs 14 15 export HIVE_HOME=/home/shirdrn/cloud/programs/hive-0.11-shark-0.9.0 16 export PATH=$PATH:$HIVE_HOME/bin 17 18 export SPARK_HOME=/home/shirdrn/cloud/programs/spark-0.9.0-incubating-bin-hadoop1 19 export PATH=$PATH:$SPARK_HOME/bin 20 21 export SHARK_HOME=/home/shirdrn/cloud/programs/shark-0.9.0 22 export PATH=$PATH:$SHARK_HOME/bin Hive安装配置 这里,我们使用一个用来与Shark进行整合而开发的版本的Hive软件包,可以在这里https://github.com/amplab/hive/releases选择对应的版本。 例如,在主节点m1上准备Hive的软件包: 1 wget https://github.com/amplab/hive/archive/v0.11-shark-0.9.0.tar.gz 2 mv v0.11-shark-0.9.0 hive-0.11-shark-0.9.0.tar.gz 3 tar xvzf hive-0.11-shark-0.9.0.tar.gz 然后修改Hive的配置文件,指定在HDFS上的目录即可,如下所示: 01 <?xml version="1.0"?> 02 <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> 03 04 <configuration> 05 <property> 06 <name>hive.metastore.warehouse.dir</name> 07 <value>/hive</value> 08 <description>location of default database for the warehouse</description> 09 </property> 10 </configuration> 简单使用这样配置即可。 安装配置Shark 由于我之前先安装配置了Spark-0.9.0,没有找到对应版本的Shark(只找到了Shark-0.8.1的编译包),所以直接从github下载Shark-0.9.0分支的源码,使用sbt进行构建,执行如下命令行: 1 git clone https://github.com/amplab/shark.git -b branch-0.9.0 shark-0.9.0 2 cd shark-0.9.0 3 sbt/sbt package 上面最后一步使用sbt构建的过程可能需要下载很多依赖包,等待的时间比较长。构建成功之后,可以修改Shark的配置文件: 1 cd shark-0.9.0/conf 2 mv shark-env.sh.template shark-env.sh 3 mv log4j.properties.template log4j.properties 然后修改shark-env.sh,修改内容如下所示: 1 export SHARK_MASTER_MEM=512m 2 export SPARK_MEM=512m 3 export SCALA_HOME=/usr/scala/scala-2.10.3 4 export HIVE_CONF_DIR=/home/shirdrn/cloud/programs/hive-0.11-shark-0.9.0/conf 编译构建并配置Shark,就可以启动Shark Shell,类似Hive Shell一样,执行HQL。 验证Shark 首先,需要启动Hadoop和Spark集群。 启动Shark可以执行如下明命令: 1 bin/shark 这样就进入了Shark的Shell环境,类似于Hive。现在,我们可以通过如下一个简单的例子来验证Shark,如下所示: 1 CREATE DATABASE user_db; 2 CREATE TABLE users (login STRING, password STRING, id INT, group INT, user STRING, home STRING, cmd STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY ':' LINES TERMINATED BY '\n'; 3 LOAD DATA LOCAL INPATH '/etc/passwd' INTO TABLE users; 对应于本地Unix系统的/etc/passwd文件,我们创建了一个数据库user_db,然后在该数据库中创建了一个users表,最后将本地/etc/passwd文件上传到HDFS,作为Hive表数据。 执行查询: 1 SELECT * FROM users; 验证结果示例如下: 01 shark> SELECT * FROM users; 02 223.128: [Full GC 99467K->25184K(506816K), 0.1482850 secs] 03 OK 04 root x 0 0 root /root /bin/bash 05 bin x 1 1 bin /bin /sbin/nologin 06 daemon x 2 2 daemon /sbin /sbin/nologin 07 adm x 3 4 adm /var/adm /sbin/nologin 08 lp x 4 7 lp /var/spool/lpd /sbin/nologin 09 sync x 5 0 sync /sbin /bin/sync 10 shutdown x 6 0 shutdown /sbin /sbin/shutdown 11 halt x 7 0 halt /sbin /sbin/halt 12 mail x 8 12 mail /var/spool/mail /sbin/nologin 13 uucp x 10 14 uucp /var/spool/uucp /sbin/nologin 14 operator x 11 0 operator /root /sbin/nologin 15 games x 12 100 games /usr/games /sbin/nologin 16 gopher x 13 30 gopher /var/gopher /sbin/nologin 17 ftp x 14 50 FTP User /var/ftp /sbin/nologin 18 nobody x 99 99 Nobody / /sbin/nologin 19 dbus x 81 81 System message bus / /sbin/nologin 20 usbmuxd x 113 113 usbmuxd user / /sbin/nologin 21 avahi-autoipd x 170 170 Avahi IPv4LL Stack /var/lib/avahi-autoipd /sbin/nologin 22 vcsa x 69 69 virtual console memory owner /dev /sbin/nologin 23 rtkit x 499 497 RealtimeKit /proc /sbin/nologin 24 abrt x 173 173 /etc/abrt /sbin/nologin 25 haldaemon x 68 68 HAL daemon / /sbin/nologin 26 saslauth x 498 76 "Saslauthd user" /var/empty/saslauth /sbin/nologin 27 postfix x 89 89 /var/spool/postfix /sbin/nologin 28 ntp x 38 38 /etc/ntp /sbin/nologin 29 apache x 48 48 Apache /var/www /sbin/nologin 30 avahi x 70 70 Avahi mDNS/DNS-SD Stack /var/run/avahi-daemon /sbin/nologin 31 pulse x 497 496 PulseAudio System Daemon /var/run/pulse /sbin/nologin 32 gdm x 42 42 /var/lib/gdm /sbin/nologin 33 sshd x 74 74 Privilege-separated SSH /var/empty/sshd /sbin/nologin 34 tcpdump x 72 72 / /sbin/nologin 35 shirdrn x 500 500 Jeff Stone /home/shirdrn /bin/bash 36 mysql x 27 27 MySQL Server /var/lib/mysql /bin/bash 37 Time taken: 2.742 seconds 可以在HDFS上查询Hive对应的数据存储位置信息: 1 hadoop fs -lsr /hive 示例如下所示: 1 [shirdrn@m1 hive-0.11-shark-0.9.0]$ hadoop fs -lsr /hive 2 drwxr-xr-x - shirdrn supergroup 0 2014-03-16 06:06 /hive/user_db.db 3 drwxr-xr-x - shirdrn supergroup 0 2014-03-16 06:07 /hive/user_db.db/users 4 -rw-r--r-- 3 shirdrn supergroup 1567 2014-03-16 06:07 /hive/user_db.db/users/passwd
摘要 本文提出了分布式内存抽象的概念——弹性分布式数据集(RDD,Resilient Distributed Datasets),它具备像MapReduce等数据流模型的容错特性,并且允许开发人员在大型集群上执行基于内存的计算。现有的数据流系统对两种应用的处理并不高效:一是迭代式算法,这在图应用和机器学习领域很常见;二是交互式数据挖掘工具。这两种情况下,将数据保存在内存中能够极大地提高性能。为了有效地实现容错,RDD提供了一种高度受限的共享内存,即RDD是只读的,并且只能通过其他RDD上的批量操作来创建。尽管如此,RDD仍然足以表示很多类型的计算,包括MapReduce和专用的迭代编程模型(如Pregel)等。我们实现的RDD在迭代计算方面比Hadoop快20多倍,同时还可以在5-7秒内交互式地查询1TB数据集。 1.引言 无论是工业界还是学术界,都已经广泛使用高级集群编程模型来处理日益增长的数据,如MapReduce和Dryad。这些系统将分布式编程简化为自动提供位置感知性调度、容错以及负载均衡,使得大量用户能够在商用集群上分析超大数据集。 大多数现有的集群计算系统都是基于非循环的数据流模型。从稳定的物理存储(如分布式文件系统)中加载记录,记录被传入由一组确定性操作构成的DAG,然后写回稳定存储。DAG数据流图能够在运行时自动实现任务调度和故障恢复。 尽管非循环数据流是一种很强大的抽象方法,但仍然有些应用无法使用这种方式描述。我们就是针对这些不太适合非循环模型的应用,它们的特点是在多个并行操作之间重用工作数据集。这类应用包括:(1)机器学习和图应用中常用的迭代算法(每一步对数据执行相似的函数);(2)交互式数据挖掘工具(用户反复查询一个数据子集)。基于数据流的框架并不明确支持工作集,所以需要将数据输出到磁盘,然后在每次查询时重新加载,这带来较大的开销。 我们提出了一种分布式的内存抽象,称为弹性分布式数据集(RDD,Resilient Distributed Datasets)。它支持基于工作集的应用,同时具有数据流模型的特点:自动容错、位置感知调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。 RDD提供了一种高度受限的共享内存模型,即RDD是只读的记录分区的集合,只能通过在其他RDD执行确定的转换操作(如map、join和group by)而创建,然而这些限制使得实现容错的开销很低。与分布式共享内存系统需要付出高昂代价的检查点和回滚机制不同,RDD通过Lineage来重建丢失的分区:一个RDD中包含了如何从其他RDD衍生所必需的相关信息,从而不需要检查点操作就可以重构丢失的数据分区。尽管RDD不是一个通用的共享内存抽象,但却具备了良好的描述能力、可伸缩性和可靠性,但却能够广泛适用于数据并行类应用。 第一个指出非循环数据流存在不足的并非是我们,例如,Google的Pregel[21],是一种专门用于迭代式图算法的编程模型;Twister[13]和HaLoop[8],是两种典型的迭代式MapReduce模型。但是,对于一些特定类型的应用,这些系统提供了一个受限的通信模型。相比之下,RDD则为基于工作集的应用提供了更为通用的抽象,用户可以对中间结果进行显式的命名和物化,控制其分区,还能执行用户选择的特定操作(而不是在运行时去循环执行一系列MapReduce步骤)。RDD可以用来描述Pregel、迭代式MapReduce,以及这两种模型无法描述的其他应用,如交互式数据挖掘工具(用户将数据集装入内存,然后执行ad-hoc查询)。 Spark是我们实现的RDD系统,在我们内部能够被用于开发多种并行应用。Spark采用Scala语言[5]实现,提供类似于DryadLINQ的集成语言编程接口[34],使用户可以非常容易地编写并行任务。此外,随着Scala新版本解释器的完善,Spark还能够用于交互式查询大数据集。我们相信Spark会是第一个能够使用有效、通用编程语言,并在集群上对大数据集进行交互式分析的系统。 我们通过微基准和用户应用程序来评估RDD。实验表明,在处理迭代式应用上Spark比Hadoop快高达20多倍,计算数据分析类报表的性能提高了40多倍,同时能够在5-7秒的延时内交互式扫描1TB数据集。此外,我们还在Spark之上实现了Pregel和HaLoop编程模型(包括其位置优化策略),以库的形式实现(分别使用了100和200行Scala代码)。最后,利用RDD内在的确定性特性,我们还创建了一种Spark调试工具rddbg,允许用户在任务期间利用Lineage重建RDD,然后像传统调试器那样重新执行任务。 本文首先在第2部分介绍了RDD的概念,然后第3部分描述Spark API,第4部分解释如何使用RDD表示几种并行应用(包括Pregel和HaLoop),第5部分讨论Spark中RDD的表示方法以及任务调度器,第6部分描述具体实现和rddbg,第7部分对RDD进行评估,第8部分给出了相关研究工作,最后第9部分总结。 2.弹性分布式数据集(RDD) 本部分描述RDD和编程模型。首先讨论设计目标(2.1),然后定义RDD(2.2),讨论Spark的编程模型(2.3),并给出一个示例(2.4),最后对比RDD与分布式共享内存(2.5)。 2.1 目标和概述 我们的目标是为基于工作集的应用(即多个并行操作重用中间结果的这类应用)提供抽象,同时保持MapReduce及其相关模型的优势特性:即自动容错、位置感知性调度和可伸缩性。RDD比数据流模型更易于编程,同时基于工作集的计算也具有良好的描述能力。 在这些特性中,最难实现的是容错性。一般来说,分布式数据集的容错性有两种方式:即数据检查点和记录数据的更新。我们面向的是大规模数据分析,数据检查点操作成本很高:需要通过数据中心的网络连接在机器之间复制庞大的数据集,而网络带宽往往比内存带宽低得多,同时还需要消耗更多的存储资源(在内存中复制数据可以减少需要缓存的数据量,而存储到磁盘则会拖慢应用程序)。所以,我们选择记录更新的方式。但是,如果更新太多,那么记录更新成本也不低。因此,RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列转换记录下来(即Lineage),以便恢复丢失的分区。 虽然只支持粗粒度转换限制了编程模型,但我们发现RDD仍然可以很好地适用于很多应用,特别是支持数据并行的批量分析应用,包括数据挖掘、机器学习、图算法等,因为这些程序通常都会在很多记录上执行相同的操作。RDD不太适合那些异步更新共享状态的应用,例如并行web爬行器。因此,我们的目标是为大多数分析型应用提供有效的编程模型,而其他类型的应用交给专门的系统。 2.2 RDD抽象 RDD是只读的、分区记录的集合。RDD只能基于在稳定物理存储中的数据集和其他已有的RDD上执行确定性操作来创建。这些确定性操作称之为转换,如map、filter、groupBy、join(转换不是程开发人员在RDD上执行的操作)。 RDD不需要物化。RDD含有如何从其他RDD衍生(即计算)出本RDD的相关信息(即Lineage),据此可以从物理存储的数据计算出相应的RDD分区。 2.3 编程模型 在Spark中,RDD被表示为对象,通过这些对象上的方法(或函数)调用转换。 定义RDD之后,程序员就可以在动作中使用RDD了。动作是向应用程序返回值,或向存储系统导出数据的那些操作,例如,count(返回RDD中的元素个数),collect(返回元素本身),save(将RDD输出到存储系统)。在Spark中,只有在动作第一次使用RDD时,才会计算RDD(即延迟计算)。这样在构建RDD的时候,运行时通过管道的方式传输多个转换。 程序员还可以从两个方面控制RDD,即缓存和分区。用户可以请求将RDD缓存,这样运行时将已经计算好的RDD分区存储起来,以加速后期的重用。缓存的RDD一般存储在内存中,但如果内存不够,可以写到磁盘上。 另一方面,RDD还允许用户根据关键字(key)指定分区顺序,这是一个可选的功能。目前支持哈希分区和范围分区。例如,应用程序请求将两个RDD按照同样的哈希分区方式进行分区(将同一机器上具有相同关键字的记录放在一个分区),以加速它们之间的join操作。在Pregel和HaLoop中,多次迭代之间采用一致性的分区置换策略进行优化,我们同样也允许用户指定这种优化。 2.4 示例:控制台日志挖掘 本部分我们通过一个具体示例来阐述RDD。假定有一个大型网站出错,操作员想要检查Hadoop文件系统(HDFS)中的日志文件(TB级大小)来找出原因。通过使用Spark,操作员只需将日志中的错误信息装载到一组节点的内存中,然后执行交互式查询。首先,需要在Spark解释器中输入如下Scala命令: 1 lines = spark.textFile("hdfs://...") 2 errors = lines.filter(_.startsWith("ERROR")) 3 errors.cache() 第1行从HDFS文件定义了一个RDD(即一个文本行集合),第2行获得一个过滤后的RDD,第3行请求将errors缓存起来。注意在Scala语法中filter的参数是一个闭包。 这时集群还没有开始执行任何任务。但是,用户已经可以在这个RDD上执行对应的动作,例如统计错误消息的数目: 1 errors.count() 用户还可以在RDD上执行更多的转换操作,并使用转换结果,如: 1 // Count errors mentioning MySQL: 2 errors.filter(_.contains("MySQL")).count() 3 // Return the time fields of errors mentioning 4 // HDFS as an array (assuming time is field 5 // number 3 in a tab-separated format): 6 errors.filter(_.contains("HDFS")) 7 .map(_.split('\t')(3)) 8 .collect() 使用errors的第一个action运行以后,Spark会把errors的分区缓存在内存中,极大地加快了后续计算速度。注意,最初的RDD lines不会被缓存。因为错误信息可能只占原数据集的很小一部分(小到足以放入内存)。 最后,为了说明模型的容错性,图1给出了第3个查询的Lineage图。在lines RDD上执行filter操作,得到errors,然后再filter、map后得到新的RDD,在这个RDD上执行collect操作。Spark调度器以流水线的方式执行后两个转换,向拥有errors分区缓存的节点发送一组任务。此外,如果某个errors分区丢失,Spark只在相应的lines分区上执行filter操作来重建该errors分区。 图1 示例中第三个查询的Lineage图。(方框表示RDD,箭头表示转换) 2.5 RDD与分布式共享内存 为了进一步理解RDD是一种分布式的内存抽象,表1列出了RDD与分布式共享内存(DSM,Distributed Shared Memory)[24]的对比。在DSM系统中,应用可以向全局地址空间的任意位置进行读写操作。(注意这里的DSM,不仅指传统的共享内存系统,还包括那些通过分布式哈希表或分布式文件系统进行数据共享的系统,比如Piccolo[28])DSM是一种通用的抽象,但这种通用性同时也使得在商用集群上实现有效的容错性更加困难。 RDD与DSM主要区别在于,不仅可以通过批量转换创建(即“写”)RDD,还可以对任意内存位置读写。也就是说,RDD限制应用执行批量写操作,这样有利于实现有效的容错。特别地,RDD没有检查点开销,因为可以使用Lineage来恢复RDD。而且,失效时只需要重新计算丢失的那些RDD分区,可以在不同节点上并行执行,而不需要回滚整个程序。 表1 RDD与DSM对比对比项目RDD分布式共享内存(DSM)读批量或细粒度操作细粒度操作写批量转换操作细粒度操作一致性不重要(RDD是不可更改的)取决于应用程序或运行时容错性细粒度,低开销(使用Lineage)需要检查点操作和程序回滚落后任务的处理任务备份很难处理任务安排基于数据存放的位置自动实现取决于应用程序(通过运行时实现透明性)如果内存不够与已有的数据流系统类似性能较差(交换?)注意,通过备份任务的拷贝,RDD还可以处理落后任务(即运行很慢的节点),这点与MapReduce[12]类似。而DSM则难以实现备份任务,因为任务及其副本都需要读写同一个内存位置。 与DSM相比,RDD模型有两个好处。第一,对于RDD中的批量操作,运行时将根据数据存放的位置来调度任务,从而提高性能。第二,对于基于扫描的操作,如果内存不足以缓存整个RDD,就进行部分缓存。把内存放不下的分区存储到磁盘上,此时性能与现有的数据流系统差不多。 最后看一下读操作的粒度。RDD上的很多动作(如count和collect)都是批量读操作,即扫描整个数据集,可以将任务分配到距离数据最近的节点上。同时,RDD也支持细粒度操作,即在哈希或范围分区的RDD上执行关键字查找。 3. Spark编程接口 Spark用Scala[5]语言实现了RDD的API。Scala是一种基于JVM的静态类型、函数式、面向对象的语言。我们选择Scala是因为它简洁(特别适合交互式使用)、有效(因为是静态类型)。但是,RDD抽象并不局限于函数式语言,也可以使用其他语言来实现RDD,比如像Hadoop[2]那样用类表示用户函数。 要使用Spark,开发者需要编写一个driver程序,连接到集群以运行Worker,如图2所示。Driver定义了一个或多个RDD,并调用RDD上的动作。Worker是长时间运行的进程,将RDD分区以Java对象的形式缓存在内存中。 图2 Spark的运行时。用户的driver程序启动多个worker,worker从分布式文件系统中读取数据块,并将计算后的RDD分区缓存在内存中。 再看看2.4中的例子,用户执行RDD操作时会提供参数,比如map传递一个闭包(closure,函数式编程中的概念)。Scala将闭包表示为Java对象,如果传递的参数是闭包,则这些对象被序列化,通过网络传输到其他节点上进行装载。Scala将闭包内的变量保存为Java对象的字段。例如,var x = 5; rdd.map(_ + x) 这段代码将RDD中的每个元素加5。总的来说,Spark的语言集成类似于DryadLINQ。 RDD本身是静态类型对象,由参数指定其元素类型。例如,RDD[int]是一个整型RDD。不过,我们举的例子几乎都省略了这个类型参数,因为Scala支持类型推断。 虽然在概念上使用Scala实现RDD很简单,但还是要处理一些Scala闭包对象的反射问题。如何通过Scala解释器来使用Spark还需要更多工作,这点我们将在第6部分讨论。不管怎样,我们都不需要修改Scala编译器。 3.1 Spark中的RDD操作 表2列出了Spark中的RDD转换和动作。每个操作都给出了标识,其中方括号表示类型参数。前面说过转换是延迟操作,用于定义新的RDD;而动作启动计算操作,并向用户程序返回值或向外部存储写数据。 表3 Spark中支持的RDD转换和动作转换map(f : T ) U) : RDD[T] ) RDD[U] filter(f : T ) Bool) : RDD[T] ) RDD[T] flatMap(f : T ) Seq[U]) : RDD[T] ) RDD[U] sample(fraction : Float) : RDD[T] ) RDD[T] (Deterministic sampling) groupByKey() : RDD[(K, V)] ) RDD[(K, Seq[V])] reduceByKey(f : (V; V) ) V) : RDD[(K, V)] ) RDD[(K, V)] union() : (RDD[T]; RDD[T]) ) RDD[T] join() : (RDD[(K, V)]; RDD[(K, W)]) ) RDD[(K, (V, W))] cogroup() : (RDD[(K, V)]; RDD[(K, W)]) ) RDD[(K, (Seq[V], Seq[W]))] crossProduct() : (RDD[T]; RDD[U]) ) RDD[(T, U)] mapValues(f : V ) W) : RDD[(K, V)] ) RDD[(K, W)] (Preserves partitioning) sort(c : Comparator[K]) : RDD[(K, V)] ) RDD[(K, V)] partitionBy(p : Partitioner[K]) : RDD[(K, V)] ) RDD[(K, V)]动作count() : RDD[T] ) Long collect() : RDD[T] ) Seq[T] reduce(f : (T; T) ) T) : RDD[T] ) T lookup(k : K) : RDD[(K, V)] ) Seq[V] (On hash/range partitioned RDDs) save(path : String) : Outputs RDD to a storage system, e.g., HDFS注意,有些操作只对键值对可用,比如join。另外,函数名与Scala及其他函数式语言中的API匹配,例如map是一对一的映射,而flatMap是将每个输入映射为一个或多个输出(与MapReduce中的map类似)。 除了这些操作以外,用户还可以请求将RDD缓存起来。而且,用户还可以通过Partitioner类获取RDD的分区顺序,然后将另一个RDD按照同样的方式分区。有些操作会自动产生一个哈希或范围分区的RDD,像groupByKey,reduceByKey和sort等。 4. 应用程序示例 现在我们讲述如何使用RDD表示几种基于数据并行的应用。首先讨论一些迭代式机器学习应用(4.1),然后看看如何使用RDD描述几种已有的集群编程模型,即MapReduce(4.2),Pregel(4.3),和Hadoop(4.4)。最后讨论一下RDD不适合哪些应用(4.5)。 4.1 迭代式机器学习 很多机器学习算法都具有迭代特性,运行迭代优化方法来优化某个目标函数,例如梯度下降方法。如果这些算法的工作集能够放入内存,将极大地加速程序运行。而且,这些算法通常采用批量操作,例如映射和求和,这样更容易使用RDD来表示。 例如下面的程序是逻辑回归[15]的实现。逻辑回归是一种常见的分类算法,即寻找一个最佳分割两组点(即垃圾邮件和非垃圾邮件)的超平面w。算法采用梯度下降的方法:开始时w为随机值,在每一次迭代的过程中,对w的函数求和,然后朝着优化的方向移动w。 1 val points = spark.textFile(...) 2 .map(parsePoint).persist() 3 var w = // random initial vector 4 for (i <- 1 to ITERATIONS) { 5 val gradient = points.map{ p => 6 p.x * (1/(1+exp(-p.y*(w dot p.x)))-1)*p.y 7 }.reduce((a,b) => a+b) 8 w -= gradient 9 } 首先定义一个名为points的缓存RDD,这是在文本文件上执行map转换之后得到的,即将每个文本行解析为一个Point对象。然后在points上反复执行map和reduce操作,每次迭代时通过对当前w的函数进行求和来计算梯度。7.1小节我们将看到这种在内存中缓存points的方式,比每次迭代都从磁盘文件装载数据并进行解析要快得多。 已经在Spark中实现的迭代式机器学习算法还有:kmeans(像逻辑回归一样每次迭代时执行一对map和reduce操作),期望最大化算法(EM,两个不同的map/reduce步骤交替执行),交替最小二乘矩阵分解和协同过滤算法。Chu等人提出迭代式MapReduce也可以用来实现常用的学习算法[11]。 4.2 使用RDD实现MapReduce MapReduce模型[12]很容易使用RDD进行描述。假设有一个输入数据集(其元素类型为T),和两个函数myMap: T => List[(Ki, Vi)] 和 myReduce: (Ki; List[Vi]) ) List[R],代码如下: 1 data.flatMap(myMap) 2 .groupByKey() 3 .map((k, vs) => myReduce(k, vs)) 如果任务包含combiner,则相应的代码为: 1 data.flatMap(myMap) 2 .reduceByKey(myCombiner) 3 .map((k, v) => myReduce(k, v)) ReduceByKey操作在mapper节点上执行部分聚集,与MapReduce的combiner类似。 4.3 使用RDD实现Pregel Pregel[21]是面向图算法的基于BSP范式[32]的编程模型。程序由一系列超步(Superstep)协调迭代运行。在每个超步中,各个顶点执行用户函数,并更新相应的顶点状态,变异图拓扑,然后向下一个超步的顶点集发送消息。这种模型能够描述很多图算法,包括最短路径,双边匹配和PageRank等。 以PageRank为例介绍一下Pregel的实现。当前PageRank[7]记为r,顶点表示状态。在每个超步中,各个顶点向其所有邻居发送贡献值r/n,这里n是邻居的数目。下一个超步开始时,每个顶点将其分值(rank)更新为 α/N + (1 - α) * Σci,这里的求和是各个顶点收到的所有贡献值的和,N是顶点的总数。 Pregel将输入的图划分到各个worker上,并存储在其内存中。在每个超步中,各个worker通过一种类似MapReduce的Shuffle操作交换消息。 Pregel的通信模式可以用RDD来描述,如图3。主要思想是:将每个超步中的顶点状态和要发送的消息存储为RDD,然后根据顶点ID分组,进行Shuffle通信(即cogroup操作)。然后对每个顶点ID上的状态和消息应用用户函数(即mapValues操作),产生一个新的RDD,即(VertexID, (NewState, OutgoingMessages))。然后执行map操作分离出下一次迭代的顶点状态和消息(即mapValues和flatMap操作)。代码如下: 查看源代码打印帮助 1 val vertices = // RDD of (ID, State) pairs 2 val messages = // RDD of (ID, Message) pairs 3 val grouped = vertices.cogroup(messages) 4 val newData = grouped.mapValues { 5 (vert, msgs) => userFunc(vert, msgs) 6 // returns (newState, outgoingMsgs) 7 }.cache() 8 val newVerts = newData.mapValues((v,ms) => v) 9 val newMsgs = newData.flatMap((id,(v,ms)) => ms) 图3 使用RDD实现Pregel时,一步迭代的数据流。(方框表示RDD,箭头表示转换) 需要注意的是,这种实现方法中,RDD grouped,newData和newVerts的分区方法与输入RDD vertices一样。所以,顶点状态一直存在于它们开始执行的机器上,这跟原Pregel一样,这样就减少了通信成本。因为cogroup和mapValues保持了与输入RDD相同的分区方法,所以分区是自动进行的。 完整的Pregel编程模型还包括其他工具,比如combiner,附录A讨论了它们的实现。下面将讨论Pregel的容错性,以及如何在实现相同容错性的同时减少需要执行检查点操作的数据量。 我们差不多用了100行Scala代码在Spark上实现了一个类Pregel的API。7.2小节将使用PageRank算法评估它的性能。 4.3.1 Pregel容错 当前,Pregel基于检查点机制来为顶点状态及其消息实现容错[21]。然而作者是这样描述的:通过在其它的节点上记录已发消息日志,然后单独重建丢失的分区,只需要恢复局部数据即可。上面提到这两种方式,RDD都能够很好地支持。 通过4.3小节的实现,Spark总是能够基于Lineage实现顶点和消息RDD的重建,但是由于过长的Lineage链,恢复可能会付出高昂的代价。因为迭代RDD依赖于上一个RDD,对于部分分区来说,节点故障可能会导致这些分区状态的所有迭代版本丢失,这就要求使用一种“级联-重新执行”[20]的方式去依次重建每一个丢失的分区。为了避免这个问题,用户可以周期性地在顶点和消息RDD上执行save操作,将状态信息保存到持久存储中。然后,Spark能够在失败的时候自动地重新计算这些丢失的分区(而不是回滚整个程序)。 最后,我们意识到,RDD也能够实现检查点数据的reduce操作,这要求通过一种高效的检查点方案来表达检查点数据。在很多Pregel作业中,顶点状态都包括可变与不可变的组件,例如,在PageRank中,与一个顶点相邻的顶点列表是不可变的,但是它们的排名是可变的,在这种情况下,我们可以使用一个来自可变数据的单独RDD来替换不可变RDD,基于这样一个较短的Lineage链,检查点仅仅是可变状态,图4解释了这种方式。 图4 经过优化的Pregel使用RDD的数据流。可变状态RDD必须设置检查点,不可变状态才可被快速重建。 在PageRank中,不可变状态(相邻顶点列表)远大于可变状态(浮点值),所以这种方式能够极大地降低开销。 4.4 使用RDD实现HaLoop HaLoop[8]是Hadoop的一个扩展版本,它能够改善具有迭代特性的MapReduce程序的性能。基于HaLoop编程模型的应用,使用reduce阶段的输出作为map阶段下一轮迭代的输入。它的循环感知任务调度器能够保证,在每一轮迭代中处理同一个分区数据的连续map和reduce任务,一定能够在同一台物理机上执行。确保迭代间locality特性,reduce数据在物理节点之间传输,并且允许数据缓存在本地磁盘而能够被后续迭代重用。 使用RDD来优化HaLoop,我们在Spark上实现了一个类似HaLoop的API,这个库只使用了200行Scala代码。通过partitionBy能够保证跨迭代的分区的一致性,每一个阶段的输入和输出被缓存以用于后续迭代。 4.5 不适合使用RDD的应用 在2.1节我们讨论过,RDD适用于具有批量转换需求的应用,并且相同的操作作用于数据集的每一个元素上。在这种情况下,RDD能够记住每个转换操作,对应于Lineage图中的一个步骤,恢复丢失分区数据时不需要写日志记录大量数据。RDD不适合那些通过异步细粒度地更新来共享状态的应用,例如Web应用中的存储系统,或者增量抓取和索引Web数据的系统,这样的应用更适合使用一些传统的方法,例如数据库、RAMCloud[26]、Percolator[27]和Piccolo[28]。我们的目标是,面向批量分析应用的这类特定系统,提供一种高效的编程模型,而不是一些异步应用程序。 5. RDD的描述及作业调度 我们希望在不修改调度器的前提下,支持RDD上的各种转换操作,同时能够从这些转换获取Lineage信息。为此,我们为RDD设计了一组小型通用的内部接口。 简单地说,每个RDD都包含:(1)一组RDD分区(partition,即数据集的原子组成部分);(2)对父RDD的一组依赖,这些依赖描述了RDD的Lineage;(3)一个函数,即在父RDD上执行何种计算;(4)元数据,描述分区模式和数据存放的位置。例如,一个表示HDFS文件的RDD包含:各个数据块的一个分区,并知道各个数据块放在哪些节点上。而且这个RDD上的map操作结果也具有同样的分区,map函数是在父数据上执行的。表3总结了RDD的内部接口。 表3 Spark中RDD的内部接口操作含义partitions()返回一组Partition对象preferredLocations(p)根据数据存放的位置,返回分区p在哪些节点访问更快dependencies()返回一组依赖iterator(p, parentIters)按照父分区的迭代器,逐个计算分区p的元素partitioner()返回RDD是否hash/range分区的元数据信息设计接口的一个关键问题就是,如何表示RDD之间的依赖。我们发现RDD之间的依赖关系可以分为两类,即:(1)窄依赖(narrow dependencies):子RDD的每个分区依赖于常数个父分区(即与数据规模无关);(2)宽依赖(wide dependencies):子RDD的每个分区依赖于所有父RDD分区。例如,map产生窄依赖,而join则是宽依赖(除非父RDD被哈希分区)。另一个例子见图5。 图5 窄依赖和宽依赖的例子。(方框表示RDD,实心矩形表示分区) 区分这两种依赖很有用。首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区。例如,逐个元素地执行map、然后filter操作;而宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle,这与MapReduce类似。第二,窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,而且不同节点之间可以并行计算;而对于一个宽依赖关系的Lineage图,单个节点失效可能导致这个RDD的所有祖先丢失部分分区,因而需要整体重新计算。 通过RDD接口,Spark只需要不超过20行代码实现便可以实现大多数转换。5.1小节给出了例子,然后我们讨论了怎样使用RDD接口进行调度(5.2),最后讨论一下基于RDD的程序何时需要数据检查点操作(5.3)。 5.1 RDD实现举例 HDFS文件:目前为止我们给的例子中输入RDD都是HDFS文件,对这些RDD可以执行:partitions操作返回各个数据块的一个分区(每个Partition对象中保存数据块的偏移),preferredLocations操作返回数据块所在的节点列表,iterator操作对数据块进行读取。 map:任何RDD上都可以执行map操作,返回一个MappedRDD对象。该操作传递一个函数参数给map,对父RDD上的记录按照iterator的方式执行这个函数,并返回一组符合条件的父RDD分区及其位置。 union:在两个RDD上执行union操作,返回两个父RDD分区的并集。通过相应父RDD上的窄依赖关系计算每个子RDD分区(注意union操作不会过滤重复值,相当于SQL中的UNION ALL)。 sample:抽样与映射类似,但是sample操作中,RDD需要存储一个随机数产生器的种子,这样每个分区能够确定哪些父RDD记录被抽样。 join:对两个RDD执行join操作可能产生窄依赖(如果这两个RDD拥有相同的哈希分区或范围分区),可能是宽依赖,也可能两种依赖都有(比如一个父RDD有分区,而另一父RDD没有)。 5.2 Spark任务调度器 调度器根据RDD的结构信息为每个动作确定有效的执行计划。调度器的接口是runJob函数,参数为RDD及其分区集,和一个RDD分区上的函数。该接口足以表示Spark中的所有动作(即count、collect、save等)。 总的来说,我们的调度器跟Dryad类似,但我们还考虑了哪些RDD分区是缓存在内存中的。调度器根据目标RDD的Lineage图创建一个由stage构成的无回路有向图(DAG)。每个stage内部尽可能多地包含一组具有窄依赖关系的转换,并将它们流水线并行化(pipeline)。stage的边界有两种情况:一是宽依赖上的Shuffle操作;二是已缓存分区,它可以缩短父RDD的计算过程。例如图6。父RDD完成计算后,可以在stage内启动一组任务计算丢失的分区。 图6 Spark怎样划分任务阶段(stage)的例子。实线方框表示RDD,实心矩形表示分区(黑色表示该分区被缓存)。要在RDD G上执行一个动作,调度器根据宽依赖创建一组stage,并在每个stage内部将具有窄依赖的转换流水线化(pipeline)。 本例不用再执行stage 1,因为B已经存在于缓存中了,所以只需要运行2和3。 调度器根据数据存放的位置分配任务,以最小化通信开销。如果某个任务需要处理一个已缓存分区,则直接将任务分配给拥有这个分区的节点。否则,如果需要处理的分区位于多个可能的位置(例如,由HDFS的数据存放位置决定),则将任务分配给这一组节点。 对于宽依赖(例如需要Shuffle的依赖),目前的实现方式是,在拥有父分区的节点上将中间结果物化,简化容错处理,这跟MapReduce中物化map输出很像。 如果某个任务失效,只要stage中的父RDD分区可用,则只需在另一个节点上重新运行这个任务即可。如果某些stage不可用(例如,Shuffle时某个map输出丢失),则需要重新提交这个stage中的所有任务来计算丢失的分区。 最后,lookup动作允许用户从一个哈希或范围分区的RDD上,根据关键字读取一个数据元素。这里有一个设计问题。Driver程序调用lookup时,只需要使用当前调度器接口计算关键字所在的那个分区。当然任务也可以在集群上调用lookup,这时可以将RDD视为一个大的分布式哈希表。这种情况下,任务和被查询的RDD之间的并没有明确的依赖关系(因为worker执行的是lookup),如果所有节点上都没有相应的缓存分区,那么任务需要告诉调度器计算哪些RDD来完成查找操作。 5.3 检查点 尽管RDD中的Lineage信息可以用来故障恢复,但对于那些Lineage链较长的RDD来说,这种恢复可能很耗时。例如4.3小节中的Pregel任务,每次迭代的顶点状态和消息都跟前一次迭代有关,所以Lineage链很长。如果将Lineage链存到物理存储中,再定期对RDD执行检查点操作就很有效。 一般来说,Lineage链较长、宽依赖的RDD需要采用检查点机制。这种情况下,集群的节点故障可能导致每个父RDD的数据块丢失,因此需要全部重新计算[20]。将窄依赖的RDD数据存到物理存储中可以实现优化,例如前面4.1小节逻辑回归的例子,将数据点和不变的顶点状态存储起来,就不再需要检查点操作。 当前Spark版本提供检查点API,但由用户决定是否需要执行检查点操作。今后我们将实现自动检查点,根据成本效益分析确定RDD Lineage图中的最佳检查点位置。 值得注意的是,因为RDD是只读的,所以不需要任何一致性维护(例如写复制策略,分布式快照或者程序暂停等)带来的开销,后台执行检查点操作。 我们使用10000行Scala代码实现了Spark。系统可以使用任何Hadoop数据源(如HDFS,Hbase)作为输入,这样很容易与Hadoop环境集成。Spark以库的形式实现,不需要修改Scala编译器。 这里讨论关于实现的三方面问题:(1)修改Scala解释器,允许交互模式使用Spark(6.1);(2)缓存管理(6.2);(3)调试工具rddbg(6.3)。 6. 实现 6.1 解释器的集成 像Ruby和Python一样,Scala也有一个交互式shell。基于内存的数据可以实现低延时,我们希望允许用户从解释器交互式地运行Spark,从而在大数据集上实现大规模并行数据挖掘。 Scala解释器通常根据将用户输入的代码行,来对类进行编译,接着装载到JVM中,然后调用类的函数。这个类是一个包含输入行变量或函数的单例对象,并在一个初始化函数中运行这行代码。例如,如果用户输入代码var x = 5,接着又输入println(x),则解释器会定义一个包含x的Line1类,并将第2行编译为println(Line1.getInstance().x)。 在Spark中我们对解释器做了两点改动: 类传输:解释器能够支持基于HTTP传输类字节码,这样worker节点就能获取输入每行代码对应的类的字节码。 改进的代码生成逻辑:通常每行上创建的单态对象通过对应类上的静态方法进行访问。也就是说,如果要序列化一个闭包,它引用了前面代码行中变量,比如上面的例子Line1.x,Java不会根据对象关系传输包含x的Line1实例。所以worker节点不会收到x。我们将这种代码生成逻辑改为直接引用各个行对象的实例。图7说明了解释器如何将用户输入的一组代码行解释为Java对象。图6 Spark怎样划分任务阶段(stage)的例子。实线方框表示RDD,实心矩形表示分区(黑色表示该分区被缓存)。要在RDD G上执行一个动作,调度器根据宽依赖创建一组stage,并在每个stage内部将具有窄依赖的转换流水线化(pipeline)。 本例不用再执行stage 1,因为B已经存在于缓存中了,所以只需要运行2和3。 调度器根据数据存放的位置分配任务,以最小化通信开销。如果某个任务需要处理一个已缓存分区,则直接将任务分配给拥有这个分区的节点。否则,如果需要处理的分区位于多个可能的位置(例如,由HDFS的数据存放位置决定),则将任务分配给这一组节点。 对于宽依赖(例如需要Shuffle的依赖),目前的实现方式是,在拥有父分区的节点上将中间结果物化,简化容错处理,这跟MapReduce中物化map输出很像。 如果某个任务失效,只要stage中的父RDD分区可用,则只需在另一个节点上重新运行这个任务即可。如果某些stage不可用(例如,Shuffle时某个map输出丢失),则需要重新提交这个stage中的所有任务来计算丢失的分区。 最后,lookup动作允许用户从一个哈希或范围分区的RDD上,根据关键字读取一个数据元素。这里有一个设计问题。Driver程序调用lookup时,只需要使用当前调度器接口计算关键字所在的那个分区。当然任务也可以在集群上调用lookup,这时可以将RDD视为一个大的分布式哈希表。这种情况下,任务和被查询的RDD之间的并没有明确的依赖关系(因为worker执行的是lookup),如果所有节点上都没有相应的缓存分区,那么任务需要告诉调度器计算哪些RDD来完成查找操作。 5.3 检查点 尽管RDD中的Lineage信息可以用来故障恢复,但对于那些Lineage链较长的RDD来说,这种恢复可能很耗时。例如4.3小节中的Pregel任务,每次迭代的顶点状态和消息都跟前一次迭代有关,所以Lineage链很长。如果将Lineage链存到物理存储中,再定期对RDD执行检查点操作就很有效。 一般来说,Lineage链较长、宽依赖的RDD需要采用检查点机制。这种情况下,集群的节点故障可能导致每个父RDD的数据块丢失,因此需要全部重新计算[20]。将窄依赖的RDD数据存到物理存储中可以实现优化,例如前面4.1小节逻辑回归的例子,将数据点和不变的顶点状态存储起来,就不再需要检查点操作。 当前Spark版本提供检查点API,但由用户决定是否需要执行检查点操作。今后我们将实现自动检查点,根据成本效益分析确定RDD Lineage图中的最佳检查点位置。 值得注意的是,因为RDD是只读的,所以不需要任何一致性维护(例如写复制策略,分布式快照或者程序暂停等)带来的开销,后台执行检查点操作。 我们使用10000行Scala代码实现了Spark。系统可以使用任何Hadoop数据源(如HDFS,Hbase)作为输入,这样很容易与Hadoop环境集成。Spark以库的形式实现,不需要修改Scala编译器。 这里讨论关于实现的三方面问题:(1)修改Scala解释器,允许交互模式使用Spark(6.1);(2)缓存管理(6.2);(3)调试工具rddbg(6.3)。 6. 实现 6.1 解释器的集成 像Ruby和Python一样,Scala也有一个交互式shell。基于内存的数据可以实现低延时,我们希望允许用户从解释器交互式地运行Spark,从而在大数据集上实现大规模并行数据挖掘。 Scala解释器通常根据将用户输入的代码行,来对类进行编译,接着装载到JVM中,然后调用类的函数。这个类是一个包含输入行变量或函数的单例对象,并在一个初始化函数中运行这行代码。例如,如果用户输入代码var x = 5,接着又输入println(x),则解释器会定义一个包含x的Line1类,并将第2行编译为println(Line1.getInstance().x)。 在Spark中我们对解释器做了两点改动: 类传输:解释器能够支持基于HTTP传输类字节码,这样worker节点就能获取输入每行代码对应的类的字节码。 改进的代码生成逻辑:通常每行上创建的单态对象通过对应类上的静态方法进行访问。也就是说,如果要序列化一个闭包,它引用了前面代码行中变量,比如上面的例子Line1.x,Java不会根据对象关系传输包含x的Line1实例。所以worker节点不会收到x。我们将这种代码生成逻辑改为直接引用各个行对象的实例。图7说明了解释器如何将用户输入的一组代码行解释为Java对象。图8 首轮迭代后Hadoop、HadoopBinMen、Spark运行时间对比 后续迭代。图9显示了后续迭代的平均耗时,图8对比了不同聚类大小条件下耗时情况,我们发现在100个节点上运行Logistic回归程序,Spark比Hadoop、HadoopBinMem分别快25.3、20.7倍。从图8(b)可以看到,Spark仅仅比Hadoop、HadoopBinMem分别快1.9、3.2倍,这是因为K-means程序的开销取决于计算(用更多的节点有助于提高计算速度的倍数)。 后续迭代中,Hadoop仍然从HDFS读取文本数据作为输入,所以从首轮迭代开始Hadoop的迭代时间并没有明显的改善。使用预先转换的SequenceFile文件(Hadoop内建的二进制文件格式),HadoopBinMem在后续迭代中节省了解析的代价,但是仍然带来的其他的开销,如从HDFS读SequenceFile文件并转换成Java对象。因为Spark直接读取缓存于RDD中的Java对象,随着聚类尺寸的线性增长,迭代时间大幅下降。 图9:首轮及其后续迭代平均时间对比理解速度提升。我们非常惊奇地发现,Spark甚至胜过了基于内存存储二进制数据的Hadoop(HadoopBinMem),幅度高达20倍之多,Hadoop运行慢是由于如下几个原因: Hadoop软件栈的最小开销 读数据时HDFS栈的开销 将二进制记录转换成内存Java对象的代价 为了估测1,我们运行空的Hadoop作业,仅仅执行作业的初始化、启动任务、清理工作就至少耗时25秒。对于2,我们发现为了服务每一个HDFS数据块,HDFS进行了多次复制以及计算校验和操作。 为了估测3,我们在单个节点上运行了微基准程序,在输入的256M数据上计算Logistic回归,结果如表5所示。首先,在内存中的HDFS文件和本地文件的不同导致通过HDFS接口读取耗时2秒,甚至数据就在本地内存中。其次,文本和二进制格式输入的不同造成了解析耗时7秒的开销。最后,预解析的二进制文件转换为内存中的Java对象,耗时3秒。每个节点处理多个块时这些开销都会累积起来,然而通过缓存RDD作为内存中的Java对象,Spark只需要耗时3秒。 表5 Logistic回归迭代时间内存中的HDFS文件内存中的本地文件缓存的RDD文本输入 二进制输入15.38 (0.26) 8.38 (0.10)13.13 (0.26) 6.86 (0.02)2.93 (0.31) 2.93 (0.31)7.2 PageRank 通过使用存储在HDFS上的49G Wikipedia导出数据,我们比较了使用RDD实现的Pregel与使用Hadoop计算PageRank的性能。PageRank算法通过10轮迭代处理了大约400万文章的链接图数据,图10显示了在30个节点上,Spark处理速度是Hadoop的2倍多,改进后对输入进行Hash分区速度提升到2.6倍,使用Combiner后提升到3.6倍,这些结果数据也随着节点扩展到60个时同步放大。 图10 迭代时间对比 7.3 容错恢复 基于K-means算法应用程序,我们评估了在单点故障(SPOF)时使用Lneage信息创建RDD分区的开销。图11显示了,K-means应用程序运行在75个节点的集群中进行了10轮迭代,我们在正常操作和进行第6轮迭代开始时一个节点发生故障的情况下对耗时进行了对比。没有任何失败,每轮迭代启动了400个任务处理100G数据。 图11 SPOF时K-means应用程序迭代时间 第5轮迭代结束时大约耗时58秒,第6轮迭代时Kill掉一个节点,该节点上的任务都被终止(包括缓存的分区数据)。Spark调度器调度这些任务在其他节点上重新并行运行,并且重新读取基于Lineage信息重建的RDD输入数据并进行缓存,这使得迭代计算耗时增加到80秒。一旦丢失的RDD分区被重建,平均迭代时间又回落到58秒。 7.4 内存不足时表现 到现在为止,我们能保证集群中的每个节点都有足够的内存去缓存迭代过程中使用的RDD,如果没有足够的内存来缓存一个作业的工作集,Spark又是如何运行的呢?在实验中,我们通过在每个节点上限制缓存RDD所需要的内存资源来配置Spark,在不同的缓存配置条件下执行Logistic回归,结果如图12。我们可以看出,随着缓存的减小,性能平缓地下降。 图12 Spark上运行Logistic回归的性能表现 7.5 基于Spark构建的用户应用程序 In-Memory分析。视频分发公司Conviva使用Spark极大地提升了为客户处理分析报告的速度,以前基于Hadoop使用大约20个Hive[3]查询来完成,这些查询作用在相同的数据子集上(满足用户提供的条件),但是在不同分组的字段上执行聚合操作(SUM、AVG、COUNT DISTINCT等)需要使用单独的MapReduce作业。该公司使用Spark只需要将相关数据加载到内存中一次,然后运行上述聚合操作,在Hadoop集群上处理200G压缩数据并生成报耗时20小时,而使用Spark基于96G内存的2个节点耗时30分钟即可完成,速度提升40倍,主要是因为不需要再对每个作业重复地执行解压缩和过滤操作。 城市交通建模。在Berkeley的Mobile Millennium项目[17]中,基于一系列分散的汽车GPS监测数据,研究人员使用并行化机器学习算法来推算公路交通拥堵状况。数据来自市区10000个互联的公路线路网,还有600000个由汽车GPS装置采集到的样本数据,这些数据记录了汽车在两个地点之间行驶的时间(每一条路线的行驶时间可能跨多个公路线路网)。使用一个交通模型,通过推算跨多个公路网行驶耗时预期,系统能够估算拥堵状况。研究人员使用Spark实现了一个可迭代的EM算法,其中包括向Worker节点广播路线网络信息,在E和M阶段之间执行reduceByKey操作,应用从20个节点扩展到80个节点(每个节点4核),如图13(a)所示: 图13 每轮迭代运行时间(a)交通建模应用程序(b)基于Spark的社交网络的Spam分类社交网络Spam分类。Berkeley的Monarch项目[31]使用Spark识别Twitter消息上的Spam链接。他们在Spark上实现了一个类似7.1小节中示例的Logistic回归分类器,不同的是使用分布式的reduceByKey操作并行对梯度向量求和。图13(b)显示了基于50G数据子集训练训练分类器的结果,整个数据集是250000的URL、至少10^7个与网络相关的特征/维度,内容、词性与访问一个URL的页面相关。随着节点的增加,这并不像交通应用程序那样近似线性,主要是因为每轮迭代的固定通信代价较高。 7.6 交互式数据挖掘 为了展示Spark交互式处理大数据集的能力,我们在100个m2.4xlarge EC2实例(8核68G内存)上使用Spark分析1TB从2008-10到2009-4这段时间的Wikipedia页面浏览日志数据,在整个输入数据集上简单地查询如下内容以获取页面浏览总数:(1)全部页面;(2)页面的标题能精确匹配给定的关键词;(3)页面的标题能部分匹配给定的关键词。 图14 显示了分别在整个、1/2、1/10的数据上查询的响应时间,甚至1TB数据在Spark上查询仅耗时5-7秒,这比直接操作磁盘数据快几个数量级。例如,从磁盘上查询1TB数据耗时170秒,这表明了RDD缓存使得Spark成为一个交互式数据挖掘的强大工具。 8. 相关工作 分布式共享内存(DSM)。RDD可以看成是一个基于DSM研究[24]得到的抽象。在2.5节我们讨论过,RDD提供了一个比DSM限制更严格的编程模型,并能在节点失效时高效地重建数据集。DSM通过检查点[19]实现容错,而Spark使用Lineage重建RDD分区,这些分区可以在不同的节点上重新并行处理,而不需要将整个程序回退到检查点再重新运行。RDD能够像MapReduce一样将计算推向数据[12],并通过推测执行来解决某些任务计算进度落后的问题,推测执行在一般的DSM系统上是很难实现的。 In-Memory集群计算。Piccolo[28]是一个基于可变的、In-Memory的分布式表的集群编程模型。因为Piccolo允许读写表中的记录,它具有与DSM类似的恢复机制,需要检查点和回滚,但是不能推测执行,也没有提供类似groupBy、sort等更高级别的数据流算子,用户只能直接读取表单元数据来实现。可见,Piccolo是比Spark更低级别的编程模型,但是比DSM要高级。 RAMClouds[26]适合作为Web应用的存储系统,它同样提供了细粒度读写操作,所以需要通过记录日志来实现容错。 数据流系统。RDD借鉴了DryadLINQ[34]、Pig[25]和FlumeJava[9]的“并行收集”编程模型,通过允许用户显式地将未序列化的对象保存在内存中,以此来控制分区和基于key随机查找,从而有效地支持基于工作集的应用。RDD保留了那些数据流系统更高级别的编程特性,这对那些开发人员来说也比较熟悉,而且,RDD也能够支持更多类型的应用。RDD新增的扩展,从概念上看很简单,其中Spark是第一个使用了这些特性的系统,类似DryadLINQ编程模型,能够有效地支持基于工作集的应用。 面向基于工作集的应用,已经开发了一些专用系统,像Twister[13]、HaLoop[8]实现了一个支持迭代的MapReduce模型;Pregel[21],支持图应用的BSP计算模型。RDD是一个更通用的抽象,它能够描述支持迭代的MapReduce、Pregel,还有现有一些系统未能处理的应用,如交互式数据挖掘。特别地,它能够让开发人员动态地选择操作来运行在RDD上(如查看查询的结果以决定下一步运行哪个查询),而不是提供一系列固定的步骤去执行迭代,RDD还支持更多类型的转换。 最后,Dremel[22]是一个低延迟查询引擎,它面向基于磁盘存储的大数据集,这类数据集是把嵌套记录数据生成基于列的格式。这种格式的数据也能够保存为RDD并在Spark系统中使用,但Spark也具备将数据加载到内存来实现快速查询的能力。 Lineage。我们通过参考[6]到[10]做过调研,在科学计算和数据库领域,对于一些应用,如需要解释结果以及允许被重新生成、工作流中发现了bug或者数据集丢失需要重新处理数据,表示数据的Lineage和原始信息一直以来都是一个研究课题。RDD提供了一个受限的编程模型,在这个模型中使用细粒度的Lineage来表示是非常容易的,因此它可以被用于容错。 缓存系统。Nectar[14]能够通过识别带有程序分析的子表达式,跨DryadLINQ作业重用中间结果,如果将这种能力加入到基于RDD的系统会非常有趣。但是Nectar并没有提供In-Memory缓存,也不能够让用户显式地控制应该缓存那个数据集,以及如何对其进行分区。Ciel[23]同样能够记住任务结果,但不能提供In-Memory缓存并显式控制它。 语言迭代。DryadLINQ[34]能够使用LINQ获取到表达式树然后在集群上运行,Spark系统的语言集成与它很类似。不像DryadLINQ,Spark允许用户显式地跨查询将RDD存储到内存中,并通过控制分区来优化通信。Spark支持交互式处理,但DryadLINQ却不支持。 关系数据库。从概念上看,RDD类似于数据库中的视图,缓存RDD类似于物化视图[29]。然而,数据库像DSM系统一样,允许典型地读写所有记录,通过记录操作和数据的日志来实现容错,还需要花费额外的开销来维护一致性。RDD编程模型通过增加更多限制来避免这些开销。 9. 总结 我们提出的RDD是一个面向,运行在普通商用机集群之上并行数据处理应用的分布式内存抽象。RDD广泛支持基于工作集的应用,包括迭代式机器学习和图算法,还有交互式数据挖掘,然而它保留了数据流模型中引人注目的特点,如自动容错恢复,处理执行进度落后的任务,以及感知调度。它是通过限制编程模型,进而允许高效地重建RDD分区来实现的。RDD实现处理迭代式作业的速度超过Hadoop大约20倍,而且还能够交互式查询数百G数据。 致谢 首先感谢Spark用户,包括Timothy Hunter、Lester Mackey、Dilip Joseph、Jibin Zhan和Teodor Moldovan,他们在真实的应用中使用Spark,提出了宝贵的建议,同时也发现了一些新的研究挑战。这次研究离不开以下组织或团体的大力支持:Berkeley AMP Lab创立赞助者Google和SAP,AMP Lab赞助者Amazon Web Services、Cloudera、Huawei、IBM、Intel、Microsoft、NEC、NetApp和VMWare,国家配套资金加州MICRO项目(助学金 06-152,07-010),国家自然科学基金 (批准 CNS-0509559),加州大学工业/大学合作研究项目 (UC Discovery)授予的COM07-10240,以及自然科学和加拿大工程研究理事会。
Oozie所支持工作流,工作流定义通过将多个Hadoop Job的定义按照一定的顺序组织起来,然后作为一个整体按照既定的路径运行。一个工作流已经定义了,通过启动该工作流Job,就会执行该工作流中包含的多个Hadoop Job,直到完成,这就是工作流Job的生命周期。 那么,现在我们有一个工作流Job,希望每天半夜00:00启动运行,我们能够想到的就是通过写一个定时脚本来调度程序运行。如果我们有多个工作流Job,使用crontab的方式调用可能需要编写大量的脚本,还要通过脚本来控制好各个工作流Job的执行时序问题,不但脚本不好维护,而且监控也不方便。基于这样的背景,Oozie提出了Coordinator的概念,他们能够将每个工作流Job作为一个动作(Action)来运行,相当于工作流定义中的一个执行节点(我们可以理解为工作流的工作流),这样就能够将多个工作流Job组织起来,称为Coordinator Job,并指定触发时间和频率,还可以配置数据集、并发数等。一个Coordinator Job包含了在Job外部设置执行周期和频率的语义,类似于在工作流外部增加了一个协调器来管理这些工作流的工作流Job的运行。 运行Coordinator Job 我们先看一下官方发行包自带的一个简单的例子oozie-3.3.2\examples\src\main\apps\cron,它能够实现定时调度一个工作流Job运行,这个例子中给出的一个空的工作流Job,也是为了演示能够使用Coordinator系统给调度起来。这个例子有3个配置文件,我们不修改workflow.xml配置内容。修改后分别如下所示: job.properties配置 1 nameNode=hdfs://m1:9000 2 jobTracker=m1:19830 3 queueName=default 4 examplesRoot=examples 5 6 oozie.coord.application.path=${nameNode}/user/${user.name}/${examplesRoot}/apps/cron 7 start=2014-03-04T19:00Z 8 end=2014-03-06T01:00Z 9 workflowAppUri=${nameNode}/user/${user.name}/${examplesRoot}/apps/cron 修改了Hadoop集群的配置,以及调度起止时间范围。 workflow.xml配置 1 <workflow-app xmlns="uri:oozie:workflow:0.2" name="no-op-wf"> 2 <start to="end"/> 3 <end name="end"/> 4 </workflow-app> 是一个空Job,没做任何修改。 coordinator.xml配置 01 <coordinator-app name="cron-coord" frequency="${coord:minutes(2)}" start="${start}"end="${end}" timezone="UTC" xmlns="uri:oozie:coordinator:0.2"> 02 <action> 03 <workflow> 04 <app-path>${workflowAppUri}</app-path> 05 <configuration> 06 <property> 07 <name>jobTracker</name> 08 <value>${jobTracker}</value> 09 </property> 10 <property> 11 <name>nameNode</name> 12 <value>${nameNode}</value> 13 </property> 14 <property> 15 <name>queueName</name> 16 <value>${queueName}</value> 17 </property> 18 </configuration> 19 </workflow> 20 </action> 21 </coordinator-app> 修改上述coordinator.xml配置文件,将定时调度频率改为2分钟,然后需要将他们上传到HDFS上: 1 hadoop fs -rm /user/shirdrn/examples/apps/cron/coordinator.xml 2 hadoop fs -put /home/shirdrn/cloud/programs/oozie-3.3.2/examples/target/oozie-examples-3.3.2-examples/examples/apps/cron/coordinator.xml /user/shirdrn/examples/apps/cron/ 因为我之前已经上传过一次,所以修改了coordinator.xml文件配置内容后,一定要上传到HDFS中,而job.properties配置可以通过指定config选项来执行。启动一个Coordinator Job和启动一个Oozie工作流Job类似,执行如下命令即可: 1 bin/oozie job -oozie http://oozie-server:11000/oozie -config /home/shirdrn/cloud/programs/oozie-3.3.2/examples/target/oozie-examples-3.3.2-examples/examples/apps/cron/job.properties -run 运行上面命令,在控制台上会返回这个Job的ID,我们也可以通过Oozie的Web控制台来查看: Coordinator Job状态 Coordinator Job详情如果想要杀掉一个Job,需要指定Oozie的Job ID,可以执行如下命令: 1 bin/oozie job -oozie http://oozie-server:11000/oozie -kill 0000065-140302210847342-oozie-shir-C Coordinator应用(Coordinator Application) Coordinator应用是指当满足一定条件时,会触发Oozie工作流Job(在Coordinator中将工作流Job定义为一个动作(Action))。其中,触发条件可以是一个时间频率、一个dataset实例是否可用,或者可能是外部的其他事件。 Coordinator Job是一个Coordinator应用的运行实例,这个Coordinator Job是在Oozie提供的Coordinator引擎上运行的,并且这个实例从指定的时间开始,直到运行结束。一个Coordinator Job具有以上几个状态: PREP RUNNING RUNNINGWITHERROR PREPSUSPENDED SUSPENDED SUSPENDEDWITHERROR PREPPAUSED PAUSED PAUSEDWITHERROR SUCCEEDED DONEWITHERROR KILLED FAILED 从状态字符串的含义,我们大概就能知道它的含义,这里不做过多解释,可以查阅官方文档。现在,我们关注一下这些状态之间是怎样转移的,从一个状态变成哪些状态是合法的,如下表所示: 转移前状态 转以后状态集合 PREP PREPSUSPENDED | PREPPAUSED | RUNNING | KILLED RUNNING RUNNINGWITHERROR | SUSPENDED | PAUSED | SUCCEEDED | KILLED RUNNINGWITHERROR RUNNING | SUSPENDEDWITHERROR | PAUSEDWITHERROR | DONEWITHERROR | KILLED | FAILED PREPSUSPENDED PREP | KILLED SUSPENDED RUNNING | KILLED SUSPENDEDWITHERROR RUNNINGWITHERROR | KILLED PREPPAUSED PREP | KILLED PAUSED SUSPENDED | RUNNING | KILLED PAUSEDWITHERROR SUSPENDEDWITHERROR | RUNNINGWITHERROR | KILLED 我们可以看到,Coordinator Job的状态比一个基本的Oozie工作流Job的状态要复杂的多,因为Coordinator Job的基本执行单元可能是一个基本Oozie Job,而且外加了一些调度信息,必然要增加额外的状态来描述。 Coordinator动作(Coordinator Action) 一个Coordinator Job会创建并执行Coordinator 动作(Coordinator Action)。通常一个Coordinator 动作是一个工作流Job,这个工作流Job会生成一个dataset实例并处理这个数据集。当一个一个Coordinator 动作被创建以后,它会一直等待满足执行条件的所有输入事件的完成然后执行,或者发生超时。 每个Coordinator Job都有一个驱动事件,来决定它所包含的Coordinator动作的初始化(创建)。对于同步Coordinator Job(synchronous coordinator job)来说,触发执行频率(frequency)就是一个驱动事件。 同样,组成Coordinator Job的基本单元是Coordinator 动作(Coordinator Action),它不像Oozie工作流Job只有OK和Error两个执行结果,一个Coordinator 动作的状态集合,如下所示: WAITING READY SUBMITTED TIMEDOUT RUNNING KILLED SUCCEEDED FAILED 一个Coordinator 动作的状态变迁情况,如下表所示: 转移前状态 转以后状态集合 WAITING READY | TIMEDOUT | KILLED READY SUBMITTED | KILLED SUBMITTED RUNNING | KILLED | FAILED RUNNING SUCCEEDED | KILLED | FAILED Coordinator应用定义(Coordinator Application Definition) 一个同步的Coordinator应用定义的语法格式,如下所示: 01 <coordinator-app name="[NAME]" frequency="[FREQUENCY]" start="[DATETIME]" end="[DATETIME]" timezone="[TIMEZONE]" xmlns="uri:oozie:coordinator:0.1"> 02 <controls> 03 <timeout>[TIME_PERIOD]</timeout> 04 <concurrency>[CONCURRENCY]</concurrency> 05 <execution>[EXECUTION_STRATEGY]</execution> 06 </controls> 07 <datasets> 08 <include>[SHARED_DATASETS]</include> 09 ... 10 <!-- Synchronous datasets --> 11 <dataset name="[NAME]" frequency="[FREQUENCY]" initial-instance="[DATETIME]"timezone="[TIMEZONE]"> 12 <uri-template>[URI_TEMPLATE]</uri-template> 13 </dataset> 14 ... 15 </datasets> 16 <input-events> 17 <data-in name="[NAME]" dataset="[DATASET]"> 18 <instance>[INSTANCE]</instance> 19 ... 20 </data-in> 21 ... 22 <data-in name="[NAME]" dataset="[DATASET]"> 23 <start-instance>[INSTANCE]</start-instance> 24 <end-instance>[INSTANCE]</end-instance> 25 </data-in> 26 ... 27 </input-events> 28 <output-events> 29 <data-out name="[NAME]" dataset="[DATASET]"> 30 <instance>[INSTANCE]</instance> 31 </data-out> 32 ... 33 </output-events> 34 <action> 35 <workflow> 36 <app-path>[WF-APPLICATION-PATH]</app-path> 37 <configuration> 38 <property> 39 <name>[PROPERTY-NAME]</name> 40 <value>[PROPERTY-VALUE]</value> 41 </property> 42 ... 43 </configuration> 44 </workflow> 45 </action> 46 </coordinator-app> 基于上述定义语法格式,我们分别说明对应元素的含义,如下所示: control元素 control元素定义了一个Coordinator Job的控制信息,主要包括如下三个配置元素: 元素名称 含义说明 timeout 超时时间,单位为分钟。当一个Coordinator Job启动的时候,会初始化多个Coordinator动作,timeout用来限制这个初始化过程。默认值为-1,表示永远不超时,如果为0 则总是超时。 concurrency 并发数,指多个Coordinator Job并发执行,默认值为1。 execution 配置多个Coordinator Job并发执行的策略:默认是FIFO。另外还有两种:LIFO(最新的先执行)、LAST_ONLY(只执行最新的Coordinator Job,其它的全部丢弃)。 throttle 一个Coordinator Job初始化时,允许Coordinator动作处于WAITING状态的最大数量。 Dataset元素 Coordinator Job中有一个Dataset的概念,它可以为实际计算提供计算的数据,主要是指HDFS上的数据目录或文件,能够配置数据集生成的频率(Frequency)、URI模板、时间等信息,下面看一下dataset的语法格式: 1 <dataset name="[NAME]" frequency="[FREQUENCY]" initial-instance="[DATETIME]"timezone="[TIMEZONE]"> 2 <uri-template>[URI TEMPLATE]</uri-template> 3 <done-flag>[FILE NAME]</done-flag> 4 </dataset> 举例如下: 1 <dataset name="stats_hive_table" frequency="${coord:days(1)}" initial-instance="2014-03-05T00:00Z" timezone="America/Los_Angeles"> 2 <uri-template> 3 hdfs://m1:9000/hive/warehouse/user_events/${YEAR}${MONTH}/${DAY}/data 4 </uri-template> 5 <done-flag>donefile.flag</done-flag> 6 </dataset> 上面会每天都会生成一个用户事件表,可以供Hive查询分析,这里指定了这个数据集的位置,后续计算会使用这部分数据。其中,uri-template指定了一个匹配的模板,满足这个模板的路径都会被作为计算的基础数据。 另外,还有一种定义dataset集合的方式,将多个dataset合并成一个组来定义,语法格式如下所示: 1 <datasets> 2 <include>[SHARED_DATASETS]</include> 3 ... 4 <dataset name="[NAME]" frequency="[FREQUENCY]" initial-instance="[DATETIME]"timezone="[TIMEZONE]"> 5 <uri-template>[URI TEMPLATE]</uri-template> 6 </dataset> 7 ... 8 </datasets> input-events和output-events元素 一个Coordinator应用的输入事件指定了要执行一个Coordinator动作必须满足的输入条件,在Oozie当前版本,只支持使用dataset实例。 一个Coordinator动作可能会生成一个或多个dataset实例,在Oozie当前版本,输出事件只支持输出dataset实例。 EL常量 常量表示形式 含义说明 ${coord:minutes(int n)} 返回日期时间:从一开始,周期执行n分钟 ${coord:hours(int n)} 返回日期时间:从一开始,周期执行n * 60分钟 ${coord:days(int n)} 返回日期时间:从一开始,周期执行n * 24 * 60分钟 ${coord:months(int n)} 返回日期时间:从一开始,周期执行n * M * 24 * 60分钟(M表示一个月的天数) ${coord:endOfDays(int n)} 返回日期时间:从当天的最晚时间(即下一天)开始,周期执行n * 24 * 60分钟 ${coord:endOfMonths(1)} 返回日期时间:从当月的最晚时间开始(即下个月初),周期执行n * 24 * 60分钟 ${coord:current(int n)} 返回日期时间:从一个Coordinator动作(Action)创建时开始计算,第n个dataset实例执行时间 ${coord:dataIn(String name)} 在输入事件(input-events)中,解析dataset实例包含的所有的URI ${coord:dataOut(String name)} 在输出事件(output-events)中,解析dataset实例包含的所有的URI ${coord:offset(int n, String timeUnit)} 表示时间偏移,如果一个Coordinator动作创建时间为T,n为正数表示向时刻T之后偏移,n为负数向向时刻T之前偏移,timeUnit表示时间单位(选项有MINUTE、HOUR、DAY、MONTH、YEAR) ${coord:hoursInDay(int n)} 指定的第n天的小时数,n>0表示向后数第n天的小时数,n=0表示当天小时数,n<0表示向前数第n天的小时数 ${coord:daysInMonth(int n)} 指定的第n个月的天数,n>0表示向后数第n个月的天数,n=0表示当月的天数,n<0表示向前数第n个月的天数 ${coord:tzOffset()} ataset对应的时区与Coordinator Job的时区所差的分钟数 ${coord:latest(int n)} 最近以来,当前可以用的第n个dataset实例 ${coord:future(int n, int limit)} 当前时间之后的dataset实例,n>=0,当n=0时表示立即可用的dataset实例,limit表示dataset实例的个数 ${coord:nominalTime()} nominal时间等于Coordinator Job启动时间,加上多个Coordinator Job的频率所得到的日期时间。例如:start=”2009-01-01T24:00Z”,end=”2009-12-31T24:00Z”,frequency=”${coord:days(1)}”,frequency=”${coord:days(1)},则nominal时间为:2009-01-02T00:00Z、2009-01-03T00:00Z、2009-01-04T00:00Z、…、2010-01-01T00:00Z ${coord:actualTime()} Coordinator动作的实际创建时间。例如:start=”2011-05-01T24:00Z”,end=”2011-12-31T24:00Z”,frequency=”${coord:days(1)}”,则实际时间为:2011-05-01,2011-05-02,2011-05-03,…,2011-12-31 ${coord:user()} 启动当前Coordinator Job的用户名称 ${coord:dateOffset(String baseDate, int instance, String timeUnit)} 计算新的日期时间的公式:newDate = baseDate + instance * timeUnit,如:baseDate=’2009-01-01T00:00Z’,instance=’2′,timeUnit=’MONTH’,则计算得到的新的日期时间为’2009-03-01T00:00Z’。 ${coord:formatTime(String timeStamp, String format)} 格式化时间字符串,format指定模式 配置举例 下面,根据官网上给出的例子,进行说明,配置例子如下所示: 01 <coordinator-app name="hello2-coord" frequency="${coord:days(7)}" 02 start="2009-01-07T24:00Z" end="2009-12-12T24:00Z" timezone="UTC" 03 xmlns="uri:oozie:coordinator:0.1"> 04 <datasets> 05 <dataset name="logs" frequency="${coord:days(1)}" 06 initial-instance="2009-01-01T24:00Z" timezone="UTC"> 07 <uri-template>hdfs://bar:8020/app/logs/${YEAR}${MONTH}/${DAY} 08 </uri-template> 09 </dataset> 10 <dataset name="weeklySiteAccessStats" frequency="${coord:days(7)}" 11 initial-instance="2009-01-07T24:00Z" timezone="UTC"> 12 <uri-template>hdfs://bar:8020/app/weeklystats/${YEAR}/${MONTH}/${DAY} 13 </uri-template> 14 </dataset> 15 </datasets> 16 <input-events> 17 <data-in name="input" dataset="logs"> 18 <start-instance>${coord:current(-6)}</start-instance> 19 <end-instance>${coord:current(0)}</end-instance> 20 </data-in> 21 </input-events> 22 <output-events> 23 <data-out name="output" dataset="siteAccessStats"> 24 <instance>${coord:current(0)}</instance> 25 </data-out> 26 </output-events> 27 <action> 28 <workflow> 29 <app-path>hdfs://bar:8020/usr/joe/logsprocessor-wf</app-path> 30 <configuration> 31 <property> 32 <name>wfInput</name> 33 <value>${coord:dataIn('input')}</value> 34 </property> 35 <property> 36 <name>wfOutput</name> 37 <value>${coord:dataOut('output')}</value> 38 </property> 39 </configuration> 40 </workflow> 41 </action> 42 </coordinator-app> 名称为logs的dataset实例频率为1天,它配置的初始实例时间为2009-01-07T24:00Z,则在input-events输入事件中开始实例(start-instance)时间为6天前,即2009-01-01T24:00Z,结束实例(end-instance)时间为当天时间。 后半部分中定义了action,其中${coord:dataIn(‘input’)}表示解析名称为input的输入事件所关联的URI(即HDFS上的文件或目录)。
Oozie是一个开源的工作流调度系统,它能够管理逻辑复杂的多个Hadoop作业,按照指定的顺序将其协同运行起来。例如,我们可能有这样一个需求,某个业务系统每天产生20G原始数据,我们每天都要对其进行处理,处理步骤如下所示: 通过Hadoop先将原始数据同步到HDFS上; 借助MapReduce计算框架对原始数据进行转换,生成的数据以分区表的形式存储到多张Hive表中; 需要对Hive中多个表的数据进行JOIN处理,得到一个明细数据Hive大表; 将明细数据进行复杂的统计分析,得到排序后的报表信息; 需要将统计分析得到的结果数据同步到业务系统中,供业务调用使用。 上述过程可以通过工作流系统来编排任务,最终生成一个工作流实例,然后每天定时启动运行这个实例即可。在这种依赖于Hadoop存储和处理能力要求的应用场景下,Oozie可能能够简化任务调度和执行。 这里,我们在CentOS 6.2系统下安装Oozie-3.3.2,需要安装相关的依赖软件包,下面我们一步一步地进行安装,包括安装配置依赖软件包。这里,我们使用MySQL数据库存储Oozie数据,Hadoop使用的是1.2.1版本。 安装Oozie Server Oozie Server可以为我们提供很多管理Job的便捷功能,比如,通过可视化界面去管理Job的运行状态,同时也支持我构建含有多个复杂Hadoop Job流程,各个Job之间的依赖关系完全可以通过一个工作流配置文件组装起来,然后由Oozie Server其管理执行。 安装Maven构建工具 下载安装,执行如下命令: 1 wget http://mirrors.hust.edu.cn/apache/maven/maven-3/3.2.1/binaries/apache-maven-3.2.1-bin.tar.gz 2 tar xvzf apache-maven-3.2.1-bin.tar.gz 加入环境变量,使变量配置生效: 1 export MAVEN_HOME=/home/shirdrn/cloud/programs/apache-maven-3.2.1 2 export PATH=$PATH:$MAVEN_HOME/bin 安装MySQL数据库 安装MySQL数据库,执行如下命令: 1 sudo rpm -e --nodeps mysql 2 yum list | grep mysql 3 sudo yum install -y mysql-server mysql mysql-deve 为root用户设置密码: 1 mysqladmin -u root password '8YOhyo988_Kjo0' 然后可以使用root账号登录MySQL数据库,进行管理: 1 mysql -u root -p 输入密码登录成功。 安装配置Tomcat 下载安装Tomcat Web服务器: 1 wget http://apache.dataguru.cn/tomcat/tomcat-7/v7.0.52/bin/apache-tomcat-7.0.52.tar.gz 2 tar xvzf apache-tomcat-7.0.52.tar.gz 设置环境变量: 1 export CATALINA_HOME=/home/shirdrn/cloud/programs/apache-tomcat-7.0.52 2 export PATH=$PATH:$CATALINA_HOME/bin 如果使用MySQL存储Oozie数据,需要将MySQL的驱动程序拷贝到Tomcat安装目录下,亦即$CATALINA_HOME/lib下面。 准备ExtJS工具包 下载ExtJS压缩包: 1 wget http://extjs.com/deploy/ext-2.2.zip 安装Oozie 下载安装,执行如下命令: 1 wget http://mirror.bit.edu.cn/apache/oozie/3.3.2/oozie-3.3.2.tar.gz 2 tar xvzf oozie-3.3.2.tar.gz 3 cd oozie-3.3.2 4 bin/mkdistro.sh -DskipTests 构建成后,可以在oozie-3.3.2/distro/target目录下看到构建后的文件,例如我的路径是/home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2,内容如下所示: 1 [shirdrn@oozie-server oozie-3.3.2]$ pwd 2 /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2 3 [shirdrn@oozie-server oozie-3.3.2]$ ls 4 bin lib oozie-core oozie-sharelib-3.3.2.tar.gz 5 conf libtools oozie-examples.tar.gz oozie.war 6 docs.zip oozie-client-3.3.2.tar.gz oozie-server release-log.txt 将OOZIE_HOME变量指向该目录,修改~/.bashrc文件: 1 export OOZIE_HOME=/home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2 2 export PATH=$PATH:$OOZIE_HOME/bin 将ExtJS工具包拷贝到目录$OOZIE_HOME中: 1 cp ~/cloud/programs/oozie-3.3.2/ext-2.2.zip $OOZIE_HOME/ 在上面的目录下创建libext目录,并将hadoop相关的jar库文件拷贝到libext下面,我使用的是Hadoop 1.2.1版本: 1 [shirdrn@oozie-server oozie-3.3.2]$ mkdir libext 2 [shirdrn@oozie-server oozie-3.3.2]$ cp ~/cloud/programs/hadoop-1.2.1/hadoop-*.jar libext/ 3 [shirdrn@oozie-server oozie-3.3.2]$ cp ~/cloud/programs/hadoop-1.2.1/lib/*.jar ./libext/ 同时,我们使用了MySQL来存储Oozie的元数据,现在需要将MySQL的驱动程序添加到libext目录下: 1 cp ~/packages/mysql-connector-java-5.1.29/mysql-connector-java-5.1.29/mysql-connector-java-5.1.29-bin.jar libext/ 执行下面的命令开始安装: 1 bin/oozie-setup.sh prepare-war 运行结果,示例如下: 01 setting CATALINA_OPTS="$CATALINA_OPTS -Xmx1024m" 02 03 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/asm-3.2.jar 04 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/aspectjrt-1.6.11.jar 05 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/aspectjtools-1.6.11.jar 06 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-beanutils-1.7.0.jar 07 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-beanutils-core-1.8.0.jar 08 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-cli-1.2.jar 09 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-codec-1.4.jar 10 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-collections-3.2.1.jar 11 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-configuration-1.6.jar 12 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-daemon-1.0.1.jar 13 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-digester-1.8.jar 14 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-el-1.0.jar 15 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-httpclient-3.0.1.jar 16 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-io-2.1.jar 17 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-lang-2.4.jar 18 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-logging-1.1.1.jar 19 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-logging-api-1.0.4.jar 20 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-math-2.1.jar 21 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/commons-net-3.1.jar 22 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/core-3.1.1.jar 23 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-ant-1.2.1.jar 24 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-capacity-scheduler-1.2.1.jar 25 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-client-1.2.1.jar 26 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-core-1.2.1.jar 27 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-examples-1.2.1.jar 28 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-fairscheduler-1.2.1.jar 29 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-minicluster-1.2.1.jar 30 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-test-1.2.1.jar 31 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-thriftfs-1.2.1.jar 32 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hadoop-tools-1.2.1.jar 33 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/hsqldb-1.8.0.10.jar 34 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jackson-core-asl-1.8.8.jar 35 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jackson-mapper-asl-1.8.8.jar 36 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jasper-compiler-5.5.12.jar 37 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jasper-runtime-5.5.12.jar 38 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jdeb-0.8.jar 39 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jersey-core-1.8.jar 40 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jersey-json-1.8.jar 41 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jersey-server-1.8.jar 42 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jets3t-0.6.1.jar 43 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jetty-6.1.26.jar 44 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jetty-util-6.1.26.jar 45 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/jsch-0.1.42.jar 46 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/junit-4.5.jar 47 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/kfs-0.2.2.jar 48 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/log4j-1.2.15.jar 49 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/mockito-all-1.8.5.jar 50 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/mysql-connector-java-5.1.29-bin.jar 51 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/oro-2.0.8.jar 52 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/servlet-api-2.5-20081211.jar 53 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/slf4j-api-1.4.3.jar 54 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/slf4j-log4j12-1.4.3.jar 55 INFO: Adding extension: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/libext/xmlenc-0.52.jar 56 57 New Oozie WAR file with added 'ExtJS library, JARs' at /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/oozie-server/webapps/oozie.war 58 59 60 INFO: Oozie is ready to be started 这样,上述已经生成了/home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/oozie-server/webapps/oozie.war文件。 配置Oozie 修改conf/oozie-site.xml配置文件,内容如下所示: 01 <property> 02 <name>oozie.service.JPAService.jdbc.driver</name> 03 <value>com.mysql.jdbc.Driver</value> 04 <description> 05 JDBC driver class. 06 </description> 07 </property> 08 <property> 09 <name>oozie.service.JPAService.jdbc.url</name> 10 <value>jdbc:mysql://mysql-server:3306/oozie</value> 11 <description> 12 JDBC URL. 13 </description> 14 </property> 15 <property> 16 <name>oozie.service.JPAService.jdbc.username</name> 17 <value>shirdrn</value> 18 <description> 19 DB user name. 20 </description> 21 </property> 22 <property> 23 <name>oozie.service.JPAService.jdbc.password</name> 24 <value>0o21e</value> 25 <description> 26 DB user password. 27 IMPORTANT: if password is emtpy leave a 1 space string, the service trims the value, 28 if empty Configuration assumes it is NULL. 29 </description> 30 </property> 默认情况下,Oozie的配置中有个配置项oozie.service.JPAService.create.db.schema,值为false,设置非自动创建数据库,我们保持默认设置,这样可以通过手动创建Oozie数据库,并对其进行权限控制。然后,我们在MySQL数据库中创建数据库,名称为oozie,并进行访问授权: 1 CREATE DATABASE oozie; 2 GRANT ALL ON oozie.* TO 'shirdrn'@'oozie-server' IDENTIFIED BY '0o21e'; 3 FLUSH PRIVILEGES; 然后可以执行如下命令,生成Oozie所需要的数据表: 1 bin/ooziedb.sh create -sqlfile oozie.sql -run 查看控制台输出日志,没有报错,并且在当前目录下可以看到,同时也生成了oozie.sql脚本文件。到MySQL数据库中可以看到生成的表,说明上述操作执行成功。 下面可以启动Oozie,使用如下命令: 1 bin/oozied.sh start 启动信息,示例如下所示: 01 Setting OOZIE_HOME: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2 02 Setting OOZIE_CONFIG: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/conf 03 Sourcing: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/conf/oozie-env.sh 04 setting CATALINA_OPTS="$CATALINA_OPTS -Xmx1024m" 05 Setting OOZIE_CONFIG_FILE: oozie-site.xml 06 Setting OOZIE_DATA: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/data 07 Setting OOZIE_LOG: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/logs 08 Setting OOZIE_LOG4J_FILE: oozie-log4j.properties 09 Setting OOZIE_LOG4J_RELOAD: 10 10 Setting OOZIE_HTTP_HOSTNAME: oozie-server 11 Setting OOZIE_HTTP_PORT: 11000 12 Setting OOZIE_ADMIN_PORT: 11001 13 Setting OOZIE_HTTPS_PORT: 11443 14 Setting OOZIE_BASE_URL: http://oozie-server:11000/oozie 15 Setting CATALINA_BASE: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/oozie-server 16 Setting OOZIE_HTTPS_KEYSTORE_FILE: /home/shirdrn/.keystore 17 Setting OOZIE_HTTPS_KEYSTORE_PASS: password 18 Setting CATALINA_OUT: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/logs/catalina.out 19 Setting CATALINA_PID: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/oozie-server/temp/oozie.pid 20 21 Using CATALINA_OPTS: -Xmx1024m -Dderby.stream.error.file=/home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/logs/derby.log 22 Adding to CATALINA_OPTS: -Doozie.home.dir=/home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2 -Doozie.config.dir=/home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/conf -Doozie.log.dir=/home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/logs -Doozie.data.dir=/home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/data -Doozie.config.file=oozie-site.xml -Doozie.log4j.file=oozie-log4j.properties -Doozie.log4j.reload=10 -Doozie.http.hostname=m1 -Doozie.admin.port=11001 -Doozie.http.port=11000 -Doozie.https.port=11443 -Doozie.base.url=http://m1:11000/oozie -Doozie.https.keystore.file=/home/shirdrn/.keystore -Doozie.https.keystore.pass=password -Djava.library.path= 23 24 Using CATALINA_BASE: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/oozie-server 25 Using CATALINA_HOME: /home/shirdrn/cloud/programs/apache-tomcat-7.0.52 26 Using CATALINA_TMPDIR: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/oozie-server/temp 27 Using JRE_HOME: /usr/java/jdk1.7.0_25/ 28 Using CLASSPATH: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/oozie-server/bin/tomcat-juli.jar:/home/shirdrn/cloud/programs/apache-tomcat-7.0.52/bin/bootstrap.jar 29 Using CATALINA_PID: /home/shirdrn/cloud/programs/oozie-3.3.2/distro/target/oozie-3.3.2-distro/oozie-3.3.2/oozie-server/temp/oozie.pid 从上面日志可以看到,Oozie管理控制台连接为http://oozie-server:11000/oozie,可以看到图形化界面。 整合Oozie和Hadoop 我们的Hadoop平台使用的是用户shirdrn,用户组为shirdrn,这里配置Hadoop代理用户也使用该用户,部署Oozie的主机名为oozie-server。修改Hadoop的配置文件core-site.xml,增加如下配置内容: 1 <!-- OOZIE --> 2 <property> 3 <name>hadoop.proxyuser.shirdrn.hosts</name> 4 <value>oozie-server</value> 5 </property> 6 <property> 7 <name>hadoop.proxyuser.shirdrn.groups</name> 8 <value>shirdrn</value> 9 </property> 修改完上述配置后,需要重新启动Hadoop集群才能生效。 安装Oozie Client 我们可以通过在外部的一个Oozie客户端去提交工作流任务,实际上就是一个客户端程序,通过与Oozie Server进行交互,提交任务,并由Oozie Server去调用执行。 我们可以回到前面解压缩Oozie发行包oozie-3.3.2.tar.gz的目录下,通过前面的构建,现在已经可以看到有一个client目录,该目录下就是Oozie的客户端相关文件。含有Oozie客户端脚本的路径,我这里为/home/shirdrn/cloud/programs/oozie-3.3.2/client/target/oozie-client-3.3.2-client/oozie-client-3.3.2。 查看Oozie客户端运行job的命令帮助信息,可以执行如下命令: 1 cd /home/shirdrn/cloud/programs/oozie-3.3.2/client/target/oozie-client-3.3.2-client/oozie-client-3.3.2 2 bin/oozie help 3 bin/oozie help job 我们可以找到,Oozie发行包中自带的examples,我这里对应的目录是/home/shirdrn/cloud/programs/oozie-3.3.2/examples/target/oozie-examples-3.3.2-examples/examples/apps,我们可以通过运行这些例子来验证安装是否成功。 首先,将Oozie自带的examples上传到HDFS上: 1 bin/hadoop fs -mkdir /oozie 2 bin/hadoop fs -copyFromLocal /home/shirdrn/cloud/programs/oozie-3.3.2/examples/target/oozie-examples-3.3.2-examples/examples /user/shirdrn/examples 我们拿examples中的map-reduce来进行验证,修改job.properties文件,配置内容如下所示: 1 nameNode=hdfs://m1:9000 2 jobTracker=m1:19830 3 queueName=default 4 examplesRoot=examples 5 6 oozie.wf.application.path=${nameNode}/user/${user.name}/${examplesRoot}/apps/map-reduce 7 outputDir=map-reduce 我的环境下,Namenode服务端口为hdfs://m1:9000,JobTracker为m1:19830,运行任务,执行如下命令: 1 cd /home/shirdrn/cloud/programs/oozie-3.3.2/client/target/oozie-client-3.3.2-client/oozie-client-3.3.2 2 bin/oozie job -oozie http://oozie-server:11000/oozie -config /home/shirdrn/cloud/programs/oozie-3.3.2/examples/target/oozie-examples-3.3.2-examples/examples/apps/map-reduce/job.properties -run 可以通过OozieWeb管理控制台查看提交运行的任务,如图所示: 以及,job配置,运行状态等信息,如图所示:上面命令选项-run表示直接运行一个job,当然你可以使用其他选项,如-submit是提交job,-rerun是重新运行job,-suspend是挂起job等等,可以查看命令帮助,或参考相关文档。
文本分类,首先它是分类问题,应该对应着分类过程的两个重要的步骤,一个是使用训练数据集训练分类器,另一个就是使用测试数据集来评价分类器的分类精度。然而,作为文本分类,它还具有文本这样的约束,所以对于文本来说,需要额外的处理过程,我们结合使用libsvm从宏观上总结一下,基于libsvm实现文本分类实现的基本过程,如下所示: 选择文本训练数据集和测试数据集:训练集和测试集都是类标签已知的; 训练集文本预处理:这里主要包括分词、去停用词、建立词袋模型(倒排表); 选择文本分类使用的特征向量(词向量):最终的目标是使得最终选出的特征向量在多个类别之间具有一定的类别区分度,可以使用相关有效的技术去实现特征向量的选择,由于分词后得到大量的词,通过选择降维技术能很好地减少计算量,还能维持分类的精度; 输出libsvm支持的量化的训练样本集文件:类别名称、特征向量中每个词元素分别到数字编号的映射转换,以及基于类别和特征向量来量化文本训练集,能够满足使用libsvm训练所需要的数据格式; 测试数据集预处理:同样包括分词(需要和训练过程中使用的分词器一致)、去停用词、建立词袋模型(倒排表),但是这时需要加载训练过程中生成的特征向量,用特征向量去排除多余的不在特征向量中的词(也称为降维); 输出libsvm支持的量化的测试样本集文件:格式和训练数据集的预处理阶段的输出相同。 使用libsvm训练文本分类器:使用训练集预处理阶段输出的量化的数据集文件,这个阶段也需要做很多工作(后面会详细说明),最终输出分类模型文件 使用libsvm验证分类模型的精度:使用测试集预处理阶段输出的量化的数据集文件,和分类模型文件来验证分类的精度。 分类模型参数寻优:如果经过libsvm训练出来的分类模型精度很差,可以通过libsvm自带的交叉验证(Cross Validation)功能来实现参数的寻优,通过搜索参数取值空间来获取最佳的参数值,使分类模型的精度满足实际分类需要。 基于上面的分析,分别对上面每个步骤进行实现,最终完成一个分类任务。 数据集选择 我们选择了搜狗的语料库,可以参考后面的链接下载语料库文件。这里,需要注意的是,分别准备一个训练数据集和一个测试数据集,不要让两个数据集有交叉。例如,假设有C个类别,选择每个分类的下的N篇文档作为训练集,总共的训练集文档数量为C*N,剩下的每一类下M篇作为测试数据集使用,测试数据集总共文档数等于C*M。 数据集文本预处理 我们选择使用ICTCLAS分词器,使用该分词器可以不需要预先建立自己的词典,而且分词后已经标注了词性,可以根据词性对词进行一定程度过滤(如保留名词,删除量词、叹词等等对分类没有意义的词汇)。 下载ICTCLAS软件包,如果是在Win7 64位系统上使用Java实现分词,选择如下两个软件包: 20131115123549_nlpir_ictclas2013_u20131115_release.zip 20130416090323_Win-64bit-JNI-lib.zip 将第二个软件包中的NLPIR_JNI.dll文件拷贝到C:\Windows\System32目录下面,将第一个软件包中的Data目录和NLPIR.dll、NLPIR.lib、NLPIR.h、NLPIR.lib文件拷贝到Java工程根目录下面。 对于其他操作系统,可以到ICTCLAS网站(http://ictclas.nlpir.org/downloads)下载对应版本的软件包。 下面,我们使用Java实现分词,定义分词器接口,以便切换其他分词器实现时,容易扩展,如下所示: 1 package org.shirdrn.document.processor.common; 2 3 import java.io.File; 4 import java.util.Map; 5 6 public interface DocumentAnalyzer { 7 Map<String, Term> analyze(File file); 8 } 增加一个外部的停用词表,这个我们直接封装到抽象类AbstractDocumentAnalyzer中去了,该抽象类就是从一个指定的文件或目录读取停用词文件,将停用词加载到内存中,在分词的过程中对词进行进一步的过滤。然后基于上面的实现,给出包裹ICTCLAS分词器的实现,代码如下所示: 01 package org.shirdrn.document.processor.analyzer; 02 03 import java.io.BufferedReader; 04 import java.io.File; 05 import java.io.FileInputStream; 06 import java.io.IOException; 07 import java.io.InputStreamReader; 08 import java.util.HashMap; 09 import java.util.Map; 10 11 import kevin.zhang.NLPIR; 12 13 import org.apache.commons.logging.Log; 14 import org.apache.commons.logging.LogFactory; 15 import org.shirdrn.document.processor.common.DocumentAnalyzer; 16 import org.shirdrn.document.processor.common.Term; 17 import org.shirdrn.document.processor.config.Configuration; 18 19 public class IctclasAnalyzer extends AbstractDocumentAnalyzer implementsDocumentAnalyzer { 20 21 private static final Log LOG = LogFactory.getLog(IctclasAnalyzer.class); 22 private final NLPIR analyzer; 23 24 public IctclasAnalyzer(Configuration configuration) { 25 super(configuration); 26 analyzer = new NLPIR(); 27 try { 28 boolean initialized = NLPIR.NLPIR_Init(".".getBytes(charSet), 1); 29 if(!initialized) { 30 throw new RuntimeException("Fail to initialize!"); 31 } 32 } catch (Exception e) { 33 throw new RuntimeException("", e); 34 } 35 } 36 37 @Override 38 public Map<String, Term> analyze(File file) { 39 String doc = file.getAbsolutePath(); 40 LOG.info("Process document: file=" + doc); 41 Map<String, Term> terms = new HashMap<String, Term>(0); 42 BufferedReader br = null; 43 try { 44 br = new BufferedReader(new InputStreamReader(newFileInputStream(file), charSet)); 45 String line = null; 46 while((line = br.readLine()) != null) { 47 line = line.trim(); 48 if(!line.isEmpty()) { 49 byte nativeBytes[] = analyzer.NLPIR_ParagraphProcess(line.getBytes(charSet), 1); 50 String content = new String(nativeBytes, 0, nativeBytes.length, charSet); 51 String[] rawWords = content.split("\\s+"); 52 for(String rawWord : rawWords) { 53 String[] words = rawWord.split("/"); 54 if(words.length == 2) { 55 String word = words[0]; 56 String lexicalCategory = words[1]; 57 Term term = terms.get(word); 58 if(term == null) { 59 term = new Term(word); 60 // TODO set lexical category 61 term.setLexicalCategory(lexicalCategory); 62 terms.put(word, term); 63 } 64 term.incrFreq(); 65 LOG.debug("Got word: word=" + rawWord); 66 } 67 } 68 } 69 } 70 } catch (IOException e) { 71 e.printStackTrace(); 72 } finally { 73 try { 74 if(br != null) { 75 br.close(); 76 } 77 } catch (IOException e) { 78 LOG.warn(e); 79 } 80 } 81 return terms; 82 } 83 84 } 它是对一个文件进行读取,然后进行分词,去停用词,最后返回的Map包含了的集合,此属性包括词性(Lexical Category)、词频、TF等信息。 这样,遍历数据集目录和文件,就能去将全部的文档分词,最终构建词袋模型。我们使用Java中集合来存储文档、词、类别之间的关系,如下所示: 01 private int totalDocCount; 02 private final List<String> labels = new ArrayList<String>(); 03 // Map<类别, 文档数量> 04 private final Map<String, Integer> labelledTotalDocCountMap = new HashMap<String, Integer>(); 05 // Map<类别, Map<文档 ,Map<词, 词信息>>> 06 private final Map<String, Map<String, Map<String, Term>>> termTable = 07 new HashMap<String, Map<String, Map<String, Term>>>(); 08 // Map<词 ,Map<类别, Set<文档>>> 09 private final Map<String, Map<String, Set<String>>> invertedTable = 10 new HashMap<String, Map<String, Set<String>>>(); 基于训练数据集选择特征向量 上面已经构建好词袋模型,包括相关的文档和词等的关系信息。现在我们来选择用来建立分类模型的特征词向量,首先要选择一种度量,来有效地选择出特征词向量。基于论文《A comparative study on feature selection in text categorization》,我们选择基于卡方统计量(chi-square statistic, CHI)技术来实现选择,这里根据计算公式: 其中,公式中各个参数的含义,说明如下: N:训练数据集文档总数 A:在一个类别中,包含某个词的文档的数量 B:在一个类别中,排除该类别,其他类别包含某个词的文档的数量 C:在一个类别中,不包含某个词的文档的数量 D:在一个类别中,不包含某个词也不在该类别中的文档的数量 要想进一步了解,可以参考这篇论文。 使用卡方统计量,为每个类别下的每个词都进行计算得到一个CHI值,然后对这个类别下的所有的词基于CHI值进行排序,选择出最大的topN个词(很显然使用堆排序算法更合适);最后将多个类别下选择的多组topN个词进行合并,得到最终的特征向量。 其实,这里可以进行一下优化,每个类别下对应着topN个词,在合并的时候可以根据一定的标准,将各个类别都出现的词给出一个比例,超过指定比例的可以删除掉,这样可以使特征向量在多个类别分类过程中更具有区分度。这里,我们只是做了个简单的合并。 我们看一下,用到的存储结构,使用Java的集合来存储: 1 // Map<label, Map<word, term>> 2 private final Map<String, Map<String, Term>> chiLabelToWordsVectorsMap = newHashMap<String, Map<String, Term>>(0); 3 // Map<word, term>, finally merged vector 4 private final Map<String, Term> chiMergedTermVectorMap = new HashMap<String, Term>(0); 下面,实现特征向量选择计算的实现,代码如下所示: 001 package org.shirdrn.document.processor.component.train; 002 003 import java.util.Iterator; 004 import java.util.Map; 005 import java.util.Map.Entry; 006 import java.util.Set; 007 008 import org.apache.commons.logging.Log; 009 import org.apache.commons.logging.LogFactory; 010 import org.shirdrn.document.processor.common.AbstractComponent; 011 import org.shirdrn.document.processor.common.Context; 012 import org.shirdrn.document.processor.common.Term; 013 import org.shirdrn.document.processor.utils.SortUtils; 014 015 public class FeatureTermVectorSelector extends AbstractComponent { 016 017 private static final Log LOG = LogFactory.getLog(FeatureTermVectorSelector.class); 018 private final int keptTermCountEachLabel; 019 020 public FeatureTermVectorSelector(Context context) { 021 super(context); 022 keptTermCountEachLabel = context.getConfiguration().getInt("processor.each.label.kept.term.count", 3000); 023 } 024 025 @Override 026 public void fire() { 027 // compute CHI value for selecting feature terms 028 // after sorting by CHI value 029 for(String label : context.getVectorMetadata().getLabels()) { 030 // for each label, compute CHI vector 031 LOG.info("Compute CHI for: label=" + label); 032 processOneLabel(label); 033 } 034 035 // sort and select CHI vectors 036 Iterator<Entry<String, Map<String, Term>>> chiIter = 037 context.getVectorMetadata().chiLabelToWordsVectorsIterator(); 038 while(chiIter.hasNext()) { 039 Entry<String, Map<String, Term>> entry = chiIter.next(); 040 String label = entry.getKey(); 041 LOG.info("Sort CHI terms for: label=" + label + ", termCount=" + entry.getValue().size()); 042 Entry<String, Term>[] a = sort(entry.getValue()); 043 for (int i = 0; i < Math.min(a.length, keptTermCountEachLabel); i++) { 044 Entry<String, Term> termEntry = a[i]; 045 // merge CHI terms for all labels 046 context.getVectorMetadata().addChiMergedTerm(termEntry.getKey(), termEntry.getValue()); 047 } 048 } 049 } 050 051 @SuppressWarnings("unchecked") 052 private Entry<String, Term>[] sort(Map<String, Term> terms) { 053 Entry<String, Term>[] a = new Entry[terms.size()]; 054 a = terms.entrySet().toArray(a); 055 SortUtils.heapSort(a, true, keptTermCountEachLabel); 056 return a; 057 } 058 059 private void processOneLabel(String label) { 060 Iterator<Entry<String, Map<String, Set<String>>>> iter = 061 context.getVectorMetadata().invertedTableIterator(); 062 while(iter.hasNext()) { 063 Entry<String, Map<String, Set<String>>> entry = iter.next(); 064 String word = entry.getKey(); 065 Map<String, Set<String>> labelledDocs = entry.getValue(); 066 067 // A: doc count containing the word in this label 068 int docCountContainingWordInLabel = 0; 069 if(labelledDocs.get(label) != null) { 070 docCountContainingWordInLabel = labelledDocs.get(label).size(); 071 } 072 073 // B: doc count containing the word not in this label 074 int docCountContainingWordNotInLabel = 0; 075 Iterator<Entry<String, Set<String>>> labelledIter = 076 labelledDocs.entrySet().iterator(); 077 while(labelledIter.hasNext()) { 078 Entry<String, Set<String>> labelledEntry = labelledIter.next(); 079 String tmpLabel = labelledEntry.getKey(); 080 if(!label.equals(tmpLabel)) { 081 docCountContainingWordNotInLabel += entry.getValue().size(); 082 } 083 } 084 085 // C: doc count not containing the word in this label 086 int docCountNotContainingWordInLabel = 087 getDocCountNotContainingWordInLabel(word, label); 088 089 // D: doc count not containing the word not in this label 090 int docCountNotContainingWordNotInLabel = 091 getDocCountNotContainingWordNotInLabel(word, label); 092 093 // compute CHI value 094 int N = context.getVectorMetadata().getTotalDocCount(); 095 int A = docCountContainingWordInLabel; 096 int B = docCountContainingWordNotInLabel; 097 int C = docCountNotContainingWordInLabel; 098 int D = docCountNotContainingWordNotInLabel; 099 int temp = (A*D-B*C); 100 double chi = (double) N*temp*temp / (A+C)*(A+B)*(B+D)*(C+D); 101 Term term = new Term(word); 102 term.setChi(chi); 103 context.getVectorMetadata().addChiTerm(label, word, term); 104 } 105 } 106 107 private int getDocCountNotContainingWordInLabel(String word, String label) { 108 int count = 0; 109 Iterator<Entry<String,Map<String,Map<String,Term>>>> iter = 110 context.getVectorMetadata().termTableIterator(); 111 while(iter.hasNext()) { 112 Entry<String,Map<String,Map<String,Term>>> entry = iter.next(); 113 String tmpLabel = entry.getKey(); 114 // in this label 115 if(tmpLabel.equals(label)) { 116 Map<String, Map<String, Term>> labelledDocs = entry.getValue(); 117 for(Entry<String, Map<String, Term>> docEntry : labelledDocs.entrySet()) { 118 // not containing this word 119 if(!docEntry.getValue().containsKey(word)) { 120 ++count; 121 } 122 } 123 break; 124 } 125 } 126 return count; 127 } 128 129 private int getDocCountNotContainingWordNotInLabel(String word, String label) { 130 int count = 0; 131 Iterator<Entry<String,Map<String,Map<String,Term>>>> iter = 132 context.getVectorMetadata().termTableIterator(); 133 while(iter.hasNext()) { 134 Entry<String,Map<String,Map<String,Term>>> entry = iter.next(); 135 String tmpLabel = entry.getKey(); 136 // not in this label 137 if(!tmpLabel.equals(label)) { 138 Map<String, Map<String, Term>> labelledDocs = entry.getValue(); 139 for(Entry<String, Map<String, Term>> docEntry : labelledDocs.entrySet()) { 140 // not containing this word 141 if(!docEntry.getValue().containsKey(word)) { 142 ++count; 143 } 144 } 145 } 146 } 147 return count; 148 } 149 150 } 输出量化数据文件 特征向量已经从训练数据集中计算出来,接下来需要对每个词给出一个唯一的编号,从1开始,这个比较容易,输出特征向量文件,为测试验证的数据集所使用,文件格式如下所示: 01 认识 1 02 代理权 2 03 病理 3 04 死者 4 05 影子 5 06 生产国 6 07 容量 7 08 螺丝扣 8 09 大钱 9 10 壮志 10 11 生态圈 11 12 好事 12 13 全人类 13 由于libsvm使用的训练数据格式都是数字类型的,所以需要对训练集中的文档进行量化处理,我们使用TF-IDF度量,表示词与文档的相关性指标。 然后,需要遍历已经构建好的词袋模型,并使用已经编号的类别和特征向量,对每个文档计算TF-IDF值,每个文档对应一条记录,取出其中一条记录,输出格式如下所示: 1 8 9219:0.24673737883635047 453:0.09884635754820137 10322:0.21501394457319623 11947:0.27282495932970074 6459:0.41385272697452935 46:0.24041607991272138 8987:0.14897255497578704 4719:0.22296154731520754 10094:0.13116443653818177 5162:0.17050804524212404 2419:0.11831944042647048 11484:0.3501901869096251 12040:0.13267440708284894 8745:0.5320327758892881 9048:0.11445287153209653 1989:0.04677087098649205 7102:0.11308242956243426 3862:0.12007217405755069 10417:0.09796211412332205 5729:0.148037967054332 11796:0.08409157900442304 9094:0.17368658217203461 3452:0.1513474608736807 3955:0.0656773581702849 6228:0.4356889927309336 5299:0.15060439516792662 3505:0.14379243687841153 10732:0.9593462052245719 9659:0.1960034406311122 8545:0.22597403804274924 6767:0.13871522631066047 8566:0.20352452713417019 3546:0.1136541497082903 6309:0.10475466997804883 10256:0.26416957780238604 10288:0.22549409383630933 第一列的8表示类别编号,其余的每一列是词及其权重,使用冒号分隔,例如“9219:0.24673737883635047”表示编号为9219的词,对应的TF-IDF值为0.24673737883635047。如果特征向量有个N个,那么每条记录就对应着一个N维向量。 对于测试数据集中的文档,也使用类似的方法,不过首先需要加载已经输出的特征向量文件,从而构建一个支持libsvm格式的输出测试集文件。 使用libsvm训练文本分类器 前面的准备工作已经完成,现在可以使用libsvm工具包训练文本分类器。在使用libsvm的开始,需要做一个尺度变换操作(有时也称为归一化),有利于libsvm训练出更好的模型。我们已经知道前面输出的数据中,每一维向量都使用了TF-IDF的值,但是TF-IDF的值可能在一个不规范的范围之内(因为它依赖于TF和IDF的值),例如0.19872~8.3233,所以可以使用libsvm将所有的值都变换到同一个范围之内,如0~1.0,或者-1.0~1.0,可以根据实际需要选择。我们看一下命令: 1 F:\libsvm-3.0\windows>svm-scale.exe -l 0 -u 1 C:\\Users\\thinkpad\\Desktop\\vector\\train.txt > C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt 尺度变换后输出到文件train-scale.txt中,它可以直接作为libsvm训练的数据文件,我使用Java版本的libsvm代码,输入参数如下所示: 1 train -h 0 -t 0 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt 这里面,-t 0表示使用线性核函数,我发现在进行文本分类时,线性核函数比libsvm默认的-t 2非线性核函数的效果要要好一些。最后输出的是模型文件model.txt,内容示例如下所示: 01 svm_type c_svc 02 kernel_type linear 03 nr_class 10 04 total_sv 54855 05 rho -0.26562545584492675 -0.19596934447720876 0.24937032535471693 0.3391566771481882 -0.19541394291523667 -0.20017990510840347 -0.27349052681332664 -0.08694672836814998 -0.33057155365157015 0.06861675551386985 0.5815821822995312 0.7781870137763507 0.054722797451472065 0.07912846180263113 -0.01843419889020123 0.15110176721612528 -0.08484865489154271 0.46608205351462983 0.6643550487438468 -0.003914533674948038 -0.014576392246426623 -0.11384567944039309 0.09257404411884447 -0.16845445862600575 0.18053514069700813 -0.5510915276095857 -0.4885382860289285 -0.6057167948571457 -0.34910272249526764 -0.7066730463805829 -0.6980796972363181 -0.639435517196082 -0.8148772080348755 -0.5201121512955246 -0.9186975203736724 -0.008882360255733836 -0.0739010940085453 0.10314117392946448 -0.15342997221636115 -0.10028736061509444 0.09500443080371801 -0.16197536915675026 0.19553010464320583 -0.030005330377757263 -0.24521471309904422 06 label 8 4 7 5 10 9 3 2 6 1 07 nr_sv 6542 5926 5583 4058 5347 6509 5932 4050 6058 4850 08 SV 09 0.16456599916886336 0.22928285261208994 0.921277302054534 0.39377902901901013 0.4041207410447258 0.2561997963212561 0.0 0.0819993502684317 0.12652009525418703 9219:0.459459 453:0.031941 10322:0.27027 11947:0.0600601 6459:0.168521 46:0.0608108 8987:0.183784 4719:0.103604 10094:0.0945946 5162:0.0743243 2419:0.059744 11484:0.441441 12040:0.135135 8745:0.108108 9048:0.0440154 1989:0.036036 7102:0.0793919 3862:0.0577064 10417:0.0569106 5729:0.0972222 11796:0.0178571 9094:0.0310078 3452:0.0656566 3955:0.0248843 6228:0.333333 5299:0.031893 3505:0.0797101 10732:0.0921659 9659:0.0987654 8545:0.333333 6767:0.0555556 8566:0.375 3546:0.0853659 6309:0.0277778 10256:0.0448718 10288:0.388889 10 ... ... 上面,我们只是选择了非默认的核函数,还有其他参数可以选择,比如代价系数c,默认是1,表示在计算线性分类面时,可以容许一个点被分错。这时候,可以使用交叉验证来逐步优化计算,选择最合适的参数。 使用libsvm,指定交叉验证选项的时候,只输出经过交叉验证得到的分类器的精度,而不会输出模型文件,例如使用交叉验证模型运行时的参数示例如下: 1 -h 0 -t 0 -c 32 -v 5 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt 用-v启用交叉验证模式,参数-v 5表示将每一类下面的数据分成5份,按顺序1对2,2对3,3对4,4对5,5对1分别进行验证,从而得出交叉验证的精度。例如,下面是我们的10个类别的交叉验证运行结果: 1 Cross Validation Accuracy = 71.10428571428571% 在选好各个参数以后,就可以使用最优的参数来计算输出模型文件。 使用libsvm验证文本分类器精度 前面已经训练出来分类模型,就是最后输出的模型文件。现在可以使用测试数据集了,通过使用测试数据集来做对基于文本分类模型文件预测分类精度进行验证。同样,需要做尺度变换,例如: 1 F:\libsvm-3.0\windows>svm-scale.exe -l 0 -u 1 C:\\Users\\thinkpad\\Desktop\\vector\\test.txt > C:\\Users\\thinkpad\\Desktop\\vector\\test-scale.txt 注意,这里必须和训练集使用相同的尺度变换参数值。 我还是使用Java版本的libsvm进行预测,验证分类器精度,svm_predict类的输入参数: 1 C:\\Users\\thinkpad\\Desktop\\vector\\test-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt C:\\Users\\thinkpad\\Desktop\\vector\\predict.txt 这样,预测结果就在predict.txt文件中,同时输出分类精度结果,例如: 1 Accuracy = 66.81% (6681/10000) (classification) 如果觉得分类器精度不够,可以使用交叉验证去获取更优的参数,来训练并输出模型文件,例如,下面是几组结果: 01 train -h 0 -t 0 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt 02 Accuracy = 67.10000000000001% (6710/10000) (classification) 03 04 train -h 0 -t 0 -c 32 -v 5 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt C:\\Users\\thinkpad\\Desktop\\vector\\model.txt 05 Cross Validation Accuracy = 71.10428571428571% 06 Accuracy = 66.81% (6681/10000) (classification) 07 08 train -h 0 -t 0 -c 8 -m 1024 C:\\Users\\thinkpad\\Desktop\\vector\\train-scale.txt 09 C:\\Users\\thinkpad\\Desktop\\vector\\model.txt 10 Cross Validation Accuracy = 74.3240057320121% 11 Accuracy = 67.88% (6788/10000) (classification) 第一组是默认情况下c=1时的精度为 67.10000000000001%; 第二组使用了交叉验证模型,交叉验证精度为71.10428571428571%,获得参数c=32,使用该参数训练输出模型文件,基于该模型文件进行预测,最终的精度为66.81%,可见没有使用默认c参数值的精度高; 第三组使用交叉验证,精度比第二组高一些,输出模型后并进行预测,最终精度为67.88%,略有提高。 可以基于这种方式不断地去寻找最优的参数,从而使分类器具有更好的精度。 总结 文本分类有其特殊性,在使用libsvm分类,或者其他的工具,都不要忘记,有如下几点需要考虑到: 其实文本在预处理过程进行的很多过程对最后使用工具进行分类都会有影响。 最重要的就是文本特征向量的选择方法,如果文本特征向量选择的很差,即使再好的分类工具,可能训练得到的分类器都未必能达到更好。 文本特征向量选择的不好,在训练调优分类器的过程中,虽然找到了更好的参数,但是这本身可能会是一个不好的分类器,在实际预测中可以出现误分类的情况。 选择训练集和测试集,对整个文本预处理过程,以及使用分类工具进行训练,都有影响。
对于MySQL数据库一般用途的主从复制,可以实现数据的备份(如果希望在主节点失效后,能够使从节点自动接管,就需要更加复杂的配置,这里暂时先不考虑),如果主节点出现硬件故障,数据库服务器可以直接手动切换成备份节点(从节点),继续提供服务。基本的主从复制配置起来非常容易,这里我们做个简单的记录总结。 我们选择两台服务器来进行MySQL的主从复制实践,一台m1作为主节点,另一台nn作为从节点。 两台机器上都需要安装MySQL数据库,如果想要卸掉默认安装的,可以执行如下命令: 1 sudo rpm -e --nodeps mysql 2 yum list | grep mysql 现在可以在CentOS 6.4上直接执行如下命令进行安装: 1 sudo yum install -y mysql-server mysql mysql-deve 为root用户设置密码: 1 mysqladmin -u root password 'shiyanjun' 然后可以直接通过MySQL客户端登录: 1 mysql -u root -p 主节点配置 首先,考虑到数据库的安全,以及便于管理,我们需要在主节点m1上增加一个专用的复制用户,使得任意想要从主节点进行复制从节点都必须使用这个账号: 1 CREATE USER repli_user; 2 GRANT REPLICATION SLAVE ON *.* TO 'repli_user'@'%' IDENTIFIED BY 'shiyanjun'; 这里还进行了操作授权,使用这个换用账号来执行集群复制。如果想要限制IP端段,也可以在这里进行配置授权。 然后,在主节点m1上,修改MySQL配置文件/etc/my.cnf,使其支持Master复制功能,修改后的内容如下所示: 01 [mysqld] 02 datadir=/var/lib/mysql 03 socket=/var/lib/mysql/mysql.sock 04 user=mysql 05 # Disabling symbolic-links is recommended to prevent assorted security risks 06 symbolic-links=0 07 server-id=1 08 log-bin=m-bin 09 log-bin-index=m-bin.index 10 11 [mysqld_safe] 12 log-error=/var/log/mysqld.log 13 pid-file=/var/run/mysqld/mysqld.pid server-id指明主节点的身份,从节点通过这个server-id来识别该节点是Master节点(复制架构中的源数据库服务器节点)。 如果MySQL当前已经启动,修改完集群复制配置后需要重启服务器: 1 sudo service mysqld restart 从节点配置 接着,类似地进行从节点nn的配置,同样修改MySQL配置文件/etc/my.cnf,使其支持Slave端复制功能,修改后的内容如下所示: 01 [mysqld] 02 datadir=/var/lib/mysql 03 socket=/var/lib/mysql/mysql.sock 04 user=mysql 05 # Disabling symbolic-links is recommended to prevent assorted security risks 06 symbolic-links=0 07 server-id=2 08 relay-log=slave-relay-bin 09 relay-log-index=slave-relay-bin.index 10 11 [mysqld_safe] 12 log-error=/var/log/mysqld.log 13 pid-file=/var/run/mysqld/mysqld.pid 同样,如果MySQL当前已经启动,修改完集群复制配置后需要重启服务器: 1 sudo service mysqld restart 然后,需要使从节点nn指向主节点,并启动Slave复制,执行如下命令: 1 CHANGE MASTER TO MASTER_HOST='m1', MASTER_PORT=3306, MASTER_USER='repli_user', MASTER_PASSWORD='shiyanjun'; 2 START SLAVE; 验证集群复制 这时,可以在主节点m1上执行相关操作,验证从节点nn同步复制了主节点的数据库中的内容变更。 如果此时,我们已经配置好了主从复制,那么对于主节点m1上MysQL数据库的任何变更都会复制到从节点nn上,包括建库建表、插入更新等操作,下面我们从建库开始: 在主节点m1上建库建表: 01 CREATE DATABASE workflow; 02 CREATE TABLE `workflow`.`project` ( 03 `id` int(11) NOT NULL AUTO_INCREMENT, 04 `name` varchar(100) NOT NULL, 05 `type` tinyint(4) NOT NULL DEFAULT '0', 06 `description` varchar(500) DEFAULT NULL, 07 `create_at` date DEFAULT NULL, 08 `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATECURRENT_TIMESTAMP, 09 `status` tinyint(4) NOT NULL DEFAULT '0', 10 PRIMARY KEY (`id`) 11 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 在m1上查看binlog内容,执行命令: 1 SHOW BINLOG EVENTS\G binlog内容内容如下所示: 01 *************************** 1. row *************************** 02 Log_name: m-bin.000001 03 Pos: 4 04 Event_type: Format_desc 05 Server_id: 1 06 End_log_pos: 106 07 Info: Server ver: 5.1.73-log, Binlog ver: 4 08 *************************** 2. row *************************** 09 Log_name: m-bin.000001 10 Pos: 106 11 Event_type: Query 12 Server_id: 1 13 End_log_pos: 197 14 Info: CREATE DATABASE workflow 15 *************************** 3. row *************************** 16 Log_name: m-bin.000001 17 Pos: 197 18 Event_type: Query 19 Server_id: 1 20 End_log_pos: 671 21 Info: CREATE TABLE `workflow`.`project` ( 22 `id` int(11) NOT NULL AUTO_INCREMENT, 23 `name` varchar(100) NOT NULL, 24 `type` tinyint(4) NOT NULL DEFAULT '0', 25 `description` varchar(500) DEFAULT NULL, 26 `create_at` date DEFAULT NULL, 27 `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 28 `status` tinyint(4) NOT NULL DEFAULT '0', 29 PRIMARY KEY (`id`) 30 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 31 3 rows in set (0.00 sec) 通过上述binlog内容,我们大概可以看到MySQL的binlog都记录那些信息,一个事件对应一行记录。这些记录信息的组织结构如下所示: Log_name:日志名称,指定的记录操作的binlog日志名称,这里是m-bin.000001,与我们前面在/etc/my.cnf中配置的相对应 Pos:记录事件的起始位置 Event_type:事件类型 End_log_pos:记录事件的结束位置 Server_id:服务器标识 Info:事件描述信息 然后,我们可以查看在从节点nn上复制的情况。通过如下命令查看从节点nn上数据库和表的信息: 1 SHOW DATABASES; 2 USE workflow; 3 SHOW TABLES; 4 DESC project; 我们再看一下执行插入语句的情况。在主节点m1上执行如下SQL语句: 1 INSERT INTO `workflow`.`project` VALUES(1, 'Avatar-II', 1, 'Avatar-II project', '2014-02-16', '2014-02-16 11:09:54', 0); 可以在从节点上执行查询,看到从节点nn上复制了主节点m1上执行的INSERT语句的记录: 1 SELECT * FROM workflow.project; 验证复制成功。 复制常用命令 下面,我们总结了几个在MySQL主从复制场景中常用到的几个相关命令: 终止主节点复制 1 STOP MASTER; 清除主节点复制文件 1 RESET MASTER; 终止从节点复制 1 STOP SLAVE; 清除从节点复制文件 1 RESET SLAVE; 查看主节点复制状态 1 SHOW MASTER STATUS\G; 结果示例: 1 *************************** 1. row *************************** 2 File: m-bin.000001 3 Position: 956 4 Binlog_Do_DB: 5 Binlog_Ignore_DB: 6 1 row in set (0.00 sec) 查看从节点复制状态 1 SHOW SLAVE STATUS\G; 结果示例: 01 *************************** 1. row *************************** 02 Slave_IO_State: Waiting for master to send event 03 Master_Host: m1 04 Master_User: repli_user 05 Master_Port: 3306 06 Connect_Retry: 60 07 Master_Log_File: m-bin.000001 08 Read_Master_Log_Pos: 956 09 Relay_Log_File: slave-relay-bin.000002 10 Relay_Log_Pos: 1097 11 Relay_Master_Log_File: m-bin.000001 12 Slave_IO_Running: Yes 13 Slave_SQL_Running: Yes 14 Replicate_Do_DB: 15 Replicate_Ignore_DB: 16 Replicate_Do_Table: 17 Replicate_Ignore_Table: 18 Replicate_Wild_Do_Table: 19 Replicate_Wild_Ignore_Table: 20 Last_Errno: 0 21 Last_Error: 22 Skip_Counter: 0 23 Exec_Master_Log_Pos: 956 24 Relay_Log_Space: 1252 25 Until_Condition: None 26 Until_Log_File: 27 Until_Log_Pos: 0 28 Master_SSL_Allowed: No 29 Master_SSL_CA_File: 30 Master_SSL_CA_Path: 31 Master_SSL_Cert: 32 Master_SSL_Cipher: 33 Master_SSL_Key: 34 Seconds_Behind_Master: 0 35 Master_SSL_Verify_Server_Cert: No 36 Last_IO_Errno: 0 37 Last_IO_Error: 38 Last_SQL_Errno: 0 39 Last_SQL_Error: 40 1 row in set (0.00 sec) 查看BINLOG列表 1 SHOW BINARY LOGS\G
我们使用的是Sqoop-1.4.4,在进行关系型数据库与Hadoop/Hive数据同步的时候,如果使用--incremental选项,如使用append模式,我们需要记录一个--last-value的值,如果每次执行同步脚本的时候,都需要从日志中解析出来这个--last-value的值,然后重新设置脚本参数,才能正确同步,保证从关系型数据库同步到Hadoop/Hive的数据不发生重复的问题。 而且,我们我们需要管理我们使用的这些脚本,每次执行之前可能要获取指定参数值,或者修改参数。Sqoop也提供了一种比较方面的方式,那就是直接创建一个Sqoop job,通过job来管理特定的同步任务。就像我们前面提到的增量同步问题,通过创建sqoop job可以保存上一次同步时记录的--last-value的值,也就不用再费劲去解析获取了,每次想要同步,这个job会自动从job保存的数据中获取到。 sqoop job命令使用 Sqoop job相关的命令有两个: bin/sqoop job bin/sqoop-job 使用这两个都可以。我们先看看sqoop job命令的基本用法: 创建job:--create 删除job:--delete 执行job:--exec 显示job:--show 列出job:--list 下面,我们基于增量同步数据这个应用场景,创建一个sqoop job,命令如下所示: 1 bin/sqoop job --create your-sync-job -- import --connect jdbc:mysql://10.95.3.49:3306/workflow --table project --username shirdrn -P --hive-import --incremental append --check-column id --last-value 1 -- --default-character-set=utf-8 创建了job,id为“your-sync-job”,它是将MySQL数据库workflow中的project表同步到Hive表中,而且--incremental append选项使用append模式,--last-value为1,从MySQL表中自增主键id=1开始同步。然后我们根据这个job的id去查询job详细配置情况: 1 bin/sqoop job --show your-sync-job 结果示例,如下所示: 01 Job: your-sync-job 02 Tool: import 03 Options: 04 ---------------------------- 05 verbose = false 06 incremental.last.value = 1 07 db.connect.string = jdbc:mysql://10.95.3.49:3306/workflow 08 codegen.output.delimiters.escape = 0 09 codegen.output.delimiters.enclose.required = false 10 codegen.input.delimiters.field = 0 11 hbase.create.table = false 12 db.require.password = true 13 hdfs.append.dir = true 14 db.table = project 15 import.fetch.size = null 16 codegen.input.delimiters.escape = 0 17 codegen.input.delimiters.enclose.required = false 18 db.username = shirdrn 19 codegen.output.delimiters.record = 10 20 import.max.inline.lob.size = 16777216 21 hcatalog.create.table = false 22 db.clear.staging.table = false 23 incremental.col = id 24 codegen.input.delimiters.record = 0 25 enable.compression = false 26 hive.overwrite.table = false 27 hive.import = true 28 codegen.input.delimiters.enclose = 0 29 hive.drop.delims = false 30 codegen.output.delimiters.enclose = 0 31 hdfs.delete-target.dir = false 32 codegen.output.dir = . 33 codegen.auto.compile.dir = true 34 mapreduce.num.mappers = 4 35 import.direct.split.size = 0 36 export.new.update = UpdateOnly 37 codegen.output.delimiters.field = 1 38 incremental.mode = AppendRows 39 hdfs.file.format = TextFile 40 codegen.compile.dir = /tmp/sqoop-shirdrn/compile/a1ed2c6097c4534d20f2ea981662556e 41 direct.import = false 42 hive.fail.table.exists = false 43 tool.arguments.0 = --default-character-set=utf-8 44 db.batch = false 通过incremental.last.value = 1可以看到,通过该选项来控制增量同步开始记录。 接着,可以使用创建的这个job id来运行它,执行如下命令: 1 bin/sqoop job --exec your-sync-job 可以查询,MySQL数据库workflow中的project表中的数据被同步到Hive表中。 这时,可以通过bin/sqoop job --show your-sync-job命令,查看当前的sqoop job配置情况,可以看到如下变化: 1 incremental.last.value = 7 从MySQL表中增量同步的起始id变为7,下次同步就会把id大于7的记录同步到Hive表中。可以在MySQL表中再INSERT一条记录,再次执行your-sync-job,能够正确地进行增量同步。 Sqoop job安全配置 默认情况下,创建的每个job在运行的时候都不会进行安全的认证。如果我们希望限制指定的sqoop job的执行,只有经过认证以后才能执行,这时候可以使用sqoop job的安全选项。Sqoop安装目录下,通过修改配置文件conf/sqoop-site.xml可以对job进行更高级的配置。实际上,我们使用了Sqoop的metastore工具,它能够对Sqoop进行细粒度的配置。 我们要将MySQL数据库中的数据同步到Hive表,每次执行sqoop job都需要输入访问MySQL数据库的连接账号信息,可以设置sqoop.metastore.client.record.password的值为true。如果在conf/sqoop-site.xml中增加如下配置,会将连接账号信息存储到Sqoop的metastore中: 1 <property> 2 <name>sqoop.metastore.client.record.password</name> 3 <value>true</value> 4 <description>If true, allow saved passwords in the metastore. </description> 5 </property> 如果想要限制从外部调用执行Sqoop job,如将Sqoop job提交给Oozie调度程序,也会通过上面Sqoop的metastore配置的内容来进行验证。 另外,Sqoop的metastore工具,可以允许我们指定为外部,例如使用外部主机上的MySQL数据库来存储元数据,可以在conf/sqoop-site.xml配置如下: 01 <property> 02 <name>sqoop.metastore.client.autoconnect.url</name> 03 <value>jdbc:mysql://10.95.3.49:3306/sqoop_metastore</value> 04 <description>The connect string to use when connecting to a 05 job-management metastore. If unspecified, uses ~/.sqoop/. 06 You can specify a different path here. 07 </description> 08 </property> 09 <property> 10 <name>sqoop.metastore.client.autoconnect.username</name> 11 <value>shirdrn</value> 12 <description>The username to bind to the metastore. 13 </description> 14 </property> 15 <property> 16 <name>sqoop.metastore.client.autoconnect.password</name> 17 <value>108loIOL</value> 18 <description>The password to bind to the metastore. 19 </description> 20 </property> 还有一个可与选择的配置项是,可以设置是否自动连接到外部metastore数据库,通过如下配置指定: 1 <property> 2 <name>sqoop.metastore.client.enable.autoconnect</name> 3 <value>false</value> 4 <description>If true, Sqoop will connect to a local metastore for job management when no other metastore arguments are provided. 5 </description> 6 </property> 这样,你可以通过MySQL的授权机制,来限制指定的用户和主机(或IP地址)访问Sqoop的metadata,也能起到一定的安全访问限制。
Sqoop可以在HDFS/Hive和关系型数据库之间进行数据的导入导出,其中主要使用了import和export这两个工具。这两个工具非常强大,提供了很多选项帮助我们完成数据的迁移和同步。比如,下面两个潜在的需求: 业务数据存放在关系数据库中,如果数据量达到一定规模后需要对其进行分析或同统计,单纯使用关系数据库可能会成为瓶颈,这时可以将数据从业务数据库数据导入(import)到Hadoop平台进行离线分析。 对大规模的数据在Hadoop平台上进行分析以后,可能需要将结果同步到关系数据库中作为业务的辅助数据,这时候需要将Hadoop平台分析后的数据导出(export)到关系数据库。 这里,我们介绍Sqoop完成上述基本应用场景所使用的import和export工具,通过一些简单的例子来说明这两个工具是如何做到的。 工具通用选项 import和export工具有些通用的选项,如下表所示: 选项 含义说明 --connect <jdbc-uri> 指定JDBC连接字符串 --connection-manager <class-name> 指定要使用的连接管理器类 --driver <class-name> 指定要使用的JDBC驱动类 --hadoop-mapred-home <dir> 指定$HADOOP_MAPRED_HOME路径 --help 打印用法帮助信息 --password-file 设置用于存放认证的密码信息文件的路径 -P 从控制台读取输入的密码 --password <password> 设置认证密码 --username <username> 设置认证用户名 --verbose 打印详细的运行信息 --connection-param-file <filename> 可选,指定存储数据库连接参数的属性文件 数据导入工具import import工具,是将HDFS平台外部的结构化存储系统中的数据导入到Hadoop平台,便于后续分析。我们先看一下import工具的基本选项及其含义,如下表所示: 选项 含义说明 --append 将数据追加到HDFS上一个已存在的数据集上 --as-avrodatafile 将数据导入到Avro数据文件 --as-sequencefile 将数据导入到SequenceFile --as-textfile 将数据导入到普通文本文件(默认) --boundary-query <statement> 边界查询,用于创建分片(InputSplit) --columns <col,col,col…> 从表中导出指定的一组列的数据 --delete-target-dir 如果指定目录存在,则先删除掉 --direct 使用直接导入模式(优化导入速度) --direct-split-size <n> 分割输入stream的字节大小(在直接导入模式下) --fetch-size <n> 从数据库中批量读取记录数 --inline-lob-limit <n> 设置内联的LOB对象的大小 -m,--num-mappers <n> 使用n个map任务并行导入数据 -e,--query <statement> 导入的查询语句 --split-by <column-name> 指定按照哪个列去分割数据 --table <table-name> 导入的源表表名 --target-dir <dir> 导入HDFS的目标路径 --warehouse-dir <dir> HDFS存放表的根路径 --where <where clause> 指定导出时所使用的查询条件 -z,--compress 启用压缩 --compression-codec <c> 指定Hadoop的codec方式(默认gzip) --null-string <null-string> 果指定列为字符串类型,使用指定字符串替换值为null的该类列的值 --null-non-string <null-string> 如果指定列为非字符串类型,使用指定字符串替换值为null的该类列的值 下面,我们通过实例来说明,在实际中如何使用这些选项。 将MySQL数据库中整个表数据导入到Hive表 1 bin/sqoop import --connect jdbc:mysql://10.95.3.49:3306/workflow --table project --username shirdrn -P --hive-import -- --default-character-set=utf-8 将MySQL数据库workflow中project表的数据导入到Hive表中。 将MySQL数据库中多表JION后的数据导入到HDFS 1 bin/sqoop import --connect jdbc:mysql://10.95.3.49:3306/workflow --username shirdrn -P --query 'SELECT users.*, tags.tag FROM users JOIN tags ON (users.id = tags.user_id) WHERE $CONDITIONS' --split-by users.id --target-dir/hive/tag_db/user_tags -- --default-character-set=utf-8 这里,使用了--query选项,不能同时与--table选项使用。而且,变量$CONDITIONS必须在WHERE语句之后,供Sqoop进程运行命令过程中使用。上面的--target-dir指向的其实就是Hive表存储的数据目录。 将MySQL数据库中某个表的数据增量同步到Hive表 1 bin/sqoop job --create your-sync-job -- import --connect jdbc:mysql://10.95.3.49:3306/workflow --table project --username shirdrn -P --hive-import --incremental append --check-column id --last-value 1 -- --default-character-set=utf-8 这里,每次运行增量导入到Hive表之前,都要修改--last-value的值,否则Hive表中会出现重复记录。 将MySQL数据库中某个表的几个字段的数据导入到Hive表 1 bin/sqoop import --connect jdbc:mysql://10.95.3.49:3306/workflow --username shirdrn --P --table tags --columns 'id,tag' --create-hive-table -target-dir/hive/tag_db/tags -m 1 --hive-table tags --hive-import -- --default-character-set=utf-8 我们这里将MySQL数据库workflow中tags表的id和tag字段的值导入到Hive表tag_db.tags。其中--create-hive-table选项会自动创建Hive表,--hive-import选项会将选择的指定列的数据导入到Hive表。如果在Hive中通过SHOW TABLES无法看到导入的表,可以在conf/hive-site.xml中显式修改如下配置选项: 1 <property> 2 <name>javax.jdo.option.ConnectionURL</name> 3 <value>jdbc:derby:;databaseName=hive_metastore_db;create=true</value> 4 </property> 然后再重新运行,就能看到了。 使用验证配置选项 1 sqoop import --connect jdbc:mysql://db.foo.com/corp --table EMPLOYEES --validate --validator org.apache.sqoop.validation.RowCountValidator --validation-threshold org.apache.sqoop.validation.AbsoluteValidationThreshold --validation-failurehandler org.apache.sqoop.validation.AbortOnFailureHandler 上面这个是官方用户手册上给出的用法,我们在实际中还没用过这个,有感兴趣的可以验证尝试一下。 数据导出工具export export工具,是将HDFS平台的数据,导出到外部的结构化存储系统中,可能会为一些应用系统提供数据支持。我们看一下export工具的基本选项及其含义,如下表所示: 选项 含义说明 --validate <class-name> 启用数据副本验证功能,仅支持单表拷贝,可以指定验证使用的实现类 --validation-threshold <class-name> 指定验证门限所使用的类 --direct 使用直接导出模式(优化速度) --export-dir <dir> 导出过程中HDFS源路径 -m,--num-mappers <n> 使用n个map任务并行导出 --table <table-name> 导出的目的表名称 --call <stored-proc-name> 导出数据调用的指定存储过程名 --update-key <col-name> 更新参考的列名称,多个列名使用逗号分隔 --update-mode <mode> 指定更新策略,包括:updateonly(默认)、allowinsert --input-null-string <null-string> 使用指定字符串,替换字符串类型值为null的列 --input-null-non-string <null-string> 使用指定字符串,替换非字符串类型值为null的列 --staging-table <staging-table-name> 在数据导出到数据库之前,数据临时存放的表名称 --clear-staging-table 清除工作区中临时存放的数据 --batch 使用批量模式导出 下面,我们通过实例来说明,在实际中如何使用这些选项。这里,我们主要结合一个实例,讲解如何将Hive中的数据导入到MySQL数据库。 首先,我们准备几个表,MySQL数据库为tag_db,里面有两个表,定义如下所示: 01 CREATE TABLE tag_db.users ( 02 id INT(11) NOT NULL AUTO_INCREMENT, 03 name VARCHAR(100) NOT NULL, 04 PRIMARY KEY (`id`) 05 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 06 07 CREATE TABLE tag_db.tags ( 08 id INT(11) NOT NULL AUTO_INCREMENT, 09 user_id INT NOT NULL, 10 tag VARCHAR(100) NOT NULL, 11 PRIMARY KEY (`id`) 12 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 这两个表中存储的是基础数据,同时对应着Hive中如下两个表: 01 CREATE TABLE users ( 02 id INT, 03 name STRING 04 ); 05 06 CREATE TABLE tags ( 07 id INT, 08 user_id INT, 09 tag STRING 10 ); 我们首先在上述MySQL的两个表中插入一些测试数据: 1 INSERT INTO tag_db.users(name) VALUES('jeffery'); 2 INSERT INTO tag_db.users(name) VALUES('shirdrn'); 3 INSERT INTO tag_db.users(name) VALUES('sulee'); 4 5 INSERT INTO tag_db.tags(user_id, tag) VALUES(1, 'Music'); 6 INSERT INTO tag_db.tags(user_id, tag) VALUES(1, 'Programming'); 7 INSERT INTO tag_db.tags(user_id, tag) VALUES(2, 'Travel'); 8 INSERT INTO tag_db.tags(user_id, tag) VALUES(3, 'Sport'); 然后,使用Sqoop的import工具,将MySQL两个表中的数据导入到Hive表,执行如下命令行: 1 bin/sqoop import --connect jdbc:mysql://10.95.3.49:3306/tag_db --table users --username shirdrn -P --hive-import -- --default-character-set=utf-8 2 bin/sqoop import --connect jdbc:mysql://10.95.3.49:3306/tag_db --table tags --username shirdrn -P --hive-import -- --default-character-set=utf-8 导入成功以后,再在Hive中创建一个用来存储users和tags关联后数据的表: 1 CREATE TABLE user_tags ( 2 id STRING, 3 name STRING, 4 tag STRING 5 ); 执行如下HQL语句,将关联数据插入user_tags表: 1 FROM users u JOIN tags t ON u.id=t.user_id INSERT INTO TABLE user_tags SELECTCONCAT(CAST(u.id AS STRING), CAST(t.id AS STRING)), u.name, t.tag; 将users.id与tags.id拼接的字符串,作为新表的唯一字段id,name是用户名,tag是标签名称。 再在MySQL中创建一个对应的user_tags表,如下所示: 1 CREATE TABLE tag_db.user_tags ( 2 id varchar(200) NOT NULL, 3 name varchar(100) NOT NULL, 4 tag varchar(100) NOT NULL 5 ); 使用Sqoop的export工具,将Hive表user_tags的数据同步到MySQL表tag_db.user_tags中,执行如下命令行: 1 bin/sqoop export --connect jdbc:mysql://10.95.3.49:3306/tag_db --username shirdrn --P --table user_tags --export-dir /hive/user_tags --input-fields-terminated-by '\001' -- --default-character-set=utf-8 执行导出成功后,可以在MySQL的tag_db.user_tags表中看到对应的数据。 如果在导出的时候出现类似如下的错误: 01 14/02/27 17:59:06 INFO mapred.JobClient: Task Id : attempt_201402260008_0057_m_000001_0, Status : FAILED 02 java.io.IOException: Can't export data, please check task tracker logs 03 at org.apache.sqoop.mapreduce.TextExportMapper.map(TextExportMapper.java:112) 04 at org.apache.sqoop.mapreduce.TextExportMapper.map(TextExportMapper.java:39) 05 at org.apache.hadoop.mapreduce.Mapper.run(Mapper.java:145) 06 at org.apache.sqoop.mapreduce.AutoProgressMapper.run(AutoProgressMapper.java:64) 07 at org.apache.hadoop.mapred.MapTask.runNewMapper(MapTask.java:764) 08 at org.apache.hadoop.mapred.MapTask.run(MapTask.java:364) 09 at org.apache.hadoop.mapred.Child$4.run(Child.java:255) 10 at java.security.AccessController.doPrivileged(Native Method) 11 at javax.security.auth.Subject.doAs(Subject.java:396) 12 at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1190) 13 at org.apache.hadoop.mapred.Child.main(Child.java:249) 14 Caused by: java.util.NoSuchElementException 15 at java.util.AbstractList$Itr.next(AbstractList.java:350) 16 at user_tags.__loadFromFields(user_tags.java:225) 17 at user_tags.parse(user_tags.java:174) 18 at org.apache.sqoop.mapreduce.TextExportMapper.map(TextExportMapper.java:83) 19 ... 10 more 通过指定字段分隔符选项--input-fields-terminated-by,指定Hive中表字段之间使用的分隔符,供Sqoop读取解析,就不会报错了。
Spark是一个快速、通用的计算集群框架,它的内核使用Scala语言编写,它提供了Scala、Java和Python编程语言high-level API,使用这些API能够非常容易地开发并行处理的应用程序。 下面,我们通过搭建Spark集群计算环境,并进行简单地验证,来体验一下使用Spark计算的特点。无论从安装运行环境还是从编写处理程序(用Scala,Spark默认提供的Shell环境可以直接输入Scala代码进行数据处理),我们都会觉得比Hadoop MapReduce计算框架要简单得多,而且,Spark可以很好地与HDFS进行交互(从HDFS读取数据,以及写数据到HDFS中)。 安装配置 下载安装配置Scala 1 wget http://www.scala-lang.org/files/archive/scala-2.10.3.tgz 2 tar xvzf scala-2.10.3.tgz 在~/.bashrc中增加环境变量SCALA_HOME,并使之生效: 1 export SCALA_HOME=/usr/scala/scala-2.10.3 2 export PATH=$PATH:$SCALA_HOME/bin 下载安装配置Spark 我们首先在主节点m1上配置Spark程序,然后将配置好的程序文件复制分发到集群的各个从结点上。下载解压缩: 1 wget http://d3kbcqa49mib13.cloudfront.net/spark-0.9.0-incubating-bin-hadoop1.tgz 2 tar xvzf spark-0.9.0-incubating-bin-hadoop1.tgz 在~/.bashrc中增加环境变量SPARK_HOME,并使之生效: 1 export SPARK_HOME=/home/shirdrn/cloud/programs/spark-0.9.0-incubating-bin-hadoop1 2 export PATH=$PATH:$SPARK_HOME/bin 在m1上配置Spark,修改spark-env.sh配置文件: 1 cd /home/shirdrn/cloud/programs/spark-0.9.0-incubating-bin-hadoop1/conf 2 cp spark-env.sh.template spark-env.sh 在该脚本文件中,同时将SCALA_HOME配置为Unix环境下实际指向路径,例如: 1 export SCALA_HOME=/usr/scala/scala-2.10.3 修改conf/slaves文件,将计算节点的主机名添加到该文件,一行一个,例如: 1 s1 2 s2 3 s3 最后,将Spark的程序文件和配置文件拷贝分发到从节点机器上: 1 scp -r ~/cloud/programs/spark-0.9.0-incubating-bin-hadoop1 shirdrn@s1:~/cloud/programs/ 2 scp -r ~/cloud/programs/spark-0.9.0-incubating-bin-hadoop1 shirdrn@s2:~/cloud/programs/ 3 scp -r ~/cloud/programs/spark-0.9.0-incubating-bin-hadoop1 shirdrn@s3:~/cloud/programs/ 启动Spark集群 我们会使用HDFS集群上存储的数据作为计算的输入,所以首先要把Hadoop集群安装配置好,并成功启动,我这里使用的是Hadoop 1.2.1版本。启动Spark计算集群非常简单,执行如下命令即可: 1 cd /home/shirdrn/cloud/programs/spark-0.9.0-incubating-bin-hadoop1/ 2 sbin/start-all.sh 可以看到,在m1上启动了一个名称为Master的进程,在s1上启动了一个名称为Worker的进程,如下所示,我这里也启动了Hadoop集群: 主节点m1上: 1 54968 SecondaryNameNode 2 55651 Master 3 55087 JobTracker 4 54814 NameNode 5 6 从节点s1上: 7 33592 Worker 8 33442 TaskTracker 9 33336 DataNode 各个进程是否启动成功,也可以查看日志来诊断,例如: 1 主节点上: 2 tail -100f $SPARK_HOME/logs/spark-shirdrn-org.apache.spark.deploy.master.Master-1-m1.out 3 从节点上: 4 tail -100f $SPARK_HOME/logs/spark-shirdrn-org.apache.spark.deploy.worker.Worker-1-s1.out Spark集群计算验证 我们使用我的网站的访问日志文件来演示,示例如下: 1 27.159.254.192 - - [21/Feb/2014:11:40:46 +0800] "GET /archives/526.html HTTP/1.1" 200 12080 "http://shiyanjun.cn/archives/526.html" "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko/20100101 Firefox/11.0" 2 120.43.4.206 - - [21/Feb/2014:10:37:37 +0800] "GET /archives/417.html HTTP/1.1" 200 11464 "http://shiyanjun.cn/archives/417.html/" "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko/20100101 Firefox/11.0" 统计该文件里面IP地址出现频率,来验证Spark集群能够正常计算。另外,我们需要从HDFS中读取这个日志文件,然后统计IP地址频率,最后将结果再保存到HDFS中的指定目录。 首先,需要启动用来提交计算任务的Spark Shell: 1 bin/spark-shell 在Spark Shell上只能使用Scala语言写代码来运行。 然后,执行统计IP地址频率,在Spark Shell中执行如下代码来实现: 1 val file = sc.textFile("hdfs://m1:9000/user/shirdrn/wwwlog20140222.log") 2 val result = file.flatMap(line => line.split("\\s+.*")).map(word => (word,1)).reduceByKey((a, b) => a + b) 上述的文件hdfs://m1:9000/user/shirdrn/wwwlog20140222.log是输入日志文件。处理过程的日志信息,示例如下所示: 01 14/03/06 21:59:22 INFO MemoryStore: ensureFreeSpace(784) called with curMem=43296, maxMem=311387750 02 14/03/06 21:59:22 INFO MemoryStore: Block broadcast_11 stored as values to memory (estimated size 784.0 B, free 296.9 MB) 03 14/03/06 21:59:22 INFO FileInputFormat: Total input paths to process : 1 04 14/03/06 21:59:22 INFO SparkContext: Starting job: collect at <console>:13 05 14/03/06 21:59:22 INFO DAGScheduler: Registering RDD 84 (reduceByKey at <console>:13) 06 14/03/06 21:59:22 INFO DAGScheduler: Got job 10 (collect at <console>:13) with 1 output partitions (allowLocal=false) 07 14/03/06 21:59:22 INFO DAGScheduler: Final stage: Stage 20 (collect at <console>:13) 08 14/03/06 21:59:22 INFO DAGScheduler: Parents of final stage: List(Stage 21) 09 14/03/06 21:59:22 INFO DAGScheduler: Missing parents: List(Stage 21) 10 14/03/06 21:59:22 INFO DAGScheduler: Submitting Stage 21 (MapPartitionsRDD[84] at reduceByKey at <console>:13), which has no missing parents 11 14/03/06 21:59:22 INFO DAGScheduler: Submitting 1 missing tasks from Stage 21 (MapPartitionsRDD[84] at reduceByKey at <console>:13) 12 14/03/06 21:59:22 INFO TaskSchedulerImpl: Adding task set 21.0 with 1 tasks 13 14/03/06 21:59:22 INFO TaskSetManager: Starting task 21.0:0 as TID 19 on executor localhost: localhost (PROCESS_LOCAL) 14 14/03/06 21:59:22 INFO TaskSetManager: Serialized task 21.0:0 as 1941 bytes in 0 ms 15 14/03/06 21:59:22 INFO Executor: Running task ID 19 16 14/03/06 21:59:22 INFO BlockManager: Found block broadcast_11 locally 17 14/03/06 21:59:22 INFO HadoopRDD: Input split:hdfs://m1:9000/user/shirdrn/wwwlog20140222.log:0+4179514 18 14/03/06 21:59:23 INFO Executor: Serialized size of result for 19 is 738 19 14/03/06 21:59:23 INFO Executor: Sending result for 19 directly to driver 20 14/03/06 21:59:23 INFO TaskSetManager: Finished TID 19 in 211 ms on localhost (progress: 0/1) 21 14/03/06 21:59:23 INFO TaskSchedulerImpl: Remove TaskSet 21.0 from pool 22 14/03/06 21:59:23 INFO DAGScheduler: Completed ShuffleMapTask(21, 0) 23 14/03/06 21:59:23 INFO DAGScheduler: Stage 21 (reduceByKey at <console>:13) finished in 0.211 s 24 14/03/06 21:59:23 INFO DAGScheduler: looking for newly runnable stages 25 14/03/06 21:59:23 INFO DAGScheduler: running: Set() 26 14/03/06 21:59:23 INFO DAGScheduler: waiting: Set(Stage 20) 27 14/03/06 21:59:23 INFO DAGScheduler: failed: Set() 28 14/03/06 21:59:23 INFO DAGScheduler: Missing parents for Stage 20: List() 29 14/03/06 21:59:23 INFO DAGScheduler: Submitting Stage 20 (MapPartitionsRDD[86] at reduceByKey at <console>:13), which is now runnable 30 14/03/06 21:59:23 INFO DAGScheduler: Submitting 1 missing tasks from Stage 20 (MapPartitionsRDD[86] at reduceByKey at <console>:13) 31 14/03/06 21:59:23 INFO TaskSchedulerImpl: Adding task set 20.0 with 1 tasks 32 14/03/06 21:59:23 INFO Executor: Finished task ID 19 33 14/03/06 21:59:23 INFO TaskSetManager: Starting task 20.0:0 as TID 20 on executor localhost: localhost (PROCESS_LOCAL) 34 14/03/06 21:59:23 INFO TaskSetManager: Serialized task 20.0:0 as 1803 bytes in 0 ms 35 14/03/06 21:59:23 INFO Executor: Running task ID 20 36 14/03/06 21:59:23 INFO BlockManager: Found block broadcast_11 locally 37 14/03/06 21:59:23 INFO BlockFetcherIterator$BasicBlockFetcherIterator: Getting 1 non-zero-bytes blocks out of 1 blocks 38 14/03/06 21:59:23 INFO BlockFetcherIterator$BasicBlockFetcherIterator: Started 0 remote gets in 1 ms 39 14/03/06 21:59:23 INFO Executor: Serialized size of result for 20 is 19423 40 14/03/06 21:59:23 INFO Executor: Sending result for 20 directly to driver 41 14/03/06 21:59:23 INFO TaskSetManager: Finished TID 20 in 17 ms on localhost (progress: 0/1) 42 14/03/06 21:59:23 INFO TaskSchedulerImpl: Remove TaskSet 20.0 from pool 43 14/03/06 21:59:23 INFO DAGScheduler: Completed ResultTask(20, 0) 44 14/03/06 21:59:23 INFO DAGScheduler: Stage 20 (collect at <console>:13) finished in 0.016 s 45 14/03/06 21:59:23 INFO SparkContext: Job finished: collect at <console>:13, took 0.242136929 s 46 14/03/06 21:59:23 INFO Executor: Finished task ID 20 47 res14: Array[(String, Int)] = Array((27.159.254.192,28), (120.43.9.81,40), (120.43.4.206,16), (120.37.242.176,56), (64.31.25.60,2), (27.153.161.9,32), (202.43.145.163,24), (61.187.102.6,1), (117.26.195.116,12), (27.153.186.194,64), (123.125.71.91,1), (110.85.106.105,64), (110.86.184.182,36), (27.150.247.36,52), (110.86.166.52,60), (175.98.162.2,20), (61.136.166.16,1), (46.105.105.217,1), (27.150.223.49,52), (112.5.252.6,20), (121.205.242.4,76), (183.61.174.211,3), (27.153.230.35,36), (112.111.172.96,40), (112.5.234.157,3), (144.76.95.232,7), (31.204.154.144,28), (123.125.71.22,1), (80.82.64.118,3), (27.153.248.188,160), (112.5.252.187,40), (221.219.105.71,4), (74.82.169.79,19), (117.26.253.195,32), (120.33.244.205,152), (110.86.165.8,84), (117.26.86.172,136), (27.153.233.101,8), (123.12... 可以看到,输出了经过map和reduce计算后的部分结果。 最后,我们想要将结果保存到HDFS中,只要输入如下代码: 1 result.saveAsTextFile("hdfs://m1:9000/user/shirdrn/wwwlog20140222.log.result") 查看HDFS上的结果数据: 查看源代码打印帮助 1 [shirdrn@m1 ~]$ hadoop fs -cat /user/shirdrn/wwwlog20140222.log.result/part-00000 |head -5 2 (27.159.254.192,28) 3 (120.43.9.81,40) 4 (120.43.4.206,16) 5 (120.37.242.176,56) 6 (64.31.25.60,2)
基本思想 假设待排序的记录存放在数组R[0..n-1]中。初始时,R[0]自成1个有序区,无序区为R[1..n-1]。 从i=1起直至i=n-1为止,依次将R[i]插入当前的有序区R[0..i-1]中,生成含n个记录的有序区。 算法实现 直接插入排序算法,Java实现,代码如下所示: 01 public abstract class Sorter { 02 public abstract void sort(int[] array); 03 } 04 05 public class StraightInsertionSorter extends Sorter { 06 07 @Override 08 public void sort(int[] array) { 09 int tmp; 10 for (int i = 1; i < array.length; i++) { 11 tmp = array[i]; // array[i]的拷贝 12 // 如果右侧无序区第一个元素array[i] < 左侧有序区最大的array[i-1], 13 // 需要将有序区比array[i]大的元素向后移动。 14 if (array[i] < array[i - 1]) { 15 int j = i - 1; 16 while (j >= 0 && tmp < array[j]) { // 从右到左扫描有序区 17 array[j + 1] = array[j]; // 将左侧有序区中元素比array[i]大的array[j+1]后移 18 j--; 19 } 20 // 如果array[i]>=左侧有序区最大的array[i-1],或者经过扫描移动后,找到一个比array[i]小的元素 21 // 将右侧无序区第一个元素tmp = array[i]放到正确的位置上 22 array[j + 1] = tmp; 23 } 24 } 25 } 26 } 直接插入排序算法,Python实现,代码如下所示: 01 class Sorter: 02 ''' 03 Abstract sorter class, which provides shared methods being used by 04 subclasses. 05 ''' 06 __metaclass__ = ABCMeta 07 08 @abstractmethod 09 def sort(self, array): 10 pass 11 12 class StraightInsertionSorter(Sorter): 13 ''' 14 Straight insertion sorter 15 ''' 16 def sort(self, array): 17 i = 0 18 length = len(array) 19 while i<length -1: 20 k = i 21 j = i 22 while j<length: 23 if array[j]<array[k]: 24 k = j 25 j = j + 1 26 if k!=i: 27 array[k], array[i] = array[i], array[k] 28 i = i + 1 排序过程 直接插入排序的执行过程,如下所示: 初始化无序区和有序区:数组第一个元素为有序区,其余的元素作为无序区。 遍历无序区,将无序区的每一个元素插入到有序区正确的位置上。具体执行过程为: 每次取出无序区的第一个元素,如果该元素tmp大于有序区最后一个元素,不做任何操作; 如果tmp小于有序区最后一个元素,说明需要插入到有序区最后一个元素前面的某个位置,从后往前扫描有序区,如果有序区元素大于tmp,将有序区元素后移(第一次后移:tmp小于有序区最大的元素,有序区最大的元素后移覆盖无序区第一个元素,而无序区第一个元素的已经拷贝到tmp中;第二次后移:tmp小于有序区从后向前第二个的元素,有序区从后向前第二个元素后移覆盖有序区最大元素的位置,而有序区最后一个元素已经拷贝到无序区第一个元素的位置上;以此类推),直到找到一个元素比tmp小的元素(如果没有找到,就插入到有序区首位置),有序区后移操作停止。 接着,将tmp插入到:从有序区由前至后找到的第一个比tmp小的元素的后面即可。此时,有序区增加一个元素,无序区减少一个元素,直到无序区元素个数为0,排序结束。 下面,通过实例来演示执行直接插入排序的过程,假设待排序数组为array = {94,12,34,76,26,9,0,37,55,76,37,5,68,83,90,37,12,65,76,49},数组大小为20,则执行排序过程如下所示: 初始有序区为{94},无序区为{12,34,76,26,9,0,37,55,76,37,5,68,83,90,37,12,65,76,49}。 对于array[1] = 12(无序区第一个元素):临时拷贝tmp = array[1] = 12,tmp = 12小于有序区{94}最后一个元素(94),因为有序区只有一个元素,所以将tmp插入到有序区首位置,此时,有序区为{12,94},无序区为{34,76,26,9,0,37,55,76,37,5,68,83,90,37,12,65,76,49}。 对于array[2] = 34(无序区第一个元素):临时拷贝tmp = array[2] = 34,tmp = 34小于有序区{12,94}最后一个元素(94),将94后移覆盖array[2],亦即:array[2] = 94;继续将tmp = 34与有序区{12,94}从后向前第二个元素比较,tmp = 34 > 12,则直接将tmp = 34插入到12后面的位置。此时,有序区为{12,34,94},无序区为{76,26,9,0,37,55,76,37,5,68,83,90,37,12,65,76,49}。 对于array[3] = 76(无序区第一个元素):临时拷贝tmp = array[3] = 76,tmp = 76小于有序区{12,34,94}最后一个元素(94),将94后移覆盖array[3],亦即:array[3] = 94;继续将tmp = 76与有序区{12,34,94}从后向前第二个元素比较,tmp = 76 > 34,则直接将tmp = 76插入到34后面的位置。此时,有序区为{12,34,76,94},无序区为{26,9,0,37,55,76,37,5,68,83,90,37,12,65,76,49}。 …… 依此类推执行,直到无序区没有元素为止,则有序区即为排序后的数组。 算法分析 时间复杂度 最好情况:有序 通过直接插入排序的执行过程可以看到,如果待排序数组恰好为有序,则每次从大小为n-1的无序区数组取出一个元素,和有序区最后一个元素比较,一定是比最后一个元素大,需要插入到有序区最后一个元素的后面,也就是原地插入。 可见,比较次数为n-1次,数组元素移动次数为0次。 最坏情况:逆序 每次从无序区取出第一个元素,首先需要与有序区最后一个元素比较一次,然后继续从有序区的最后一个元素比较,直到比较到有序区第一个元素,然后插入到有序区首位置。 每次从无序区取出第一个元素,移动放到拷贝tmp中,然后再将tmp与有序区元素比较,这个比较过程一共移动的次数为:有序区数组大小,最后还要将拷贝tmp移动插入到有序区的位置上。 在这个过程中: 有序区数组大小为1时,比较2次,移动3次; 有序区数组大小为2时,比较3次,移动4次; …… 有序区数组大小为n-1时,比较n次,移动n+1次。 可见: 比较的次数为:2+3+……+n = (n+2)(n-1)/2 移动的此时为:3+4+……+n+1 = (n+4)(n-1)/2 通过上面两种情况的分析,直接插入排序的时间复杂度为O(n2)。 空间复杂度 在直接插入排序的过程中,只用到一个tmp临时存放待插入元素,因此空间复杂度为O(1)。 排序稳定性 通过上面的例子来看: 当有序区为{0,9,12,26,34,37,55,76,94},无序区为{76,37,5,68,83,90,37,12,65,76,49}的时候,执行下一趟直接插入排序: 对于array[9] = 76(无序区第一个元素): 临时拷贝tmp = array[9] = 76,tmp = 76小于有序区{0,9,12,26,34,37,55,76,94}最后一个元素(94),将94后移覆盖array[9],亦即:array[9] = 94;继续将tmp = 76与有序区{0,9,12,26,34,37,55,76,94}从后向前第二个元素(76)比较,tmp = 76 = 76,则直接将tmp = 76插入到有序区数组元素76后面的位置。此时,有序区为{0,9,12,26,34,37,55,76,76,94},无序区为{37,5,68,83,90,37,12,65,76,49}。 继续执行直至完成的过程中,对于两个相等的数组元素,原始为排序数组中索引位置的大小关系并没有发生改变,也就是说,对于值相等的元素e,存在ei1,ei2,……eik,其中i1,i2……ik是数组元素e在为排序数组中的索引位置,排序前有i1<i2<……<ik,排序后仍然有j1<j2<……<jk,其中j1<j2<……<jk为排序后值相等的元素e的索引。 可见,直接插入排序是稳定的。
基本思想 n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果: 初始状态:无序区为R[1..n],有序区为空。 第1趟排序:在无序区R[1..n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1] 交换,使R[1..1]和R[2..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。 …… 第i趟排序:第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R[i..n](1≤i≤n-1)。 该趟排序从当前无序区中选出关键字最小的记录R[k],将它与无序区的第1个记录R[i]交换,使R[1..i] 和R[i+1..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。 这样,n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果。 算法实现 直接选择排序算法,Java实现,代码如下所示: 01 public abstract class Sorter { 02 public abstract void sort(int[] array); 03 } 04 05 public class StraightSelectionSorter extends Sorter { 06 07 @Override 08 public void sort(int[] array) { 09 int tmp; // 用于交换数据的暂存单元 10 for (int i = 0; i < array.length - 1; i++) { // 这里只要从0~array.length-2即可 11 int k = i; 12 for (int j = i + 1; j < array.length; j++) { // 该循环可以找到右侧无序区最小的元素array[k] 13 if (array[k] > array[j]) { 14 k = j; 15 } 16 } 17 if (k != i) { // 如果array[i]不是无序区最小的,需要与无序区最小的进行交换 18 tmp = array[i]; 19 array[i] = array[k]; 20 array[k] = tmp; 21 } 22 // 如果array[i]是无序区最小的元素,不需要执行交换 23 } 24 } 25 } 直接选择排序算法,Python实现,代码如下所示: 01 class Sorter: 02 ''' 03 Abstract sorter class, which provides shared methods being used by 04 subclasses. 05 ''' 06 __metaclass__ = ABCMeta 07 08 @abstractmethod 09 def sort(self, array): 10 pass 11 12 class StraightSelectionSort(Sorter): 13 ''' 14 Straight selection sorter 15 ''' 16 def sort(self, array): 17 i = 0 18 length = len(array) 19 while i<length -1: 20 k = i 21 j = i 22 while j<length: 23 if array[j]<array[k]: 24 k = j 25 j = j + 1 26 if k!=i: 27 array[k], array[i] = array[i], array[k] 28 i = i + 1 排序过程 首先,从0~n-1个元素中找到最小的元素,交换到0位置上; 其次,再从1~n-1中找到次最小的元素,交换到1位置上; ……; 最后从n-2~n-1中找到最小的元素,交换到n-2位置上。n-1位置上一定是最大的元素,所以总共进行n-1趟选择排序。 需要注意的是: 每次确定无序区后,其中除了第一个元素之外的其它元素(设为e)与第一个元素(设为E)比较,只有满足e<E的时候才需要交换一次。 排序过程示例如下: 假设待排序数组为array = {94,12,34,76,26,9,0,37,55,76,37,5,68,83,90,37,12,65,76,49},数组大小为20。 第1趟选择排序 从array[1,n-1]中找到最小的元素: array[0] = 94>array[1] = 12,交换array[0]与array[1],即array[0] = 12array[2] = 34不成立,不交换; array[0] = 12>array[3] = 76不成立,不交换; array[0] = 12>array[4] = 26不成立,不交换; array[0] = 12>array[5] = 9,交换array[0]与array[5],即array[0] = 9array[6] = 0,交换array[0]与array[6],即array[0] = 0array[7] = 37不成立,不交换; array[0] = 0>array[8] = 55不成立,不交换; array[0] = 0>array[9] = 76不成立,不交换; array[0] = 0>array[10] = 37不成立,不交换; array[0] = 0>array[11] = 5不成立,不交换; array[0] = 0>array[12] = 68不成立,不交换; array[0] = 0>array[13] = 83不成立,不交换; array[0] = 0>array[14] = 90不成立,不交换; array[0] = 0>array[15] = 37不成立,不交换; array[0] = 0>array[16] = 12不成立,不交换; array[0] = 0>array[17] = 65不成立,不交换; array[0] = 0>array[18] = 76不成立,不交换; array[0] = 0>array[19] = 49不成立,不交换。 此时数组状态如下:{0,94,34,76,26,12,9,37,55,76,37,5,68,83,90,37,12,65,76,49}。 此时,有序区为{0},无序区为{94,34,76,26,12,9,37,55,76,37,5,68,83,90,37,12,65,76,49}。 第2趟选择排序 从array[2,n-1]中找到最小的元素: array[1] = 94>array[2] = 34,交换array[1]与array[2],即array[1] = 34array[3] = 76不成立,不交换; array[1] = 34>array[4] = 26,交换array[1]与array[4],即array[1] = 26array[5] = 12,交换array[1]与array[5],即array[1] = 12array[6] = 9,交换array[1]与array[6],即array[1] = 9array[7] = 37不成立,不交换; array[1] = 0>array[8] = 55不成立,不交换; array[1] = 0>array[9] = 76不成立,不交换; array[1] = 0>array[10] = 37不成立,不交换; array[1] = 9>array[11] = 5,交换array[1]与array[11],即array[1] = 5array[12] = 68不成立,不交换; array[1] = 5>array[13] = 83不成立,不交换; array[1] = 5>array[14] = 90不成立,不交换; array[1] = 5>array[15] = 37不成立,不交换; array[1] = 5>array[16] = 12不成立,不交换; array[1] = 5>array[17] = 65不成立,不交换; array[1] = 5>array[18] = 76不成立,不交换; array[1] = 5>array[19] = 49不成立,不交换。 此时数组状态如下:{0,5,94,76,34,26,12,37,55,76,37,9,68,83,90,37,12,65,76,49}。 此时,有序区为{0,5},无序区为{94,76,34,26,12,37,55,76,37,9,68,83,90,37,12,65,76,49}。 第3趟选择排序 排序后数组状态为:{0,5,9,94,76,34,26,37,55,76,37,12,68,83,90,37,12,65,76,49}。 此时,有序区为{0,5,9},无序区为{94,76,34,26,37,55,76,37,12,68,83,90,37,12,65,76,49}。 第4趟选择排序 排序后数组状态变化: {0,5,9,76,94,34,26,37,55,76,37,12,68,83,90,37,12,65,76,49}, {0,5,9,34,94,76,26,37,55,76,37,12,68,83,90,37,12,65,76,49}, {0,5,9,26,94,76,34,37,55,76,37,12,68,83,90,37,12,65,76,49}, {0,5,9,12,94,76,34,37,55,76,37,26,68,83,90,37,12,65,76,49}, {0,5,9,12,94,76,34,37,55,76,37,26,68,83,90,37,12,65,76,49}, 此时,有序区为{0,5,9,12},无序区为{94,76,34,37,55,76,37,26,68,83,90,37,12,65,76,49}。 …… 第n-1趟选择排序 依此类推,执行n-1趟选择排序,最后得到:有序区为{0,5,9,12,12,26,34,37,37,37,49,55,65,68,76,76,76,83,90},无序区为{94},此时整个数组已经有序,n-1趟选择排序后,排序完成。 算法分析 时间复杂度 记录比较次数: 无论待排序数组初始状态如何,都要进行n-1趟选择排序: 第1趟:比较n-1次; 第2趟:比较n-2次; …… 第n-1趟:比较1次。 从而,总共的比较次数为:1+2+……+(n-1) = n(n-1)/2 记录移动次数: 如果待排序数组为正序,则记录不需要交换,记录移动次数为0; 如果当排序数组为逆序,则: 第1趟:交换1次,移动3次; 第2趟:交换1次,移动3次; …… 第n-1趟:交换1次,移动3次。 从而,总共的移动次数为:3(n-1) = 3(n-1)。 因此,时间复杂度为O(n2)。 空间复杂度 在选择排序的过程中,设置一个变量用来交换元素,所以空间复杂度为O(1)。 排序稳定性 选择排序是就地排序。 通过上面的排序过程中数组的状态变化可以看出:直接选择排序是不稳定的。
基本思想 将被排序的记录数组R[0..n-1]垂直排列,每个记录R[i]看作是重量为R[i].key的气泡。根据轻气泡不能在重气泡之下的原则,从下往上扫描数组R:凡扫描到违反本原则的轻气泡,就使其 向上”飘浮”。如此反复进行,直到最后任何两个气泡都是轻者在上,重者在下为止。 具体过程,如下所示: 初始状态:R[0..n-1]为无序区。 第一趟扫描:从无序区底部向上依次比较相邻的两个气泡的重量,若发现轻者在下、重者 在上,则交换二者的位置,即依次比较(R[n-1], R[n-2])、(R[n-2], R[n-3])、…、(R[1], R[0]);对于每对气泡(R[j+1], R[j]),若R[j+1].key第一趟扫描完毕时,”最轻”的气泡就飘浮到该区间的顶部,即关键字最小的记录被放在最高位置R[0]上。 第二趟扫描:扫描R[1..n-1]。扫描完毕时,”次轻”的气泡飘浮到R[1]的位置上……最后,经过n-1趟扫描可得到有序区R[0..n-1]。 注意: 第i趟扫描时,R[0..i-1]和R[i..n-1]分别为当前的有序区和无序区。扫描仍是从无序区底 部向上直至该区顶部。扫描完毕时,该区中最轻气泡飘浮到顶部位置R[i]上,结果是R[0..i]变为新的有序区。 算法实现 冒泡排序算法,Java实现,代码如下所示: 01 public abstract class Sorter { 02 public abstract void sort(int[] array); 03 } 04 05 public class BubbleSorter extends Sorter { 06 07 @Override 08 public void sort(int[] array) { 09 int tmp; // 用于交换数据的暂存单元 10 for (int i = array.length - 1; i >= 0; i--) { // 将数组最小索引一端视为“水面” 11 // 将数组最小索引一端视为“水底”,“气泡”从“水底”向“水面”上浮 12 // 因为i每增加1,就有一个上浮到最终排序位置,所以,只需要对1~i个元素进行交换排序 13 for (int j = 1; j <= i; j++) { 14 if (array[j - 1] < array[j]) { // 如果上浮过程中发现存在比当前元素小的,就交换,将小的交换到“水面” 15 tmp = array[j - 1]; 16 array[j - 1] = array[j]; 17 array[j] = tmp; 18 } 19 } 20 } 21 } 22 } 冒泡排序算法,Python实现,代码如下所示: 01 class Sorter: 02 ''' 03 Abstract sorter class, which provides shared methods being used by 04 subclasses. 05 ''' 06 __metaclass__ = ABCMeta 07 08 @abstractmethod 09 def sort(self, array): 10 pass 11 12 class BubbleSorter(Sorter): 13 ''' 14 Bubble sorter 15 ''' 16 def sort(self, array): 17 length = len(array) 18 i = length - 1 19 while i>=0: 20 j = 1 21 while j<=i: 22 if array[j-1]<array[j]: 23 array[j-1], array[j] = array[j], array[j-1] 24 j = j + 1 25 i = i - 1 排序过程 冒泡排序的执行过程如下: 首先,将待排序数组视为一个无序区。 从数组一端开始,让元素小的逐步移动到另一端,称为气泡的上浮过程,直到整个数组变成一个有序区。 下面,我们通过例子还说明排序过程。假设待排序数组为array = {94,12,34,76,26,9,0,37,55,76,37,5,68,83,90,37,12,65,76,49},数组大小为20。 将数组最小索引一端视为“水底”,排序过程如下所示: 01 {94,34,76,26,12,9,37,55,76,37,5,68,83,90,37,12,65,76,49, 0} 02 {94,76,34,26,12,37,55,76,37,9,68,83,90,37,12,65,76,49, 5,0} 03 {94,76,34,26,37,55,76,37,12,68,83,90,37,12,65,76,49, 9,5,0} 04 {94,76,34,37,55,76,37,26,68,83,90,37,12,65,76,49, 12,9,5,0} 05 {94,76,37,55,76,37,34,68,83,90,37,26,65,76,49, 12,12,9,5,0} 06 {94,76,55,76,37,37,68,83,90,37,34,65,76,49, 26,12,12,9,5,0} 07 {94,76,76,55,37,68,83,90,37,37,65,76,49, 34,26,12,12,9,5,0} 08 {94,76,76,55,68,83,90,37,37,65,76,49, 37,34,26,12,12,9,5,0} 09 {94,76,76,68,83,90,55,37,65,76,49, 37,37,34,26,12,12,9,5,0} 10 {94,76,76,83,90,68,55,65,76,49, 37,37,37,34,26,12,12,9,5,0} 11 {94,76,83,90,76,68,65,76,55, 49,37,37,37,34,26,12,12,9,5,0} 12 {94,83,90,76,76,68,76,65, 55,49,37,37,37,34,26,12,12,9,5,0} 13 {94,90,83,76,76,76,68, 65,55,49,37,37,37,34,26,12,12,9,5,0} 14 {94,90,83,76,76,76, 68,65,55,49,37,37,37,34,26,12,12,9,5,0} 15 {94,90,83,76,76, 76,68,65,55,49,37,37,37,34,26,12,12,9,5,0} 16 {94,90,83,76, 76,76,68,65,55,49,37,37,37,34,26,12,12,9,5,0} 17 {94,90,83, 76,76,76,68,65,55,49,37,37,37,34,26,12,12,9,5,0} 18 {94,90, 83,76,76,76,68,65,55,49,37,37,37,34,26,12,12,9,5,0} 19 {94, 90,83,76,76,76,68,65,55,49,37,37,37,34,26,12,12,9,5,0} 20 { 94,90,83,76,76,76,68,65,55,49,37,37,37,34,26,12,12,9,5,0} 上图是冒泡排序过程中执行各趟排序,整个数组中元素的位置信息:左上半部分是无序区,右下半部分是有序区。 算法分析 时间复杂度 最好情况:有序 数组元素需要两两比较,一趟排序完成。 比较次数:n-1 交换次数:0 最坏情况:逆序 需要进行n-1趟排序。 有序区数组大小为0时:比较n-1次,交换n-1次,移动3(n-1)次; 有序区数组大小为1时:比较n-2次,交换n-2次,移动3(n-2)次; …… 有序区数组大小为n-3时:比较2次,交换2次,移动3*2次; 有序区数组大小为n-2时:比较1次,交换1次,移动3*1次; 比较次数为:1+2+……+(n-1) = n(n-1)/2 移动次数为:3(1+2+……+(n-1)) = 3n(n-1)/2 综上,冒泡排序的时间复杂度为O(n2)。 空间复杂度 冒泡排序属于交换排序,在排序过程中,只需要用到一个用来执行元素交换的变量即可。因此,空间复杂度为O(1)。 排序稳定性 冒泡排序是就地排序。 冒泡排序是稳定的。