Hadoop的砖块们--第1章 远程过程调用RPC

本文深入剖析Hadoop中RPC(远程过程调用)的工作原理,包括代理类模式、客户端/服务端网络编程、NIO模式及Hadoop RPC的创建与执行过程。通过NameNode和DataNode的具体案例,详细讲解了RPC的通信流程。
摘要由CSDN通过智能技术生成

    砖块盖起大厦。“Hadoop的砖块们”,就是逐一分析Hadoop技术的重要技术组成元素。Hadoop的最重要的砖块是远程过程调用RPC。对于RPC来说,《Hadoop技术内幕》一书讲的非常清晰,这里就不重复了。


要先熟悉代理类模式,客户端/服务端网络编程。

1. 代理类

这里的代码主要来自《Hadoop技术内幕》。

packagecom.brian.hadoop.rpc;

importjava.lang.reflect.Proxy;

importjava.lang.reflect.Method;

importjava.lang.reflect.InvocationHandler;

/*

这个例子演示如何使用Proxy代理类

*/

public class MyProxy{

static interfaceCalculatorProtecal{

public int add(int x,int y);

public intsubtract(int x, int y);

}

static class Serverimplements CalculatorProtecal{

public int add(int x,int y){

return x + y;

}

public intsubtract(int x, int y){

return x - y;

}

}

static classCalculatorHandler implements InvocationHandler{

private Objectobjorg;

publicCalculatorHandler(Object obj){

this.objorg =obj;

}

public Objectinvoke(Object Proxy, Method method, Object[] args)

throwsThrowable{

Object result;

System.out.println("\nbefore invoke");

result =method.invoke(this.objorg, args);

System.out.println("\nafter invoke");

return result;

}

}

public static voidmain(String[] args){

System.out.println("ProxyDemo:");

CalculatorProtecalserver = new Server();

InvocationHandlerhandler = new CalculatorHandler(server);

CalculatorProtecalclient = (CalculatorProtecal)Proxy.newProxyInstance(

server.getClass().getClassLoader(),

server.getClass().getInterfaces(),

handler);

int r =client.add(5,1);

int x =client.subtract(5,1);

System.out.println("r= " + r + ", x = " + x);

}

}





2.常规的BS模式

先写一个常规的BS模式,熟悉网络编程的模式。Hadoop用的是NIO,比常规方式更复杂,不太容易理解。

2.1服务端代码Server.java

packagecom.brian.hadoop.net;

importjava.net.ServerSocket;

import java.net.Socket;

import java.io.InputStream;

import java.io.OutputStream;

import java.io.IOException;

import java.io.PrintWriter;

import java.util.Scanner;

import java.lang.Runnable;

import java.lang.Thread;

class Server{

public static voidmain(String[] args){

try{

ServerSocket s =new ServerSocket(8189);

System.out.println("newServersocket ok");

while(true){

Socketincoming = s.accept();

Runnable r =new MyThreadHandler(incoming);

Thread t =new Thread(r);

t.start();

}

}

catch (IOExceptione){

System.out.println("ServerSocketIOException ok");

System.out.println(e);

}

}

}




2.2客户端代码Client.java


package com.brian.hadoop.net;

import java.net.Socket;

importjava.io.DataInputStream;

importjava.io.DataOutputStream;

import java.io.IOException;

import java.io.PrintWriter;

importjava.io.BufferedReader;

importjava.io.InputStreamReader;

import java.lang.Thread;

import java.util.Scanner;

class Client{

public static voidmain(String[] args){

try{

Socket s = newSocket("127.0.0.1", 8189);

System.out.println("newsocket ok");

try{

BufferedReaderin = new BufferedReader(newInputStreamReader(s.getInputStream()));

PrintWriterout = new PrintWriter(s.getOutputStream());

String tmp;

int i = 0;

while (true){

out.println("s--"+ i);

out.flush();

tmp =in.readLine();

System.out.println(tmp);

i++;

}

}finally{

System.out.println("SocketException, finally");

s.close();

}

}

catch (IOExceptione){

System.out.println("SocketIOException ok");

System.out.println(e);

}

}

}



2.3 服务端事件处理的线程类MyThreadHandler

packagecom.brian.hadoop.net;

import java.net.Socket;

import java.lang.Runnable;

import java.io.InputStream;

import java.io.OutputStream;

import java.io.PrintWriter;

import java.io.IOException;

import java.util.Scanner;

class MyThreadHandlerimplements Runnable{

private Socket incoming;

publicMyThreadHandler(Socket incoming){

this.incoming =incoming;

}

public void run(){

try{

InputStreaminstream = this.incoming.getInputStream();

OutputStreamoutstream = this.incoming.getOutputStream();

Scanner in = newScanner(instream);

PrintWriter out =new PrintWriter(outstream, true);

while(in.hasNextLine()){

String line =in.nextLine();

out.println("Echo:"+ line);

if(line.trim().equals("bye")){

break;

}

Thread.sleep(1000);

}

this.incoming.close();

}catch (IOExceptione){

System.out.println(e);

}catch(InterruptedException e){

System.out.println(e);

}

}

}



3. NIO模式

JavaNIO的网络编程比常规的BS模式更快,它是非阻塞的。比较复杂,推荐两个地方:其一,http://xm-king.iteye.com/blog/766330;其二,《JavaNIO》一书。








4. HadoopRPC

RPC就是远程过程调用RemoteProcedure Call。如果本地主机的一个进程,想调用远程主机上的一个进程的某个功能,它应该怎么做呢?

a)远程主机运行服务器端,本地主机运行客户端,通常情况下,远程主机的服务端是一直在运行的。

b)本地主机的客户端通过网络连接到远程主机的指定端口,然后,将要调用的函数名和调用参数通过传给远程主机的服务端。

c)远程主机接收到了函数名和调用参数的时候,它根据函数名找到这个函数,然后根据调用参数执行这个函数,然后把结果通过网络返回到本地主机的客户端。

这里的服务端和客户端的通信方式,可以是常规的B/S模式,也可以是NIOB/S模式,这就是前面的两个例子。


最简单的RPC,可以通过传递字符串的方式进行,也就是说,客户端把要调用的函数名和调用参数,都转换成字符串,然后将字符串传递给服务端,然后在服务端将字符串解析出来,然后调用相应的函数,计算,再讲结果以字符串的方式返回给本地主机客户端,客户端解析字符串获取返回值。这种方式适应于调用参数的类型是简单类型,诸如int,float, char等等。


如果调用参数是对象,字符串方式没效率也不够方便,那么要将参数存储到一个更大的对象里,然后它进行序列化,序列化之后,再从网络传递到远程主机的服务器端,由服务端进行解析,然后调用相应函数,计算,然后将结果序列化,再返回给本地主机的客户端,本地主机的客户端再进行反序列化,获取计算结果。因为远程过程调用多了一个将参数和返回值进行序列化的过程,所以,以代理模式进行调用,代理类在真正进行调用之前对参数进行处理,包括存储和序列化。


4.1 RPC的创建过程

下面,我们以NameNode类和DataNode类为例,看它们是如何实现RPC---- JobTrackerTaskTracker的代码比较长,看起来比较麻烦,就不选它们了。

4.1.1NameNode节点启动

NameNode节点先启动

NameNode.java里,有两个跟RPC有关的import:

importorg.apache.hadoop.ipc.RPC;

importorg.apache.hadoop.ipc.RPC.Server;


NameNode类,定义列RPCServer:

privateServer server;


对象server的由RPC类的getServer函数创建:

this.server= RPC.getServer(this, socAddr.getHostName(), socAddr.getPort(),

handlerCount,false, conf, namesystem.getDelegationTokenSecretManager());


RPC类的getServer函数执行:

newServer(instance, conf, bindAddress, port, numHandlers, verbose,secretManager);

这里的ServerRPC的内部Server类,它继承org.apache.hadoop.ipc.Server类,它的构造函数执行语句是:

super(bindAddress,port, Invocation.class, numHandlers, conf,

classNameBase(instance.getClass().getName()),secretManager);

this.instance= instance;

this.verbose= verbose;


先执行的是org.apache.hadoop.ipc.Server类的构造函数,在它的构造函数,除了设置参数之外,最重要的是创建两个对象:

listener= new Listener();

responder= new Responder();

Listener类是线程类,监听端口。Responder类也是线程类,将RPC结果返回给客户端。


然后,NameNode节点运行org.apache.hadoop.ipc.Serverjoin函数,进入无限循环,运行wait函数。

至此,NameNode节点启动完毕。

4.1.2DataNode节点的启动

DataNode节点的启动晚于NameNode节点的启动。


DataNode节点,引用如下:

importorg.apache.hadoop.ipc.RPC;

importorg.apache.hadoop.ipc.RemoteException;

importorg.apache.hadoop.ipc.Server;


DataNode类定义了一个RPC.Server:

publicServer ipcServer;


ipcServer的创建如下:

ipcServer= RPC.getServer(this, ipcAddr.getHostName(), ipcAddr.getPort(),

conf.getInt("dfs.datanode.handler.count",3), false, conf,

blockTokenSecretManager);

ipcServer的创建过程跟NameNode是相似的,不再详述。

4.1.3DataNode节点和NameNode节点的RPC通信

DataNode节点来说,如何跟NameNode节点进行通信是最重要的,这个通信是RPC方式进行的。DataNode的初始化是在函数startDataNode里进行的。


DataNode节点和NameNode节点以接口DatanodeProtocol进行通信的。


DataNode类,定义如下:

publicDatanodeProtocol namenode = null;

startDataNode函数,创建namenode

this.namenode= (DatanodeProtocol)

RPC.waitForProxy(DatanodeProtocol.class,DatanodeProtocol.versionID, nameNodeAddr, conf);

这里是从远程主机,也就是NameNode节点,创建代理类的对象。


RPCwaitForProxy函数执行,再次调用:

waitForProxy(protocol,clientVersion, addr, conf, 0, Long.MAX_VALUE)

然后,再次调用:

getProxy(protocol,clientVersion, addr, conf, rpcTimeout)

然后,再次调用:

getProxy(protocol,clientVersion, addr, conf, NetUtils.getDefaultSocketFactory(conf),rpcTimeout)

然后,再次调用:

getProxy(protocol,clientVersion, addr, ugi, conf, factory, rpcTimeout)

然后,再次调用:

getProxy(protocol,clientVersion, addr, ticket, conf, factory, rpcTimeout, null)


getProxy函数,创建代理类的关键代码是:

finalInvoker invoker = new Invoker(protocol, addr, ticket, conf, factory,rpcTimeout, connectionRetryPolicy);

VersionedProtocolproxy = (VersionedProtocol)Proxy.newProxyInstance(

protocol.getClassLoader(),new Class[]{protocol}, invoker);


Invoker类是RPC类的内部类,它实现了InvocationHandler接口。Invoker类的构造函数执行如下:

this.remoteId= Client.ConnectionId.getConnectionId(address, protocol,

ticket,rpcTimeout, connectionRetryPolicy, conf);

this.client= CLIENTS.getClient(conf, factory);

这里的Client就是ipc.Client类,ConnectionIdClient的内部类,第一条语句获取一个remoteId,主要是记录各种参数。

CLIENTS的类型是ClientCache类,主要是缓存Client,以供复用,实例化如下:

ClientCacheCLIENTS=new ClientCache();

CLIENTSgetClient函数,会创建client,代码如下:

client= new Client(ObjectWritable.class, conf, factory);

这个创建的过程,也大多是保存各种参数。


对于Proxy.newProxyInstance函数,我们在实现代理类的时候已经熟悉了。

至此,创建代理类的过程就完成了。

4.2 RPC的执行过程

DataNode执行DatanodeProtocal接口的sendHeartBeat函数,过程是怎样的?

DataNode类在offerService函数执行sendHeartbeat函数。


然后,调用RPC的内部类Invoker类的invoke函数,也就是:

invoke(Objectproxy, Method method, Object[] args)


然后,在invoke函数,调用:

ObjectWritablevalue = (ObjectWritable)client.call(new Invocation(method, args),remoteId);


这里的Invocation类是RPC类的内部类。这个类的作用,就是将调用的方法和参数存储在Invocation的实例里,Invocation是可以序列化的,这样就能将调用函数名和参数传递给RPC的服务端。


然后,执行到Client对象的call函数即call(Writableparam, ConnectionId remoteId)

Callcall = new Call(param);

Connectionconnection = getConnection(remoteId, call);

connection.sendParam(call);

synchronized(call) {

while(!call.done) {

...

call.wait();

...

}

}

return call.value;


Call类是Client类的内部类,存贮Invocation类的param,等待执行完了,存储结果。


Connection类是一个线程类,存储call和远程主机的信息,它的sendParam函数把call写入到一个输出流,这个输出流通过Socket方式连接NameNode节点。


然后,数据通过javaNIO通信方式,传递到NameNode的指定端口,这个端口,由Server类的Listener线程类监听。


于是,就到了Server类的Listener线程类。Listener线程类负责接收DataNode节点发过来的数据,然后用Server类的内部类ConnectionReadAndProcess函数进行处理,然后,调用ConnectionprocessData函数。


processData函数会将参数从输入流中读出来,关键代码如下:

Writableparam = ReflectionUtils.newInstance(paramClass, conf);

param.readFields(dis);

Callcall = new Call(id, param, this);

callQueue.put(call);


这里的param就是DataNode节点传的参数param,两者是一样的,它用反序列化的方式从输入流中读出来,然后放到队列callQueue等待执行。


Server类里有一个线程数组,Server启动后,这些线程会运行,这些线程不断地从callQueue取出Call类的对象,执行Call,执行成功后,设置Response,然后由responder线程调用doRespond,把结果回传给DataNode,关键代码如下:

finalCall call = callQueue.take();

...

value= call(call.connection.protocol, call.param, call.timestamp);

...

setupResponse(buf,call, (error == null) ? Status.SUCCESS : Status.ERROR, value,errorClass, error);

...

responder.doRespond(call);


函数call是在在org.apache.hadoop.Server类定义的,但没实现,而是在RPC类的内部类Sever实现了,函数原型如下:

publicWritable call(Class<?> protocol, Writable param, longreceivedTime)


函数call根据Call类的实例call,生成Method,然后调用Method函数获取结果,这个Java的反射,其关键代码如下:

Invocationcall = (Invocation)param;

Methodmethod = protocol.getMethod(call.getMethodName(),call.getParameterClasses());

...

Objectvalue = method.invoke(instance, call.getParameters());

...

return newObjectWritable(method.getReturnType(), value);


responderResponder类的实例,Responder是线程类,它的run函数也有做doRespond的步骤。

至此,RPC的执行过程就结束了。

转载于:https://my.oschina.net/u/3226063/blog/826102

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值