Hadoop-0.20.0源代码分析(10)

DFSClient是分布式文件系统客户端,它能够连接到Hadoop文件系统执行指定任务,那么它要与Namenode与Datanode基于一定的协议来进行通信。这个通信过程中,涉及到不同进程之间的通信。在org.apache.hadoop.ipc包中,定义了进程间通信的Client端与Server端的抽象,也就是基于C/S模式进行通信。这里先对org.apache.hadoop.ipc包中有关类的源代码阅读分析。

首先看该包中类的继承关系,如下所示:

。java.lang.Object 。org.apache.hadoop.ipc.Client 。org.apache.hadoop.ipc.RPC 。org.apache.hadoop.ipc.Server 。org.apache.hadoop.ipc.RPC.Server 。java.lang.Throwable (implements java.io.Serializable) 。java.lang.Exception 。java.io.IOException 。org.apache.hadoop.ipc.RemoteException 。org.apache.hadoop.ipc.RPC.VersionMismatch

我阅读该包源程序的方法是,先从C/S通讯的两端Client类与Server类来阅读分析,然后再对实现的一个RPC类进行分析。

Client类

首先从Client客户端类的实现开始,该类定义了如下属性:

private Hashtable<ConnectionId, Connection> connections = new Hashtable<ConnectionId, Connection>(); // 客户端维护到服务端的一组连接 private Class<? extends Writable> valueClass; // class of call values private int counter; // counter for call ids private AtomicBoolean running = new AtomicBoolean(true); // 客户端进程是否在运行 final private Configuration conf; // 配置类实例 final private int maxIdleTime; // 连接的最大空闲时间 final private int maxRetries; //Socket连接时,最大Retry次数 private boolean tcpNoDelay; // 设置TCP连接是否延迟 private int pingInterval; // ping服务端的间隔 private SocketFactory socketFactory; // Socket工厂,用来创建Socket连接 private int refCount = 1; final private static String PING_INTERVAL_NAME = "ipc.ping.interval"; // 通过配置文件读取ping间隔 final static int DEFAULT_PING_INTERVAL = 60000; // 默认ping间隔为1分钟 final static int PING_CALL_ID = -1;

从属性可以看出,一个Clinet主要处理的是与服务端进行连接的工作,包括连接的创建、监控等。为了能够了解到Client如何实现它所抽象的操作,先分别看一下该类定义的5个内部类:

  • Client.Call内部类

该内部类,是客户端调用的一个抽象,主要定义了一次调用所需要的条件,以及修改Client客户端的一些全局统计变量,如下所示:

private class Call { int id; // 调用ID Writable param; // 调用参数 Writable value; // 调用返回的值 IOException error; // 异常信息 boolean done; // 调用是否完成 protected Call(Writable param) { this.param = param; synchronized (Client.this) { this.id = counter++; // 互斥修改法:对多个连接的调用线程进行统计 } } /** 调用完成,设置标志,唤醒其它线程 */ protected synchronized void callComplete() { this.done = true; notify(); // 唤醒其它调用者 } /** * 调用出错,同样置调用完成标志,并设置出错信息 */ public synchronized void setException(IOException error) { this.error = error; callComplete(); } /** * 调用完成,设置调用返回的值 */ public synchronized void setValue(Writable value) { this.value = value; callComplete(); } }

上面的Call内部类主要是对一次调用的实例进行监视与管理,即使获取调用返回值,如果出错则获取出错信息,同时修改Client全局统计变量。

  • Client.ConnectionId内部类

该内部类是一个连接的实体类,标识了一个连接实例的Socket地址、用户信息UserGroupInformation、连接的协议类。每个连接都是通过一个该类的实例唯一标识。如下所示:

InetSocketAddress address; UserGroupInformation ticket; Class<?> protocol; private static final int PRIME = 16777619;

该类中有一个用来判断两个连接ConnectionId实例是否相等的equals方法:

@Override public boolean equals(Object obj) { if (obj instanceof ConnectionId) { ConnectionId id = (ConnectionId) obj; return address.equals(id.address) && protocol == id.protocol && ticket == id.ticket; } return false; }

只有当Socket地址、用户信息UserGroupInformation、连接的协议类这三个属性的值相等时,才被认为是同一个ConnectionId实例。

  • Client.ParallelResults内部类

该内部类是用来收集在并行调用环境中结果的实体类,如下所示:

private static class ParallelResults { private Writable[] values; // 并行调用对应多次调用,对应多个返回值 private int size; // 并行调用返回值个数统计 private int count; // 并行调用次数 public ParallelResults(int size) { this.values = new Writable[size]; this.size = size; } /** 收集并行调用返回值 */ public synchronized void callComplete(ParallelCall call) { values[call.index] = call.value; // 存储返回值 count++; // 统计返回值个数 if (count == size) // 并行调用的多个调用完成 notify(); // 唤醒下一个实例 } }

  • Client.ParallelCall内部类

该内部类继承自上面的内部类Call,只是返回值使用上面定义的ParallelResults实体类来封装,如下所示:

private class ParallelCall extends Call { private ParallelResults results; private int index; public ParallelCall(Writable param, ParallelResults results, int index) { super(param); this.results = results; this.index = index; } /** 收集并行调用返回结果值 */ protected void callComplete() { results.callComplete(this); } }

  • Client.Connection内部类

该类是一个连接管理内部线程类,该内部类是一个连接线程,继承自Thread类。它读取每一个Call调用实例执行后从服务端返回的响应信息,并通知其他调用实例。每一个连接具有一个连接到远程主机的Socket,该Socket能够实现多路复用,使得多个调用复用该Socket,客户端收到的调用得到的响应可能是无序的。

该类定义的属性如下所示:

private InetSocketAddress server; // 服务端ip:port private ConnectionHeader header; // 连接头信息,该实体类封装了连接协议与用户信息UserGroupInformation private ConnectionId remoteId; // 连接ID private Socket socket = null; // 客户端已连接的Socket private DataInputStream in; private DataOutputStream out; private Hashtable<Integer, Call> calls = new Hashtable<Integer, Call>(); // 当前活跃的调用列表 private AtomicLong lastActivity = new AtomicLong();// 最后I/O活跃的时间 private AtomicBoolean shouldCloseConnection = new AtomicBoolean(); // 连接是否关闭 private IOException closeException; // 连接关闭原因

上面使用到了java.util.concurrent.atomic包中的一些工具,像AtomicLong、AtomicBoolean,这些类能够以原子方式更新其值,支持在单个变量上解除锁二实现线程的安全。这些类能够使用get方法读取volatile变量的内存效果,set方法可以设置对应变量的内存值。通过后面的代码可以看到该类工具类的使用。例如:

private void touch() { lastActivity.set(System.currentTimeMillis()); }

上面定义的calls集合,是用来保存当前活跃的调用实例,以键值对的形式保存,键是一个Call的id,值是Call的实例,因此,该类一定提供了向该集合中添加新的调用实例、移除调用实例等等操作,分别将方法签名列表如下:

/** * 向calls集合中添加一个<Call.id, Call> */ private synchronized boolean addCall(Call call); /* * 等待某个调用线程唤醒自己,可能开始如下操作: * 1、读取RPC响应数据 * 2、idle时间过长 * 3、被标记为应该关闭 * 4、客户端已经终止 */ private synchronized boolean waitForWork(); /* * 接收到响应(因为每次从DataInputStream in中读取响应信息只有一个,无需同步) */ private void receiveResponse(); /* * 关闭连接,需要迭代calls集合,清除连接 */ private synchronized void close();

可以看到,当每次调用touch方法的时候,都会将lastActivity原子变量设置为系统的当前时间,更新了变量的值。该操作是对多个线程进行互斥的,也就是每次修改lastActivity的值的时候,都会对该变量加锁,从内存中读取该变量的当前值,因此可能会出现阻塞的情况。

下面看一个Connection实例的构造实现:

public Connection(ConnectionId remoteId) throws IOException { this.remoteId = remoteId; // 远程服务端连接 this.server = remoteId.getAddress(); // 远程服务器地址 if (server.isUnresolved()) { throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName()); } UserGroupInformation ticket = remoteId.getTicket(); // 用户信息 Class<?> protocol = remoteId.getProtocol(); // 协议 header = new ConnectionHeader(protocol == null ? null : protocol.getName(), ticket); // 连接头信息 this.setName("IPC Client (" + socketFactory.hashCode() +") connection to " + remoteId.getAddress().toString() + " from " + ((ticket==null)?"an unknown user":ticket.getUserName())); this.setDaemon(true); // 并设置一个连接为后台线程 }

通过Collection实例的构造,可以看到,客户端所拥有的Connection实例,通过一个远程ConnectionId实例来建立到客户端到服务端的连接。接着看一下Connection线程类线程体代码:

public void run() { if (LOG.isDebugEnabled()) LOG.debug(getName() + ": starting, having connections " + connections.size()); while (waitForWork()) {// 等待某个连接实例空闲,如果存在则唤醒它执行一些任务 receiveResponse(); // 接收RPC响应 } close(); // 关闭 if (LOG.isDebugEnabled()) LOG.debug(getName() + ": stopped, remaining connections " + connections.size()); }

客户端Client类提供的最基本的功能就是执行RPC调用,其中,提供了两种调用方式,一种就是串行单个调用,另一种就是并行调用,分别介绍如下。首先是串行单个调用的实现方法call,如下所示:

/* * 执行一个调用,通过传递参数值param到运行在addr上的IPC服务器,IPC服务器基于protocol与用户的ticket来认证,并响应客户端 */ public Writable call(Writable param, InetSocketAddress addr, Class<?> protocol, UserGroupInformation ticket) throws InterruptedException, IOException { Call call = new Call(param); // 使用请求参数值构造一个Call实例 Connection connection = getConnection(addr, protocol, ticket, call); // 从连接池connections中获取到一个连接(或可能创建一个新的连接) connection.sendParam(call); // 向IPC服务器发送参数 boolean interrupted = false; synchronized (call) { while (!call.done) { try { call.wait(); // 等待IPC服务器响应 } catch (InterruptedException ie) { interrupted = true; } } if (interrupted) { // set the interrupt flag now that we are done waiting Thread.currentThread().interrupt(); } if (call.error != null) { if (call.error instanceof RemoteException) { call.error.fillInStackTrace(); throw call.error; } else { // local exception throw wrapException(addr, call.error); } } else { return call.value; // 调用返回的响应值 } } }

然后,就是并行调用的实现call方法,如下所示:

/* * 执行并行调用 * 每个参数都被发送到相关的IPC服务器,然后等待服务器响应信息 */ public Writable[] call(Writable[] params, InetSocketAddress[] addresses, Class<?> protocol, UserGroupInformation ticket) throws IOException { if (addresses.length == 0) return new Writable[0]; ParallelResults results = new ParallelResults(params.length); // 根据待调用的参数个数来构造一个用来封装并行调用返回值的ParallelResults对象 synchronized (results) { for (int i = 0; i < params.length; i++) { ParallelCall call = new ParallelCall(params[i], results, i); // 构造并行调用实例 try { Connection connection = getConnection(addresses[i], protocol, ticket, call); // 从连接池中获取到一个连接 connection.sendParam(call); // 发送调用参数 } catch (IOException e) { LOG.info("Calling "+addresses[i]+" caught: " + e.getMessage(),e); results.size--; // 更新统计数据 } } while (results.count != results.size) { try { results.wait(); // 等待一组调用的返回值(如果调用失败,会返回错误信息或null值,但与计数相关) } catch (InterruptedException e) {} } return results.values; // 调用返回一组响应值 } }

客户端可以根据服务端暴露的远程地址,来与服务器建立连接,并执行RPC调用,发送调用参数数据。

Server端有点复杂,后面单独分析其实现过程和机制。

你可能感兴趣的:(Hadoop-0.20.0源代码分析(10))