RPC的层次感
作为RPC调用来说,更多的还需要考虑用户体验度,对于用户来说,就是面向接口调用。
然而实际上一个RPC的端点,可能既需要作为consumer去调用远程的provider的服务,而自身也作为一个provider提供着一些服务,作为这个端点来说,它对外提供的服务,对它自身肯定是一些本地的方法调用。
所以我们在设计RPC通信时,调用的时候,不单单需要考虑只有远程调用,而应该首先去判断一下这个服务的实现方法在本地有没有,如果有的话,没必要再走RPC最终再调到自己身上来。
public static <T>T proxyGet(Class<T> interfaceInfo){
//实现各个版本的动态代理。。。。
ClassLoader loader = interfaceInfo.getClassLoader();
Class<?>[] methodInfo = {interfaceInfo};
//TODO LOCAL REMOTE 实现: 用到dispatcher 直接给你返回,还是本地调用的时候也代理一下
Dispatcher dis =Dispatcher.getDis();
return (T) Proxy.newProxyInstance(loader, methodInfo, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object res=null;
Object o = dis.get(interfaceInfo.getName());
if(o== null){
//就要走rpc
String name = interfaceInfo.getName();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
//TODO rpc 就像小火车拉货 content是service的具体数据,但是还需要header层完成IO传输的控制
MyContent content = new MyContent();
content.setArgs(args);
content.setName(name);
content.setMethodName(methodName);
content.setParameterTypes(parameterTypes);
//TODO 未来的小火车可能会变
/**
* 1,缺失了注册发现,zk
* 2,第一层负载面向的provider
* 3,consumer 线程池 面向 service;并发就有木桶,倾斜
* serviceA
* ipA:port
* socket1
* socket2
* ipB:port
*/
CompletableFuture resF = ClientFactory.transport(content);
res = resF.get();//阻塞的
}else{
//就是local
//插入一些插件的机会,做一些扩展
System.out.println("lcoal FC....");
Class<?> clazz = o.getClass();
try {
Method m = clazz.getMethod(method.getName(), method.getParameterTypes());
res = m.invoke(o, args);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return res;
//TODO 应该在service的方法执行的时候确定是本地的还是远程的,用到dispatcher来区分下
}
});
}
上面的代码就是一个实现的案例,就是先判断一下是否可以走本地调用,如果本地不存在, 再走RPC远程调用。
RPC这种调用过程,到底服务端可以接受多大的并发?
首先我们要明白,建立连接这件事情没什么重量,因此可以建立非常多的并发连接。
但建立连接后的事情才是真正的瓶颈所在,这里有2个维度:
1、对接受连接的io需要进行系统调用的读取,这个读取是一个从内核到用户空间的搬运过程,也是线性的。(重)
2、读到的数据,对其的处理,也就是计算能力
除此之外,还要考虑,数据在经过传递,首先到达的是系统的内核积压队列buffer中,这也意味着,资源是有限的。这个资源的多大也决定着并发能力。
而优化的方式,一般是2点:
1、在通信协议层面减轻负担,简单来说,字符串代表的意义使用位来代替,让请求头尽可能的小。
2、整体来看,要选用好的压缩算法,让传输包越小越好,这样虽然会增加CPU的负担,但CPU还是要比io快的多了
整体RPC的流向以及优化思考
进阶一些,实际上真实框架级别的RPC调用,还需要一个重要角色就是注册中心来实现解耦,provider注册服务到注册中心,consumer通过服务发现在注册中心找到对应的服务。
但这里有个点,调用者要调用的远程服务可能同时有多个提供者都提供这个服务的调用,那么此时就形成一个1:N的比例,这里可以做负载均衡选取服务节点。
然后,关于调用的形式,还可以借用一些第三方框架(如spring),来更加优雅的通过注解或者xml,来实现这个动态代理的远程调用过程。
调用的过程最开始其实就是一个封装数据包的过程,这牵扯到要使用的通信协议,并根据通信协议的不同,而决定了下层通信IO框架模型的不同。
通信协议分为有状态和无状态的。
有状态协议
有状态的协议肯定是需要consumer端和provider端同时去协商保留通信过程中的一些信息,因此这个过程的发送和接受都是可以异步的,连接也是可以共享使用的,因为有状态的通信,使得发送和接受总能通过传输中的内容来正确的匹配到对方。
无状态协议
说起无状态协议,我们很容易想到的就是HTTP,那HTTP可以完成这种RPC的调用吗?
传统的web通信一般都是采用的HTTP协议,我们都知道想要保证消息的标识性,需要server端通过session来额外存储一些信息,来让client多次请求之间产生了状态关联。
但其实本质来说,两端说是使用HTTP协议,只是说严格遵从HTTP协议的约束,但实际是否可以产生有状态的动作,还是可以通过两端控制来完成的,毕竟,HTTP本质也是一个TCP连接,它只不过是一个协议。
只要保障发送和返回是一个原子操作,然后基于http的keepalived概念,也可以双方约定不断开连接,这样的话,一次发送和返回的原子操作后,这个连接还是可以被复用给其他的client的,只不过是串行使用连接。
最终,我们发现,其实HTTP也可以完成RPC这种有状态的调用。
但,现实来说,很多时候可能provider端是你无法轻易触碰修改的,是不可控的,那这时候,provider就没有为这个有状态协议做出什么记忆操作,那这时候,有状态的通信也就无法维持了。
所以回到原点:有状态的协议肯定是需要consumer端和provider端同时去协商保留通信过程中的一些信息 ,否则就不谈了。