HDFS1.0架构
采用Master/Slave结构,一个HDFS集群中包含了一个单独的NameNode和多个DataNode节点。
Master/Slave也就是主从设备模式,核心思想是基于分而治之的思想,将一个原始任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些任务,原始任务的结果是通过整合各个子任务的处理结果形成的。

NameNode
NameNode负责管理分布式的元数据:
1目录树结构;
2文件到数据库Block的映射关系;
3Block副本及其存储位置等管理数据;
4DataNode的状态监控:
aNameNode和DataNode通过有时间间隔的心跳来传递管理信息和数据信息;
bNameNode通过心跳机制,可以获得每个DataNode上保存的Block块信息,DataNode的健康状况,命令DataNode启动停止等;
c出现某个DataNode节点故障,NameNode会将其负责的block在其他DataNode上备份。
5数据保存在内存中,同时在磁盘中保存了两个元数据管理文件,fsimage和editlog:
afsimage:是内存命名空间元数据在外存的镜像文件;
beditlog:则是各种元数据操作的write-ahead-log文件,在体现到内存数据变化前首先会经操作记入editlog,防止数据丢失;
c二者集合就可以构造完整的内存数据。
Secondary NameNode
Hadoop2.x已经用高可用替代掉Secondary NameNode。
SecondaryNameNode并不是NameNode的热备机,他会定期从NameNode拉取fsimage和editlog文件,将两个文件进行合并,形成新的fsimage文件并传回NameNode,减轻NameNode的工作压力,本质上是一个提供检查点功能服务的服务点。
DataNode
负责数据块实际存储和读写,HDFS1.0的时候block默认大小是68M,HDFS2.0改为128M,上传大文件的时候,HDFS会自动分割成固定大小的Block块,每个块以多备份的形式存储,默认是三份。
HDFS中平均寻址时间大概为10ms;经过前人的大量测试发现,寻址时间为传输时间的1%时,为最佳状态;所以最佳传输时间为10ms/0.01=1000ms=1s;目前磁盘的传输速率普遍为100MB/s;计算出最佳block大小:100MB/s x 1s = 100MB;所以我们设定block大小为128MB,随着新一代磁盘传输速率的提升,块的大小将会被设置的更大。
文件写入过程
1Client 调用 DistributedFileSystem 对象的 create 方法,创建一个文件输出流(FSDataOutputStream)对象;
org.apache.hadoop.hdfs.DistributedFileSystem是Hadoop抽象文件系统的一个具体文件系统,当以hdfs模式通过FileSystem.get()获取具体文件系统时,返回一个DistributedFileSystem对象。对HDFS的用户来说,DistributedFileSystem就代表了Hadoop分布式文件系统,用户只要操作DistributedFileSystem的对象来进行文件目录的建立、数据的存取操作。
2通过 DistributedFileSystem 对象与集群的 NameNode 进行一次 RPC 远程调用,在 HDFS 的 Namespace 中创建一个文件条目(Entry),此时该条目没有任何的 Block,NameNode 会返回该数据每个块需要拷贝的 DataNode 地址信息;
RPC(采用客户端/服务端的模式,通过request-response消息模式实现)的三个过程:
a通讯协议== 比如:你需要找人在国外干活,那么你可以直接飞过去或者打电话或者通过互联网的形式,去找人,这个找人的过程就是通讯协议;
b寻址== 既然要找人干活,肯定要知道地址在哪,飞过去需要找到详细地址,打电话需要知道电话号码,互联网需要知道IP是多少;
c数据序列化== 就是说,语言需要互通,才能够让别人干活,之间需要一个大家都懂的语言去交流。
3通过 FSDataOutputStream 对象,开始向 DataNode 写入数据,数据首先被写入 FSDataOutputStream 对象内部的数据队列中,数据队列由 DataStreamer 使用,它通过选择合适的 DataNode 列表来存储副本,从而要求 NameNode 分配新的 block;
4DataStreamer 将数据包以流式传输的方式传输到分配的第一个 DataNode 中,该数据流将数据包存储到第一个 DataNode 中并将其转发到第二个 DataNode 中,接着第二个 DataNode 节点会将数据包转发到第三个 DataNode 节点;
5DataNode 确认数据传输完成,最后由第一个 DataNode 通知 client 数据写入成功;
6完成向文件写入数据,Client 在文件输出流(FSDataOutputStream)对象上调用 close 方法,完成文件写入;
7调用 DistributedFileSystem 对象的 complete 方法,通知 NameNode 文件写入成功,NameNode 会将相关结果记录到 editlog 中。

/**
* 创建文件并写入数据
*/
public void createHdfs() throws IOException {
// 配置文件
Configuration conf = new Configuration();
// 获得文件系统 distributedFileSystem和fileSystem的区别是一个是分布式文件系统,一个是文件系统
DistributedFileSystem dfs = (DistributedFileSystem) FileSystem.get(conf);
// 调用API
dfs.mkdirs(new Path("/tmp/lsj"));
// 关闭流
dfs.close();
}
文件读取过程
1Client 通过 DistributedFileSystem 对象与集群的 NameNode 进行一次 RPC 远程调用,获取文件 block 位置信息;
2NameNode 返回存储的每个块的 DataNode 列表;
3Client 将连接到列表中最近的 DataNode
4Client 开始从 DataNode 并行读取数据;
5一旦 Client 获得了所有必须的 block,它就会将这些 block 组合起来形成一个文件
6在处理 Client 的读取请求时,HDFS 会利用机架感知选举最接近 Client 位置的副本,这将会减少读取延迟和带宽消耗。

/**
* 读取文件,在控制台输出
*/
public void readHdfs() throws IOException {
// 配置文件
Configuration conf = new Configuration();
// 获得文件系统
FileSystem fileSystem = FileSystem.get(conf);
// 调用API
FSDataInputStream out = fileSystem.open(new Path("/tmp/lsj"));
// 关闭
fileSystem.close();
}
HDFS创建文件
1客户端通过ClientProtocol协议向RpcServer发起创建文件的RPC请求。
ClientPrototol中,与客户端读取文件相关的方法主要有两个,getBlockLocations()和reportBadBlocks(),客户端会调用getBlockLocations()获取HDFS文件指定范围内所有数据块的位置信息,然后客户端会根据这些位置信息从数据节点读取数据块;客户端会调用reportBadBlocks()方法向Namenode汇报错误的数据块。
2FSNamesystem封装了各种HDFS操作的实现细节,RpcServer调用FSNamesystem中的相关方法以创建目录。
RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。
3进一步的,FSDirectory封装了各种目录树操作的实现细节,FSNamesystem调用FSDirectory中的相关方法在目录树中创建目标文件,并通过日志系统备份文件系统的修改。
4最后,RpcServer将RPC响应返回给客户端。
HDFS1.0存在的弊端
●NameNode 的单点问题,如果 NameNode 挂掉了,数据读写都会受到影响,HDFS 整体将变得不可用,这在生产环境中是不可接受的;
●水平扩展问题,随着集群规模的扩大,1.0 时集群规模达到3000时,会导致整个集群管理的文件数目达到上限(因为 NameNode 要管理整个集群 block 元信息、数据目录信息等)。
●在1.x,使用Jobtracker负责任务调度和资源管理,单点负担过重,如果任务只需要map任务可能会造成资源浪费。

HDFS2.0的优化
1HA(High Availability 高可用方案):存在一个Active NameNode 和Standby NameNode,这是为了解决 NameNode 单点问题;
2NameNode Federation(联邦):是用来解决 HDFS 集群的线性扩展能力,使NameNode可以横向扩展成多个,每个NameNode分管一部分目录。
3在2.x中,新增了yarn作为集群的调度工具,在yarn中,使用ResourceManager进行资源管理,,单独开启一个Container作为ApplicationMaster来进行任务管理。

HDFS2.0的HA实现

Active NameNode和Standby NameNode:
两台NameNode形成互备,一台处于Active状态为主NameNode,另外一台处于Standby(待机)状态,为备NameNode,只有主NameNode才能对外提供读写服务。
ZKFailoverController(主备切换控制器,FC):
ZKFailoverController 作为独立的进程运行,对 NameNode 的主备切换进行总体控制。ZKFailoverController 能及时检测到 NameNode 的健康状况,在主 NameNode 故障时借助 Zookeeper 实现自动的主备选举和切换(当然 NameNode 目前也支持不依赖于 Zookeeper 的手动主备切换)。
Zookeeper 集群:
为主备切换控制器提供主备选举支持。
共享存储系统:
共享存储系统是实现 NameNode 的高可用最为关键的部分,共享存储系统保存了 NameNode 在运行过程中所产生的 HDFS 的元数据。主 NameNode 和备 NameNode 通过共享存储系统实现元数据同步。在进行主备切换的时候,新的主NameNode 在确认元数据完全同步之后才能继续对外提供服务。
DataNode 节点:
因为主 NameNode 和备 NameNode 需要共享 HDFS 的数据块和 DataNode 之间的映射关系,为了使故障切换能够快速进行,DataNode 会同时向主 NameNode 和备 NameNode 上报数据块的位置信息。
FailoverController
FC 最初的目的是为了实现 SNN 和 ANN 之间故障自动切换,FC 是独立与 NN 之外的故障切换控制器,ZKFC 作为 NameNode 机器上一个独立的进程启动 ,它启动的时候会创建 HealthMonitor 和 ActiveStandbyElector 这两个主要的内部组件,其中:
1HealthMonitor:主要负责检测 NameNode 的健康状态,如果检测到 NameNode 的状态发生变化,会回调 ZKFailoverController 的相应方法进行自动的主备选举;
2ActiveStandbyElector:主要负责完成自动的主备选举,内部封装了 Zookeeper 的处理逻辑,一旦 Zookeeper 主备选举完成,会回调 ZKFailoverController 的相应方法来进行 NameNode 的主备状态切换。
自动触发主备选举
NameNode 在选举成功后,会在 zk 上创建了一个 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点,而没有选举成功的备 NameNode 会监控这个节点,通过 Watcher 来监听这个节点的状态变化事件,ZKFC 的 ActiveStandbyElector 主要关注这个节点的 NodeDeleted 事件(这部分实现跟 Kafka 中 Controller 的选举一样)。
如果 Active NameNode 对应的 HealthMonitor 检测到 NameNode 的状态异常时, ZKFailoverController 会主动删除当前在 Zookeeper 上建立的临时节点 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock,这样处于 Standby 状态的 NameNode 的 ActiveStandbyElector 注册的监听器就会收到这个节点的 NodeDeleted 事件。收到这个事件之后,会马上再次进入到创建 /hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点的流程,如果创建成功,这个本来处于 Standby 状态的 NameNode 就选举为主 NameNode 并随后开始切换为 Active 状态。
当然,如果是 Active 状态的 NameNode 所在的机器整个宕掉的话,那么根据 Zookeeper 的临时节点特性,/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 节点会自动被删除,从而也会自动进行一次主备切换。
HDFS 脑裂问题
在实际中,NameNode 可能会出现这种情况,NameNode 在垃圾回收(GC)时,可能会在长时间内整个系统无响应,因此,也就无法向 zk 写入心跳信息,这样的话可能会导致临时节点掉线,备 NameNode 会切换到 Active 状态,这种情况,可能会导致整个集群会有同时有两个 NameNode,这就是脑裂问题。
脑裂问题的解决方案是隔离(Fencing),主要是在以下三处采用隔离措施:
1第三方共享存储:任一时刻,只有一个 NN 可以写入;
2DataNode:需要保证只有一个 NN 发出与管理数据副本有关的删除命令;
3Client:需要保证同一时刻只有一个 NN 能够对 Client 的请求发出正确的响应。
关于这个问题目前解决方案的实现如下:
1ActiveStandbyElector 为了实现 fencing,会在成功创建 Zookeeper 节点 hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock 从而成为 Active NameNode 之后,创建另外一个路径为 /hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 的持久节点,这个节点里面保存了这个 Active NameNode 的地址信息;
2Active NameNode 的 ActiveStandbyElector 在正常的状态下关闭 Zookeeper Session 的时候,会一起删除这个持久节点;
3但如果 ActiveStandbyElector 在异常的状态下 Zookeeper Session 关闭 (比如前述的 Zookeeper 假死),那么由于 /hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb 是持久节点,会一直保留下来,后面当另一个 NameNode 选主成功之后,会注意到上一个 Active NameNode 遗留下来的这个节点,从而会回调 ZKFailoverController 的方法对旧的 Active NameNode 进行 fencing。
在进行 fencing 的时候,会执行以下的操作:
1首先尝试调用这个旧 Active NameNode 的 HAServiceProtocol RPC 接口的 transitionToStandby 方法,看能不能把它转换为 Standby 状态;
2如果 transitionToStandby 方法调用失败,那么就执行 Hadoop 配置文件之中预定义的隔离措施。
Hadoop 目前主要提供两种隔离措施,通常会选择第一种:
1sshfence:通过 SSH 登录到目标机器上,执行命令 fuser 将对应的进程杀死;
2shellfence:执行一个用户自定义的 shell 脚本来将对应的进程隔离。
只有在成功地执行完成 fencing 之后,选主成功的 ActiveStandbyElector 才会回调 ZKFailoverController 的 becomeActive 方法将对应的 NameNode 转换为 Active 状态,开始对外提供服务。
NameNode 选举的实现机制与 Kafka 的 Controller 类似,那么 Kafka 是如何避免脑裂问题的呢?
1Controller 给 Broker 发送的请求中,都会携带 controller epoch 信息,如果 broker 发现当前请求的 epoch 小于缓存中的值,那么就证明这是来自旧 Controller 的请求,就会拒绝这个请求,正常情况下是没什么问题的;
2但是异常情况下呢?如果 Broker 先收到异常 Controller 的请求进行处理呢?现在看 Kafka 在这一部分并没有适合的方案;
3正常情况下,Kafka 新的 Controller 选举出来之后,Controller 会向全局所有 broker 发送一个 metadata 请求,这样全局所有 Broker 都可以知道当前最新的 controller epoch,但是并不能保证可以完全避免上面这个问题,还是有出现这个问题的几率的,只不过非常小,而且即使出现了由于 Kafka 的高可靠架构,影响也非常有限,至少从目前看,这个问题并不是严重的问题。
第三方存储(共享存储)
上述 HA 方案还有一个明显缺点,那就是第三方存储节点有可能失效,之前有很多共享存储的实现方案,目前社区已经把由 Clouderea 公司实现的基于 QJM 的方案合并到 HDFS 的 trunk 之中并且作为默认的共享存储实现,本部分只针对基于 QJM 的共享存储方案的内部实现原理进行分析。
QJM(Quorum Journal Manager)本质上是利用 Paxos 协议(这个算法有点难懂,建议不看😓)来实现的,QJM 在 2F+1 个 JournalNode 上存储 NN 的 editlog,每次写入操作都通过 Paxos 保证写入的一致性,它最多可以允许有 F 个 JournalNode 节点同时故障,其实现如下:

Active NameNode 首先把 EditLog 提交到 JournalNode 集群,然后 Standby NameNode 再从 JournalNode 集群定时同步 EditLog。
还有一点需要注意的是,在 2.0 中不再有 SNN 这个角色了,NameNode 在启动后,会先加载 FSImage 文件和共享目录上的 EditLog Segment 文件,之后 NameNode 会启动 EditLogTailer 线程和 StandbyCheckpointer 线程,正式进入 Standby 模式,其中:
1EditLogTailer 线程的作用是定时从 JournalNode 集群上同步 EditLog;
2StandbyCheckpointer 线程的作用其实是为了替代 Hadoop 1.x 版本之中的 Secondary NameNode 的功能,StandbyCheckpointer 线程会在 Standby NameNode 节点上定期进行 Checkpoint,将 Checkpoint 之后的 FSImage 文件上传到 Active NameNode 节点。
HDFS 2.0 Federation 实现
在 1.0 中,HDFS 的架构设计有以下缺点:
1namespace 扩展性差:在单一的 NN 情况下,因为所有 namespace 数据都需要加载到内存,所以物理机内存的大小限制了整个 HDFS 能够容纳文件的最大个数(namespace 指的是 HDFS 中树形目录和文件结构以及文件对应的 block 信息);
2性能可扩展性差:由于所有请求都需要经过 NN,单一 NN 导致所有请求都由一台机器进行处理,很容易达到单台机器的吞吐;
3隔离性差:多租户的情况下,单一 NN 的架构无法在租户间进行隔离,会造成不可避免的相互影响。
而 Federation 的设计就是为了解决这些问题,采用 Federation 的最主要原因是设计实现简单,而且还能解决问题。
Federation 架构
Federation 的架构设计如下图所示:

Federation 的核心设计思想
Federation 的核心思想是将一个大的 namespace 划分多个子 namespace,并且每个 namespace 分别由单独的 NameNode 负责,这些 NameNode 之间互相独立,不会影响,不需要做任何协调工作(其实跟拆集群有一些相似),集群的所有 DataNode 会被多个 NameNode 共享。
其中,每个子 namespace 和 DataNode 之间会由数据块管理层作为中介建立映射关系,数据块管理层由若干数据块池(Pool)构成,每个数据块只会唯一属于某个固定的数据块池,而一个子 namespace 可以对应多个数据块池。每个 DataNode 需要向集群中所有的 NameNode 注册,且周期性地向所有 NameNode 发送心跳和块报告,并执行来自所有 NameNode 的命令。
●一个 block pool 由属于同一个 namespace 的数据块组成,每个 DataNode 可能会存储集群中所有 block pool 的数据块;
●每个 block pool 内部自治,也就是说各自管理各自的 block,不会与其他 block pool 交流,如果一个 NameNode 挂掉了,不会影响其他 NameNode;
●某个 NameNode 上的 namespace 和它对应的 block pool 一起被称为 namespace volume,它是管理的基本单位。当一个 NameNode/namespace 被删除后,其所有 DataNode 上对应的 block pool 也会被删除,当集群升级时,每个 namespace volume 可以作为一个基本单元进行升级。
Paxos 协议
Paxos 算法解决的问题是一个分布式系统如何就某个值(决议)达成一致,是一种基于消息传递模型的一致性算法。
package com.lsj.paxos;
import java.util.Map;
/**
* @author LY
* @time 2022/2/31 20:28
* 接收者
*/
public class Acceptor {
/**
* 接收者的id
*/
int id;
/**
* 提案Map类型,key是提案编号,value是提案值
*/
private Map<Integer, String> proposal;
/**
* 接收到的最大的提案编号N
*/
int resN;
public int getId(){
return id;
}
public void setId(int id){
this.id = id;
}
public Map<Integer, String> getProposal() {
return proposal;
}
public void setProposal(Map<Integer, String> proposal) {
this.proposal = proposal;
}
public int getResN() {
return resN;
}
public void setResN(int resN) {
this.resN = resN;
}
/**
* 提案
* @param proposerN 提案编号
* @return 提案
*/
public Map<Integer, String> prepareReq(int proposerN){
if (proposerN<this.resN){
// 不响应
System.out.println("proposerN:"+proposerN+",this.resN"+resN);
return null;
}else {
this.resN = proposerN;
// 响应pok
return this.proposal;
}
}
/**
* 第二阶段的接受请求
* @param map 提案
* @return 接收请求
*/
public String acceptReq(Map<Integer, String> map){
for (Map.Entry<Integer,String> entry: map.entrySet()){
if (entry.getKey()>=this.resN){
this.setProposal(map);
return "aok";
}
}
return "no";
}
}
package com.lsj.paxos;
import java.util.Map;
/**
* @author LY
* @time 2022/2/31 21:01
* 提议者
*/
public class Proposer {
/**
* 提议者id(唯一标识)
*/
private int id;
/**
* 提案Map类型,key 为提案编号,value提案值
*/
private Map<Integer,String> proposal;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Map<Integer, String> getProposal() {
return proposal;
}
public void setProposal(Map<Integer, String> proposal) {
this.proposal = proposal;
}
}
package com.lsj.paxos;
import java.util.Random;
/**
* @author LY
* @time 2021/8/31 21:02
* 随机数(模拟由于网络通信挂掉的提交者或者接受者的id)
*/
public class RandomUtils {
public static int randomAcceptorId(){
Random random = new Random();
int id = random.nextInt(Common.ACCEPTOR_COUNT)+1;
System.out.println(id);
return id;
}
public static int randomProposerId(){
Random random = new Random();
int id = random.nextInt(Common.PROPOSER_COUNT)+1;
return id;
}
}
package com.lsj.paxos;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author LY
* @time 2021/8/31 20:44
* 公有的数据类
*/
public class Common {
/**
* 提交者的数量
*/
public static final int PROPOSER_COUNT = 2;
/**
* 接收者的数量
*/
public static final int ACCEPTOR_COUNT = 3;
/**
* 全局的提案编号
* AtomicInteger属于原子操作类,用于高并发环境,安全,可以不加锁实现线程安全,开销小
*/
public static AtomicInteger proposerN = new AtomicInteger(0);
}
package com.lsj.paxos;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author LY
* @time 2021/8/31 21:03
* 测试
*/
public class Test {
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
/**
* 接收者集合
*/
private static List<Acceptor> acceptorList = new ArrayList<>();
/**
* 提交者集合
*/
private static List<Proposer> proposerList = new ArrayList<>();
/**
* 初始化方法
*/
public static void init() {
//初始化接受者 acceptor
for (int i = 1; i <= Common.ACCEPTOR_COUNT; i++) {
Acceptor acceptor = new Acceptor();
acceptor.setId(i);
acceptor.setResN(0);
acceptor.setProposal(new HashMap<>());
acceptorList.add(acceptor);
}
//初始化提交者proposer
for (int i = 1; i <= Common.PROPOSER_COUNT; i++) {
Proposer proposer = new Proposer();
proposer.setId(i);
proposer.setProposal(new HashMap<>());
proposerList.add(proposer);
}
}
public static void main(String[] args) throws InterruptedException {
init();
//宕机id
int id = RandomUtils.randomProposerId();
for (Proposer proposer : proposerList) {
if (id != proposer.getId()) {
//使用Random来模拟网络通信阻塞(宕机)
executorService.execute(new Runnable() {
@Override
public void run() {
try {
guocheng(proposer);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
Thread.sleep(4000);
//宕机的机器重启了
for (Proposer proposer1 : proposerList) {
if (proposer1.getId() == id) {
//找到宕机的机器
executorService.execute(new Runnable() {
//模拟重启
@Override
public void run() {
try {
System.out.println("提交者id:" + id + "重启了");
guocheng(proposer1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
public static void guocheng(Proposer proposer) throws InterruptedException {
/**
* 1、生成提案
* 先判断(学习)之前的接受者里面有没有接受之前提议者的提案,没有就自己生成一个提案
* 如果有接受者已经接受了之前提议者的提案,无论自己的提案编号大还是小,都得把自己的提案的value指定为之前的那个提案的value
*/
if (!chickAccept()) {
//没有接受过提案
HashMap<Integer, String> map = new HashMap<>();
map.put(Common.proposerN.incrementAndGet(), "提案" + proposer.getId());
proposer.setProposal(map);
} else {
//之前有接受者接受过提案,只能乖乖用之前的提案值(也就是Map的value使用之前的提案的)
for (Acceptor acceptor : acceptorList) {
Map<Integer, String> proposal = acceptor.getProposal();
if (proposal.size() != 0) {
for (Map.Entry<Integer, String> entry : proposal.entrySet()) {
Map<Integer, String> map = new HashMap<>();
map.put(Common.proposerN.incrementAndGet(), entry.getValue());
proposer.setProposal(map);
break;
}
break;
}
}
}
// 提案编号
Integer var1 = 0;
Map<Integer, String> proporsal = proposer.getProposal();
Set<Map.Entry<Integer, String>> entries = proporsal.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
var1 = entry.getKey();
}
/**
* 2、prepare请求(这里是对所有的接受者进行请求)
* 统计 prepare请求 pok响应的个数
*/
AtomicInteger var2 = new AtomicInteger(0);
Map<Integer, String> var3;
String maxAcceptV;
int maxAcceptN = 0;
boolean flg = true;
for (Acceptor acceptor : acceptorList) {
var3 = acceptor.prepareReq(var1);
if (var3 != null) {
var2.incrementAndGet();
for (Map.Entry<Integer, String> var4 : var3.entrySet()) {
Integer key = var4.getKey();
if (flg) {
maxAcceptN = key;
flg = false;
}
if (maxAcceptN < key) {
maxAcceptN = key;
maxAcceptV = var4.getValue();
}
}
}
}
//判断是否收到超过一半响应(包括一半)
//阶段2,accept请求
AtomicInteger aokCount = new AtomicInteger(0);
Boolean half = chickHalf(Common.ACCEPTOR_COUNT, var2.intValue());
if (half) {
for (Acceptor acceptor : acceptorList) {
String req = acceptor.acceptReq(proporsal);
if ("aok".equals(req)) {
aokCount.incrementAndGet();
}
}
} else {
guocheng(proposer);
}
//如果过半,V被确定,不过半,重新发起Prepare请求
Boolean var4 = chickHalf(Common.ACCEPTOR_COUNT, aokCount.intValue());
if (var4) {
//输出一下每个acceptor的AcceptV
for (Acceptor acceptor : acceptorList) {
for (Map.Entry<Integer, String> entry : acceptor.getProposal().entrySet()) {
System.out.println("接受者的id:" + acceptor.getId() + "最终acceptN :" + entry.getKey() + ",最终acceptV:" + entry.getValue());
}
}
//结束
} else {
guocheng(proposer);
}
}
/**
* 判断是否超过一半响应
*
* @param total
* @param var1
* @return true 过半 false 不过半
*/
public static Boolean chickHalf(int total, int var1) {
double var = total / 2;
return !(var > var1);
}
/**
* @return false 没有接收提案
*/
public static Boolean chickAccept() {
boolean res = true;
for (Acceptor acceptor : acceptorList) {
Map<Integer, String> proporsal = acceptor.getProposal();
if (proporsal.size() == 0) {
//之前没有接受过提案
res = false;
break;
}
}
return res;
}
}
hadoop的常用配置文件有哪些
●hadoop-env.sh: 用于定义hadoop运行环境相关的配置信息,比如配置JAVA_HOME环境变量、为hadoop的JVM指定特定的选项、指定日志文件所在的目录路径以及master和slave文件的位置等;
●core-site.xml: 用于定义系统级别的参数,如HDFS URL、Hadoop的临时目录以及用于rack-aware集群中的配置文件的配置等,此中的参数定义会覆盖core-default.xml文件中的默认配置;
HDFS采用一种称为机架感知(rack-aware)的策略来改进数据的可靠性、可用性和网络带宽的利用率,一方面,通过一个机架感知的过程,NameNode可以确定每个DataNode所属的机架ID;另一方面,在读取数据时,为了减少整体的带宽小号和降低整体的带宽延时,HDFS会尽量读取程序读取距离客户端最近的副本。
大概意思就是为了让机架间的带宽在读数据时候尽可能多的将不用的带宽用起来,所以将数据写的时候备份到其他机架上,读的时候就可以读取最近的副本,也可以一起读,怎么快怎么来。
●hdfs-site.xml: HDFS的相关设定,如文件副本的个数、块大小及是否使用强制权限等,此中的参数定义会覆盖hdfs-default.xml文件中的默认配置;
●mapred-site.xml:HDFS的相关设定,如reduce任务的默认个数、任务所能够使用内存的默认上下限等,此中的参数定义会覆盖mapred-default.xml文件中的默认配置;
小文件过多会有什么危害,如何避免?
Hadoop上大量HDFS元数据信息存储在NameNode内存中,因此过多的小文件必定会压垮NameNode的内存。
每个元数据对象约占150byte,所以如果有1千万个小文件,每个文件占用一个block,则NameNode大约需要2G空间。如果存储1亿个文件,则NameNode需要20G空间。
显而易见的解决这个问题的方法就是合并小文件,可以选择在客户端上传时执行一定的策略先合并,或者是使用Hadoop的CombineFileInputFormat<K,V>实现小文件的合并。
CombineFileInputFormat:其实就是将HDFS上多个小文件合并到大文件中,并再每行存储了这行数据的文件路径。
启动hadoop集群会分别启动哪些进程,各自的作用
●NameNode:
○维护文件系统树及整棵树内所有的文件和目录。这些信息永久保存在本地磁盘的两个文件中:命名空间镜像文件、编辑日志文件;
○记录每个文件中各个块所在的数据节点信息,这些信息在内存中保存,每次启动系统时重建这些信息;
○负责响应客户端的 数据块位置请求 。也就是客户端想存数据,应该往哪些节点的哪些块存;客户端想取数据,应该到哪些节点取;
○接受记录在数据存取过程中,datanode节点报告过来的故障、损坏信息;
●SecondaryNameNode(非HA模式):
○实现namenode容错的一种机制。定期合并编辑日志与命名空间镜像,当namenode挂掉时,可通过一定步骤进行上顶。(注意 并不是NameNode的备用节点);
●DataNode:
○根据需要存取并检索数据块;
○定期向namenode发送其存储的数据块列表;
●ResourceManager:
○负责Job的调度,将一个任务与一个NodeManager相匹配。也就是将一个MapReduce之类的任务分配给一个从节点的NodeManager来执行;
●NodeManager:
○运行ResourceManager分配的任务,同时将任务进度向application master报告;
●JournalNode(HA下启用):
○高可用情况下存放namenode的editlog文件;
○两个NameNode为了数据同步,会通过一组称作JournalNodes的独立进程进行相互通信。
16万+

被折叠的 条评论
为什么被折叠?



