一、搭建完全分布式hdfs集群
上一张我们学习了HDFS伪分布式集群的搭建。实际上完全分布式集群的搭建也大同小异。
我们前一章在同一个node1节点上分别部署配置了namenode、datanode、snn 三个角色,同理,这里讲下完全分布式环境的搭建。
完全分布式环境搭建,将hdfs中的多个角色分布在不同的物理机上,而不是同一台
我们多准备出2台机器,如果在多台机器的情况下,namenode所在的机器就不要和snn以及datanode所在的机器同一台。
所以现在总共的机器有3台,分别命名为node1、node2、node3,其中node1上一章中,我们搭建了namenode在其上面,node2,node3则可以作为从机,其中
- node2需要搭建snn和datanode1
- node3需要搭建一个datanode2 即可。
1、准备工作
还是老样子,按照伪分布式集群的方法,将基础环境搭建好,比如设置好网络、安装jdk、hadopp等。
这里主要说一下,由于node1中已经用ssh进行免密了,我们只需要将node1中ssh生成的公钥分发给node2、node3即可实现node2、node3免密连接node1.
在node1机器上执行下面命令
scp ./id_dsa.pub node3:/root/.ssh/node1.pub
注意上面是将node1中生成的ssh公钥分发给node3机器,且命名为node1.pub(便于区分),这时候可以登录到node3中查看.ssh目录
[root@localhost .ssh]# ll
total 12
-rw-r--r-- 1 root root 182 May 10 20:38 known_hosts
-rw-r--r-- 1 root root 616 May 10 21:07 node1.pub
我们需要将node3中的node1.pub内容,拷贝一份到同一个目录下的authorized_keys文件中,因为ssh命令会读取这个文件的内容进行免登录到对应的node节点上。
同样的操作,给node2节点。
2、修改配置
我们之前在node1中配置了伪分布式集群,完全分布式的配置需要再做一些修改,如果你想要保存伪分布式集群的配置,可以拷贝一份hadoop中etc目录的hadoop目录,并重命名为hadoop-local(这个目录就是备份你之前伪分布式集群的配置)。
cd /opt/bigdata/hadoop-2.6.5/etc
cp -r hadoop/ hadoop-local
[root@node1 etc]# ll
total 8
drwxrwxr-x 2 haizhang haizhang 4096 May 10 21:18 hadoop
drwxr-xr-x 2 root root 4096 May 10 21:15 hadoop-local
其中hdfs启动的时候只会去读取hadoop目录的,并不会读取hadoop-local目录的。
node1中是要配置namenode节点的,所以我们需要修改core-site.xml文件
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://node1:9000</value>
</property>
</configuration>
并且修改hdfs-site.xml文件如下:
<configuration>
<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>/var/bigdata/hadoop/full/dfs/name</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>/var/bigdata/hadoop/full/dfs/data</value>
</property>
<property>
<name>dfs.namenode.secondary.http-address</name>
<value>node2:50090</value>
</property>
<property>
<name>dfs.namenode.checkpoint.dir</name>
<value>/var/bigdata/hadoop/full/dfs/secondary</value>
</property>
</configuration>
其中上面我们将snn的通信节点地址
dfs.namenode.secondary.http-address
改为node2 ,而namenode,datanode,snn的存放数据路径都变成/var/bigdata/hadoop/full/dfs/
开头的。文件的block备份的个数dfs.replication
就可以改成2个。
我们还需要修改slaves文件,指定datanode节点
node2
node3
然后,我们将node1中的/opt目录下的bigdata目录复制分发给node2、node3节点中。
bigdata目录装载着hadoop2.6.5的一些配置和bin脚本信息等。
scp -r ./bigdata/ node3:`pwd`
scp -r ./bigdata/ node2:`pwd`
此时可以看到node2和node3中就有个bigdata目录了。
3、格式化启动
当上面的工作搞定后,需要在node1中进行格式化
[root@node1 full]# hdfs namenode -format
格式化成功后,可以看到/var/bigdata/hadoop/full/dfs
中只有name目录,没有datanode和snn中所需要的目录(和伪分布式的不一样)
[root@node1 dfs]# pwd
/var/bigdata/hadoop/full/dfs
[root@node1 dfs]# ll
total 0
drwxr-xr-x 3 root root 21 May 10 21:39 name
然后我们在node1中启动hdfs
[root@node1 dfs]# start-dfs.sh
Starting namenodes on [node1]
node1: starting namenode, logging to /opt/bigdata/hadoop-2.6.5/logs/hadoop-root-namenode-node1.out
node3: starting datanode, logging to /opt/bigdata/hadoop-2.6.5/logs/hadoop-root-datanode-localhost.localdomain.out
node2: starting datanode, logging to /opt/bigdata/hadoop-2.6.5/logs/hadoop-root-datanode-localhost.localdomain.out
Starting secondary namenodes [node2]
node2: starting secondarynamenode, logging to /opt/bigdata/hadoop-2.6.5/logs/hadoop-root-secondarynamenode-localhost.localdomain.out
根据日志可以发现,node1启动了之后,会启动node3和node2两个datanode,并且启动node2中的snn。我们之前没有对node3和node2进行格式化,但是node1启动的时候会帮助他们完成格式化动作,我们可以进入node2中查看:
[root@node2 dfs]# pwd
/var/bigdata/hadoop/full/dfs
[root@node2 dfs]# ll
total 0
drwx------ 3 root root 40 May 10 21:43 data
drwxr-xr-x 3 root root 40 May 10 21:44 secondary
可以看到/var/bigdata/hadoop/full/dfs
存在data目录和secondary目录,其中node3的如下:
[root@localhost dfs]# pwd
/var/bigdata/hadoop/full/dfs
[root@localhost dfs]# ll
total 0
drwx------ 3 root root 40 May 10 21:43 data
node3中只存在一个datanode对应的存放数据data目录
扩展:如果secondary namenode没启动,namenode启动之后会做合并日志操作码?
namenode第一次启动的时候,会去合并fsImage和editlog日志,然后生成新的fsImage,在这之后,就一直写editlog,不会再做合并了,也就是说namenode启动后只会合并一次日志。所以必须要snn帮助合并日志,因为比较影响io性能。
二、搭建HA模式
1、hdfs集群中的一些问题
我们经常使用主从模式来搭建一个集群,例如上面,使用了一个namenode作为主节点存放文件元数据信息。
这种单个主节点的主从模式会产生几个个问题:
- 单点故障,主节点宕机,导致集群不可用
- 主节点压力大,会导致内存不够等情况
如果hdfs中的namenode部署在一台机器上,很容易受这台机器的内存影响,内存不够的时候,会不能够存储文件元数据而出现错误。
2、hdfs解决方案
单点故障的应对策略
解决方案:
使用高可用的方案,使用多个namenode节点,主备切换
也就是说namenode其中一台为主节点,另一台为备用节点,只有主节点才能提供服务。备节点不会提供服务,只会一直运行等着,当主节点宕机,立刻顶上去运行。对客户端来说就好像没出现过问题一样。
Hadoop2.x只支持HA的一主一备
压力过大,内存受限制
解决方案:
联帮机制: Federation(元数据分片)
多个namenode,管理不同的元数据,互不影响
3、图解HA模式
HA问题:
- 数据同步、数据一致性问题
数据同步问题的其中一种解决方案就是采用阻塞同步模型的通信机制
该模型的好处就是保证数据的强一致性。
在上图中,假设客户端想要在hdfs中创建一个目录a,就会发送创建操作指令给主namenode(NN Active) ,此时阻塞同步模型的做法是,主NN得到了创建指令操作后,会在内存中创建该目录,与此同时,这个指令也会由主NN传给备份NN(NN Stanby),当备份NN 在内存中创建完毕后,会返回告诉主NN它创建成功了,只有当主备NameNode都创建完成,主NameNode才会告诉客户端创建成功了。从而保证了数据的强一致性。
阻塞同步模型的解决办法最大的问题在于,如果主NN 传递这个指令给备份的NN ,突然备份NN宕机了,或者备份NN执行很久等原因,没创建成功。主NN就可能会等很久。这就破坏了主Namenode对外提供给客户端服务的可用性。
我们添加备份NameNode主要的目的就是避免单点故障带来的服务不可用性。阻塞模型显然不适
当然数据同步的另一种解决方案就是采用异步同步模型的通信机制
当主NN收到客户但操作时,异步通知备NN同步数据,然后主NN发完通知后继续处理客户端业务,不会等备NN应答。这样就可以提高整体的服务可用性,但可能会导致数据的不一致情况出现。
要理解上面这种阻塞同步和异步这两种模式所带来的问题,就要了解下分布式环境下数据一致性问题的一个处理原则:CAP原则
CAP原则
分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。实际上,分布式环境下集群中有三个指标Consistency、Availability、Partition tolerance,也就是CAP原则
这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
- Consistency
Consistency 中文叫做"一致性"。意思是,写操作之后的读操作,必须返回该值。举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。
接下来,用户的读操作就会得到 v1。这就叫一致性。
问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。
为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。
这样的话,用户向 G2 发起读操作,也能得到 v1。
- Availability
Availability 中文叫做"可用性",意思是只要收到用户的请求,服务器就必须给出回应。
用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。
- Partition tolerance
Partition tolerance,中文叫做"分区容错"。
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。
CP 常用于Redis集群模式、MongoDb、Hbase中
Consistency 和 Availability 的矛盾
一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性不。
如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。
综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。
在什么场合,可用性高于一致性?
举例来说,发布一张网页到 CDN,多个服务器有这张网页的副本。后来发现一个错误,需要更新网页,这时只能每个服务器都更新一遍。
一般来说,网页的更新不是特别强调一致性。短时期内,一些用户拿到老版本,另一些用户拿到新版本,问题不会特别大。当然,所有人最终都会看到新版本。所以,这个场合就是可用性高于一致性。
HA模式保证最终一致性方案
了解完上面的CAP理论后,前面所讲述到的同步/异步通讯模型各有优缺点,我们可以采用一种折中的方式,也就是保证可用性和最终一致性 的一个解决方案。
我们可以在主NN和备NN加一个相对来说存储可靠且高可用的第三方工具,那么主NN接收到命令只需要往这个可靠的中间插件中存放,备NN再进行读取即可,当然这之间的时间差不要太大。
但是这个位于主备NameNode中间的存储单元,也不能是单点的,要保证可用性。但如果这个中间的存储单元又做成集群,数据之间的同步就会有延迟,也就是说最终还是离不开CAP这个理论。
一个比较好的解决方案就是,中间的存储单元做成集群,主NN传递指令给这中间存储单元集群中的一台机器上,然后中间存储单元集群间做同步,只要有一半以上的存储单元收到这条指令之后,就返回给主NN,告诉它保存成功了,这时候主NN就可以大胆的告诉客户端,目录创建成功。当备NN读取的时候,就会去读取存放最新数据的中间存储单元,这样就保证了最终的数据一致性。
Paxos算法
上面我们提到了,让中间存储单元过半收到指令后,就表示成功存储了。这个过半操作,就保证了消息传递的一致性。下面引入了Paxos算法。
实际上Paxos算法中的一个典型例子就是ZK中的ZAB协议。还是以主备NN的数据同步为例子:
在主备NN中间,添加了JN集群节点(也就是中间进行存储数据,保证主备NN数据一致性的集群)。其中JN集群需要保证可靠性,它的集群模式可以使用主从模式,在进行主从模式的搭建的时候,需要满足以下几点:
- 明确分布式节点数目,主从节点不能是任意多个,必须要将哪些节点设置成主,哪些节点设置成从,主从分明,且节点数要固定写死。
- 节点权重是否明确;
在集群选举主节点的时候,需要按照节点权重进行选举,例如ZK的ZAB协议提到的主从选举一样,按照事务zxid大小投票选举。 在主从集群中,一开始的集群状态是无主状态。那么选举主节点的时候会分成两步走:
1、根据节点权重选出主节点候选者
2、如果选处的主节点候选者有多个,就按照各个节点的随机id(唯一的,可以比较大小的),来判断比较大小,最后选出主节点。
JN集群节点采用主从模式的一个好处就是,主节点只负责增删改操作,从节点负责查询。从节点收到增删改操作的时候,会转发给主节点,主节点操作完毕会同步修改结果到从节点,只要有半数以上的从节点同步了,就表示操作完成。这种模式可以提升处理效率。增加可用性。
4、自动化实现NN主备切换
上面提到的主备NN,在hdfs集群启动的时候需要手工指定哪个节点为主,哪个为从,并且主宕机了也要手工替换。在企业生产环境,必须要求自动化实现这种选举替换操作。
其中JN集群我们上面介绍过了,主要用来存NN的EditsLog数据,让备NN做同步。这里面新引入了HDFS中的一个新进程ZKFailOverController
。它存在三只手 也就是三个作用:
- 第一个作用是:监控NameNode、OS、硬件的健康状态这个进程需要和NN在同一台物理机上 。 原因是因为,如果存放到不同物理机上,需要通过网络才能检测到对应NN的状态,网络就可能产生延迟,导致数据不可靠的问题。如果存放在同台物理机上,就可以避免通过网络传输,获得最新最准确的NN状态。
- 第二个作用是:用来连接ZK集群。当NameNode启动的时候,如何知道哪个NameNode是主,哪个是备呢? 这个任务就要交给ZK集群,那么ZKFC 的另一个作用就是,连接ZK集群,然后同时向ZK集群中的同一个目录下创建同一个节点,相当于图上的2个ZKFC 进行抢锁,互相挣抢创建这个节点,谁创建成功了,就表示谁是主NameNode。当然,如果另一个ZKFC没有抢到这把锁,也就是没有成功创建这个节点,他就会在这个节点上创建自己的监听事件。
那么当主NameNode宕机了,ZKFC就会把之前创建的充当锁的节点删除,那么当删除节点了之后,另一个ZKFC添加的监听事件就会备zk触发,回调ZKFC中的方法,然后立刻创建节点充当锁,并触发备NameNode转换成主NameNode这一个过程。 - 第三个作用是:当监听备份NameNode节点的ZKFC,收到回调监听事件之后,说明主节点可能已经宕机了,但是ZKFC还会偷偷的过去宕机的NameNode主节点处亲自检测下,看看是不是真的宕机了。如果发现主NameNode真的宕机了,才会把备NameNode升为主节点。
另一种情况
如果之前的主NameNode没有宕机,而是监听它的ZKFC挂掉了,此时就会在ZK集群触发节点删除事件,因为ZKFC之加锁的时候创建的节点是临时节点,它的意思是只要ZKFC挂掉了,就会导致创建的临时节点也一同删除,并触发监听回调事件告知监听备NameNode的ZKFC节点。这个时候备ZKFC会去检测主NameNode是否宕机,如果发现它还没宕机,就把正在运行的主NameNode做降级操作,然后再将备NameNode升级成主节点。
降级操作,这样做的目的是,如果主NN所在的物理机的ZKFC挂掉了,说明可能那台物理机有问题,并且主NN缺少了ZKFC的监控,不安全。故此让另一个健全的物理机的NN做主节点最好。
还有一种比较极端的情况
如果zk集群和监听主NN的ZKFC因为网络原因,连接不上了,但是主NN可以和客户端通,可以和DataNode连通,可以和JN连通,唯独不能被zk和备ZKFC连通。那么锁就会被删除,从而触发监听回调事件告诉备ZKFC,备ZKFC去第一步先去ZK集群中创建锁,只有创建好锁,ZKFC才能拥有干活的能力。第二步,去检测主NN的状态,可怕的是,由于主NN所在的机器网络出现问题,检测不到主NN的状态,这个时候备ZKFC也不敢直接执行把主NN降级为备NN这个操作。因为主NN是可以和客户端打通的,并且只能够不断的重试试探。就会出现问题。最后,集群中只会出现一个主NameNode
5、HA总结
注意:
HA模式中不需要有SecondaryNameNode
,因为有NameNode Standby
角色进行滚动Editlog
和FsImage
日志。SNN只有在非HA模式下才使用。
三、HDFS-Federation解决方案
NameNode压力过大,内存受限的问题的解决办法是采用联帮机制:
- 元数据分治,复用DN存储,DN目录隔离block
如下图所示:
分成多个NameNode,每个NameNode存的文件元数据,互不能共享,也就是相互独立。比如图终的NN-k想要访问NN-n的某个元数据,只能通过NN-n节点来获取,而没有办法直接获取,元数据访问具有隔离性。
NameNode只存放文件元数据,而DataNode存放的是文件块数据。上图中的集群里,有几个独立的NameNode,那么他们全部都复用一套存储节点dataNode,也就是说他们存文件都存在同一批DataNode节点上。那么DataNode如何区分存放对应的NameNode的文件块数据呢?
这里就要讲述到DN使用目录进行隔离block块
它的含义是DataNode为不同NameNode建立不同的目录,来存放他们各自传递过来的文件块。比如有个客户端,它的操作如下:
- 客户端传递文件a到namenode1中,此时DataNode把这个a文件的文件块存放到DataNode划分的目录/nn1下。
- 客户端又传递同一份文件a到namenode2中,此时DataNode把这个a文件的文件块存放到DataNode划分的目录/nn2下。
- 客户端又传递同一份文件a到namenode3中,此时DataNode把这个a文件的文件块存放到DataNode划分的目录/nn3下。
依次类推。
这样就可以隔离同一份文件在不同NameNode上传,导致DN文件块存放混乱的情况。
使用多个NN,来存放不同的文件元数据信息,可以缓解单个NN的压力过大,内存受限制的问题。并且不同部门上传的文件数据,可以存到不同的NN中。这样也起到了文件分治管理的作用。
以一个例子讲解联帮机制的好处
如上图,没有使用联帮机制的情况下。假设有一个部门,它把1个1000MB大小的文件FileX,存到NameNodea中,假设这个文件切割成了1000份,也就是相当于500台DataNode,每台存放2个文件块。
另一个部门,它也上传1份1000MB大小的文件Filey,存到NameNodeb中,假设这个文件切割成了1000份,也就是相当于500台DataNode,每台存放2个文件块。
两个部门各自使用自己的DataNode节点,此时就会出现一个问题,并行度不高。最多也只能够使用500台DN的节点进行处理数据,不能充分利用另一个部门的500台DN的机器性能。
如果使用了联帮机制,就会变成下图:
我们将这两个部门的所有DataNode节点进行复用,也就是进行合并,大家一起使用。此时不同部门上传文件在不同NameNode节点保存完后,再统一打散到这1000台集群上,这样一来,并行度就比原来的高出一半。而如果一个部门上传文件时候,恰好另一个部门没在上传,那么,这个部门就能充分利用另一个部门的500台DN机器性能,这也就是DN复用的一个好处。当然DN会为不同NN传递的文件块存放到不同的目录下,以达到分治隔离的作用。
各个部门的数据自己进行访问的时候,直接访问DataNode为他创建存放数据的目录即可。也就是下图所示:
发现NameNode实际上是和这1000台机器的某个目录进行绑定,大家只能够访问自己对应目录下面的datanode数据。
但这又会存在一个问题:
假设有个部门c,他们想要访问部门a、b两个部门的数据,这时候就比较麻烦,部门c的客户端一会要调部门a的Namenode获取文件元信息访问DN,一会又要调部门b的Namenode获取文件元信息访问DN。就会非常影响用户的体验感。这个时候,就出现了一层代理层,也就是虚拟文件系统,统一管理DataNode中不同NameNode节点对应的存放数据目录。
虚拟文件系统会挂载着部门a和部门b对应存放数据的DN目录。比如上图,部门a存放数据的时候是通过NNa中存放到DN上的a目录下的,部门b存放数据的时候是b通过NNb中存放到DN上的b目录下的。此时虚拟文件系统就会存在a,b这两个虚拟目录,然后将这两个虚拟目录指向对应的NN,比如a目录是NNa看管的,所以将虚拟目录a指向NNa。
这时候客户端想要存放个文件,比如放到/a/b.txt,只需要把路径告诉虚拟文件系统,文件系统就可以通过虚拟目录映射到对应的NN,然后对文件分块存到对应的DN的目录下即可。
这样客户端只需要通过访问连接虚拟文件系统,通过这层代理去访问对应的NN,再到DN拿数据就行,这对于客户端来说就好比是个黑盒,把要到哪个目录上取数据或者存数据这个操作丢给虚拟文件系统,至于要去连哪个NameNode节点,访问哪个DataNode,这件事情已经被虚拟文件系统这个代理层包装好了。这也一定程度上对外屏蔽了底层的技术实现复杂度,只需对外提供一个接口给调用方调用即可。