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配置方式




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