这篇文章主要写 Hadoop RPC Client 的设计 与实现 . 在讲解的时候, 以 ProtobufRpcEngine为实例, 然后分步进行叙述.
一.Client端架构
Client类只有一个入口, 就是call()方法。 代理类会调用Client.call()方法将RPC请求发送到远程服务器, 然后等待远程服务器的响应。 如果远程服务器响应请求时出现异常, 则在call()方法中抛出异常。
在 call 方法中先将远程调用信息封装成一个 Client.Call 对象(保存了完成标志、返回信息、异常信息等),然后得到 connection 对象用于管理 Client 与 Server 的 Socket 连接。
getConnection 方法中通过 setupIOstreams 建立与 Server 的 socket 连接,启动 Connection 线程,监听 socket 读取 server 响应。
call() 方法发送 RCP 请求。
call() 方法调用 Call.wait() 在 Call 对象上等待 Server 响应信息。
Connection 线程收到响应信息设置 Call 对象返回信息字段,并调用 Call.notify() 唤醒 call() 方法线程读取 Call 对象返回值。
二.Client端创建流程
下面是创建Client端的代码.协议采用proto, 所以产生的RpcEngine是 ProtobufRpcEngine. 所以接下来的文章是以ProtobufRpcEngine为蓝本进行源码分析.
我只放了Server端, 详细的代码请查看:
Hadoop3.2.1 【 HDFS 】源码分析 : RPC原理 [六] ProtobufRpcEngine 使用
public static void main(String[] args) throws Exception {
//1. 构建配置对象
Configuration conf = new Configuration();
//2. 设置协议的RpcEngine为ProtobufRpcEngine .
RPC.setProtocolEngine(conf, Server.MetaInfoProtocol.class,
ProtobufRpcEngine.class);
//3. 拿到代理对象
Server.MetaInfoProtocol proxy = RPC.getProxy(Server.MetaInfoProtocol.class, 1L,
new InetSocketAddress("localhost", 7777), conf);
//4. 构建发送请求对象
CustomProtos.GetMetaInfoRequestProto obj = CustomProtos.GetMetaInfoRequestProto.newBuilder().setPath("/meta").build();
//5. 将请求对象传入, 获取响应信息
CustomProtos.GetMetaInfoResponseProto metaData = proxy.getMetaInfo(null, obj);
//6. 输出数据
System.out.println(metaData.getInfo());
}
上面的代码, 主要是分三部分.
1.构建配置对象&设置RpcEngine引擎
2.获取代理对象
3.设置请求参数&通过代理对象请求.
4.处理结果
第一条和第二条,我就不细说了. 这个很简单,就是使用proto定义一个协议, 绑定到RPC.Builder的实现对象里面.
我们直接看这段, 获取代理对象.
Server.MetaInfoProtocol proxy = RPC.getProxy(Server.MetaInfoProtocol.class, 1L,
new InetSocketAddress("localhost", 7777), conf);
也是就是通过RPC.getProxy方法获取协议的代理对象.
/**
* Construct a client-side proxy object with the default SocketFactory
* @param <T>
*
* @param protocol 协议
* @param clientVersion 客户端的版本
* @param addr 请求地址
* @param conf 配置文件
* @return a proxy instance
* @throws IOException
*/
public static <T> T getProxy(Class<T> protocol,
long clientVersion,
InetSocketAddress addr, Configuration conf)
throws IOException {
return getProtocolProxy(protocol, clientVersion, addr, conf).getProxy();
}
接续看,这里面就一句getProtocolProxy,加断点一直跟进
/**
* Get a protocol proxy that contains a proxy connection to a remote server
* and a set of methods that are supported by the server
*
* @param protocol protocol
* @param clientVersion client's version
* @param addr server address
* @param ticket security ticket
* @param conf configuration
* @param factory socket factory
* @param rpcTimeout max time for each rpc; 0 means no timeout
* @param connectionRetryPolicy retry policy
* @param fallbackToSimpleAuth set to true or false during calls to indicate if
* a secure client falls back to simple auth
* @return the proxy
* @throws IOException if any error occurs
*/
public static <T> ProtocolProxy<T> getProtocolProxy(Class<T> protocol,
long clientVersion,
InetSocketAddress addr,
UserGroupInformation ticket,
Configuration conf,
SocketFactory factory,
int rpcTimeout,
RetryPolicy connectionRetryPolicy,
AtomicBoolean fallbackToSimpleAuth)
throws IOException {
if (UserGroupInformation.isSecurityEnabled()) {
SaslRpcServer.init(conf);
}
return getProtocolEngine(protocol, conf).getProxy(protocol, clientVersion,
addr, ticket, conf, factory, rpcTimeout, connectionRetryPolicy,
fallbackToSimpleAuth, null);
}
debug界面是这样的:
核心的是这句
return getProtocolEngine(protocol, conf).getProxy(protocol, clientVersion,
addr, ticket, conf, factory, rpcTimeout, connectionRetryPolicy,
fallbackToSimpleAuth, null);
首先通过 getProtocolEngine 获取RPC Engine [ ProtobufRpcEngine] ,
// return the RpcEngine configured to handle a protocol
static synchronized RpcEngine getProtocolEngine(Class<?> protocol,
Configuration conf) {
//从缓存中获取RpcEngine ,
// 这个是提前设置的
// 通过 RPC.setProtocolEngine(conf, MetaInfoProtocol.class,ProtobufRpcEngine.class);
RpcEngine engine = PROTOCOL_ENGINES.get(protocol);
if (engine == null) {
//通过这里 获取RpcEngine的实现类 , 这里我们获取的是 ProtobufRpcEngine.class
Class<?> impl = conf.getClass(ENGINE_PROP+"."+protocol.getName(),
WritableRpcEngine.class);
// impl : org.apache.hadoop.ipc.ProtobufRpcEngine
engine = (RpcEngine)ReflectionUtils.newInstance(impl, conf);
PROTOCOL_ENGINES.put(protocol, engine);
}
return engine;
}
然后再调用 ProtobufRpcEngine的 getProxy方法.将协议,客户端的版本号. socket地址, ticket , 配置文件, socket 的创建工厂对象[ StandardSocketFactory ] , PRC 服务的超时时间, connetion的重试策略,以及权限等信息,传入.
@Override
@SuppressWarnings("unchecked")
public <T> ProtocolProxy<T> getProxy(Class<T> protocol, long clientVersion,
InetSocketAddress addr, UserGroupInformation ticket, Configuration conf,
SocketFactory factory, int rpcTimeout, RetryPolicy connectionRetryPolicy,
AtomicBoolean fallbackToSimpleAuth, AlignmentContext alignmentContext)
throws IOException {
//构造一个实现了InvocationHandler接口的invoker 对象
// (动态代理机制中的InvocationHandler对象会在invoke()方法中代理所有目标接口上的 调用,
// 用户可以在invoke()方法中添加代理操作)
final Invoker invoker = new Invoker(protocol, addr, ticket, conf, factory,
rpcTimeout, connectionRetryPolicy, fallbackToSimpleAuth,
alignmentContext);
//然后调用Proxy.newProxylnstance()获取动态代理对象,并通过ProtocolProxy返回
return new ProtocolProxy<T>(protocol, (T) Proxy.newProxyInstance(
protocol.getClassLoader(), new Class[]{protocol}, invoker), false);
}
在getProxy 这个方法中. 主要是分两步,
1. 构造一个实现了InvocationHandler接口的invoker 对象 (动态代理机制中的InvocationHandler对象会在invoke()方法中代理所有目标接口上的 调用, 用户可以在invoke()方法中添加代理操作
2.调用Proxy.newProxylnstance()获取动态代理对象,并通过ProtocolProxy返回
我们先看Invoker的创建.
private Invoker(Class<?> protocol, InetSocketAddress addr,
UserGroupInformation ticket, Configuration conf, SocketFactory factory,
int rpcTimeout, RetryPolicy connectionRetryPolicy,
AtomicBoolean fallbackToSimpleAuth, AlignmentContext alignmentContext)
throws IOException {
this(protocol,
Client.ConnectionId.getConnectionId(addr, protocol, ticket, rpcTimeout, connectionRetryPolicy, conf), conf, factory);
this.fallbackToSimpleAuth = fallbackToSimpleAuth;
this.alignmentContext = alignmentContext;
}
主要是:
this(protocol, Client.ConnectionId.getConnectionId(addr, protocol, ticket, rpcTimeout, connectionRetryPolicy, conf), conf, factory);
我们先看
Client.ConnectionId.getConnectionId(addr, protocol, ticket, rpcTimeout, connectionRetryPolicy, conf), conf, factory)
这里会调用getConnectionId方法 构建一个Client.ConnectionId对象.
ConnectionId : 这个类 持有 请求地址 和 用户的ticketclient 连接 server 的唯一凭证 : [remoteAddress, protocol, ticket]
/**
* Returns a ConnectionId object.
* @param addr Remote address for the connection.
* @param protocol Protocol for RPC.
* @param ticket UGI
* @param rpcTimeout timeout
* @param conf Configuration object
* @return A ConnectionId instance
* @throws IOException
*/
static ConnectionId getConnectionId(InetSocketAddress addr,
Class<?> protocol, UserGroupInformation ticket, int rpcTimeout,
RetryPolicy connectionRetryPolicy, Configuration conf) throws IOException {
//构建重试策略
if (connectionRetryPolicy == null) {
//设置最大重试次数 默认值: 10
final int max = conf.getInt(
CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_KEY,
CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_DEFAULT);
// 设置重试间隔: 1 秒
final int retryInterval = conf.getInt(
CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_RETRY_INTERVAL_KEY,
CommonConfigurationKeysPublic
.IPC_CLIENT_CONNECT_RETRY_INTERVAL_DEFAULT);
//创建重试策略实例 RetryUpToMaximumCountWithFixedSleep
// 重试10次, 每次间隔1秒
connectionRetryPolicy = RetryPolicies.retryUpToMaximumCountWithFixedSleep(
max, retryInterval, TimeUnit.MILLISECONDS);
}
//创建ConnectionId :
// 这个类 持有 请求地址 和 用户的ticket
// client 连接 server 的唯一凭证 : [remoteAddress, protocol, ticket]
return new ConnectionId(addr, protocol, ticket, rpcTimeout,
connectionRetryPolicy, conf);
}
在getConnectionId这个方法里面会干两个事, 创一个重试策略 [RetryUpToMaximumCountWithFixedSleep]. 然后构建一个ConnectionId对象.
ConnectionId(InetSocketAddress address, Class<?> protocol,
UserGroupInformation ticket, int rpcTimeout,
RetryPolicy connectionRetryPolicy, Configuration conf) {
// 协议
this.protocol = protocol;
// 请求地址
this.address = address;
//用户 ticket
this.ticket = ticket;
//设置超时时间
this.rpcTimeout = rpcTimeout;
//设置重试策略 默认: 重试10次, 每次间隔1秒
this.connectionRetryPolicy = connectionRetryPolicy;
// 单位 10秒
this.maxIdleTime = conf.getInt(
CommonConfigurationKeysPublic.IPC_CLIENT_CONNECTION_MAXIDLETIME_KEY,
CommonConfigurationKeysPublic.IPC_CLIENT_CONNECTION_MAXIDLETIME_DEFAULT);
// sasl client最大重试次数 5 次
this.maxRetriesOnSasl = conf.getInt(
CommonConfigurationKeys.IPC_CLIENT_CONNECT_MAX_RETRIES_ON_SASL_KEY,
CommonConfigurationKeys.IPC_CLIENT_CONNECT_MAX_RETRIES_ON_SASL_DEFAULT);
//指示客户端将在套接字超时时进行重试的次数,以建立服务器连接。 默认值: 45
this.maxRetriesOnSocketTimeouts = conf.getInt(
CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_ON_SOCKET_TIMEOUTS_KEY,
CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_ON_SOCKET_TIMEOUTS_DEFAULT);
//使用TCP_NODELAY标志绕过Nagle的算法传输延迟。 默认值: true
this.tcpNoDelay = conf.getBoolean(
CommonConfigurationKeysPublic.IPC_CLIENT_TCPNODELAY_KEY,
CommonConfigurationKeysPublic.IPC_CLIENT_TCPNODELAY_DEFAULT);
// 从客户端启用低延迟连接 默认 false
this.tcpLowLatency = conf.getBoolean(
CommonConfigurationKeysPublic.IPC_CLIENT_LOW_LATENCY,
CommonConfigurationKeysPublic.IPC_CLIENT_LOW_LATENCY_DEFAULT
);
// 启用从RPC客户端到服务器的ping操作 默认值: true
this.doPing = conf.getBoolean(
CommonConfigurationKeys.IPC_CLIENT_PING_KEY,
CommonConfigurationKeys.IPC_CLIENT_PING_DEFAULT);
// 设置ping 操作的间隔, 默认值 : 1分钟
this.pingInterval = (doPing ? Client.getPingInterval(conf) : 0);
this.conf = conf;
}
回到之前的调用Invoker 另一个构造方法,但是入参会变.
/**
* This constructor takes a connectionId, instead of creating a new one.
*/
private Invoker(Class<?> protocol, Client.ConnectionId connId,
Configuration conf, SocketFactory factory) {
this.remoteId = connId;
this.client = CLIENTS.getClient(conf, factory, RpcWritable.Buffer.class);
this.protocolName = RPC.getProtocolName(protocol);
this.clientProtocolVersion = RPC
.getProtocolVersion(protocol);
}
这个是Invoker真正的构建方法,这里面会将刚刚构建好的ConnectionId 赋值给remoteId 字段.
并且创建一个Client 对象.
// 获取/创建 客户端
this.client = CLIENTS.getClient(conf, factory, RpcWritable.Buffer.class);
我们看下getClient 方法. 这里面会先尝试从缓存中获取client对象, 如果没有的话,在自己创建一个,并且加到缓存中.
为什么会放到缓存中呢??
当client和server再次通讯的时候,可以复用这个client .
/**
* 如果没有缓存的client存在的话
* 根据用户提供的SocketFactory 构造 或者 缓存一个IPC 客户端
*
* Construct & cache an IPC client with the user-provided SocketFactory
* if no cached client exists.
*
* @param conf Configuration
* @param factory SocketFactory for client socket
* @param valueClass Class of the expected response
* @return an IPC client
*/
public synchronized Client getClient(Configuration conf,
SocketFactory factory, Class<? extends Writable> valueClass) {
// Construct & cache client.
//
// The configuration is only used for timeout,
// and Clients have connection pools. So we can either
// (a) lose some connection pooling and leak sockets, or
// (b) use the same timeout for all configurations.
//
// Since the IPC is usually intended globally, notper-job, we choose (a).
//从缓存中获取Client
Client client = clients.get(factory);
if (client == null) {
//client在缓存中不存在, 创建一个.
client = new Client(valueClass, conf, factory);
//缓存创建的client
clients.put(factory, client);
} else {
//client的引用计数+1
client.incCount();
}
if (Client.LOG.isDebugEnabled()) {
Client.LOG.debug("getting client out of cache: " + client);
}
// 返回client
return client;
}
到这里 Invoker 对象就创建完了.
回到 ProtobufRpcEngine 的getProxy 方法 .
//然后调用Proxy.newProxylnstance()获取动态代理对象,并通过ProtocolProxy返回
return new ProtocolProxy<T>(protocol, (T) Proxy.newProxyInstance(
protocol.getClassLoader(), new Class[]{protocol}, invoker), false);
构建一个ProtocolProxy 对象返回
/**
* Constructor
*
* @param protocol protocol class
* @param proxy its proxy
* @param supportServerMethodCheck If false proxy will never fetch server
* methods and isMethodSupported will always return true. If true,
* server methods will be fetched for the first call to
* isMethodSupported.
*/
public ProtocolProxy(Class<T> protocol, T proxy,
boolean supportServerMethodCheck) {
this.protocol = protocol;
this.proxy = proxy;
this.supportServerMethodCheck = supportServerMethodCheck;
}
然后我们看Client端的第四步 , 根据proto协议,构建一个请求对象.
这个没啥可说的.是proto自动生成的,我们只是创建了一下而已.
//4. 构建发送请求对象
CustomProtos.GetMetaInfoRequestProto obj = CustomProtos.GetMetaInfoRequestProto.newBuilder().setPath("/meta").build();
然后就是Client端的第5步了将请求对象传入, 获取响应信息
//5. 将请求对象传入, 获取响应信息
CustomProtos.GetMetaInfoResponseProto metaData = proxy.getMetaInfo(null, obj);
Client端的最后一步输出响应信息.
//6. 输出数据
System.out.println(metaData.getInfo());
------------------华丽的分割线-------------------------------------------------------------------
咦,到这里有点懵, 请求server端的代码呢??? 请求怎么发出去的??? 怎么拿到响应信息的呢????
嗯嗯,是动态代理. ProtobufRpcEngine 的getProxy 方法 .
return new ProtocolProxy<T>(protocol, (T) Proxy.newProxyInstance(
protocol.getClassLoader(), new Class[]{protocol}, invoker), false);
主要是这个
(T) Proxy.newProxyInstance(
protocol.getClassLoader(), new Class[]{protocol}, invoker)
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {........................
}
这个方法的作用就是创建一个代理类对象,
它接收三个参数,我们来看下几个参数的含义:
loader : 一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载
interfaces: 一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。
h: 一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。
我们直接看 ProtobufRpcEngine#Invoker中的 invoke方法
/**
*
* ProtobufRpcEngine.Invoker.invoker() 方法主要做了三件事情:
* 1.构造请求头域,
* 使用protobuf将请求头序列化,这个请求头域 记录了当前RPC调用是什么接口的什么方法上的调用;
* 2.通过RPC.Client类发送请求头以 及序列化好的请求参数。
* 请求参数是在ClientNamenodeProtocolPB调用时就已经序列化好的,
* 调用Client.call()方法时,
* 需要将请求头以及请求参数使用一个RpcRequestWrapper对象封装;
* 3.获取响应信息,序列化响应信息并返回。
*
*
* This is the client side invoker of RPC method. It only throws
* ServiceException, since the invocation proxy expects only
* ServiceException to be thrown by the method in case protobuf service.
*
* ServiceException has the following causes:
* <ol>
* <li>Exceptions encountered on the client side in this method are
* set as cause in ServiceException as is.</li>
* <li>Exceptions from the server are wrapped in RemoteException and are
* set as cause in ServiceException</li>
* </ol>
*
* Note that the client calling protobuf RPC methods, must handle
* ServiceException by getting the cause from the ServiceException. If the
* cause is RemoteException, then unwrap it to get the exception thrown by
* the server.
*/
@Override
public Message invoke(Object proxy, final Method method, Object[] args)
throws ServiceException {
long startTime = 0;
if (LOG.isDebugEnabled()) {
startTime = Time.now();
}
// pb接口的参数只有两个,即RpcController + Message
if (args.length != 2) { // RpcController + Message
throw new ServiceException(
"Too many or few parameters for request. Method: ["
+ method.getName() + "]" + ", Expected: 2, Actual: "
+ args.length);
}
if (args[1] == null) {
throw new ServiceException("null param while calling Method: ["
+ method.getName() + "]");
}
// if Tracing is on then start a new span for this rpc.
// guard it in the if statement to make sure there isn't
// any extra string manipulation.
// todo 这个是啥
Tracer tracer = Tracer.curThreadTracer();
TraceScope traceScope = null;
if (tracer != null) {
traceScope = tracer.newScope(RpcClientUtil.methodToTraceString(method));
}
//构造请求头域,标明在什么接口上调用什么方法
RequestHeaderProto rpcRequestHeader = constructRpcRequestHeader(method);
if (LOG.isTraceEnabled()) {
LOG.trace(Thread.currentThread().getId() + ": Call -> " +
remoteId + ": " + method.getName() +
" {" + TextFormat.shortDebugString((Message) args[1]) + "}");
}
//获取请求调用的参数,例如RenameRequestProto
final Message theRequest = (Message) args[1];
final RpcWritable.Buffer val;
try {
//调用RPC.Client发送请求
val = (RpcWritable.Buffer) client.call(RPC.RpcKind.RPC_PROTOCOL_BUFFER,
new RpcProtobufRequest(rpcRequestHeader, theRequest), remoteId,
fallbackToSimpleAuth, alignmentContext);
} catch (Throwable e) {
if (LOG.isTraceEnabled()) {
LOG.trace(Thread.currentThread().getId() + ": Exception <- " +
remoteId + ": " + method.getName() +
" {" + e + "}");
}
if (traceScope != null) {
traceScope.addTimelineAnnotation("Call got exception: " +
e.toString());
}
throw new ServiceException(e);
} finally {
if (traceScope != null) traceScope.close();
}
if (LOG.isDebugEnabled()) {
long callTime = Time.now() - startTime;
LOG.debug("Call: " + method.getName() + " took " + callTime + "ms");
}
if (Client.isAsynchronousMode()) {
final AsyncGet<RpcWritable.Buffer, IOException> arr
= Client.getAsyncRpcResponse();
final AsyncGet<Message, Exception> asyncGet
= new AsyncGet<Message, Exception>() {
@Override
public Message get(long timeout, TimeUnit unit) throws Exception {
return getReturnMessage(method, arr.get(timeout, unit));
}
@Override
public boolean isDone() {
return arr.isDone();
}
};
ASYNC_RETURN_MESSAGE.set(asyncGet);
return null;
} else {
return getReturnMessage(method, val);
}
}
艾玛,这个有点长啊.
挑几点重要的说.
- 构造请求头域,标明在什么接口上调用什么方法
- 获取请求调用的参数
- 调用RPC.Client发送请求
- 获取响应信息
下面分别来说:
1.构造请求头域,标明在什么接口上调用什么方法
//构造请求头域,标明在什么接口上调用什么方法
RequestHeaderProto rpcRequestHeader = constructRpcRequestHeader(method);
2.获取请求调用的参数
//获取请求调用的参数,这个是才client端代码就创建好的,通过参数传进来的.
// 例如 GetMetaInfoRequestProto
final Message theRequest = (Message) args[1];
3.调用RPC.Client发送请求
//调用RPC.Client发送请求
val = (RpcWritable.Buffer) client.call(RPC.RpcKind.RPC_PROTOCOL_BUFFER,
new RpcProtobufRequest(rpcRequestHeader, theRequest), remoteId,
fallbackToSimpleAuth, alignmentContext);
4. 获取响应信息
Client 获取响应信息, 不过是同步还是异步获取响应信息,都会调用这个方法: getReturnMessage(method, val);
getReturnMessage(method, val);
困死了,先发出来, invoker 后面的我再补充. .........