一、 Introduction
Hadoop是apache开发的分布式系统基础架构。用户可以在不了解分布式底层细节的情况下,开发分布式程序,利用集群进行高速运算和存储。它实现了一个分布式文件系统(hadoop distributed file system),简称hdfs。Hdfs有容错性的特点,并且设计用来部署在低廉的硬件上,提供高吞量来访问应用程序的数据,适合那些有着超大数据集的应用程序。Hdfs放宽了posix的要求,可以以流的形式访问文件系统中的数据。Hadoop最核心的设计是hdfs和mapreduce。Hdfs为海量的数据提供了存储,mapreduce为海量的数据提供了计算。Hadoop就是用来出来海量数据的。
分布式系统:通俗的说就是在物理上分开,在逻辑上统一的系统。比如分布式数据库系统,由多个计算机节点的若干个数据库系统组成,实际上整个分布式系统是一个完整的数据库。
Hadoop实际应用例子,如离线海量的日志分析,在线实时分析,海量技术的存储。
海量数据的存储可以通过很方式实现,nfs(netword file system),网络文件系统,通过若干个节点共享文件,客户端透明读写文件的技术。Nfs的缺点是文件存储在节点机,分析某个节点的文件只能通过这个节点实现,容易形成这个节点高负载,以致影响效率,单个节点的数据丢失会形成永久丢失,数据不安全。 Hadoop的hdfs体系完善了nfs的问题,hdfs,分布式文件系统,将一个文件分割成若干个小块存储在节点上,客户端通过访问映射得到一个透明的统一访问,而且这些小块都可以有不止一个副本,这样就解决了负载均衡的问题,也提高了数据的安全性。
Hadoop解决海量数据的分析计算通过mapreduce,实际上,就是map和reduce这两个阶段。Map阶段对本地局部数据进行处理,由于文件被分割成多个块存储在节点上,那么,map阶段是若干个节点并发执行的过程。Reduce阶段在一个节点上进行,通过网络将map阶段其他节点处理的数据进行汇总处理。Map和reduce还可以进行分组处理,具体很业务逻辑需求。
二、 Hadoop的安装
Hadoop有很多版本可以,官方的是apache的,非官方的有cloudera和hdp(hortonwords data platform)等。安装hadoop之前首先安装jdk,安装7以上的。将hadoop的压缩tar.gz文件传输到linux,通过sftp的put c:/xxx.xxx.xxx(注意使用sftp的put指令时sftp服务器当前目录最好选为/home/user,否则可能由于权限问题不能上传到root下的目录下),或者使用图形用户界面filezilla,如果是xshell可以使用rz等。然后将文件解压到用户目录下,比如tar -xvf hadoop……tar.gz -C /home/user/app。如果解压到root目录下,可能导致使用普通用户时无法开启hadoop服务。
注意:下载的hadoop的位数与linux系统的匹配,大概从hadoop2.5开始,都是64位的了。
1. 进入解压后的目录,hadoop的目录结构如下:
Bin | 可执行脚本 | ||||
Etc | 配置文件 | ||||
Include | 本地库相关文件 | ||||
Lib | Jar包 | ||||
Sbin | 系统相关的可执行脚本,hadoop启动和停止脚本 | ||||
Share | 开发相关jar包。
|
2. 配置hadoop
1) 配置主机名,主机名和ip地址的映射关系
修改主机名,sudo vi /etc/sysconfig/network。但是centos7之后修改主机名需要在sudo vi /etc/hostname中,也就是修改/etc/sysconfig/network是无效的。例如:
在/etc/sysconfig/network中设置
NETWORKING=yes
HOSTNAME=hello
在/etc/sysconfig中只需要填写主机名就可以,如
hello
修改完后,需要重启系统才能生效。如果需要立即生效,可以sudo hostname hello,然后退出当前用户,再次登入。
配置主机名与ip的映射关系,sudo vi /etc/hosts。例如:
192.168.0.111 hello
2) 配置java环境变量
进入hadoop的etc目录的hadoop目录,sudo vi hadoop-env.sh。
修改 export JAVA_HOME=/usr/java/jdk.xxx.xxx。JAVA_HOME填java的解压文件的路径。
3) 配置文件系统和工作目录
同样的目录下,sudo vi core-site.xml。配置hadoop参数。
注意在设置文件系统的类型时使用了ip地址,为了便于管理这个ip地址一般采用域名的方式,比如本机的域名为hadoop_1,那么相应的ip地址的地方要填写hadoop_1,同时要确定本机的域名确定为hadoop_1。修改域名使用sudo vi /etc/hosts,格式为比如:192.168.0.111 hadoop_1。如果域名不对应,那么在初始化hadoop时会报UnknownHostException。端口号可以根据实际需要设定。
例子:
<configuration>
<!-- 设置分布式文件系统的类型 -- >
<property>
<name>fs.defaultFS</name>
<value>hdfs://ip地址:9000</value>
</property>
<!-- hadoop的工作目录 -- >
<property>
<name> hadoop.tmp.dir</name>
<value>/home/app/hadoop/data/</value>
</property>
</configutration>
特别注意:请务必一定将hadoop的工作目录配置到解压的hadoop包的目录下,否则到执行yarn任务时会卡住在running job xxxxx阶段。
4) 配置运行期间相关的参数
同样的目录下,sudo vi hdfs-site.xml。例子:
<configuration>
<!-- 设置副本数-- >
<property>
<name>dfs.replication</name>
<value>1</value>
</property>
<! - -设置处理文件的大小 hadoop适合做海量大文件的处理-- >
<property>
<name> dfs.blocksize</name>
<value>256</value>
</property>
</configutration>
5) 配置mapreduce相关的参数
修改mapred-site.xml.template的名字为:mapred-site.xml。sudo vi mapred-site.xml。例子:
<configuration>
<!-- 设置mapreduce运行期间的资源调度集群,指定为yarn -- >
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configutration>
6) 配置yarn相关参数
Sudo vi yarn-site.xml。注意:下面的ip地址可以为域名。例子:
<configuration>
<!-- 指定yarn集群的资源管理主设备 -- >
<property>
<name>yarn.resourcemanager.hostname</name>
<value>ip地址</value>
</property>
< ! – 指定map产生的数据传递给reduce的机制 -- >
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
</configutration>
7) 关闭防火墙,设置开机不启动
Sudo service iptables stop。关闭防火墙。
查看软件开机启动与否状态的命令:sudo chkconfig iptables - -list。
关闭软件开机启动:sudo chkconfig iptables off。
3. 启动hadoop
首先,初始化hadoop。进入解压包的bin目录。为了操作方便可以将bin目录下的hadoop文件加入环境变量。Sudo vi /etc/profile,加入:export HADOOP_HOME=/home/user/app/hadoop和export PATH=其他的环境变量值后面:$HADOOP_HOME/bin。然后执行初始化的命令:hadoop namenode -format
启动hadoop,进入hadoop目录的sbin目录下,执行./start-dfs.sh,也可以将这个start-dfs.sh配置到profile,以便在任何目录下都可以启动,配置到profile,修改:PATH=前面的环境变量:$HADOOP_HOME/sbin,然后source /etc/profile。启动完毕后,使用jps,可以查看启动的进程,一般有一个namenode,一个secondnamenode,一个datanode。
通过hadoop/etc/slaves文件可以增删改datanode。
然后执行start-yarn.sh,启动yarn和资源管理器和节点管理器。
注意:多次初始化namenode导致namenode clusterID和datanode clusterID不一致,导致无法启动datanode,简单的解决方法是删除hadoop工作空间的所有文件,这是工作空间是自己创建和配置的,重新format namenode。但是这样会删除初始化的文件,有一定的风险,还有一种方法是:找到为hadoop创建的工作空间,一般是命名为data的目录,cd /data/dfs/,在dfs目录下有data、name、namessecondary3个目录,首先进入name/current,打开VERSION文件,复制clusterID=xxxxxxxx,然后进入/data/current目录下,编辑VERSION文件,将clusterID替换为name目录下的clusterID,就是刚复制的那一份,然后就可以了。
三、 测试hadoop
1. 测试开打hadoop网页,上传下载
开启hadoop的服务后,在浏览器输入namenode的ip地址,默认端口号是50070,就可以访问搭建的节点了。
打开是一个网站的形式。在overview列表是相关的信息。点击browse the file system列表进入目录结构。
在linux下上传文件到hadoop集群,hadoop fs -put xxxxx hdfs://localhost:9000/。上传好后,就可以在browse directory中查看了,而且还可以下载。也可以在linux中使用命令行进行下载,比如:hadoop fs -get hdfs://localhost:9000/xxxx。
2. 测试mapreduce程序
进入hadoop/share/hadoop/mapreduce,这个目录下有很多jar包。
例子1:比如运行一个pi程序:hadoop jar hadoop-mapreduce-examples-2.6.5.jar pi 5 5。
例子2:使用mapreduce计算一个文本中单词出现的次数。
为了方便测试,在hadoop/share/hadoop/mapreduce创建一个文本test.txt,写入一些单词,其中有单词是重复的。然后将test.txt上传到hadoop的hdfs,hadoop fs -mkdir hdfs://localhost:9000/testcount,这条目录可以简写为:hadoop fs -mkdir /testcount,然后建立一个目录作为上传输入的文本的目录hadoop fs -mkdir /testcount/input。
将文本放入input目录。Hadoop fs -put test.txt /testcount/input。
执行mapreduce程序,hadoop jar hadoop-mapreduce-examples-2.6.5.jar wordcount /testcount/input /testcount/output。
查看执行结果的目录,hadoop fs -ls /testcount/ouput。
直接查看结果,hadoop fs -cat /testcount/output/xxxx-r-xxx。
四、 关于hdfs的实现机制
当客户端写入文件时,会将文件按照blocksize切开,比如512m的文件,blocksize是128m,那么就会切成4个block。在集群中有很多datanode,每个datanode是一个主机。比如,这4个block,为了描述,暂且编号为1、2、3、4,将1号block放到了某个datanode的主机上,具体位置就是这个linux主机的hadoop的工作目录下,将2号block放到另一个datanode的主机上,将3和4分别都放在另外的datanode的主机上。Datanode的主机是用来负责存储整个文件分成的block的,但是当需要读取这个文件时,各个datanode都是独立的,这时就需要一个负责管理这些datanode上block和整个文件映射关系的角色,这个角色就是namenode。当某个文件存储到datanode时,会在namenode上存储这个文件的所有信息。当用户访问某个文件时,hdfs实际执行的过程是先访问namenode,将这个文件的信息反馈给客户端,然后客户端到存储这个文件的block的datanode读取这个文件。为了保证数据的可靠性和吞吐量,可以给一个文件的block存储若干个副本。
总结就是:
1,hdfs通过分布式集群存储文件,分布式集群就是分布在多个datanode,多个datanode被一个namenode管理,为客户端提供便捷的访问方式;
2,文件存储在hdfs的时候被切分成block;
3,文件的block存放在若干台datanode节点上;
4,hdfs系统中的文件与block之间的映射关系由namenode管理。
5,每个block在集群中可以存储多个副本,可以提高数据的可靠性和访问的吞吐量。
五、 Hdfs的shell操作
常用命令:
追加内容 | Hadoop fs -appendTofile <localsrc> <dst> 比如:hadoop fs -appendToFile /home/user/xxx /file/append1 |
查看文件 | Hadoop fs -cat <src> Hadoop fs -cat /file/append1 |
修改文件权限 | Hadoop fs -chgrp [-R] group path... Hadoop fs -chmod [-R] xxx path… Hadoop fs -chown [-R] xxx path… 例子:Hadoop fs -chown newuser1:newgroup /xxx/xxx |
从本地复制 | Hadoop fs -copyFromLocal [-f] [-p] <localsrc> <dst> Hadoop fs -copyFromLocal ./xxx / 功能等于-put。 |
复制到本地 | Hadoop fs -copyToLocal [-p] <src> <localdst> 相当于于-get。 |
统计目录下文件数 | Hadoop fs -count <path> |
在hdfs复制文件 | Hadoop fs -cp <src> <dst> 例子:Hadoop fs -cp /file/src /file/dst |
查看磁盘空间 | Hadoop fs -df [-h] <path> 例子:hadoop fs -df -h / |
查看文件大小 | Hadoop fs -du [-s] [-h] <path> 例子:hadoop -du -s -h /file/xxx 例子:hadoop -du -s -h http://xxx/*,统计目录下所有文件的大小。 |
从hfsd将文件下载到本地 | Hadoop fs -get <src> <localdst>
|
帮助提示 | Hadoop fs -help |
创建目录 | Hadoop fs -mkdir [-p] <path> |
删除目录或者文件 | Hadoop fs -rm [-f] [-r|-R] [-skipTrash] <src> |
删除空目录 | Hadoop fs -rmdir [--ignore-fail-non-empty] <dir> |
移动,相当于剪切 | Hadoop fs -mv <src> <dst> |
查看文件末尾 | Hadoop fs -tail [-f] <file> 适合查看内容很大的日志文件。 |
六、 集群搭建的无密登录
管理员对于集群的管理一般通过ssh协议实现,通过ssh操作远程linux系统,有两种方式,一种是用户名密码的方式,一种是密钥的方式。采用密钥方式登录就可以免开输入密码的操作。当执行start-dfs.sh等这样的操作时,实际上要都要和namenode以及若干datanode通信,这个node实际上都是主机,这个通信是通过ssh协议的,如果采用的是密码的方式,那么连接每个node的主机都需要输入密码。在linux中也可以使用命令行,通过ssh,从一个主机登录到另一个主机,比如:sudo ssh host_1,如果是密码登录,则需要输入 密码。
密钥的思路,有两台linux主机,如果a主机要登录b主机,那么需要在a中生成一对密钥,一个是公钥,一个是私钥。 然后将a主机的公钥复制到b主机。然后在b主机,将a主机的公钥匙放到b主机的授权列表authorized keys。这样就可以实现a主机对b主机的无密登录。那么,当a主机登录b主机时,1,发出请求登录,2,b主机查看授权列表,3,用a的公钥(如果在授权列表的话)加密一个随机字符串发送给a主机,4,a主机收到反馈后,使用私钥解密b主机发过来的密文,5,a主机将解密结果发送给b主机,6,b主机验证解密结果,7,如果通过验证,则a登录成功到b。
如果使用了域名,请注意配置ip和域名的映射关系,sudo vi /etc/hosts。192.168.0.1 主机名。
配置密钥登录机制:
1,在a主机执行ssh-keygen -t rsa,中间不要输入密码;
2,在默认的生成密钥的目录下,有id_rsa、id_rsa.pub、known_hosts这3个文件;
3,使用远程传送命令将公钥传到b主机,scp id_rsa.pub b的域名:/home/user/xxx;scp就是指ssh copy。
4,在b主机,在用户的目录下也生成一对密匙,ssh-keygen -t rsa,然后在生成的.ssh目录中创建一个授权列表文件,touch authorized_keys,将这个文件的权限改为600,chmod 600 authorized_keys;
5,将复制到b主机的a主机的公钥追加到创建的授权列表,cat ../id_rsa.pub >> ./authorized_keys;
6,然后从a登录到b,就不需要密码了,ssh b。
根据这个原理,那么在hadoop的伪集群中,只要设置本机的公钥追加到本机的授权列表就可以无密登录namenode和datanode了。步骤如下:
1. 在本机执行ssh-keygen -t rsa,生成ssh的密钥;
2. 在生成的密钥的.ssh目录中,touch authorized_keys,然后chmod 600 authorized_keys;
3. 将同目录下的id_rsa.pub追加到授权列表,cat id_rsa.pub >> authorized_keys;
4. 自己登录自己测试,ssh 本机域名。
配置成功后,再次启动hadoop,已经不需要输入密码了。
七、 HDFS详解
(一)Hdfs的架构
Hdfs在结构上包括namenode、datanode、secondary namenode3个部分。一般,namenode只有一个,datanode有若干个。Namenode用来保存元数据,元数据就是关于文件和文件的block的映射关系,datanode用来保存block。关于副本的创建,当客户端将文件传输到datanode后,由datanode经由pipeline将block复制给其他datanode,这样相比客户端直接将副本写到datanode即提高了传输效率也提高了传输的成功率。如果在datanode复制副本到datanode时出现异常而失败,而会将失败信息反馈给namenode,然后namenode重新安排datanode复制副本。文件的切分由客户端完成,上传文件其实就是输出流的过程,比如一个block是128m,当这个流输出到128m的时候就换一个datanode基础传输,直到传完为止。
如果客户端上传到datanode的数据都很小,那么这样就会浪费namenode的空间,也就是浪费元数据的空间,因为每个block都要在元数据中做映射,小文件越多,占用的元数据就多。这就涉及到hadoop的优化。如果namenode上的元数据是写在磁盘上的,那么也不存在优化的问题,因为磁盘的容量一般很多,而一个block产生的元数据也不过100多个byte。但是元数据存在磁盘上会大大降低hadoop处理客户端相应的速度。如果namenode的元数据存放在内存上,则存在宕机断电丢失数据的问题,数据丢失则集群覆灭。如果以内存作为缓存,定期向磁盘写入存放的数据,同样也存在断电丢失数据的风险,给系统带来不稳定因素。
1. Namenode的工作机制
实际上,hadoop的hdfs采取的是读写分离的方式,元数据在磁盘中有一份作为持久化的数据,在内存中也有一份作为查询使用,当客户要做了写相关的操作时,通过一个约64m日志文件,edits log,以日志文件的形式保存最新的元数据,然后将这些数据写到内存中,以保证内存中的数据是最新的,一旦断电,最新的数据仍然可以在edits log中找到。然后在edits log存满数据的一个时间点,将edits log的元数据合并到磁盘,保存在叫做fsimage的镜像中,然后将edits log清空,以便继续保存新的元数据。Edits log的数据合并到fsimage的操作由secondaryname执行,以减轻namenode的负载。合并过程是:1,一旦edits log写满,namenode就通知secondarynode执行checkpoint操作,就是将edits log写到fsimage;2,然后namenode停止向edits log中写数据,但是为了继续服务客户端的请求,在namenode会创建一个edits log.new的文件来记录checkpoint期间产生的元数据;3,secondarynode执行checkpoint就是将edits log和fsimage下载到secondarynode;4,然后利用自己的cpu对edits log和fsimage进行合生成fsimage.chpoint;6,合并完毕后,将fsimage.chpoint上传到namenode;7,最后,namenode将自己的fsimage和edits log删除,将上传来的fsimage.chpoint文件重命名为fsimage,将edits log.new重命名为edits log,以便进入下一次checkpoint。整个过程既不影响namenode对客户端的响应,又可以解决断电丢失数据的隐患。
一般,block的副本会放到原本放的机架以外的机架上,以分散断电故障等风险。关于上述checkpoint的触发时间有两个条件,一个是两次checkpoint的最大间隔时间,默认是3600s,一个是edits log文件的达到64m,这两个条件的参数值可以在hfds-site.xml中修改。
虽然,一个namenode和多个datanode的组合可以很多的解决响应速度和数据安全的问题,但是仍然存在服务连贯性的隐患,如果这个namenode的主机宕机了,那么服务就要被迫暂停。为了解决这个问题,可以考虑搭建的集群中配备多个namenode,如果这样就涉及到namenode的数据同步问题。
Namenode的Edits log和fsimage文件的目录路径,起始位置是自定义的hadoop的工作目录:/hadoop/data/dfs/name/current/
2. Datanode的工作原理
Datanode的功能是提供真是文件数据的存储服务。在datanode中,文件以block为最基本的存储单位。如果一个文件小于一个数据块的大小,那么并不占用整个数据块的空间。Hdfs默认的block的size是128mb。Datanode中还会保存文件的副本,以便提高访问速度和数据安全,默认副本数是3个。
Block文件的目录路径:/home/user/app/hadoop/data/dfs/data/current/BP-1440972573-192.168.0.110-1509507330970/current/finalized。
手动将同一个文件的block按照对应的文件流的连贯顺序追加起来可以像操作源文件一样操作这个追加的block。
(二)Hdfs的java客户端程序
为了方便,直接在linux平台使用eclipse开发,将linux版的eclipse导入linux,如果需要java依赖,直接将使用终端解压的java包的里面的jre目录复制到eclipse解压的目录下。
步骤:
1. 创建一个java工程。
2. 导入自定义的hdfs包。
导入hadoop的hdfs的核心包和依赖包以及common的核心包和依赖包,右键项目》》configure build path》》libraries》》add library》》user Library》》user libraries》》new》》新命名一个叫做hdfslib自定义的library》》选中新命名的lib》》add externaljars》》找到hadoop的解压包》》在/share/hadoop/hdfs目录下将hdfs的核心包导入,再次将/share/hadoop/hdfs/lib/下所有的包导入》》同样的方式,将/share/hadoop/common/下的核心包和依赖包导入》》确定,确定。
3. 将hadoop的配置文件core-site.xml和hdfs-site.xml复制到工程的src目录下;
4. 创建类,
5. 编写测试代码,
public class HdfsUtil {
public static voidmain(String[] args)throws Exception{
Configuration con= new Configuration();
FileSystem fs =FileSystem.get(con);
//download file from hdfs
Path src = newPath("hdfs://bighaha:9000/jdk-8u60-linux-x64.gz");
FSDataInputStreamin = fs.open(src);
FileOutputStreamfileOutputStream = new FileOutputStream("/home/haha/Downloads/jdk.tar.gz");
IOUtils.copy(in,fileOutputStream);
}
}
不使用hadoop的配置文件而是使用configuration对象设置hdfs路径和用户名的例子:
首先在选中测试的方法名右键》》runs as》》run configuration》》在arguments的vm arguments中添加变量-DHADOOP_USER_NAME=linux的用户名。
@Test
public voidupload()throwsException{
Configuration conf = new Configuration();
conf.set("fs.defaultFS", "hdfs://bighaha:9000/");
FileSystem fs = FileSystem.get(conf);
Path path = new Path("hdfs://bighaha:9000/upload/upload.txt");
FSDataOutputStream fos = fs.create(path);
FileInputStream fis = new FileInputStream("d:/hdfsupload.txt");
IOUtils.copy(fis, fos);
}
使用conf的参数指定访问的用户名以及包装重复代码,简化代码的例子:
private FileSystem fs= null;
@Before
public voidgetFileSystem()throwsException{
Configuration conf = new Configuration();
fs = FileSystem.get(new URI("hdfs://bighaha:9000/"),conf,"haha");
}
@Test
public voidupload2()throwsException{
fs.copyFromLocalFile(new Path("d:/hdfsupload.txt"), newPath("hdfs://bighaha:9000/upload/upload2.txt"));
}
常用的客户端操作的java代码,下面的代码都需要使用获得fs对象的方法。
private FileSystem fs= null;
@Before
public voidgetFileSystem()throwsException{
Configuration conf = new Configuration();
fs = FileSystem.get(new URI("hdfs://bighaha:9000/"),conf,"haha");
}
下载
@Test
public voiddownload()throws Exception{
fs.copyToLocalFile(false,newPath("hdfs://bighaha:9000/upload/upload4.txt"),newPath("d:/download3.txt"),true);
}
创建文件夹
@Test
public voidmkdir()throws Exception{
fs.mkdirs(newPath("/mkdir1/mkdir2/mkdir3"));
}
删除文件
public voidremove()throwsException{
fs.delete(newPath("/mkdir1"),true);
}
重命名
@Test
public voidrename()throwsException{
fs.rename(newPath("/mkdir1"),newPath("/rename"));
}
查看文件信息
@Test
public voidlistFileName()throwsException{
RemoteIterator<LocatedFileStatus> listFiles = fs.listFiles(new Path("/"), true);
while(listFiles.hasNext()){
LocatedFileStatus next = listFiles.next();
Path path = next.getPath();
String fileName= path.getName();
System.out.println(fileName);
}
}
@Test
public voidlistFile()throws Exception{
FileStatus directory = new FileStatus();
directory.setPath(newPath("/"));
FileStatus[] listStatus = fs.listStatus(directory.getPath());
for(FileStatus status:listStatus){
if(status.isDirectory()){
System.out.println("************************");
//listFile(status);
}
String name = status.getPath().getName();
System.out.println(name);
}
}
(三)Hdfs总结
通过namenode、datanode、secondarynamenode来存储和管理数据。Namenode负责维护元数据信息,通过edits log和fsimage在checkpoint时候合并来保证数据的更新,维护hdfs的目录树,响应客户端的请求,当执行checkpoint的合并时,由secondarynamenode负责代为执行响应客户端的请求。
八、 RPC框架
Remote procedure call protocol,远程过程调用协议,通过网络从远程计算机程序上请求服务,不需要了解底层网络技术,使得开发包括网络分布式多程序在内的应用程序更加容易。Rpc主要应用于分布式系统。Hadoop的hdfs就使用了rpc技术,hdfs的客户端的操作中获得文件相关信息表面上起来是直接调用了方法得到的,实际上是这个方法通过rpc调用namenode进而得到的。Datanode与namenode之间的通话也是使用的rpc。datanode要与namenode做心跳检测,datanode要向namenode做汇报,特别是核对元数据与实际的block的对应情况,比如有人误删了某个datanode的某个block的备份,这时候,一旦通过rpc检测到不对应,就可以命令datanode补上这个block的备份。
Hadoop的rpc采用java的动态代理、socket编程和反射技术实现。
Rpc框架的jar包位于hadoop的common目录下,可以配合hadoop使用rpc,也可以单独使用hadoop的rpc。
Rpc框架的使用例子:
在远程调用端服务器上创建被调用的接口和实现类以及开启服务
接口
public interface LoginService {
public staticfinallongversionID= 11L;
public String login(String username,String password);
}
实现类
public class LoginServiceImpl implements LoginService{
@Override
public String login(String username, String password){
return username+"登录成功";
}
}
启动服务的类
public class Start {
public staticvoidmain(String[] args)throwsException{
Builder builder = newRPC.Builder(new Configuration());
builder.setBindAddress("remoteAddress").setPort(8888).setProtocol(LoginService.class).setInstance(new LoginServiceImpl());
Server server = builder.build();
server.start();
}
}
在主服务器上的接口客户端请求的控制类和服务接口
控制类
public class LoginController {
public staticvoidmain(String[] args) throws Exception{
LoginService proxy = RPC.getProxy(LoginService.class, 11L, new InetSocketAddress("bighaha",9999), newConfiguration());
String uname = proxy.login("hah","123");
System.out.println(uname);
}
}
服务接口注意和远程服务接口的一致性,特别是注意版本号的一致
public interface LoginService {
public staticfinallongversionID= 11L;
public String login(String username,String password);
}
九、 Mapreduce
Mapreduce是hadoop中用于处理海量数据计算的框架,特别是用于分布式海量数据的计算。大体上,mapreduce的运算过程包括两个阶段,一个是map阶段,局部处理分布的数据,一个是reduce阶段,汇总处理局部处理的结果。
(一)在集群中使用mapreduce统计文本内词汇出现的个数的例子:
1. 编写map类负责统计每一行的词汇的出现的个数
//Mapper的4个泛型中,前两个指输入数据的类型,后两个输出数据的类型
//map和reduce的数据输入输出都是以key-value的形式封装
//默认下,框架传递给mapper的输入数据中,key要处理的是文本中一行的起始偏移量,这一行的内容作为value
/*
* 由于参数在网络中传输都是要序列化的,如果使用java的数据类型,
* 那么就需要使用jdk的序列化接口,但是jdk的序列化是比较冗余的,
* 不适合海量数据在网络的传播,为了解决这个问题,hadoop提供了自己的数据类型,以便自己提供序列化的方法
*/
public class WCMap extendsMapper<LongWritable,Text,Text,LongWritable>{
//mapreduce框架每读一次数据就调用一次这个方法
@Override
protected voidmap(LongWritable key,Text value,Mapper<LongWritable, Text, Text, LongWritable>.Context context)
throws IOException, InterruptedException {
//key是这一行数据的起始偏移量,value是这一行数据的文本内容
//将这一行的内容转换成string
String line = value.toString();
//将这一行内容按特定分隔符切分
String[] words = StringUtils.split(line,' ');
//遍历这个单词数组,输出为kv形式,k:单词,v:1
for(String word : words){
context.write(new Text(word),new LongWritable(1));
}
}
2. 编写reduce类负责汇总map统计的结果
//reduce的泛型的输入输出的类型注意与map的对应
public class WCReduce extends Reducer<Text, LongWritable,Text, LongWritable>{
//框架在map处理完成之后,将所有kv对缓存起来,进行分组,然后传递一个组<key,values[]>调用一次reduce方法
@Override
protected voidreduce(Text key,Iterable<LongWritable> values,
Reducer<Text, LongWritable, Text,LongWritable>.Context context) throwsIOException, InterruptedException {
long count = 0;
for(LongWritable value : values){
count += value.get();
}
context.write(key,newLongWritable(count));
}
}
3. 编写运行类负责初始化map和reduce,以及指定数据来源和结果存放的路径
/**
* 描述特定作业的类,指定map和reduce类对应关系的类
* 指定作业要处理的数据所在的路径
* 指定该作业输出的结果放在哪个路径
* @author Administrator
*
*/
public class WCRunner {
public staticvoidmain(String[] args)throwsException{
Configuration configuration = new Configuration();
JobConf jobconf = new JobConf(configuration);
//指定输入数据存放的路径
FileInputFormat.setInputPaths(jobconf, new Path("/wc/srcdata/"));
//指定输出数据存放的路径
FileOutputFormat.setOutputPath(jobconf, new Path("/wc/output/"));
Job wcjob = Job.getInstance(jobconf);
//设置整个job使用的类的jar包
wcjob.setJarByClass(WCRunner.class);
//指定job使用的mapper和reducer
wcjob.setMapperClass(WCMap.class);
wcjob.setReducerClass(WCReduce.class);
//指定reduce输出数据kv的类型,这个类型也可以对map起作用,如果map和reduce的输出类型不一致还可以单独指定map的输出类型
wcjob.setOutputKeyClass(Text.class);
wcjob.setOutputValueClass(LongWritable.class);
//指定map输出数据kv的类型
wcjob.setMapOutputKeyClass(Text.class);
wcjob.setMapOutputValueClass(LongWritable.class);
wcjob.waitForCompletion(true);
}
}
将工程打成jar包,上传到hadoop的节点主机。
在hadoop的节点主机将统计的文本放在指定的数据源路径,然后在上传的jar的目录下运行:hadoop jar wordcount.jarcom.hadoop.mr.wordcount.WCRunner。注意指定运行类的全路径。
Mapreduce程序也可以在本地运行,需要给本地hadoop的解压包的bin目录下添加winutils.exe和hadoop.dll文件,同时添加hadoop_home环境变量。Mapreduce在本地运行也可以指定集群中路径做文件源和文件结果路径。也就是mapreduce的程序可以和文件源分离,但是效率高的情况是mepreduce和文件源在同一个节点。
十、 Yarn框架
Yarn框架主要用来做资源(cpu、内存、磁盘、网络资源等)调配。Mapreduce代码的提交和运行通过yarn框架实现。Yarn包括resourcemanager和nodemanager,resourcemanager是主节点,nodemanager是分节点。
(一)Yarn处理mapreduce程序的流程
1. 当使用hadoop jar xxx.jar 主类全路径,执行一个jar文件时,RunJar节点就会向resourcemanager去申请执行一个job;
2. 然后resourcemanager会返回给hadoop提交job的RunJar节点job资源提交的路径(staging dir)和为本job生成的jobID;
3. 然后提交job的节点将要运行的资源提交给hdfs(默认的路径是:/tmp/xx/xx./yarn-staging/jobID/);
4. 然后提交job的节点向resourcemanager节点汇报提交结果;
5. 然后resourcemanager将本次job加入任务队列;
6. Nodemanager和resourcemanager之间通过心跳机制保持沟通,当job加入任务队列后,可以执行任务的nodemanager就领取任务;
7. Yarn框架给得到任务的nodemanager分配运行资源的容器(container),同时将job节点的相关jar包和文件给nodemanager;
8. Yarn框架启动MRAppMaster类来管理map和reduce的运行;
9. MRAppMaster将运行相关的信息注册给resourcemanager;
10. MRAppMaster在不同的nodemanager节点的资源容器中根据情况启动map任务或者reduce任务;
11. MRAppMaster将map执行完毕的数据传递给reduce,同时向resourcemanager注销任务,并将运行完毕的任务的资源回收;
上述RunJar节点与resourcemanager节点通过rpc通信。
(二)Mapreduce程序提交运行的模式
1. 本地模式运行
1) 在windows的eclipse里面直接运行main方法,将job提交给本地执行器localjobrunner执行
通过本地运行job的主方法在本地运行,在main方法中,job对象中会传入一个configuration对象,当在本地运行job的runner主函数时,conf一般是设置为空的,那么,默认产生的提交job的节点就是LocaljobRunner。
输入输出的数据可以放在本地路径下(比如:d:/xxx/xxx)。
输入输出数据也可以放在hdfs中(比如:hdfs://xxxx:9000/xx/xxx)。
2) 在linux的eclipse里面直接运行main方法,但是不添加yarn相关的配置,会提交给localjobrunner执
输入输出的数据可以放在本地路径下(比如:d:/xxx/xxx)。
输入输出数据也可以放在hdfs中(比如:hdfs://xxxx:9000/xx/xxx)。
1. 集群模式运行
1) 在linux终端
将工程打成jar包,上传到服务器,在linux终端通过命令行hadoop jar xxxx.xxx.jar xxx.xxx.xxx。由于linux终端的hadoop的mapred-site.xml配置文件中配置了mapreduce.framework.name的值为yarn。所以会以yarn模式运行。
2) 在linux的eclipse中直接运行main 方法
将hadoop的配置文件中mapred-site.xml和yarn-site.xml拷贝到src下或者在configuration对象中配置这两个文件中的配置项,同时指定jar的路径,并在这个路径下放置jar包,比如:
public class WCRunner {
public staticvoidmain(String[] args)throwsException{
Configuration configuration = new Configuration();
configuration.set("mapreduce.job.jar", "wordcount.jar");
JobConf jobconf = new JobConf(configuration);
//指定输入数据存放的路径
FileInputFormat.setInputPaths(jobconf, new Path("/wc/srcdata/"));
//指定输出数据存放的路径
FileOutputFormat.setOutputPath(jobconf, new Path("/wc/output/"));
Job wcjob = Job.getInstance(jobconf);
//设置整个job使用的类的jar包
wcjob.setJarByClass(WCRunner.class);
//指定job使用的mapper和reducer
wcjob.setMapperClass(WCMap.class);
wcjob.setReducerClass(WCReduce.class);
//指定reduce输出数据kv的类型,这个类型也可以对map起作用,如果map和reduce的输出类型不一致还可以单独指定map的输出类型
wcjob.setOutputKeyClass(Text.class);
wcjob.setOutputValueClass(LongWritable.class);
//指定map输出数据kv的类型
wcjob.setMapOutputKeyClass(Text.class);
wcjob.setMapOutputValueClass(LongWritable.class);
wcjob.waitForCompletion(true);
}
}
注意将本工程的jar包放到这个项目的路径下。
Yarn只负责要运行的job,增强了hadoop集群的通用性,因为yarn的存在使得上层既可以使用mapreduce程序也可以使用spark程序,也可以使用storm程序,以及其他新产生的运算框架。Yarn框架的分发工作的意义很大。
十一、 Mapreduce中的自定义序列、结果排序、结果分组、并发任务数、shuffle机制以及倒排索引
(一)关于使用自定义bean
如果使用自定义的数据类型,要在hadoop的各个节点之间传播,应该遵循hadoop的序列化机制,就必须实现hadoop的序列化接口。
流量求和的例子
已知提供的数据文件中,有用户手机号、登陆的域名、上传和下载的流量,求所有用户上传的和下载的流量的和。
编写一个bean以便接收上传流量和下载流量两类数据,同时使用hadoop序列化这个bean
public class FlowBean implements Writable{
private String phoneNb;
private longup_flow;
private longdown_flow;
private longsum_flow;
//在反序列化时,反射机制需要调用空参构造函数,所以需要显示定义一个空参的构造函数
public FlowBean(){}
//为了对象的初始化方便加入带参的构造函数
public FlowBean(String phoneNb, longup_flow,longdown_flow){
this.phoneNb = phoneNb;
this.up_flow = up_flow;
this.down_flow = down_flow;
this.sum_flow = up_flow+ down_flow;
}
//将对象数据序列化到流中
@Override
public voidwrite(DataOutput out)throwsIOException {
out.writeUTF(phoneNb);
out.writeLong(up_flow);
out.writeLong(down_flow);
out.writeLong(sum_flow);
}
//从对象数据流中序列化出对象
//从数据流中读出对象字段时必须跟序列化时的顺序保持一致
@Override
public voidreadFields(DataInput in) throwsIOException {
phoneNb = in.readUTF();
up_flow = in.readLong();
down_flow = in.readLong();
sum_flow = in.readLong();
}
@Override
public String toString() {
return ""+ up_flow + "\t" + down_flow+ "\t"+ sum_flow;
}
public String getPhoneNb() {
return phoneNb;
}
public voidsetPhoneNb(String phoneNb){
this.phoneNb = phoneNb;
}
public longgetUp_flow() {
return up_flow;
}
public voidsetUp_flow(longup_flow){
this.up_flow = up_flow;
}
public longgetDown_flow() {
return down_flow;
}
public voidsetDown_flow(longdown_flow){
this.down_flow = down_flow;
}
public longgetSum_flow() {
return sum_flow;
}
public voidsetSum_flow(longsum_flow){
this.sum_flow = sum_flow;
}
}
编写map类
//FlowBean是自定义的数据类,要在hadoop的各个节点之间传播,要符合hadoop的序列化机制
//就必须实现hadoop对应的序列化接口
public class FlowSumMapper extends Mapper<LongWritable, Text, Text,FlowBean>{
//得到日志中的一行数据,切分各个字段,抽取需要的字段:手机号、上行流量、下行流量,然后封装成kv对发送出去
@Override
protected voidmap(LongWritable key,Text value,Mapper<LongWritable, Text, Text, FlowBean>.Context context)
throws IOException, InterruptedException {
//获取一行数据
String line = value.toString();
//切分成各个字段
String[] fields= StringUtils.split(line, "\t");
String phoneNb = fields[1];
long up_flow = Long.parseLong(fields[7]);
long down_flow = Long.parseLong(fields[8]);
//封装数据为kv输出
context.write(newText(phoneNb),newFlowBean(phoneNb,up_flow,down_flow));
}
}
编写reduce类
public class FlowSumReduce extends Reducer<Text, FlowBean, Text,FlowBean>{
//框架中每传递一组数据,就调用一次reduce方法
//reduce中的业务就是遍历values,然后累加求和再输出
@Override
protected voidreduce(Text key,Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context)
throws IOException, InterruptedException {
long up_flow_count = 0;
long down_flow_count = 0;
for(FlowBean bean : values){
up_flow_count += bean.getUp_flow();
down_flow_count += bean.getDown_flow();
}
context.write(key,newFlowBean(key.toString(),up_flow_count, down_flow_count));
}
}
编写run类,按照正规的格式编写
//job描述和提交类的规范写法
public class FlowSumRunner extends Configured implements Tool{
@Override
public intrun(String[] args)throwsException {
Configuration conf = new Configuration();
JobConf jobconf = new JobConf(conf);
FileInputFormat.setInputPaths(jobconf, new Path(args[0]));
FileOutputFormat.setOutputPath(jobconf, new Path(args[1]));
Job job = Job.getInstance(jobconf);
job.setJarByClass(FlowSumRunner.class);
job.setMapperClass(FlowSumMapper.class);
job.setReducerClass(FlowSumReduce.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
return job.waitForCompletion(true) ? 0 : 1;
}
public staticvoidmain(String[] args)throwsException{
int res = ToolRunner.run(new Configuration(), new FlowSumRunner(), args);
System.exit(res);
}
}
将工程打成jar包,放到linux,同时在hadoop创建好文件源路径以及将分析的数据放到源路径,然后在linux执行语句,注意指定文件源和结果路径的参数:hadoop jar flowsum.jar com.hadoop.mr.flowsum.FlowSumRunner/flow/data /flow/output
(二)关于实现mapreduce计算结果的排序输出
一般mapreduce的输出结果是按照key升序排序的。如果需要按照value的值排序,那么就需要将value封装到key中。以上面的输出结果为数据来源,那么就需要将flowbean中的数据作为结果封装到key中,同时FlowBean还需要实现comparable接口,并按照流量从大到小排序。
编写FlowBean添加实现comparable接口和排序方法
public class FlowBean implements WritableComparable<FlowBean>{
……
@Override
public intcompareTo(FlowBean o){
return this.getSum_flow()>o.getSum_flow()?-1:1;
}
}
编写map\reduce\run
public class SortMR {
public staticclassSortMapper extendsMapper<LongWritable, Text, FlowBean, NullWritable>{
//得到一行数据,切分出各字段封装为一个flowbean作为key输出
@Override
protected void map(LongWritable key, Text value,
Mapper<LongWritable, Text, FlowBean,NullWritable>.Context context)
throws IOException, InterruptedException {
String line = value.toString();
String[] fields = StringUtils.split(line,"\t");
String phoneNb = fields[0];
long u_flow = Long.parseLong(fields[1]);
long d_flow = Long.parseLong(fields[2]);
context.write(new FlowBean(phoneNb,u_flow,d_flow), NullWritable.get());
}
}
public staticclassSortReducer extends Reducer<FlowBean,NullWritable, Text, FlowBean>{
@Override
protected void reduce(FlowBean key, Iterable<NullWritable> values,
Reducer<FlowBean, NullWritable, Text, FlowBean>.Context context)
throws IOException, InterruptedException {
String phoneNb = key.getPhoneNb();
context.write(new Text(phoneNb), key);
}
}
public staticvoidmain(String[] args)throwsException{
Configuration conf = new Configuration();
JobConf jobconf = new JobConf(conf);
FileInputFormat.setInputPaths(jobconf, new Path(args[0]));
FileOutputFormat.setOutputPath(jobconf, new Path(args[1]));
Job job = Job.getInstance(jobconf);
job.setJarByClass(SortMR.class);
job.setMapperClass(SortMapper.class);
job.setReducerClass(SortReducer.class);
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
System.exit(job.waitForCompletion(true)?0:1);
}
将工程打成jar包,到linux测试。
(三) 关于实现mapreduce结果的分组输出
以上述的例子为例,默认情况下,所有的key根据hadoop的HashPartitioner来分组,结果就是每个手机号都返回同一个组。如果要根据自定义的手机号的规则,比如按照归属地或者按照号码段分组,就要自定义Partitioner类。
自定义Partitioner类
public class AreaPartitioner<KEY, VALUE> extends Partitioner<KEY,VALUE> {
private staticHashMap<String, Integer> areaMap = newHashMap<>();
static{
areaMap.put("135", 0);
areaMap.put("136", 1);
areaMap.put("137", 2);
areaMap.put("138", 3);
areaMap.put("139", 4);
}
@Override
public intgetPartition(KEY key,VALUE value,intnumPartitions){
//从key中获得手机号,查询手机归属地字典,不同的省份返回不同的组号
int areaCode = areaMap.get(key.toString().substring(0,3))==null?5:areaMap.get(key.toString().substring(0,3));
return areaCode;
}
}
编写map\reduce\run
public class FlowSumByArea {
/*
* 对原始流量日志进行统计,将不同省份的用户统计输出到不同的文件
* 需要自定义分组类partition,
* 自定义reduce task的并发任务数
*/
public staticclassFlowSumAreaMapper extends Mapper<LongWritable,Text,Text,FlowBean>{
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text,FlowBean>.Context context)
throws IOException, InterruptedException {
String line = value.toString();
String[] fields = StringUtils.split(line,"\t");
String phoneNb = fields[1];
long u_flow = Long.parseLong(fields[7]);
long d_flow = Long.parseLong(fields[8]);
context.write(new Text(phoneNb), new FlowBean(phoneNb,u_flow,d_flow));
}
}
public staticclassFlowSumAreaReducer extends Reducer<Text,FlowBean,Text,FlowBean>{
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text,FlowBean, Text, FlowBean>.Context context)
throws IOException, InterruptedException {
long up_flow_count = 0;
long down_flow_count = 0;
for(FlowBean bean : values){
up_flow_count += bean.getUp_flow();
down_flow_count += bean.getDown_flow();
}
context.write(key, new FlowBean(key.toString(),up_flow_count,down_flow_count));
}
}
public staticvoidmain(String[] args)throws Exception {
Configuration conf = new Configuration();
JobConf jobconf = new JobConf(conf);
FileInputFormat.setInputPaths(jobconf, new Path(args[0]));
FileOutputFormat.setOutputPath(jobconf, new Path(args[1]));
Job job = Job.getInstance(jobconf);
job.setJarByClass(FlowSumByArea.class);
job.setMapperClass(FlowSumAreaMapper.class);
job.setReducerClass(FlowSumAreaReducer.class);
job.setPartitionerClass(AreaPartitioner.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//设置reduce的任务并发数,应该与分组的数量保持一致
job.setNumReduceTasks(6);
System.exit(job.waitForCompletion(true)?0:1);
}
}
(四) 关于map的并发任务数
在hadoop中,map任务的进程基于split切片,一个切片对应一个map进程。Split是一个逻辑的概念,指定文件的起始偏移量。Block是一个物理的概念。切片是可以配置的,比如一个block存储的文件的大小比较大,那么可以设置一个切片对应一个block,如果block存储的文件比较小,也可以设置一个切片对应多个block。
(五)关于shuffle机制
Shuffle机制是贯穿于map和reduce过程中的处理实现。Hadoop进入map和reduce程序后,会开启map task,然后根据split的大小开始处理文件,将处理的数据放入map的唯一一个的环形内存缓冲区,这个缓冲区的默认大小为100mb,如果超过这个缓冲区的阙值,一个后台进程就把内容写到磁盘指定的目录下的新建的一个溢出写文件。而且写入这个文件的数据是排序分组后写入的。如果这个溢出的文件写满了,就在新建一个文件用于写溢出的数据。等map处理数据结束,就将所有这些溢出记录的文件在合并成一个分组且排序的大文件。在reduce处理阶段,当接收到各个节点的map传送过来的文件,首先将各个节点的文件合并成一个文件,在进行相应的处理。这个过程就称为hadoop的shuffle机制。Shuffle的含义是洗牌,这个过程中的分组、排序、合并文件就很类似洗牌,所以称之为shuffle机制。
在生产过程中,可以根据生产环境,对shuffle的过程进行调优,比如设置环形内存缓冲区的大小,设置环形内存缓冲区溢出的阙值。
由于mapreduce会将中间处理过程写入磁盘,因为磁盘的空间是很大的,这使得mapreduce适合处理海量的大数据,特别是在对实时性要求不高的海量数据的处理上。
(六)Mapreduce的组件
按照文件输入到输出mapreduce的流程,mapreduce的组件包括inputformat、split、rr、map、partitioner、sort、reduce、outputformat。
(七)倒排索引
倒排索引,是将每个词汇出现的次数按照从大到小的顺序以及出现对应的出现的路径或者文件名输出。
比如有3个文档,给这个3个文档创建倒排索引。
可以分两步考虑这个问题,首先以词汇以及对应的文档为key,以词汇对应的文档中出现次数为value获得每个词汇在每个文档出现的次数,然后以词汇为key,以词汇在每个文档出现的次数的list为value获得最终的结果。
第一步,以词汇以及对应的文档为key,以词汇在对应文档中出现的次数为value,获得初步统计结果
public class InverseIndexStepFirst {
public staticclassStepFirstMap extends Mapper<LongWritable, Text, Text, LongWritable>{
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text,LongWritable>.Context context)
throws IOException, InterruptedException {
//获取一行数据
String line = value.toString();
//切分这一行
String[] fields = StringUtils.split(line, " ");
//获取这一行文件所在的文件切片
FileSplit inputSplit = (FileSplit)context.getInputSplit();
//从文件切片中获取文件名
String filename = inputSplit.getPath().getName();
for(String field : fields){
//封装kv,k是词汇-->文件名,value是1
context.write(new Text(field+"-->"+filename),newLongWritable(1));
}
}
}
public staticclassStepFirstReducer extends Reducer<Text, LongWritable, Text, LongWritable>{
//每组数据都是xx-->xx.txt (1,1,1...)
@Override
protected void reduce(Text key, Iterable<LongWritable> values,
Reducer<Text, LongWritable, Text,LongWritable>.Context context) throwsIOException, InterruptedException {
long counter = 0;
for(LongWritable value : values){
counter += value.get();
}
context.write(key, new LongWritable(counter));
}
}
public staticvoidmain(String[] args)throwsException{
Configuration conf = new Configuration();
JobConf jobconf = new JobConf(conf);
FileInputFormat.setInputPaths(jobconf, new Path(args[0]));
Path output = new Path(args[1]);
//检查参数指定的输出路径是否存在,如果存在先删除
FileSystem fs = FileSystem.get(conf);
if(fs.exists(output)){
fs.delete(output, true);
}
FileOutputFormat.setOutputPath(jobconf, output);
Job job = Job.getInstance(jobconf);
job.setJarByClass(InverseIndexStepFirst.class);
job.setMapperClass(StepFirstMap.class);
job.setReducerClass(StepFirstReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
System.exit(job.waitForCompletion(true)?0:1);
}
}
将统计源文件上传到hdfs,在linux主机使用hadoop jar ii.jar com.hadoop.mr.ii.InverseIndexStepFirst /ii/data /ii/stepfirs获得初步统计的结果,以便以这个结果为输入文件进行最终结果的统计运算。
public class InverseIndexStepResult {
public staticclassStepResultMap extends Mapper<LongWritable, Text, Text, Text>{
//map获取的值k:行起始偏移量,v:hello-->a.txt 3
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text,Text>.Context context)
throws IOException, InterruptedException {
String line = value.toString();
String[] fields = StringUtils.split(line, "\t");
String[] wordAndFilename = StringUtils.split(fields[0], "-->");
String word = wordAndFilename[0];
String filename = wordAndFilename[1];
long count = Long.parseLong(fields[1]);
context.write(new Text(word), new Text(filename+"-->"+count));
}
}
public staticclassStepResultReducer extends Reducer<Text, Text, Text, Text>{
@Override
protected void reduce(Text key, Iterable<Text> values, Reducer<Text, Text,Text, Text>.Context context)
throws IOException, InterruptedException {
String result = "";
for(Text value : values){
result += value + " ";
}
context.write(key, new Text(result));
}
}
public staticvoidmain(String[] args)throwsException{
Configuration conf = new Configuration();
JobConf jobconf = new JobConf(conf);
FileInputFormat.setInputPaths(jobconf, new Path(args[0]));
Path output = new Path(args[1]);
//检查参数指定的输出路径是否存在,如果存在先删除
FileSystem fs = FileSystem.get(conf);
if(fs.exists(output)){
fs.delete(output, true);
}
FileOutputFormat.setOutputPath(jobconf, output);
Job job = Job.getInstance(jobconf);
job.setJarByClass(InverseIndexStepResult.class);
job.setMapperClass(StepResultMap.class);
job.setReducerClass(StepResultReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
System.exit(job.waitForCompletion(true)?0:1);
}
}
以第一步的运算结果为输入数据,通过hadoop jar…得出最终的运算结果。
(八)关于一个main方法中提交多个job
实际生成中,如果需要提交多个job,一般是在shell中一句一句的提交,这样方便组合搭配job的执行。实际上,也可以在一个main中提交多个job,这样的方法是缺乏灵活性的,不推荐,例子:
public staticvoidmain(String args[])throws Exception{
Configuration conf = new Configuration();
JobConfjobconf= newJobConf(conf);
FileInputFormat.setInputPaths(jobconf, new Path(args[0]));
Path output = new Path(args[1]);
//检查参数指定的输出路径是否存在,如果存在先删除
FileSystem fs = FileSystem.get(conf);
if(fs.exists(output)){
fs.delete(output, true);
}
FileOutputFormat.setOutputPath(jobconf, output);
Job job_1 = Job.getInstance(jobconf);
Job job_2 = Job.getInstance(jobconf);
job_1.setJarByClass(InverseIndexStepResult.class);
job_1.setMapperClass(StepResultMap.class);
job_1.setReducerClass(StepResultReducer.class);
job_1.setMapOutputKeyClass(Text.class);
job_1.setMapOutputValueClass(Text.class);
job_1.setOutputKeyClass(Text.class);
job_1.setOutputValueClass(Text.class);
job_2.setJarByClass(InverseIndexStepResult.class);
job_2.setMapperClass(StepResultMap.class);
job_2.setReducerClass(StepResultReducer.class);
job_2.setMapOutputKeyClass(Text.class);
job_2.setMapOutputValueClass(Text.class);
job_2.setOutputKeyClass(Text.class);
job_2.setOutputValueClass(Text.class);
boolean result = job_1.waitForCompletion(true);
if(result){
System.exit(job_2.waitForCompletion(true)?0:1);
}
}
十二、 Zookeeper
Zookeeper是google的chubby一个开源的实现,是hadoop的分布式协调服务。它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等。
Zookeeper是一个集群。提供少量数据的存储和管理。客户端向zookeeper中上传数据,zookeeper对集群中各个节点间的数据进行同步,保持数据在节点间的一致性。提供对数据节点的监听器。
一个zookeeper集群包括leader和follower两个部分,leader是主节点,负责所有数据的写操作,follow是从节点。Leader和follow节点是在zookeeper启动过程中由集群自身特定的投票机制产生的。
Zookeeper集群主要应用于分布式应用。大部分应用需要开发私有的协调程序,zookeeper提供分布式锁服务,可以协调分布式应用。
(一)Zookeeper集群的搭建
从zookeeper的官网下载安装压缩文件包。将压缩包解压到/home/usr/App/。然后在解压的目录下的的conf目录下将zoo_sample.cfg改名为zoo.cfg,然后vi修改zoo.cfg,例如:
tickTime=2000
#工作目录,请事先在解压目录下创建data文件夹
dataDir=/usr/mycluster/zookeeper/data
#日志目录
dataLogDir=/usr/mycluster/zookeeper/dataLog
clientPort=2181
initLimit=5
syncLimit=2
server.1=server1:2888:3888
server.2=server2:2888:3888
server.3=server3:2888:3888
注意各个zookeeper节点之间配置ssh免密登录,否则不能正常选举leader和follower等角色,而导致无法形成集群。
然后在每个节点主机中都解压安装文件,并且在解压的目录都创建data目录,然后在每个data的目录下创建文件myid,在myid中填写对应这个主机的域名与配置文件中的server.X=域名:2888:3888的server.后面的id,比如server.1=…,那么就填1。
然后把第一台节点主机上配置好的安装目录复制到其他节点主机上,scp -r zookeeper-3.4.8/ 域名:/home/user/App/。复制完毕后,修改data/myid文件和conf/zoo.cfg中的工作目录。
然后每个节点的安装包目录下的bin目录下,./zkServer.sh start启动zookeeper。
在bin目录下,./zkServer.sh status可以查看zookeeper的状态。如果配置文件和主机域名以及data下面的myid文件都没有问题,集群中的节点会自动分配主节点和从节点。
在bin目录下,./zkCli.sh可以连接到本机节点上。
(二)Zookeeper数据管理
1. 简述
Zookeeper的数据管理客户所存的数据采用的是文件数的结构。每个节点就叫做一个node,每个节点的数据最好小于1mb。
比如通过./zkCli.sh进入本机节点,ls /,查看根节点下的目录;create /server 100,在根节点创建一个叫做server的节点,同时在这个节点保存了一个数据100;再次ls /,可以看到在跟节点创建的节点;get /server,可以得到这个节点的相关信息,以及在这个节点保存的数据;set /server 106,可以修改在这个节点保存的数据。
如果在一个节点机上的节点中保存了数据,那么在集群中的其他节点上可以也可以读取到这些数据。通过./zkCli.sh的命令行可以读取。如果在一个节点上修改了节点中保存的数据,其他节点会在设置的时间内(大概是实时的同步)得到修改的结果。
2. 数据管理的稳定性
Zookeeper具有一定的稳定性,如果集群中的一台节点机宕机断电,比如leader主机宕机断电,那么会有另一台follow变成从节点,而其他没有问题的节点也可以正常工作。如果节点需要正常工作,那么集群中存活的节点至少要大于集群中节点数的一半。
3. 节点类型
Zookeeper中的数据节点,即znode有两种类型,短暂的和持久的。Znode的类型在创建时确定并且之后不能修改。短暂znode类型在客户端会话结束后,会被删除,短暂znode不可以有子节点。持久znode不依赖客户端会话,只有当客户端要删除持久znode时才会被删除。Znode有四种节点目录:PERSISTENT、PERSISTENT_SEQUENTIAL、EPHEMERAL、EPHEMERAL_SEQUENTIAL。
4. Zookeeper的角色
角色有领导者(leader),负责进行投票的发起和决议,更新系统状态。学习者(learner),包括跟随着(follower)和观察者(observer),follower用于接收客户端请求并向客户端返回结果,在选举过程中参与投票。Observer可以接收客户端连接,将写请求转发给leader,但observer不参与投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度。客户端(client),请求发起方。
5. 应用场景
1) 统一命名服务
分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于记忆,通常,树形的名称结构是好的选择。Name Service是zookeeper内置的功能,只要调用api就可以。
2) 配置管理
配置的管理比如同一个分布式应用系统中有多个pc服务器,它们运行中的某些配置文件是相同的,如果修改这些相同的配置项,就需要同时修改每台应用系统的pc服务器,那么,将这些配置信息保存在zookeeper的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台机器就会收到zookeeper的通知,然后从zookeeper中获取新的配置信息到应用系统中。
比如,在./zkCli.sh,中create /节点名 键:值就可以对公用的配置信息进行管理。
3) 集群管理
集群中是需要一个总管的节点server,以便协调集群中每台机器的服务,比如有机器不能提供服务,集群总管必须知道,从而调整重置服务策略。或者,集群增加、减少服务机,也需要总管知道。Zookeeper不仅能维护当前集群中机器的服务状态,还能选出一个总管机,让它来管理集群,这个功能就是leander election。
4) 共享锁
共享所就是跨进程或者跨server机,也可以共享的锁。Zookeeper可以实现这个功能。
十三、 HA
(一)简述
High Available,高可用集群,是保证集群服务连续性的解决方案,一般有两个或两个以上的响应服务的节点,以便应对单个响应节点宕机断电而导致服务中断的隐患。
比如,在hadoop集群中,设置两个namenode节点。Namenode中通过fsimage和edits来存储元数据,为了保持两个nn节点的元数据一致,就能提高效率,将edits的存储交给第三方来管理。这个第三方是分布式应用才有利于保证edits的管理既是高可用的又是安全的。Hadoop提供qjournal框架来创建这个管理edits的分布式系统。Qjournal底层依赖zookeeper实现。Nn通过读取qjournal来获取元数据。两个nn通过zookeeper来感应对方的状态,比如在两个nn都启动zookeeper的状态管理进程zkfc,通过这个进程来管理active和standby机的切换,比如如果antive机异常引发管理进程来改变standby的状态为antive,从而实现active机和standby机的切换。但是active机的暂时性异常后恢复会导致出现两个active机出现,这成为“脑裂”。为了避免出现“脑裂”,当要一台active异常后,而另一台准备要切换为active时,zkfc会发送一条kill指令关闭原来异常的active机,如果关闭后返回值成功,就切换为active,如果超时就执行管理员自定义的shell脚本程序。这种避免brain split的机制成为fencing机制。从hadoop2.x之后,这种一对namenode的分布式成为federation。
(二)搭建HA集群
根据条件允许,ha集群可大可小,至少需要3台主机服务器。一套最简单的ha集群至少需要两个namenode,对应两个namenode的两个zkfc,一个datanode,3个zookeeper,3个qjournal。如果以3台主机为例子,那么可以将某些服务在一个节点上重合部署,如下表:
服务器1 | 服务器2 | 服务器3 |
Namenode1 | Namenode2 | datanode |
Zkfc1 | Zkfc2 |
|
Zookeeper1 | Zookeeper2 | Zookeeper3 |
Qjournal1 | Qjournal2 | Qjournal3 |
如果部署的比较宽裕,比如,以7台服务器主机为例子,如下表:
服务器1 | 服务器2 | 服务器3 | 服务器4 | 服务器5 | 服务器6 | 服务器7 |
Namenode1 | Namenode2 | Resourcemanager1 | Resourcemanager2 | Zookeeper1 | Zookeeper2 | Zookeeper3 |
Zkfc1 | Zkfc2 |
|
| Qjournalnode1 | Qjournalnode2 | Qjournalnode3 |
|
|
|
| Datanode1 Nodemanager1 | Datanode2 Nodemanager2 | Datanode3 Nodemanager3 |
说明:datanode是负责运算的节点,如果将datanode和nodemanager配置在一起,可以使得运算结果尽量从本地输出,以利于提高效率。
上述所有节点的关系主要通过配置文件实现,相关的配置文件如下:
特别注意:在配置文件键和值的内容中不能有不必要的空格,特别是配置主机名和端口号的地方,因为系统对主机名和端口号的命名有严格的要求,在主机名和端口号中不能出现‘.’‘/’‘_’‘’等非法字符。
为了保证管理的方便,最好将所有服务器的hadoop的安装包的路径名完全一致。
1. Core-site.xml
<configuration>
<! - - 由于两台namenode的active和standby身份是启动时选举产生的,所以指定hdfs的nameservice为ns1 - - >
<property>
<name>fs.defaultFS</name>
<value>hdfs://ns1/</value>
</property>
< ! - - 指定hadoop的临时目录 - - >
<property>
<name>hadoop.tmp.dir</name>
<value>/home/user/app/hadoop-2.4.1/tmp</value>
</property>
< ! - - 指定zookeeper的地址,注意端口号根据实际配置 - - >
<property>
<name>ha.zookeeper.quorum</name>
<value>服务器1的域名:2181, 服务器2的域名:2181, 服务器3的域名:2181</value>
</property>
</configuration>
2. Hdfs-site.xml
<configuration>
<! - - 指定hdfs的nameservice为ns1,需要和core-site.xml中的保持一致 - - >
<property>
<name>dfs.nameservices</name>
<value>ns1</value>
</property>
< ! - - 在ns1下面有两个namenode,分别是nn1,nn2 - - >
<property>
<name>dfs.ha.namenodes.ns1</name>
<value>nn1,nn2</value>
</property>
< ! - - nn1的RPC通信地址 - - >
<property>
<name>dfs.namenode.rpc-address.ns1.nn1</name>
<value>namenode1服务器域名:9000</value>
</property>
< ! - - nn1的http通信地址 - - >
<property>
<name>dfs.namenode.http-address.ns1.nn1</name>
<value>namenode1服务器域名:50070</value>
</property>
< ! - - nn2的RPC通信地址 - - >
<property>
<name>dfs.namenode.rpc-address.ns1.nn2</name>
<value>namenode2服务器域名:9000</value>
</property>
< ! - - nn2的http通信地址 - - >
<property>
<name>dfs.namenode.http-address.ns1.nn2</name>
<value>namenode2服务器域名:50070</value>
</property>
< ! - - 指定namenode的元数据在journalnode上的存放位置 - - >
<property>
<name>dfs.namenode.shared.edits.dir</name>
<value>qjournal://journalnade服务器1:8485; journalnade服务器2:8485; journalnade服务器3:8485/ns1</value>
</property>
< ! - - 指定journalnode在本地磁盘存放数据的位置 - - >
<property>
<name>dfs.journalnode.edits.dir</name>
<value>/home/user/App/hadoop-2.4.1/journaldata</value>
</property>
< ! - - 开启namenode失败自动切换 - - >
<property>
<name>dfs.ha.automatic-failover.enabled</name>
<value>true</value>
</property>
< ! - - 配置失败自动切换实现方式 - - >
<property>
<name>dfs.client.failover.proxy.provider.ns1</name>
<value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
</property>
< ! - - 配置隔离机制方法,多个机制用换行分割,即每个机制占用一行 - - >
<property>
<name>dfs.ha.fencing.methods</name>
<value>
sshfence
shell(/bin/true)
</value>
</property>
< ! - - 使用sshfence隔离机制时需要ssh免登录,根据实际填写私钥的路径 - - >
<property>
<name>dfs.ha.fencing.ssh.private-key-files</name>
<value>/home/user/.ssh/id_rsa</value>
</property>
< ! - - 配置sshfence隔离机制超时时间,单位ms - - >
<property>
<name>dfs.ha.fencing.ssh.connect-timeout</name>
<value>30000</value>
</property>
</configuration>
3. mapred-site.xml
<configuration>
< ! - - 指定mapreduce框架为yarn方式 - - >
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configuration>
4. yarn-site.xml
<configuration>
< ! - - 开启RM高可用 - - >
<property>
<name>yarn.resourcemanager.ha.enabled</name>
<value>true</value>
</property>
< ! - - 指定RM的cluster id - ->
<property>
<name>yarn.resourcemanager.cluster-id</name>
<value>yrc</value>
</property>
< ! - - 指定RM的名字 - - >
<property>
<name>yarn.resourcemanager.ha.rm-ids</name>
<value>rm1,rm2</value>
</property>
< ! - - 分别指定RM的地址 - - >
<property>
<name>yarn.resourcemanager.hostname.rm1</name>
<value>resourcemanager服务器1</value>
</property>
<property>
<name>yarn.resourcemanager.hostname.rm2</name>
<value>resourcemanager服务器2</value>
</property>
< ! - - 指定zk集群地址 - - >
<property>
<name>yarn.resourcemanager.zk-address</name>
<value>zookeeper服务器1:2181, zookeeper服务器2:2181, zookeeper服务器3:2181</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
</configuration>
5. slaves
slaves是指定子节点的位置,比如以7台服务器为例子,因为server1要启动HDFS,在server3要启动yarn,所以server1的slaves文件指定的是datanode的位置,作为yarn启动的server3的slaves文件指定的是nodemanager的位置。也就是说,不同的管理机对应的slaves附属机的身份也不同。
6. 配置免密登陆
以7台服务器为例,分析配置免密登陆的机组
服务器1 | 服务器2 | 服务器3 | 服务器4 | 服务器5 | 服务器6 | 服务器7 |
Namenode1 | Namenode2 | Resourcemanager1 | Resourcemanager2 | Zookeeper1 | Zookeeper2 | Zookeeper3 |
Zkfc1 | Zkfc2 |
|
| Qjournalnode1 | Qjournalnode2 | Qjournalnode3 |
|
|
|
| Datanode1 Nodemanager1 | Datanode2 Nodemanager2 | Datanode3 Nodemanager3 |
首先服务器1作为一台namenode,要和datanode通信,也要和另一台namenode通信,就是说服务器1要免密登陆到自己和服务器2,5,6,7。
1) 在服务器1上ssh-keygen -t rsa,生成密钥对,一切默认,不要填密码;
2) ssh-copy-id server1,配置到自己的免密登陆;
3) 分别ssh-copy-id server2,ssh-copy-id server5,ssh-copy-id server6,ssh-copy-id server7配置到服务器2,5,6,7的免密登陆。
然后server3和server4分别是yarn的主机,那么,yarn需要访问namemanager,也就是server5,6,7。Server3自启动使用本地方式而不是ssh方式,不需要设置自己到自己的免密登陆。所以,要设置server3到server5,6,7的免密登陆。
1) 在服务器3上ssh-keygen -t rsa,生成密钥对,一切默认,不要填密码;
2) 分别ssh-copy-id server5,ssh-copy-id server6,ssh-copy-id server7配置到服务器5,6,7的免密登陆。
然后将配置好的hadoop解压包复制到其他节点主机上。Scp -r hadoop-2.6.5 serverx:/home/user/App/。
(三)启动和测试HA集群
1. 启动ha集群
集群的首次启动必须严格按照一定的步骤:
1) 首先启动zookeeper集群
./zkServer.sh start
2) 启动journalnode
在hadoop安装包的sbin目录下,./hadoop-daemon.sh start journalnode。
3) 格式化hdfs
先将一台namenode服务器格式化,由于两台namenode服务器的数据要从始至终都保持一致,所以,不能直接格式化另一台nn服务器的数据,要将格式化完毕的nn服务器的tmp/下面的文件全部拷贝到另一台nn服务器。Scp -r tmp/ server2:/home/server2/App/hadoop-2.4.1/。也可以不用拷贝,在一台已经格式化的节点机开启namenode的情况下,在另一台nn服务器上执行hdfs namenode -bootstrapStandby。
注意:如果迫于无奈,使用了不同版本的hadoop在集群中,那么需要保证所有的namenode节点服务器使用的hadoop版本是一致的,否则在启动namenode时会出现版本号不同而不能正常启动的异常。
4) 格式化zkfc
Zkfc用户处理两个nn服务器的状态切换管理。在第一台nn服务器执行hdfs zkfc -formatZK,就可以。
步骤到这里可以进入zookeeper查看下系统做了那些处理,进入zookeeper的bin,./zkCli.sh,然后ls /。
5) 启动hdfs
在两个nn节点上,任选一个执行start-dfs.sh,注意在slaves中配置好datanode。
6) 启动yarn
由于namenode要处理客户端请求,而yarn要管理mapreduce,这两个节点机的任务都比较繁重,占用资源量大,所以把它们分开在不同的节点机上。在配置的resourcemanager的节点机上执行start-yarn.sh。
如果配置了多台yarn管理机,那么在其他yarn管理机上的sbin目录下,执行yarn-daemon.sh start resourcemanager启动yarn进程。
所有启动完毕后,通过浏览器server1:50070访问第一台nn服务器,可以查看它的状态是active的,通过浏览器server2:50070访问另一台nn服务器,可以看到其状态是standby。通过server3:8088可访问yarn服务器。
2. 测试ha集群
上传文件到/目录,hadoop fs -put xxx.xxx.xxx /。如果有多个datanode,并且设置了副本数,那么在datanode上会存储上相应的副本。
如果kill掉active的namenode,那么由于配置了namenode的zkfc,原来的standby的namenode会自动切换为active。此时如果执行hdfs的任务,比如在kill了namenode的服务器中执行hadoop fs -get /xxx.xxx.xxx,结果下载成功,说明集群依然正常工作。
如果在两个namenode都启动的情况下,将active的namenode服务器断电,(<name>dfs.ha.fencing.ssh.connect-timeout</name><value>30000</value>)
那么在设置的响应时间内,另一台standby的namenode会切换为active。
复位后,如果在上传文件过程中,使用standby的服务器上传一个比较大的文件,然后将active的namenode服务器kill掉namenode进程。结果,文件上传成功。说明集群的ha特性正常。
在bin目录下,./hdfs,可以查看hdfs的命令行说明。./hdfs haadmin,可以用来查看管理ha的功能说明。使用强制手动切换可以实现namenode节点状态的切换,hdfs haadmin -transitionToStandby nn1 --forcemanual。如果有时候启动时,出现所有的namenode都是standby时,如果情况紧急,可以暂且使用手动将一台切换为active。使用hdfs haadmin -getServiceState nn1,可以查看namenode的状态。
关于resourcemanager,一般ha里面是要配置至少两个resourcemanager的服务器的,如果是两个或以上就存在主resourcemanager和非主resourcemanager,主reourcemanager服务器的信息记录在zookeeper中,开启./zkCli.sh,get /yarn-leader-election/yrc/ActiveBreadCrumb,可以查看主resourcemanager是哪个。如果一个主resourcemanager服务器的出现异常停止resourcemanager服务,那么会再选出一个主resourcemanager服务器。但是如果有yarn 任务在进行中,主resourcemanager服务器出现异常不能继续进行yarn任务,那么不会重选出一个resourcemanager继续执行任务直到完成任务,没有resourcemanager异常连贯任务机制。
3. Ha的datanode节点和副本数量管理
通过server1:50070页面可以查看datanode节点的存活情况,如果有datanode节点异常,那么一定的超时范围后,刷新页面,就会显示新的datanode的状态。
1) 关于datanode的超时时间
datanode进程死亡或者网络故障造成datanode无法与namenode通信,namenode不会立即把该节点判定为死亡,要经过一段时间,这段时间暂称作超时时长。HDFS默认的超时时长为10分钟+30秒。如果定义超时时间为timeout,则超时时长的计算公式为:
timeout = 2 * heartbeat.recheck.interval + 10 * dfs.heartbeat.interval。
而默认的heartbeat.recheck.interval 大小为5分钟,dfs.heartbeat.interval默认为3秒。需要注意的是hdfs-site.xml 配置文件中的heartbeat.recheck.interval的单位为毫秒,dfs.heartbeat.interval的单位为秒。
在hdfs-site.xml中配置datanode的超时时间
<property>
<name>heartbeat.recheck.interval</name>
<value>2000</value>
</property>
<property>
<name>dfs.heartbeat.interval</name>
<value>1</value>
</property>
2) 关于HDFS冗余数据块的自动删除
在日常维护hadoop集群的过程中发现这样一种情况:
某个节点由于网络故障或者DataNode进程死亡,被NameNode判定为死亡,HDFS马上自动开始数据块的容错拷贝;当该节点重新添加到集群中时,由于该节点上的数据其实并没有损坏,所以造成了HDFS上某些block的备份数超过了设定的备份数。这些多余的数据块经过很长的一段时间才会被完全删除掉,该时间的长短跟数据块报告的间隔时间有关。Datanode会定期将当前该结点上所有的BLOCK信息报告给Namenode,参数dfs.blockreport.intervalMsec就是控制这个报告间隔的参数。
hdfs-site.xml文件中参数:
<property>
<name>dfs.blockreport.intervalMsec</name>
<value>10000</value>
<description>Determines block reporting interval in milliseconds.</description>
</property>
其中3600000为默认设置,3600000毫秒,即1个小时,块报告的时间间隔为1个小时,所以经过了很长时间这些多余的块才被删除掉。当把该参数调整的稍小一点的时候(60秒),多余的数据块很快就被删除了。
Datanode相关配置的修改在namenode节点服务器中修改就可以。
如果datanode扩容了,就是说新增了一个datanode节点服务器,那么删除掉拷贝的hadoop解压包下的tmp目录,启动这个datanode节点后,它就加入到集群了,那么这个新加入的datanode上会自动添加所有存储的副本。如果此时副本数超过了设置数,那么等块报告后,多余的副本数据块会被删除。
3) HA集群下的java程序的hdfs客户端
由于ha集群下,有多个namenode节点,所以在在访问namenode的编写上有所不同。而且需要将配置文件放到src目录下,才能保证通过url找到namenode节点。
测试代码:
public class HdfsUtilHA {
public staticvoidmain(String[] args)throwsException{
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://ns1/"),conf,"user");
fs.copyFromLocalFile(new Path("c:/xxx"),newPath("hdfs://ns1/"));
}
}
十四、 HIVE
Hive是基于hadoop的一个数据仓库工具,可将结构化的数据文件映射为一张数据库表,并提供简单的sql查询功能,可以将sql语句转换为mapreduce任务进行运行。可以通过类似sql语句快速实现简单的mapreduce统计,适合数据仓库的统计分析。简单的认为,就是hive将sql语句翻译给mapreduce。Hive的数据查询语句,称为hql。和hive类似的工具是impala,spark shark,spark sql,相比而言,hive是这几种中效率比较高的。
Hive的结构,包括两个部分,driver和metastore(元数据)。Driver,主要负责compiles(编译),optimizes(优化),executes(执行)。Hive访问通过cli(comand line),hwi(hie web interface浏览器),thrift server(thrift 客户端,如jdbc,odbc)三种方式实现。Metastore存储在数据库中。使用hive需要借助外部数据库,一般集成mysql。Hive也自带了一个内嵌式的小型数据库derby。
(一)安装hive
直接从官网下载hive的压缩包,解压到/home/user/App/下。在conf下,mv hive-default.xml.template hive-site.xml。然后vi hive-site.xml,修改所有的system.io.tmpdir为自己指定的目录。
启动hive前,需要先初始化schema,在hive的解压目录下,cd /scripts/metastore/upgrade/derby,修改错误脚本hive-schema-2.1.0.derby.sql,注释掉下面两句:
CREATE FUNCTION"APP"."NUCLEUS_ASCII" (C CHAR(1)) RETURNS INTEGER LANGUAGEJAVA PARAMETER STYLE JAVA READS SQL DATA CALLED ON NULL INPUT EXTERNAL NAME
CREATE FUNCTION"APP"."NUCLEUS_MATCHES" (TEXT VARCHAR(8000),PATTERNVARCHAR(8000)) RETURNS INTEGER LANGUAGE JAVA PARAMETER STYLE JAVA READS SQLDATA CALLED ON NULL INPUT EXTERNAL NAME
然后在bin目录下,初始化schema,执行./ schematool -initSchema -dbType derbycreateDatabaseIfNotExist=true。如果是mysql数据库,就执行./ schematool -initSchema -dbType mysqlcreateDatabaseIfNotExist=true。
Hive是基于hadoop的,然后,开启hadoop集群,就可以开启hive了,bin目录下,./hive。
(二)Hive基本语句
Hive语句类似sql语句。
创建数据库 | Create database xxxx; |
使用数据库 | Use xxx; |
创建表 | Create table xxx(id int,name string,age string); |
显示表 | Show tables; |
查询表 | Select * from xxx; |
(三)装配mysql
在已经安装了mysql(非系统自带的mysql)的前提下。在conf目录下,mv hive-default.xml.template hive-site.xml。然后vi hive-site.xml。
配置:
<property>
<name>javax.jdo.option.ConnectionURL</name>
<value>jdbc:mysql://ip地址:3306/hive?createDatabaseIfNotExist=true</value>
</property>
<property>
<name>javax.jdo.option.ConnectionDriverName</name>
<value>com.mysql.jdbc.Driver</value>
</property>
<property>
<name>javax.jdo.option.ConnectionUserName</name>
<value>root</value>
</property>
<property>
<name>javax.jdo.option.ConnectionPassword</name>
<value>root</value>
</property>
然后,如果没有mysql的驱动,请下载mysql的驱动,并复制到hive解压包的lib目录下。然后./hive启动hive。
(四)Hive使用详解
1. 建表
Create table test1(id int,uid begin,url string,ref_url string,ip string)
Row format delimited
Fields terminated by ‘\t’
Stored as sequencefile;
Row format …表示根据源文件当中的什么格式来分隔表的字段;stored as表示存储的文件的格式,一般是sequencefile(表示二进制文件,以键值对组织)或者textfile(文本文件)。
这种建立的表是managed_table内部表。
不管是建立的库还是表,都是放在hdfs上的。
2. 导入数据到表
在准备好数据文本的前提下,load data local inpath '/home/haha/test/phone.data' into table t1;
执行select count(*) from t1;那么就会执行mapreduce来计算count(*)。
对于客户自行上传到hive在hdfs下的表中数据,比如:hadoop fs -put new.data /user/hive/warehouse/t1。然后执行select * from t1;可以查询到自定义上传到hdfs的文本数据。对于客户上传到hive的hdfs的数据,如果与这些数据所属的表的字段和格式不一致,那么,在select * from t1;时,hive会做一些删多和补不足为null等的处理。
3. 创建external table
也可以从hdfs上导入数据到表,这相当于将hdfs上的数据做剪切操作,即将文本数据剪切到表目录下。比如:load data inpath ‘/xxx.xxx’ into table t1;这样可能会影响业务系统,因为这改变了hdfs上文件的存储位置。Hive提供了一种external table可以指定表在hdfs的位置,比如:
在hdfs建立一个用来存放hive的表的目录。将表的源数据文件上传到这个目录/external/hive。
Create external table t2(id int,name string,ip string,address string)
Row format delimited fields terminated by ‘,’
Stored as textfile
Location ‘/external/hive’;
那么,这个表中的数据就是/external/hive目录下的源数据文件中的数据。
4. hql常用语句
Hive的sql语法,一般称为hql。关于hql的用法,hive的官网也有介绍。
1) 关于drop
如果drop的是managed_table,那么在hdfs上的表的数据也都会删除掉。如果drop的是external_table,那么只是删除元数据,hdfs上的表的数据仍保留。
2) 根据另一个表创建表
Create table t3
As
Select id,name,price from t1;
常用于中间数据的存储。
3) 批量insert
Insert overwrite table t4
Select * from t1;
用于向临时表中追加数据。
4) Cluster分桶
不同的桶对应不同的文件。比如:
Create table t_cluster(id int,name string,ip string)
Clustered by(id) into 3 buckets;
用于做数据清理。
5) PARTITION创建表时分区
Create table t_partition(id int,name string,argument string,price double)
Partitioned by(month string)
Row format delimited fields terminated by ‘\t’;
Partitioned by后面的字段可以是表中存在的字段,也可是表中没有的字段。
导入数据到表中的时候,可以指定分区的标识。比如:load data local inpath ‘/home/user/test/xxx.xxx’ into table t_partition partition(month=’20170101’);在hdfs的存储中,实际上是在表下建立一个文件夹叫做month=20170101,在这个文件夹下是表的数据。
load data local inpath ‘/home/user/test/xxx.xxx’ into table t_partition partition(month=’20170201’);
如果一个表有多个分区,需要对特定分区操作使用where,比如select * from t_partition where month=20170201;select count(*) from t_partition where month=20170101;
建表之后使用alter增加分区,比如alter table t3 add partition (partCol=’dt’) location ‘external/hive/dt’;
Show partitions t_partition;查看表分区的情况。
6) 直接将select的文件写到hdfs或者本地
Insert overwrite local directory ‘/home/user/test/xxx.xxx’ select * from t1;
Insert overwrite directory ‘/hive.txt’ select * from t1;
7) Array类型
Hive支持的表的数据类型还有集合或者数组类型,比如:
数组类型的字段
create table t_array(a array<int>,b array<string>)
row format delimited fields terminated ‘\t’
collection items terminated by ‘,’;
那么数据源文本的格式如:
Haha hha,hh2,hll 123,1234,353
Haha2 hha,hh2,hll 123,1234,353
Haha3 hha,hh2,hll 123,1234,353
Haha4 hha,hh2,hll 123,1234,353
上述用,隔开的内容对应的就是表中的一个是数组类型的字段。
Map类型的字段
Create table t_map(name string,info map<string,string>)
row format delimited fields terminated ‘\t’
collection items terminated by ‘;’
map keys terminated by ‘:’;
那么对应的数据源文本的格式如:
Hah age:15;sex:1;addr:uu
Hll age:56;sex:2;addr:wer
Struct类型
结构类型,比如:
Create table t_struct(name string,info struct<age:int,tel:string,addr:string>)
row format delimited fields terminated ‘\t’
collection items terminated by ‘;’;
(五)在shell使用hql
前提是设置了HIVE_HOME。
Hive -S -e ‘select * from default.t1’;在shell执行select语句,注意表名前跟上数据库的名字。
根据shell可以使用hql的特性,可以编写shell脚本,一次性执行成批的hql语句,简化操作。
(六)用户自定义函数udf
Hive自带一些函数,比如max(),min()等。
自定义函数的方法是写一个java类,定义一个函数逻辑,打成jar包,上传到hive的lib中。在hive中创一个函数,跟jar包中的自定义的java类建立关联。
编写Jave自定义函数:
public class AreaOfPhone extends UDF{
private staticHashMap<String,String> aMap = new HashMap();
static{
aMap.put("138", "shanghai");
aMap.put("135", "wuhan");
aMap.put("136","shenzhou");
}
//使用public修饰方法
public String evaluate(String phone){
String result = aMap.get(phone.substring(0,3))==null? (phone+"\tnone"): phone+"\t"+aMap.get(phone.substring(0,3));
return result;
}
}
打成jar包,上传到带有hive的linux,在hive客户端使用add jar /home/user/test/xxx.jar,将jar包放入hive的lib。
然后在hive中创建与jar的关联,create temporary function phonearea as ‘com.hive.function.AreaOfPhone’;
准备好源数据文本,创建表:
create table t_flow(phone string,upflow int,downflow int)
row format delimited fields terminated by ‘\t’;
导入数据,load data local inpath ‘/home/user/test/xx.xx’ into table t_flow;
使用函数,select phonearea(phone),upflow,downflow from t_flow;
十五、 HBASE
Hbase是建立在hadoop文件系统上的分布式面向列的数据库。Hbase是一个数据模型,类似于大表设计,可以提供快速随机访问海量结构化数据。相比hdfs,hdfs是一个分布式文件系统。一般用于随机、实时的读写访问海量数据。比如实时的在线业务。Hbase也是一种nosql,不是关系型的,不支持事务。大表是它的一个优势,表可以达到几十亿行*几百万列。大表使得hbase可以牺牲空间,赢取时间。
(一)Hbase的表结构和存储机制
Hbase建表时不指定表的字段,不指定有哪些列,而是指定有哪些列族。比如:
列族base 列族extern
001 | Name:haha | age:22 | sex:1 | Address:beijing |
002 | Name:hiii | age:24 | grade:3 |
|
003 | Name:sss …… Name:ss2 …… Name:ss3 …… | …… |
列族中可以存储任意多个列。列以kv形式存储,列名和列值。Hbase改变键的值时不删除原来的值,而是设置版本号(一般是时间戳)来区分。
所以在hbase中要查询一个具体的字段,需要指定表名》行键》列族(column family)》列名(qualifier)》版本。
Hbase是一个大表,它的存储方式是分布式的。一个大表会分割成若干个region,通过region server程序存储在集群的节点机的hdfs上。Region server一般和datanode在一个节点机上,以便存取数据的就近化。Hmaster负责管理region server节点,负责管理region server的状态和负载均衡。hmaster上不存储表的数据。
Hbase中大表中分割的region的信息会记录到一个meta系统表中,记录行的坐标(region的起始行范围,节点主机等)。Meta表也会被分成若干个region,分布式存储在集群的节点上。然后meta的region也会记录到一个root系统表中,存储在某台region server节点机上。Zookeeper的zkCli.sh可以查询到这个root表所存储的节点机的信息。Hbase采用三级寻址机制。
(二)Hbase的安装和集群的搭建
从官网下载hbase的压缩包,注意和hadoop版本的对应。Hmaster节点可以和namenode节点错开,以降低单点内存。将压缩包解压到/home/user/App/。
在conf目录下修改配置文件:
修改 hbase-env.sh
export JAVA_HOME= /usr/local/jdk1.8.0_60
export HBASE_MANAGES_ZK=false
修改 hbase-site.xml
<property>
<name>hbase.rootdir</name>
<value>hdfs://ns1/hbase</value>
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<property>
<name>hbase.zookeeper.quorum</name>
<value>server1,server2,server3</value>
</property>
修改regionservers,regionservers配置的是region server的节点机的域名。
server1
server2
server3
为了使hbase能够读取hdfs://ns1/…,需要将hadoop的core-site.xml和hdfs-site.xml文件拷贝到conf目录下。
cp /home/user/App/hadoop-2.6.5/etc/hadoop/{core-site.xml,hdfs-site.xml} ./
然后将配置好的hbase的解压包分发到其他节点机。Scp -r hbase server1:/home/user/App/
启动各个节点的hbase。Bin目录下,./start-hbase.sh。在哪里启动了./start-hbase.sh,那个节点就是hmaster。如果配置了多个hmaster节点,则可以在相应的节点的bin目录下,执行./hbase-daemon.sh start master。
在浏览器server1:16010/mater-status访问hbase数据库。关于访问的端口,不同的hbase版本可能不同,请使用netstat -nltp,查看hmaster对应的pid所对应的访问端口。
如果配置了多个master,若其中一台hmaster异常,则其他正常的备用hmaster依然可以使用。
同步各个节点的的时间一致,否则hbase命令无法执行。
(三)Hbase的使用
在bin目录下,./hbase shell,可以进入hbase的cml客户端。实时业务使用的java的api。
1. 创建表
Create ‘table1’,{NAME => ‘base_info’,VERSION => 3},{NAME => ‘extra_info’,VERSION => 5}
create 't1', {NAME=> 'f1', VERSIONS => 1}
2. 查看表
List,列出表。
Describe ‘xxx’,查看表结构。
3. 删除表
先使表无效,disable ‘xxx’,再删除表,Drop ‘xxx’。
4. 插入数据
Put ‘xxxx’,’0001’,’base_info:name’,’haha’
一次只能插入一个kv。
5. 查询
Get ‘xxx’,’xx’
Get某个表的某一行。
带参数查询:get ‘xxx’,’0001’,{COLUMN=> ‘base_info:name’,VERSION => 2}查询某列的若干个版本。
6. 修改
Put ‘xxx’,’0001’,’base_info:name’,’newname’
7. 全表扫描
Scan ‘xxx’
Scan ‘xxx’,{RAW => true,VERSION => 3}带参数扫描。
(四)Hbase的javaAPI
创建表
public class HbaseUtil {
public staticvoidmain(String[] args)throwsException{
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum","server1,server2");
HBaseAdmin admin = new HBaseAdmin(conf);
TableName name = TableName.valueOf("table1");
HTableDescriptor dec = new HTableDescriptor(name);
HColumnDescriptor base_info = new HColumnDescriptor("base_info");
base_info.setMaxVersions(6);
HColumnDescriptor ext_info = new HColumnDescriptor("ext_info");
dec.addFamily(base_info);
dec.addFamily(ext_info);
admin.createTable(dec);
admin.close();
}
}
插入数据
@Test
public voidinsertData()throwsException{
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum","server1:2181,server2:2181");
HTable table1 = new HTable(conf, "table1");
Put name = new Put(Bytes.toBytes("cl001"));
name.add(Bytes.toBytes("base_info"),Bytes.toBytes("name"),Bytes.toBytes("haha"));
Put age = new Put(Bytes.toBytes("cl001"));
age.add(Bytes.toBytes("base_info"),Bytes.toBytes("age"),Bytes.toBytes(20));
ArrayList<Put> puts = new ArrayList();
puts.add(name);
puts.add(age);
table1.put(puts);
}
删除表
@Test
public voiddropData()throwsException{
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum","server1:2181,server2:2181");
HBaseAdmin admin = new HBaseAdmin(conf);
admin.disableTable("ta1");
admin.deleteTable("ta1");
admin.close();
}
查询数据
@Test
public voidgetDate()throwsException{
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum","server1:2181,server2:2181");
HTable table1 = new HTable(conf, "table1");
Get get = new Get(Bytes.toBytes("cl001"));
get.setMaxVersions(3);
Result result = table1.get(get);
List<Cell> cells = result.listCells();
for(KeyValue kv : result.list()){
String family = new String(kv.getFamily());
System.out.println(family);
String qulifier = new String(kv.getQualifier());
System.out.println(qulifier);
System.out.println(new String(kv.getValue()));
}
table1.close();
}
扫描数据
@Test
public voidscanData()throwsException{
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum","server1:2181,server2:2181");
HTable table1 = new HTable(conf, "table1");
//指定扫描的行间范围
Scan scan = new Scan(Bytes.toBytes("person_rk_cl001"),Bytes.toBytes("person_rk_cl200"));
//设置过滤条件
Filter filter = new PrefixFilter(Bytes.toBytes("person"));
scan.setFilter(filter);
scan.addFamily(Bytes.toBytes("base_info"));
ResultScanner scanner = table1.getScanner(scan);
for(Result r : scanner){
byte[] value = r.getValue(Bytes.toBytes("base_info"),Bytes.toBytes("name"));
System.out.println(new String(value));
}
table1.close();
}
十六、 Storm
Storm是一个分布式、容错的实时计算系统。主要应用于流式计算中,比如没有边界的数据流,实时的数据流,实时分析,机器学习,分布式rpc,实时计算。可以达到节点秒百万级的消息处理。通常结合消息队列和数据库使用。Storm涉及到一些基本概念。Topoligies,拓扑,一个集群。Spouts,拓扑的消息源。Bolts,拓扑的处理逻辑单元。Tuple,消息元组。Streams,流。Stream groupings,流的分组策略。Tasks,任务处理单元。Executor,工作流程。Workers,工作进程。一个supervisor节点机可以运行多个executor,Executor中运行多个workers,workers中可以运行多个tasks。多个supervisor节点由nimbus管理协调。
(一)搭建storm集群
Storm集群建立在zookeeper集群基础上。从官网下载storm的安装包。上传到linux节点机,解压到合适的位置。修改配置文件storm.yaml。比如:
#注意:storm.zoo…和nimbus.host顶格
storm.zookeeper.servers:
-“server1”
-“server2”
-“server3”
#注意:”server1”前面需要空格
nimbus.host: ”server1”
#配置单点supervisor的slots的数量
supervisor.slots.ports
-6701
-6702
-6703
-6704
-6705
启动storm
在nimbus机上,
以前台的方式启动nimbus,./storm nimbus
启动对外的web访问服务,./storm ui
可以通过server1:8080,访问nimbus节点。
在supervisor机上,
./storm supervisor
(二)Storm的使用
需要处理一批文本的数据,然后存入文件。
首先,编写spout组件
public class GetWordsSpout extends BaseRichSpout{
private SpoutOutputCollector collector;
//模拟一些数据,在实际生产中,可以从本地文件读取,也可以是web客户端发送过来的数据,也可以从kafka消息队列取到数据
String[] words = {"haha","heihie","hehe","kkk","lwer","eresdf"};
@Override
//spout组件的核心逻辑,不断向tuple发送数据
public voidnextTuple() {
Random random = new Random();
int index = random.nextInt(words.length);
String name = words[index];
collector.emit(newValues(name));
Utils.sleep(500L);
}
//spout的初始化方法
@Override
public voidopen(Map map,TopologyContext context,SpoutOutputCollector collector) {
this.collector = collector;
}
//声明spout组件发送的tuple中的数据的字段名
@Override
public voiddeclareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(newFields("orname"));
}
}
然后,编写第一步处理数据的bolt组件
public class ChangeBolt extends BaseBasicBolt{
@Override
public voidexecute(Tuple tuple,BasicOutputCollector collector) {
//获取上一个组件传递来的数据,即tuple中的数据
String getName = tuple.getString(0);
//处理数据
String ugetName = getName.toUpperCase();
//将处理后的数据发送出去
collector.emit(newValues(ugetName));
}
@Override
public voiddeclareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(newFields("uName"));
}
}
然后,编写第二步处理数据的bolt组件
public class PostBolt extends BaseBasicBolt{
FileWriter fileWriter = null;
//prepare方法只调用一次,可以用来初始化一个对象
@Override
public voidprepare(Map stormConf,TopologyContext context){
try{
fileWriter = new FileWriter("/home/user/stormoutput"+UUID.randomUUID());
}catch(Exception e){
throw new RuntimeException(e);
}
}
@Override
public voidexecute(Tuple tuple,BasicOutputCollector collector) {
String uName = tuple.getString(0);
String pName = uName + "_aaaa";
try{
fileWriter.write(pName);
fileWriter.write("\n");
fileWriter.flush();
}catch(Exception e){
throw new RuntimeException(e);
}
}
//如果不需要继续处理,则不必在乡下发送tuple
@Override
public voiddeclareOutputFields(OutputFieldsDeclarer arg0) {
}
}
最后,编写串联组件的类,形成完整的处理流程,将topology提交到集群,注意一旦topology提交到集群后将无休止的运行,除非人为退出或异常。
/**
* 组织各个组件形成完整的处理流程
* 将给topology提交给storm集群去运行
* @author Administrator
*/
public class TopoMain{
public staticvoidmain(String[] args)throwsException{
TopologyBuilder builder = new TopologyBuilder();
//将spout组件设置到topology中,设置并发度为4
builder.setSpout("randomspout", newGetWordsSpout(),4);
//将第一步处理数据的组件设置到topo,并且指定它接收randomspout组件的消息
builder.setBolt("changebolt", newChangeBolt(),4).shuffleGrouping("randomspout");
//将第二部处理数据的组件设置到topo,并且指定它接收changebolt组件的消息
builder.setBolt("postbolt", newPostBolt(),4).shuffleGrouping("changebolt");
//用builder创建一个topology
StormTopology topology = builder.createTopology();
//配置一些topology在集群中运行时的参数
Config conf = new Config();
conf.setNumWorkers(4);
conf.setDebug(true);
conf.setNumAckers(0);
//将这个topology提交给storm集群运行
StormSubmitter.submitTopology("demotopo",conf,topology);
}
}
将工程打成jar包,提交到storm集群。在所有节点都创建storm输出文件的目录。执行jar包,storm jar /home/user/test/xxx.jar 提交topology类的全路径。到输出文件的目录下查看输出结果,如果没有停止storm,那么结果是不断更新的。
Storm集群配置了多个worker,如果其中一个worker停止工作,不影响其他worker正常工作,而且当停止的worker再次启动后,可以自动负载均衡。
停止storm任务的命令,storm kill xxx。xxx为topology的id,即提交的jar包中指定的topology的名称。
(三)Storm和mapreduce的对比
Topology | Mapreduce |
一个mapreduce job最终会结束,而一个topology永远会运行,除非被kill。 | |
Nimbus | resourcemanager |
在storm的集群中有两种节点,控制节点(master node)和工作节点(worker node)。控制节点上运行一个nimbus后台程序,它的作用类似hadoop里面的resourcemanager。Nimbus负责在集群里面分发代码,分配计算任务给机器,并监控状态。 | |
Supervisor(worker) | Nodemanager(yarnchild) |
每个工作节点上运行一个supervisor节点。Supervisor会监听分配给它那台机器的工作,根据需要启动和关闭工作进程。每个工作进程执行一个topology的一个子集,一个运行的topology由运行在很多机器上的很多工作进程组成。 |
十七、 Kafka
Kafka是一个高吞吐量的订阅发布消息的分布式系统。Kafka处理速度快,便于容量扩展的,数据可持久化磁盘。Kafka集群中的服务器都叫做broker。Kafka有两类客户端,一类叫做producer(消息生产者),一类叫做consumer(消息消费者),客户端和broker服务器之间采用fcp协议。Kafka中的消息可以通过topic进行区分,每一个topic都会分区,以分担消费读写的负载。每一个分区都可以有多个副本,以防止数据的丢失。每个分区中的数据如果需要更新,都必须通过该分区所有副本中的leader进行更新。消费者可以分组,比如有两个消费者组A和B,共同消费一个topic:order_info,AheB所消费的消息不会重复。比如order_info中有100个消息,每个消息中一个id,编号从0-99,如果A组消费0-49,那么B组消费50-99号。消费者在具体消费某个topic中的消息时,可以指定起始偏移量。
(一)Kafka的安装和使用
从官网下载kafka的压缩包,解压到/home/user/app/。
Kafka自带了zookeeper,配置文件在config下的zookeeper.properties里面。根据官方文档的说明使用在解压目录下,bin/zookeeper-server-start.sh config/zookeeper.properties可以启动自带的zookeeper。
然后使用bin/kafka-server-start.sh config/server.properties启动kafka的服务。
然后创建一个主题,以供生产者和消费者写消息。创建主题,bin/kafka-topics.sh--create --zookeeper localhost:2181 --replication-factor 1 --partitions 1--topic
test
查看主题列表,bin/kafka-topics.sh--list --zookeeper localhost:2181
开启生产者,bin/kafka-console-producer.sh--broker-list localhost:9092 --topic
test
,开启生产者后就可以向主题发送消息了。
开启消费者,bin/kafka-console-consumer.sh --bootstrap-serverlocalhost:9092 --topic test
--from-beginning
,然后就读出消息了。然后生产者继续发送消息,则消费者又获得消息。
(二)
Kafka
集群的部署和使用
修改kafka的配置文件,server.properties,比如:
broker.id=1
zookeeper.connect=server1:2181,server2:2181,server3:2181
注意id的唯一性。
将kafka分发到其他节点机。分发好后修改server.properties里的broker.id。
然后启动各个节点的zookeeper。
然后启动各节点的kafka服务,
bin/kafka-server-start.sh config/server.properties
(bin/kafka-server-start.sh config/server-1.properties &,如果后面加&则是在后台启动运行)。如果是bin/kafka-server-start.shconfig/server.properties 1>/dev/null 2>&1 &(1>/dev/null,将标准输出重定向到一个空文件中,2>&1,将错误输出重定向到和标准输出一样的文件,&表示在后台运行)
创建主题,bin/kafka-topics.sh --create --zookeeper server1:2181--replication-factor 3 --partitions 1 --topic haha
查看主题,bin/kafka-topics.sh --describe --zookeeper server1:2181--topic haha
启动生产者,bin/kafka-console-producer.sh --broker-listserver1:9092 --topic haha
启动消费者,bin/kafka-console-consumer.sh --bootstrap-serverserver1:9092 --from-beginning --topic haha
在生产者发送消息,则消费者获得消息。
在kafka集群中会动态选举一个leader节点。
(三)
Kafka
的java客户端编程
1.
生产者
public class ProducerDemo {
public staticvoidmain(String[] args)throwsException{
Properties props = new Properties();
props.put("bootstrap.servers", "server1:9092,server2:9092,server3:9092");
props.put("zk.connect", "server1:2181,server2:2181,server3:2181");
props.put("metadata.broker.list", "server1:9092,server2:9092,server3:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> kafkaProducer = newKafkaProducer<String, String>(props);
//发送消息,读取文件,或者内存数据库,或者socket端口
for(int i = 1; i <= 1000; i++){
//向某个主题发送消息
//System.out.println(1);
Thread.sleep(500);
ProducerRecord<String, String> msg = newProducerRecord<String, String>("haha","di"+i+"gexiaoxi");
kafkaProducer.send(msg);
}
//列出topic的相关信息
List<PartitionInfo> partitions = newArrayList<PartitionInfo>() ;
partitions = kafkaProducer.partitionsFor("haha");
for(PartitionInfo p:partitions)
{
System.out.println(p);
}
System.out.println("send message over.");
kafkaProducer.close(100,TimeUnit.MILLISECONDS);
}
}
2.
消费者
public class Consumerdemo {
private staticfinalString topic= "haha";
private staticfinalInteger threads= 1;
public staticvoidmain(String[] args) {
Properties props = new Properties();
props.put("zookeeper.connect", "server1:2181,server2:2181,server3:2181");
props.put("group.id", "111");
props.put("auto.offset.reset", "smallest");
ConsumerConfig config = new ConsumerConfig(props);
ConsumerConnector consumer = Consumer.createJavaConsumerConnector(config);
Map<String, Integer> topicCountMap = new HashMap<String,Integer>();
topicCountMap.put(topic, threads);
Map<String, List<KafkaStream<byte[],byte[]>>> consumerMap = consumer.createMessageStreams(topicCountMap);
List<KafkaStream<byte[],byte[]>> streams = consumerMap.get(topic);
for(final KafkaStream<byte[],byte[]>kafkaStream: streams){
new Thread(new Runnable(){
@Override
public void run() {
for(MessageAndMetadata<byte[], byte[]> mm : kafkaStream){
String msg = new String(mm.message());
System.out.println(msg);
}
}
}).start();
}
}
}
十八、 Flume
Flume是cloudera提供的一个高可用的,高可靠的,分布式的海量日志采集、聚合和传输的系统,flume支持在日志系统中定制各类数据发送方,用于收集数据;flume提供对于数据进行简单处理,并写到各种数据接收方。
Flume的安装和使用
从官网下载压缩包,解压到/home/user/app下,进入conf目录。创建一个agent配置文件,vi demoagent.conf。编写一个agent,比如使用官网的指导文件(http://flume.apache.org/FlumeUserGuide.html):
# example.conf: A single-node Flumeconfiguration
# Name the components on this agent
a1.sources = r1
a1.sinks = k1
a1.channels = c1
# Describe/configure the source
a1.sources.r1.type = netcat
a1.sources.r1.bind = server1
a1.sources.r1.port = 44444
# Describe the sink
a1.sinks.k1.type = logger
# Use a channel which buffers events inmemory
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# Bind the source and sink to the channel
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
然后就可以启动
flume。bin/flume-ng agent --conf conf --conf-file conf/demoagent.conf --name a1 -Dflume.root.logger=INFO,console
从window的cmd模拟向flume机的4444端口发送信息,进入windows的cmd,telnet server1 44444,可以向flume发送数据。
采集命令,编写配置文件,
# example.conf: A single-node Flume configuration
# Name the components on this agent
a1.sources = r1
a1.sinks = k1
a1.channels = c1
# Describe/configure the source
a1.sources.r1.type = exec
a1.sources.r1.command = tail -F /home/server/test/flumeexec.log
a1.sources.r1.channels = c1
# Describe the sink
a1.sinks.k1.type = logger
# Use a channel which buffers events in memory
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# Bind the source and sink to the channel
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
创建测试文件。然后启动flume,bin/flume-ng agent --conf conf --conf-file conf/execagent.conf--name a1 -Dflume.root.logger=INFO,console。启动后看到测试文件中的数据被采集到flume。在测试文件目录下,下脚本,while true do echo haha >> flumeextc.log done,可以查看动态监听的效果。
十九、 Sqoop
Sqoop是hadoop体系和关系型数据库之间互导数据的工具。实际上,就是将导入导出命令转换成mapreduce程序来实现。将sqoop安装到一台节点机上就可以了。从官网下载sqoop,解压到一个节点。修改配置文件sqoop-env.sh,
Export HAOOP_HOME=/home/user/app/hadoop
export HBASE_HOME=/home/user/app/hbase
export HIVE_HOME=/home/user/app/hive
export ZOOCFGDIR=/home/user/app/zookeeper/conf
使用sqoop包括种情况:
1. 数据库中的数据导入hdfs
Sqoop import
--connect jdbc:mysql://ip地址(或者域名):3306/shujuku(数据库)
--username root --password 1234
--table xxx_xxx
--columns ‘xx, xx, xx, xx’
指定输出路径,指定数据分隔符
--connect jdbc:mysql://ip地址(或域名):3306/shujuku(数据库)
--username root --password 123
#要导入的表
--table xxx
#数据导入hdfs后存放的目录
--target-dir ‘/sqoop/dir’
#导入的数据字段之间的分隔符
--fields-terminated-by ‘\t’
指定map数量 -m
Sqoop import
--connect jdbc:mysql://ip地址(或域名):3306/shujuku(数据库)
--username root --password 123
#要导入的表
--table xxx
#数据导入hdfs后存放的目录
--target-dir ‘/sqoop/dir’
#导入的数据字段之间的分隔符
--fields-terminated-by ‘\t’
#指定做导入处理时的map任务数
-m 2
增加where条件,注意:条件必须用引号引起来
Sqoop import
--connect jdbc:mysql://ip地址(或域名):3306/shujuku(数据库)
--username root --password 123
#要导入的表
--table xxx
--where ‘id>2’
#导入的数据字段之间的分隔符
--fields-terminated-by ‘\t’
#数据导入hdfs后存放的目录
--target-dir ‘/sqoop/dir’
增加query语句(使用\将语句换行)
Sqoop import
--connect jdbc:mysql://ip地址(或域名):3306/shujuku(数据库)
--username root --password 123
--query ‘select * from xxx where id>xx and $conditions’
--split-by xxx.id
--target-dir ‘/xxx/xxx’
注意:如果使用--query这个命令的时候,需要注意的是where后面的参数,AND $CONDITIONS这个参数必须加上而且存在单引号与双引号的区别。如果--query后面使用的是双引号,那么需要在$CONDITIONS前加上\即\$CONDITIONS。如果设置map数量为1个时即-m 1,不用加上--split-by ${tablename.column},否则需要加上。
2. 将hdfs上的文件导出到数据库的表里面
Sqoop export
--connect jdbc:mysql://ip地址(或域名):3306/xxx
--username root --password 1234566
#要导出的数据的目录
--export-dir ‘/xx/xx’
#到导往的目标关系表
--table xxx
--m 1
#导出的文件的字段分隔符
--fields-terminated-by ‘\t’
使用mysql数据库,要将mysql的驱动导入sqoop的lib目录下。
比如将hdfs的数据导入mysql时,为了便于查看可以使用转义字符\,如:
./sqoop-export --connect jdbc:mysql://server:3306/hadoopurl --usernameroot --password xxxx \
--table url \
--export-dir /test/ \
--columns xxl \
--input-fields-terminated-by '\t'