大数据处理从零开始————5.HDFS的运行机制

1.HDFS数据读写流程

1.1 RPC模型概述

        HDFS是一个集群,由几十台、几百台、几千台甚至上万台节点组成,这些节点都是以机架的形式组织的,而这些机架可能摆放到同一个机房,也可能在不同的机房,甚至在不同的地域。HDFS就是运行在这些服务器的分布式文件程序,用户访问HDFS,就是访问运行在这些众节点上的分布式文件程序,通过Client命令请求服务端分布式文件系统程序,这个过程被称为远程过程调用,其协议叫做远程过程调用协议(即RPC协议)。

        RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP/IP或UDP,为通信程序之间携带信息数据。RPC将原来的本地调用转变为调用远端的服务器上的方法,给系统的处理能力和吞吐量带来了近似于无限制提升的可能。在OSI网络通信模型中,RPC跨域了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。


RPC框架

        从上面架构图中我们可以看出一个完整的RPC架构里面包含了四个核心的组件:        

        1.客户端(Client):服务的调用方。

        2.客户端存根(Client Stub):存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。

        3.服务端(Server):真正的服务提供者。

        4.服务端存根(Server Stub):接收客户端发送过来的消息,将消息解包,并调用本地的方法。

        

        在了解完一个完整RPC架构的核心组件后,我们可以明白Hadoop为什么会引入RPC这个问题?RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。HDFS的通信可能发生在:
        Client-NameNode之间,其中NameNode是服务器
        Client-DataNode之间,其中DataNode是服务器
        DataNode-NameNode之间,其中NameNode是服务器
        DataNode-DateNode之间,其中某一个DateNode是服务器,另一个是客户端
        如果我们考虑Hadoop的Map/Reduce以后,这些系统间的通信就更复杂了。为了解决这些客户机/服务器之间的跨进程通信,Hadoop引入了一个RPC框架。

        在概述的最后,我们来具体了解一下图中的1-10具体含义。

        1. 客户端函数调用:客户端程序以本地方式调用系统生成的 Client Stub 代理程序(对应图中的1)。

        2. 消息封装:Client Stub 代理程序将函数调用信息封装成消息包,并交给网络通信模块(对应图中的2)。

        3. 网络传输:消息通过网络传输到远程服务器(对应图中的3)。

        4. 接收消息:远程服务器接收到消息后,将其转发给相应的 Server Stub 代理程序(对应图中的4)。

        5. 消息解封:Server Stub 代理程序解封消息,格式化为被调用过程所需的参数,并调用对应的服务器端本地服务函数(对应图中的5)。

        6. 执行与返回:被调用的服务函数根据传入的参数执行处理,并将结果返回给 Server Stub 代理程序(对应图中的6)。

        7. 结果封装:Server Stub 代理程序将结果封装成消息,通过网络通信模块逐级传送回客户端程序(对应图中的7)。

        8. 结果接收与解码:Client Stub 代理程序接收消息并进行解码(对应图中的8)。

        9. 客户端存根返回:Client Stub 代理程序将解码后的结果传递给客户端函数(对应图中的9)。

        10. 最终结果:客户端程序获得并处理最终结果(对应图中的10)。

        RPC的目标是将步骤1到7的处理过程封装起来,使得客户端与服务器之间的交互更加简便。


1.2 Hadoop的RPC机制及实现模型

1.2.1 Hadoop的RPC机制

        同其他RPC框架一样,Hadoop RPC分为四个部分:

        序列化层:Client与Server端通信传递的信息采用Hadoop提供的序列化类或自定义的Writable类型。

        函数调用层:Hadoop RPC通过动态代理以及Java反射实现函数调用;

        网络传输层:Hadoop RPC采用了基于TCP/IP的Socket机制;

        服务器端框架层:RPC Server利用Java NIO以及事件驱动的I/O模型,提高RPC Server并发处理能力。


        Hadoop RPC在整个Hadoop中应用广泛,Client、DataNode、NameNode之间的通讯全靠它了。

        例如:操作HDFS的时候,使用的是FileSystem类,它的内部有个DFSClient对象,这个对象负责与NameNode打交道。在运行时,DFSClient在本地创建一个NameNode的代理,然后就操
作这个代理,这个代理就会通过网络,远程调用到NameNode的方法,也能返回值.

1.2.2 Hadoop的RPC实现模型

        RPC(远程过程调用)的主要特点包括透明性、高性能和可控性。透明性体现在用户对远程调用其他机器上的程序的体验上,调用方式与本地方法相似。高性能方面,RPC Server能够并发处理多个来自客户端的请求,提升了系统的响应速度与吞吐量。可控性方面,虽然JDK提供了一个重量级的RPC框架——RMI,但其可控性较差,因此Hadoop RPC实现了一个自定义的轻量级RPC框架。该框架采用了多项技术,包括动态代理、反射(动态加载类)、序列化以及非阻塞的异步IO(NIO),以提高其灵活性和效率。

        Hadoop的RPC总体架构可以简单概述为:Hadoop RPC = 动态代理 + 定制的二进制流。在这个架构中,动态代理用于实现远程方法调用的自动化,使得客户端可以像调用本地方法一样调用远程服务。而定制的二进制流则用于数据的序列化和反序列化,确保在网络传输过程中数据的高效性和完整性。这种设计使得Hadoop RPC在性能和灵活性上得到了优化,适合大数据环境中的高并发应用。

RPC总体框架

1.2.3 RPC Server模型

RPC Server模型

        从上图中可以看到RPC Server模型的组成和分工:

        Listener是监听RPC server的端口, 用来接收 RPC Client的连接请求和数据, 然后把连接转发到某个 Reader,让 Reader读取那个连接的数据。如果有多个 Reader,当有新连接过来时,就在这些 Reader间顺序分发。
        Reader负责从客户端连接中读取数据流,转化成调用对象(Call),然后放到调用队列(call queue)里。
        Handler 是真正做事的实体,RPC Server的 Call处理者和 Server. Listener通过 Call 队列交互。它从调用队列中获取调用信息,然后反射调用真正的对象得到结果,再把此次调用放到响应队列( Response Queue)里。
        Responder 不断地检查响应队列中是否有调用信息,如果有的话,就把调用的结果返回给客户端。

        Connection: RPC Server数据的接收者, 提供接收数据、解析数据包的功能。
        Call: 持有 Client 的 Call信息。

        在了解完RPC Server模型的组成和分工后,我们来看一看RPC Server主要流程。RPC Server作为服务的提供者主要由两部分组成:接收 Call调用和处理 Call 调用。
        接收 Call调用负责接收来自 RPC Client的调用请求, 编码成 Call 对象放入 Call 队列中, 这一过程由 Server. Listener监听器完成。处理 Call调用主要交由 RPC Server 端的 Handler线程,具体过程如下图。

        Listener 线程监听 RPC Client发过来的请求及数据。
        当有数据可以接收时, 调用 Connection的ReadAndProcess方法读取数据。
        Connection 边接收数据边处理数据,当接到一个完整的 Call包时,构建一个 Call 对象 Push到 Call队列中, 由 Handler 处理 Call 队列中的所有 Call 对象。
        Handler线程监听 Call队列, 如果 Call队列非空, 按FIFO 规则从 Call队列中取出 Call。
        将 Call交给 RPC. Server处理。
        借助JDK 提供的 Method, 完成对目标方法的调用, 目标方法由具体的业务逻辑实现。
        返回响应。 Server. Handler按照异步非阻塞的方式向 RPC Client发送响应, 如果有未发送的数据, 则交由 Server. Responder完成。


1.2.4 使用Hadoop RPC

        在下面的NameNode的源代码片段中,可以看到NameNode确实创建了RPC的服务端:

private void initialize(Configuration conf) throws IOException {
... ...
// create rpc server
InetSocketAddress dnSocketAddr = getServiceRpcServerAddress(conf);
if (dnSocketAddr != null) {
int serviceHandlerCount =conf.getInt(DFSConfigKeys.
DFS_NAMENODE_SERVICE_HANDLER_COUNT_KEY,DFSConfigKeys.
DFS_NAMENODE_SERVICE_HANDLER_COUNT_DEFAULT);
this.serviceRpcServer = RPC.getServer(this, dnSocketAddr.getHostName(),
dnSocketAddr.getPort(), serviceHandlerCount,false, conf,
namesystem.getDelegationTokenSecretManager());
this.serviceRPCAddress = this.serviceRpcServer.getListenerAddress();
setRpcServiceServerAddress(conf);
}
this.server = RPC.getServer(this, socAddr.getHostName(),socAddr.getPort(),
handlerCount, false, conf,namesystem.getDelegationTokenSecretManager());
... ...
}

        RPC 协议允许像调用本地服务一样调用远程服务, 而且它是与语言无关的。我们下面来看一下Hadoop RPC对外提供的接口(见类org.apache.hadoop.ipc.RPC)主要有:

        (1)public static <T> ProtocolProxy <T> getProxy/waitForProxy(...) ;构造一个客户端代理对象(该对象实现了某个协议),用于向服务器发送RPC请求。
        (2)public static Server RPC.Builder (Configuration).build();为某个协议(实际上是Java接口)实例构造一个服务器对象,用于处理客户端发送的请求。

        接着我们看看使用Hadoop RPC的四大步骤:
        1.定义RPC协议:RPC协议是客户端和服务器端之间的通信接口,定义了服务器端对外提供的服务接口。
        2.实现RPC协议:Hadoop RPC协议通常是一个Java接口,用户需要实现该接口。
        3.构造和启动RPC 服务:直接使用静态类Builder构造一个RPC Server,调用函数start()启动该Server。
        4.构造RPC Client:使用getProxy方法构造客户端代理对象,通过代理对象调用远程端的方法。

        Hadoop RPC程序简单示例:

        (1)定义RPC协议:

public interface RemoteObject extends VersionedProtocol {
    public static final int NUM = 55555;
    public String hello(String name);
}


        (2)实现RPC协议:
 

public class RemoteObjectImpl implements RemoteObject {
    @Override
    public String hello(String name) {
        System.out.println("远程对象被调用了!");
        return "你好:" + name;
    }
    @Override
    public long getProtocolVersion(String protocol,long clientVersion) throws IOException {
    return RemoteObject.NUM;
    }
}

        (3)构造RPC服务端:

public class MyServer {
    public static final String ADDRESS = "localhost";
    public static final int PORT = 12345;
    public static void main(String[] args) throws Exception {
        System.out.println("RPC服务器已启动。");
        final Server server = new RPC.Builder(new Configuration()
            .setBindAddress(MyServer.ADDRESS)
            .setPort(MyServer.PORT)
            .setProtocol(RemoteObject.class)
            .setInstance(new RemoteObjectImpl())
            .build();
        server.start();
    }
}

        (4)构造RPC客户端:

public class MyClient {
    private static final String ADDRESS = "localhost";
    private static final int PORT = 12345;
    public static void main(String[] args) throws Exception {
        System.out.println("RPC客户端已启动。");
        RemoteObject client =RPC.getProxy(RemoteObject.class,
            RemoteObject.NUM,
            new InetSocketAddress(MyClient.ADDRESS,MyClient.PORT),
            new Configuration());
        String result=client.hello("张小三");
        System.out.println(result);
    }
}

1.3 HDFS读取文件

        通过前面对 RPC 实现模型的介绍,我们对RPC 的实现原理有了基本的理解。接下来, 学习如何通过RPC的方式读取HDFS 中某一个文件数据, 文件读取流程如下图所示。

        ① 使用HDFS 提供的 Client, 向远程的NameNode 发起 RPC 读文件请求。
        ② NameNode 会视情况返回文件的部分或者全部数据块列表, 对于每个数据块, NameNode都会返回有该数据块副本的 DataNode 地址。
        ③Client会选取最近的DataNode来读取数据块; 如果 Client本身就是 DataNode, 那么将从本地直接获取数据。
        ④读取完当前数据块后, 关闭当前的 DataNode 连接, 并为读取下一个数据块寻找最佳的DataNode。

        ⑤当读完数据块列表后, 且文件读取还没有结束, Client会继续向NameNode 获取下一批数据块列表。

        ⑥每读取完一个数据块, 都会进行校验和验证, 如果读取 DataNode 时出现错误, Client会通知NameNode, 然后再从下一个拥有该数据块副本的DataNode 继续读取。

        可能直接通过上面是比较难以理解的,我们可以具体分析一下:

        在上图中,HDFS的读取过程中,Block的位置是有先后顺序的。客户端首先会到host2上读取Block1,然后再到host7上读取Block2。在这个过程中,客户端的位置会影响数据的读取效率。如果客户端位于机架之外,它会按照指定顺序访问不同的DataNode。如果客户端位于某个DataNode上并且位于同一机架内,则优先读取本机架上的数据,以提高读取效率并减少网络延迟。这种设计原则确保了HDFS能够高效、高可用地进行数据读取。

        下面是使用HDFS API读取文件:

public class FileSystemCat {
    public static void main(String[] args) throws Exception {
        String uri = "hdfs://10.10.155.110:9000/output/wordcount/part-r-00000";
        Configuration conf = new Configuration();
        FileSystem fs = FileSystem.get(URI.create(uri), conf);
        InputStream in = null;
        try {
            in = fs.open(new Path(uri));
            IOUtils.copyBytes(in, System.out, 4096, false);
        } catch (Exception e) {
            e.printStackTrace();
            IOUtils.closeStream(in);
        }
    }
}

1.4 HDFS文件的一致模型

        文件系统的一致模型:指的是文件读/写的数据可见性。当新建一个文件后,该文件能在文件系统的命名空间中立即可见。
        问题:但是,写入文件的内容并不能保证立即可见,即使数据流已经刷新并存储,可能文件长度仍然显示为0。当写入的数据超过一个块后,第一个数据块对于新的reader就是可见的。但当前正在写入的块对其他reader不可见。

        解决方案:
        HDFS提供一个方法来使所有缓存与数据节点强行同步,即对FSDataOutputStream调用sync()方法。当sync()方法返回成功之后,对于所有新的reader而言,HDFS能保证文件中到目前为止写入的数据均到达所有DataNode的写入管道并且对所有新的reader均可见。
        HDFS中关闭文件其实还隐含了执行sync()方法。

        这个一致模型和应用程序的具体设计方法有关。如果不调用sync()方法,那么一旦客户端或系统发生故障,就可能丢失数据块。所以需要在适当的地方调用sync()方法,例如在写入一定的记录或字节之后。sync()操作仍然是有开销,所以在数据健壮性和吞吐量之间就会有所取舍。怎样权衡与具体的应用相关,通过设置不同调用sync()方法的频率来衡量应用程序的性能,最终找到一个合适的频率。

        示例:当创建一个文件后,立即读取文件信息,此时文件信息是不存在的。只有当调用hsync()方法或调用了close()方法后,才能立即读取到文件信息。

Path p=new Path("/home/temp/a.txt");
FSDataOutputStream out=fs.create(p);
out.write("content".getBytes("UTF-8"));
out.hflush();
out.hsync();
assertThat(fs.getFileStatus(p).getLen(),is(((long)"content".len
gth())));

Path p=new Path("/home/temp/a.txt");
fs.create(p);
assertThat(fs.exists(p),is(true));
Path p=new Path("/home/temp/a.txt");
OutputStream out=fs.create(p);
out.write("content".getBytes("UTF-8"));
out.close();
assertThat(fs.getFileStatus(p).getLen(),is(((long)"content".len
gth())));

2.HDFS的HA机制

2.1 HDFS的HA机制

        在Hadoop 2.x之前,HDFS集群的NameNode节点存在多个问题。作为集群的管理者,NameNode存储着所有文件的元数据,一旦该节点出现故障,整个集群将不可用,直到重启NameNode或重新启动一个NameNode节点。而即使重新启动,也可能会导致部分数据丢失。对于大型集群,NameNode的冷启动时间可能需要30分钟甚至更长,这延长了系统恢复的时间,并影响日常维护。

        为了解决这些问题,Hadoop 2.x及后续版本引入了高可靠性(HA)支持。其实现方式是配置一对“活动-备用”(active-standby)NameNode。在这种架构下,当活动的NameNode失效时,备用NameNode能够迅速接管其任务,并开始处理来自客户端的请求,确保整个服务不中断。通过这种方式,Hadoop显著提高了HDFS的可用性和可靠性。

2.2 HA集群架构分析

        HDFS的高可用(HA)机制通过配置一个Active NameNode和一个Standby NameNode来解决单点故障问题,其中Active NameNode处理所有客户端操作,而Standby NameNode作为热备实时同步数据。当Active NameNode发生故障时,Standby NameNode可以迅速接管,这个过程由ZooKeeper集群协调,确保了故障转移的自动化和透明性,从而提高了整个HDFS集群的可用性和稳定性。

        (1)为了实现集群的高可用性(HA),需要对集群架构进行修改:

        为了实现集群的高可用性(HA),需要对集群架构进行一系列修改。首先,活动和备用的NameNode之间需要通过高可用的共享存储来实现编辑日志的共享,以便备用NameNode在接管工作后能够读取共享的编辑日志,从而实现与活动NameNode的状态同步。其次,DataNode需要同时向两个NameNode发送数据块处理报告,因为数据块的映射信息存储在NameNode的内存中,而不是磁盘。此外,客户端需要采用特定机制来处理NameNode的失效问题,这一机制对用户是透明的。

        需要注意的是,在活动NameNode失效后,备用NameNode能够快速接管任务,因为最新的状态存储在内存中。然而,实际观察到的失效时间可能会略长,因为系统需要谨慎地确认活动NameNode是否真的失效。此外,虽然活动NameNode和备用NameNode同时失效的概率非常低,但如果这种情况发生,管理员仍然可以声明一个备用的NameNode并进行冷启动,以恢复系统的正常运行。

        (2)HDFS NameNode 高可用整体架构:

        HDFS NameNode的高可用整体架构主要由以下几个关键组成部分构成:
        Active NN 和 Standby NN:主/备 NN,只有主 NN 才能对外提供读写服务。
        主备切换控制器 ZKFailoverController:作为独立进程运行,对 NN 的主备切换进行
总体控制。
        ZooKeeper 集群:为主备切换控制器提供主备选举支持。
        共享存储系统:主备 NN 通过共享存储系统实现元数据同步。
        DN 节点:DN 会同时向主 NN 和备 NN 上报数据块的位置信息

        (3)NameNode 的主备切换实现:

        NameNode的主备切换实现是通过ZKFailoverController、HealthMonitor和ActiveStandbyElector三个组件协同工作来完成的。

        首先,ZKFailoverController(zkfc)作为一个独立的进程在NameNode机器上启动。在启动时,zkfc会创建HealthMonitor和ActiveStandbyElector这两个主要的内部组件,并同时向这两个组件注册相应的回调方法。其次,HealthMonitor的主要职责是检测NameNode的健康状态。当它发现NameNode的状态发生变化时,会触发相应的回调,通知ZKFailoverController进行自动的主备选举。最后,ActiveStandbyElector则负责自动执行主备选举。它内部封装了ZooKeeper的处理逻辑,并在ZooKeeper完成主备选举后,调用ZKFailoverController的回调方法,以进行NameNode的主备状态切换。

        通过这三个组件的协同工作,HDFS能够实现高效且可靠的主备切换,确保系统的高可用性。

NameNode 的主备切换实现

2.3 HDFS的Federation机制

2.3.1 HDFS的Federation机制概述

        Hadoop分布式文件系统(HDFS)在早期版本中,由于只有一个NameNode来管理整个集群的元数据,随着集群规模的扩大,NameNode成为了性能瓶颈。此外,单个NameNode的设计也带来了单点故障的风险,影响了集群的可用性。为了解决这些问题,Hadoop 2.0引入了Federation机制,通过使用多个独立的NameNode和命名空间来实现水平扩展。

        

        在Federation架构中,每个NameNode管理自己的命名空间,并且它们之间是相互独立的,不需要进行协调。这样,每个NameNode可以处理一部分元数据,从而减轻单个NameNode的负担。DataNode作为数据块的存储设备,需要向所有的NameNode注册,并定期发送心跳和块报告,同时执行来自所有NameNode的命令。

        然而,Federation中的多个命名空间需要有效管理。如果采用文件名hash方法来分配文件到不同的命名空间,可能会导致数据局部性差,因为相关文件可能被分散到不同的命名空间中,这会增加访问成本。为了更好地管理这些命名空间,HDFS Federation采用了客户端挂载表(Client Side Mount Table)的方法。

        通过客户端挂载表,不同的命名空间可以被挂载到一个全局的mount-table中,这样用户就可以通过访问不同的挂载点来访问不同的命名空间,类似于在Linux系统中访问不同的挂载点。这种方法不仅实现了数据的全局共享,还可以为不同的应用程序提供定制化的命名空间视图,从而提高了数据访问的灵活性和效率。

        2.3.2 HDFS Federation的优点与不足

·        HDFS Federation通过引入多个NameNode实现了文件系统namespace的水平扩展,这不仅提升了系统的扩展性,还通过分离namespace volume增强了不同应用程序和用户之间的隔离性。此外,Block Pool抽象层的引入为HDFS架构的创新提供了可能,它允许新的文件系统在block storage上构建,使得应用程序如HBase能够直接利用block storage层,同时也为未来完全分布式namespace的实现奠定了基础。Federation的设计简洁性也是其一大优势,因为其核心设计的改变主要集中在DataNode、Config和Tools上,而NameNode本身的改动非常少,保持了原有的健壮性,并且具有良好的向后兼容性,现有的单NameNode部署配置无需任何改变即可继续使用。

        然而,HDFS Federation也存在一些不足。尽管它通过多个NameNode提供了一定程度的容错能力,但并没有完全解决单点故障问题。如果某个NameNode发生故障,它所管理的文件将无法被访问。此外,Federation采用的Client Side Mount Table虽然能够分摊文件和负载,但这种方法需要人工介入来实现理想的负载均衡,这可能会增加管理的复杂性和成本。

猜你喜欢

转载自blog.csdn.net/m0_74922316/article/details/142746373