RPC原理和实现

背景简介

RPC(Remote Procedure Call) 远程服务调用是现在常用的技术,用于多个服务间的互相调用。代码实现示例simple-rpc。至于为什么要拆成多个服务,有各种各样的解释和原因,例如解耦、独立发布部署等好处。拆分成服务之后大家各自管理自己的数据和服务,经常会有需要别人数据和服务的需求,不能像整个一体(monothetic)应用时可以直接获取方法调用,需要通过网络传输调用其他机器上的服务,这样的跨网络、进程通信手写起来非常繁琐易出错。所以出现了很多RPC框架,RPC框架的目标是让我们就想调用本地方法一样调用远程服务并且在性能、易用性等方面有一定需求。并且其他服务可能和自己使用的编程语言不相同,这时就有跨语言调用的情况。常见的RPC框架有thrift、grpc、dubbo等。

RPC原理浅析

RPC 服务方通过 RpcServer 去导出(export)远程接口方法,而客户方通过 RpcClient 去引入(import)远程接口方法。客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理RpcProxy 。代理封装调用信息并将调用转交给RpcInvoker 去实际执行。在客户端的RpcInvoker 通过连接器RpcConnector 去维持与服务端的通道RpcChannel,并使用RpcProtocol 执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。

RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用RpcProtocol 执行协议解码(decode)。解码后的调用信息传递给RpcProcessor 去控制处理调用过程,最后再委托调用给RpcInvoker 去实际执行并返回调用结果。

上面我们进一步拆解了 RPC 实现结构的各个组件组成部分,下面我们详细说明下每个组件的职责划分。

1. RpcServer  
   负责导出(export)远程接口  
2. RpcClient  
   负责导入(import)远程接口的代理实现  
3. RpcProxy  
   远程接口的代理实现  
4. RpcInvoker  
   客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回  
   服务方实现:负责调用服务端接口的具体实现并返回调用结果  
5. RpcProtocol  
   负责协议编/解码  
6. RpcConnector  
   负责维持客户方和服务方的连接通道和发送数据到服务方  
7. RpcAcceptor  
   负责接收客户方请求并返回请求结果  
8. RpcProcessor  
   负责在服务方控制调用过程,包括管理调用线程池、超时时间等  
9. RpcChannel  
   数据传输通道

序列化和反序列化

在应用中,如Java程序都是使用Java对象进行操作,最终传输到另一个台机器上,需要通过网络传输,但是网络传输只识别字节流,所以需要在应用数据和字节码进行转换的工具,一般讲这个转换过程称为编解码或序列化与反序列化。编码(Encode)或序列化(Serialize)的过程指从应用对象转化到字节流的过程,对应的工具也叫编码器(Encoder),具体编码成什么样的字节流是由对应的编码算法、工具决定的。反过来,由字节流转换为应用对象的过程叫做解码或反序列化。常用的编码工具有protobuf、kryo、Java自带的序列化和反序列化、thrift序列化等。再者我们可以将对象转换成json、xml格式字符串,然后将字符串通过字符编码,如UTF-8等编码方式进行编解码。选择序列化工具时,需要考虑是否有跨语言支持、序列化后的数据大小、性能等因素。序列化比较

服务的注册发布和监听

类似于域名访问的问题,我们无需记住一个http服务后的服务器是哪些,它们的变更对我们都是透明的。对应RPC服务,经常需要使用集群来保证服务的稳定性和共同提高系统的性能。为此需要提供一个注册中心,当服务器启动时进行服务注册,服务器宕机时注册中心能够检测到并将其从服务注册中心删除。客户端要访问一个服务时先到注册中心获取服务列表,缓存到本地,然后建立连接进行访问,并且当服务列表发生变化时会收到通知并修改本地缓存。

注册发布

服务路由

有注册中心后,调用方调用服务提供者时可以动态的获取调用方的地址等信息进行选择,类似域名的机制。这样增加了一层抽象,避免了写死ip等问题。又一次说明了Any problem in computer science can be solved by adding another level of indirection。当有多个服务提供者时,调用方需要在其中选择一个进行调用,常见的调用策略有随机、Round-Robin(轮询)、权重比例等。采取权重方式可以通过服务调用的耗时、异常数量进行动态调用权重,也可以进行人工调整。当我们需要更复杂的控制策略是,可以通过脚本编写策略,并可以动态修改。
通用的负载均衡又可以分为客户端的负载均衡和中间代理的负载均衡,客户端的负载均衡有客户端获取服务端列表,中间代理方式时客户端只需要连接一个代理服务器,有代理进行转发,可以类比nginx的作用。

IO调用方式

nio和bio

bio指的是传统的阻塞io,在Java中使用方式是Socket、ServerSocket、InputStream、OutputStream的组合。当读数据是,如果没有数据可读,该线程会被切换为阻塞状态,直到数据可读等待处理器调度运行,会进行两次上下文切换和两次用户态内核态切换,并且这样一个线程同时只能处理一个连接,线程是比较宝贵的资源,有线程协调加锁同步、上下文切换、创建和销毁线程、线程调度等开销,并且JVM中每个线程都会有1MB左右的栈大小,这样一线程一连接的方式无法应对单机数万的情况。Nio指non blocking io,即非阻塞io,当数据不可读时会返回一个错误而不是阻塞,在Java中,常用Selector、SocketChannel、ServerSocketChannel、ByteBuffer的组合实现nio服务,在一个Selector上可以监听多个连接的是否可读、可写、可连接等状态,这样一个线程就可以同时处理很多个连接,能够提高系统连接能力。

同步和异步

同步指发出一个请求后是否阻塞并一直等待结果返回,而异步可以在发送请求后先去执行其他任务,在一段时间后再获取结果或通过注册监听器设置回调。在Java中一般是通过Future或者一些Listener来实现异步调用。如ExecutorService.submit()方法返回一个Future,调用Future的get时会阻塞,可以在get时设置超时时间。Guava和Netty中的Future实现可以设置Listener在结果成功或失败时进行回调。

实现

下面利用一些好用的框架帮助我们快速的实现一个RPC。 源代码在 simple-rpc

  • netty 负责数据传输部分,netty作为异步事件驱动的高性能IO框架,使用方便久经考验,比手工编写nio代码方便不易出错。
  • kryo或protostuff 负责序列化和反序列化。google的 protobuf需要编写IDL文件然后生成,好处是能够生成各个语言的代码并且优化的比较。但是开发起来很繁琐,每次都编写修改IDL文件并生成有些痛苦。
  • zookeeper是一个分布式协调框架,可以作为一些配置数据的协调同步等。在我们的RPC框架中用作注册中心用来提供服务的注册和服务发现功能。其他类似功能的有consuletcd等,这里有它们之间的比较

请求和返回的抽象

@Data
public class Request {
private long requestId;
private Class<?> clazz;
private String method;
private Class<?>[] parameterTypes;
private Object[] params;
private long requestTime;
}
@Data
public class Response {
private long requestId;
private Object response;
}

编解码部分

public class RequestCodec extends ByteToMessageCodec<Request>{
protected void encode(ChannelHandlerContext ctx, Request msg, ByteBuf out) throws Exception {
byte[] bytes = Serializer.serialize(msg);
int length = bytes.length;
out.writeInt(length);
ByteBuf byteBuf = out.writeBytes(bytes);
}
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int length = in.readInt();
byte[] buffer = new byte[length];
in.readBytes(buffer);
Request request = Serializer.deserialize(Request.class, buffer);
out.add(request);
}
}
public class ResponseCodec extends ByteToMessageCodec<Response>{
private static final Logger LOGGER = LoggerFactory.getLogger(ResponseCodec.class);
protected void encode(ChannelHandlerContext ctx, Response msg, ByteBuf out) throws Exception {
LOGGER.info("Encode {}", msg);
byte[] bytes = Serializer.serialize(msg);
int length = bytes.length;
out.writeInt(length);
ByteBuf byteBuf = out.writeBytes(bytes);
}
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int length = in.readInt();
byte[] buffer = new byte[length];
in.readBytes(buffer);
Response response = Serializer.deserialize(Response.class, buffer);
out.add(response);
LOGGER.info("Decode Result: {}", response);
}
}

Client

public class RpcClientHandler extends SimpleChannelInboundHandler<Response>{
private static final Logger LOGGER = LoggerFactory.getLogger(RpcClientHandler.class);
protected void channelRead0(ChannelHandlerContext ctx, Response msg) throws Exception {
LOGGER.info("Receive {}", msg);
BlockingQueue<Response> blockingQueue = RpcClient.responseMap.get(msg.getRequestId());
blockingQueue.put(msg);
}
}
public class RpcClient {
private static AtomicLong atomicLong = new AtomicLong();
private String serverIp;
private int port;
private boolean started;
private Channel channel;
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
public static ConcurrentMap<Long, BlockingQueue<Response>> responseMap = new ConcurrentHashMap<Long, BlockingQueue<Response>>();
public RpcClient(String serverIp, int port) {
this.serverIp = serverIp;
this.port = port;
}
public void init() {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class)
.group(eventLoopGroup)
.handler(new ChannelInitializer<Channel>() {
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO))
.addLast(new ResponseCodec())
.addLast(new RpcClientHandler())
.addLast(new RequestCodec())
;
}
});
try {
ChannelFuture f = bootstrap.connect(serverIp, port).sync();
this.channel = f.channel();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public Response sendMessage(Class<?> clazz, Method method, Object[] args) {
Request request = new Request();
request.setRequestId(atomicLong.incrementAndGet());
request.setMethod(method.getName());
request.setParams(args);
request.setClazz(clazz);
request.setParameterTypes(method.getParameterTypes());
this.channel.writeAndFlush(request);
BlockingQueue<Response> blockingQueue = new ArrayBlockingQueue<Response>(1);
responseMap.put(request.getRequestId(), blockingQueue);
try {
return blockingQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}
public <T> T newProxy(final Class<T> serviceInterface) {
Object o = Proxy.newProxyInstance(RpcClient.class.getClassLoader(), new Class[]{serviceInterface}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return sendMessage(serviceInterface, method, args).getResponse();
}
});
return (T) o;
}
public void destroy() {
try {
this.channel.close().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}

Server

public class RpcServer {
private static final Logger LOGGER = LoggerFactory.getLogger(RpcServer.class);
private String ip;
private int port;
private boolean started = false;
private Channel channel;
private Object serviceImpl;
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workerGroup = new NioEventLoopGroup();
public RpcServer(int port, Object serviceImpl) {
this.port = port;
this.serviceImpl = serviceImpl;
}
public void init() {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(new RequestCodec())
.addLast(new RpcServerHandler(serviceImpl))
.addLast(new ResponseCodec())
;
}
});
try {
ChannelFuture sync = bootstrap.bind(port).sync();
LOGGER.info("Server Started At {}", port);
started = true;
this.channel = sync.channel();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class RpcServerHandler extends SimpleChannelInboundHandler<Request> {
private Object service;
public RpcServerHandler(Object serviceImpl) {
this.service = serviceImpl;
}
protected void channelRead0(ChannelHandlerContext ctx, Request msg) throws Exception {
String methodName = msg.getMethod();
Object[] params = msg.getParams();
Class<?>[] parameterTypes = msg.getParameterTypes();
long requestId = msg.getRequestId();
Method method = service.getClass().getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
Object invoke = method.invoke(service, params);
Response response = new Response();
response.setRequestId(requestId);
response.setResponse(invoke);
ctx.pipeline().writeAndFlush(response);
}
}

序列化部分

public class Serializer {
public static byte[] serialize(Object obj){
RuntimeSchema schema = RuntimeSchema.createFrom(obj.getClass());
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
}
public static <T> T deserialize(Class<T> clazz, byte[] bytes) {
try {
T t = clazz.newInstance();
RuntimeSchema schema = RuntimeSchema.createFrom(clazz);
ProtostuffIOUtil.mergeFrom(bytes, t, schema);
return t;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
# SimpleRpc使用示例
*需要先启动一个zookeeper作为服务注册发现中心*

// 服务接口
public interface IHello {
`
String say(String hello);

int sum(int a, int b);
int sum(Integer a, Integer b);

}
// 服务实现
public class HelloImpl implements IHello {
public String say(String hello) {
return “return “ + hello;
}

public int sum(int a, int b) {
    return a + b;
}

public int sum(Integer a, Integer b) {
    return a + b * 3;
}

}

// 客户端代码
// beanJavaConfig方式
@Bean
public CountService countService() {
RpcClientWithLB rpcClientWithLB = new RpcClientWithLB(“fyes-counter”);
rpcClientWithLB.setZkConn(“127.0.0.1:2181”);
rpcClientWithLB.init();
CountService countService = rpcClientWithLB.newProxy(CountService.class);
return countService;
}

// 服务端发布
// xml配置方式




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章