RPC泛化调用普及&应用(看完还不懂的可以用钱砸死我)

一、概念镇场子,场景知其然
1.什么是泛化调用?
泛化调用是指在调用方没有服务方提供的API(SDK)的情况下,对服务方进行调用,并且可以拿到调用结果。

2.什么时候会用到泛化调用?

测试集成平台

我们要搭建一个统一的测试平台,可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值,在线测试自己发布的RPC服务。这时我们就有一个问题要解决,我们搭建统一的测试平台实际上是作为各个RPC服务的调用端,而在RPC框架的使用中,调用端是需要依赖服务提供方提供的接口API的,而统一测试平台不可能依赖所有服务提供方的接口API。我们不能因为每有一个新的服务发布,就去修改平台的代码以及重新上线。这时我们就需要让调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起RPC调用。

网关服务

我们要搭建一个轻量级的服务网关,可以让各个业务方用HTTP的方式,通过服务网关调用其它服务。这时就有与场景一相同的问题,服务网关要作为所有RPC服务的调用端,是不能依赖所有服务提供方的接口API的,也需要调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起RPC调用。

3.目前市面上都有哪些熟知的框架使用泛化调用
是个RPC框架都会使用。外部主要代表有:阿里的Dubbo【一搜泛化调用立马蹦出来的就是它】;某公司内部:Thrift。

PS:这几年吵得很火的Serverless也会使用

二、小试牛刀,上手撸
1.某公司Thrift为例子
拉横幅:使用此功能,建议服务端和调用端使用thrift 制定版本以上

【服务方】(无需改动,正常配置即可)

Provider的XML方式

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    <bean id="serviceProcessor" class="com.xxx.GenericImpl">
    </bean>
    <bean id="serverPublisher" class="com.xxx.ThriftServerPublisher"
          init-method="publish" destroy-method="destroy">
        <property name="appKey" value="com.xxx.benchmark"/>
        <property name="port" value="9998"/>
        <property name="serviceInterface" value="com.xxx.Generic"/>
        <property name="serviceImpl" ref="serviceProcessor"/>
    </bean>
</beans>

Provider的实现

public class GenericImpl implements Generic.Iface {
    private static final Logger logger = LoggerFactory.getLogger(GenericImpl.class);
    @Override
    public void echo1() throws TException {
        logger.info("echo1");
    }
    @Override
    public String echo2(String message) throws TException {
        logger.info("echo2");
        return message;
    }
    @Override
    public SubMessage echo3(SubMessage message) throws TException {
        logger.info("echo3");
        return message;
    }
}

【调用方配置】

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="clientProxy" class="com.xxx.ThriftClientProxy" destroy-method="destroy">
        <property name="timeout" value="1000"/>
        <property name="appKey" value="xxx"/>
        <property name="remoteAppkey" value="com.xxx.benchmark"/>	
      	<!-- 实际的服务接口名 -->
        <property name="genericServiceName" value="com.xxx.Generic"/> 
        <property name="remoteServerPort" value="9998"/>
      	<!-- 目前只支持json、json-common、json-simple -->
        <property name="generic" value="json"/> 
    </bean>
</beans>

public class GenericClient {
    private static ClassPathXmlApplicationContext clientBeanFactory;

    private static GenericService client;

    public static void main(String[] args) throws TException {
        clientBeanFactory = new ClassPathXmlApplicationContext("generic.xml");
        client = clientBeanFactory.getBean(GenericService.class);
        testEcho1();
				testEcho2();
				testEcho3();
        clientBeanFactory.destroy();
        System.exit(0);
    }
}

方式二:API方式

//声明
ThriftClientProxy clientProxy = new ThriftClientProxy();
clientProxy.setAppKey("com.xxx.Client");
clientProxy.setRemoteAppkey("com.xxx.benchmark");
clientProxy.setGenericServiceName("com.xxx.Generic");
clientProxy.setRemoteServerPort(9998);
clientProxy.setFilterByServiceName(true);
clientProxy.setGeneric("json");
clientProxy.afterPropertiesSet();//触发初始化
GenericService genericClient = clientProxy.getObject();

//echo1
List<String> paramTypes = new ArrayList<String>();

List<String> paramValues = new ArrayList<String>();

String result = genericService.$invoke("echo1", paramTypes, paramValues);
System.out.println(result);

//echo2
List<String> paramTypes = new ArrayList<String>();
paramTypes.add("java.lang.String");

List<String> paramValues = new ArrayList<String>();
String expected = JacksonUtils.serialize("hello world");
paramValues.add(expected);

String result = genericService.$invoke("echo2", paramTypes, paramValues);
System.out.println(result);
assert(expected.equals(result));
  
//echo3  
List<String> paramTypes = new ArrayList<String>();
paramTypes.add("com.xxx.SubMessage");

List<String> paramValues = new ArrayList<String>();
SubMessage subMessage = new SubMessage();
subMessage.setId(1);
subMessage.setValue("hello world");
String expected = JacksonUtils.serialize(subMessage);
paramValues.add(expected);

String result = genericService.$invoke("echo3", paramTypes, paramValues);
System.out.println(result);
assert (expected.equals(result));

【解释分析】

调用方发送的数据和服务端返回的数据格式是在json格式的基础上做了定制。

具体用法:

在发送前将参数用JacksonUtils的序列化方法进行序列化,
返回的结果用JacksonUtils的解序列化方法即可,具体参考下文的示例

可选项序列化方法说明注意
jsonJacksonUtils.serialize JacksonUtils.deserialize服务端返回定制化的json,客户端需要传递定制化的json
json-commonJacksonUtils.serialize JacksonUtils.simpleDeserialize服务端返回普通的json,客户端需要传递定制化的json普通的json可以忽略setXX字段,但是:若自动生成类的字段是private(编译IDL时使用了private-members),将无法忽略
json-simpleJacksonUtils.serialize JacksonUtils.simpleDeserialize服务端返回普通的json,客户端只需传递普通的json同上

2.外部阿里为例子
Provider端

public class DemoServiceImpl implements DemoService {
    public List<String> getPermissions(Long id) {
        List<String> demo = new ArrayList<String>();
        demo.add(String.format("Permission_%d", id - 1));
        demo.add(String.format("Permission_%d", id));
        demo.add(String.format("Permission_%d", id + 1));
        return demo;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo
       http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
    <!--定义了提供方应用信息,用于计算依赖关系;在 dubbo-admin 或 dubbo-monitor 会显示这个名字,方便辨识-->
    <dubbo:application name="demotest-provider" owner="programmer" organization="dubbox"/>
    <!--使用 zookeeper 注册中心暴露服务,注意要先开启 zookeeper-->
    <dubbo:registry address="zookeeper://localhost:2181"/>
    <!-- 用dubbo协议在20880端口暴露服务 -->
    <dubbo:protocol name="dubbo" port="20880" />
    <!--使用 dubbo 协议实现定义好的 api.PermissionService 接口-->
    <dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" protocol="dubbo"/>
    <!--具体实现该接口的 bean-->  
    <bean id="demoService" class="com.alibaba.dubbo.demo.impl.DemoServiceImpl"/>
</beans>

调用段

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
      
    <dubbo:application name="demotest-consumer" owner="programmer" organization="dubbox"/>
    <!--向 zookeeper 订阅 provider 的地址,由 zookeeper 定时推送-->
    <dubbo:registry address="zookeeper://localhost:2181"/>
    <!--使用 dubbo 协议调用定义好的 api.PermissionService 接口-->
    <dubbo:reference id="permissionService" interface="com.alibaba.dubbo.demo.DemoService" generic="true"/>
</beans>

// 方式1
public class Consumer {
    public static void main(String[] args) {
        /Spring泛化调用/  
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("consumer.xml");
        context.start();
        System.out.println("consumer start");
        GenericService demoService = (GenericService) context.getBean("permissionService");
        System.out.println("consumer");
        Object result = demoService.$invoke("getPermissions", new String[] { "java.lang.Long" }, new Object[]{ 1L });
        System.out.println(result);
    }
}

//方式2
public class Consumer {
    public static void main(String[] args) {
        // 普通编码配置方式  
        ApplicationConfig application = new ApplicationConfig();  
        application.setName("dubbo-consumer");  
  
        // 连接注册中心配置  
        RegistryConfig registry = new RegistryConfig();  
        registry.setAddress("zookeeper://127.0.0.1:2181");  
  
        ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();  
        reference.setApplication(application);  
        reference.setRegistry(registry);  
        reference.setInterface("com.alibaba.dubbo.demo.DemoService");  
        reference.setGeneric(true); // 声明为泛化接口  
  
        ReferenceConfigCache cache = ReferenceConfigCache.getCache();  
        GenericService genericService = cache.get(reference);  
  
        // 基本类型以及Date,List,Map等不需要转换,直接调用  
        Object result = genericService.$invoke("getPermissions", new String[] { "java.lang.Long" }, new Object[] { 1L });  
        System.out.println(result);
    }
}

三、挖穿实现原理【以Dubbo为例子】

1.原理图

+-------------------------------------------+               +-------------------------------------------+
|  consumer 端                               |               | provider 端                                |
|                                           |               |                                           |
|                                           |               |                                           |
|                                           |               |                                           |
|                                           |               |                                           |
|                    +------------------+   |               |       +--------------+                    |
|                    |GenericImplFilter |   |  Invocation   |       |GenericFilter |                    |
|             +----> |                  +-------------------------> |              |                    |
|             |      +------------------+   |               |       +--------------+                    |
| +-----------+                             |               |                      |    +-----------+   |
| |           |                             |               |                      |    |           |   |
| |Client     |                             |               |                      +--> | Service   |   |
| |           |                             |               |                           |           |   |
| +-----------+                             |               |                           +-------+---+   |
|                                           |               |                                   |       |
|      ^             +------------------+   |               |       +--------------+            |       |
|      |             |GenericImplFilter |   |               |       |GenericFilter | <----------+       |
|      +-------------+                  | <-------------------------+              |                    |
|                    +------------------+   |               |       +--------------+                    |
|                                           |               |                                           |
|                                           |               |                                           |
|                                           |               |                                           |
|                                           |               |                                           |
+-------------------------------------------+               +-------------------------------------------+

2.简化的原理

调用端

@Activate(group = CommonConstants.CONSUMER, value = GENERIC_KEY, order = 20000)
public class GenericImplFilter extends ListenableFilter {

    private static final Logger logger = LoggerFactory.getLogger(GenericImplFilter.class);

    private static final Class<?>[] GENERIC_PARAMETER_TYPES = new Class<?>[]{String.class, String[].class, Object[].class};

    public GenericImplFilter() {
        super.listener = new GenericImplListener();
    }

    @Override
    public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {
        String generic = invoker.getUrl().getParameter(GENERIC_KEY);
        if ((invocation.getMethodName().equals($INVOKE) || invocation.getMethodName().equals($INVOKE_ASYNC))
                && invocation.getArguments() != null
                && invocation.getArguments().length == 3
                && ProtocolUtils.isGeneric(generic)) {

            invocation.setAttachment(
                    GENERIC_KEY, invoker.getUrl().getParameter(GENERIC_KEY));
        }
        return invoker.invoke(invocation);
    }

    static class GenericImplListener implements Listener {
        @Override
        public void onResponse(Result appResponse, Invoker invoker, Invocation invocation) {

        }

        @Override
        public void onError(Throwable t, Invoker invoker, Invocation invocation) {

        }
    }

}

服务端

@Activate(group = CommonConstants.PROVIDER, order = -20000)
public class GenericFilter extends ListenableFilter {

    public GenericFilter() {
        super.listener = new GenericListener();
    }

    @Override
    public Result invoke(Invoker invoker, Invocation inv) throws RpcException {
        if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
                && inv.getArguments() != null
                && inv.getArguments().length == 3
                && !GenericService.class.isAssignableFrom(invoker.getInterface())) {
            String name = ((String) inv.getArguments()[0]).trim();
            String[] types = (String[]) inv.getArguments()[1];
            Object[] args = (Object[]) inv.getArguments()[2];
            try {
                // 找到调用的接口
                Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
                Class[] params = method.getParameterTypes();
                if (args == null) {
                    args = new Object[params.length];
                }
                String generic = inv.getAttachment(GENERIC_KEY);

                if (StringUtils.isBlank(generic)) {
                    generic = RpcContext.getContext().getAttachment(GENERIC_KEY);
                }

                if (StringUtils.isEmpty(generic)
                        || ProtocolUtils.isDefaultGenericSerialization(generic)) {
                    // 将PojoUtils 转换的简单对象 转换为复杂的对象 
                    args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
                }
                return invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));
            } catch (NoSuchMethodException e) {
                throw new RpcException(e.getMessage(), e);
            } catch (ClassNotFoundException e) {
                throw new RpcException(e.getMessage(), e);
            }
        }
        return invoker.invoke(inv);
    }

    static class GenericListener implements Listener {

        @Override
        public void onResponse(Result appResponse, Invoker invoker, Invocation inv) {
            if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
                    && inv.getArguments() != null
                    && inv.getArguments().length == 3
                    && !GenericService.class.isAssignableFrom(invoker.getInterface())) {

                String generic = inv.getAttachment(GENERIC_KEY);
                if (StringUtils.isBlank(generic)) {
                    generic = RpcContext.getContext().getAttachment(GENERIC_KEY);
                }

                if (appResponse.hasException() && !(appResponse.getException() instanceof GenericException)) {
                    appResponse.setException(new GenericException(appResponse.getException()));
                }
                // 设置反序列化 讲复杂对象转换为简单的基础对象
                appResponse.setValue(PojoUtils.generalize(appResponse.getValue()));
                
            }
        }

        @Override
        public void onError(Throwable t, Invoker invoker, Invocation invocation) {

        }
    }
}

3.原理总结
基于PojoUtils 简化复杂对象,不用引入二方包。

针对 com.alibaba.dubbo.rpc.service.GenericService.$invoke(String method, String[] parameterTypes, Object[] args) 这个接口进行特殊判断,基于拦截器处理特殊拦截。

基于拦截器,消费者端 GenericImplFilter 处理

1> 不考虑dubbo 其他的复杂序列化的需求很简单,基本上啥都不做

基于拦截器,服务提供者端 GenericFilter处理,

1> 基于接口、方法名称、方法参数类型查询具体的服务、服务的方法名称;

2> 基于PojoUtils 、方法参数类型、方法参数 反序列化为复杂的参数对象PojoUtils.realize(args, params, method.getGenericParameterTypes()) ;

3> 基于PojoUtils.generalize(appResponse.getValue()) 序列化返回值。

四、扬长揭短
泛化和非泛化的优缺点分析

1.优点

实现层面:服务消费者不需要有任何接口的实现,就能完成服务的调用,比如:只需要知道服务端的appkey 端口号 以及所暴漏thrfit服务的包名+类名就可以完成调用(无参数方法)。

编码层面:快速开发,降低开发成本,加快交付周期。

2.缺点

编码层面:参数传递复杂,不方便使用,比如:出参为json,结构不透明,强依赖提供方的api定义,目录结构,需要保持更新。

配置风险和代码变更风险比较高。

一些rpc相关辅助功能支持不友好,比如熔断,限流打点等。

3.其他
性能上剧测试报告显示无差别,多余耗时主要是序列和反序列化层面。
1.泛化调用1kByte和String性能差异不大

2.正常rpc调用和泛化调用的性能差异不大
正常rpc调用性能峰值约比泛化调用大1k左右,原因是泛化调用比正常调用多了一层json序列化,此过程占用性能比例不是很高。
从热点分析树上来看,两者热点方法基本相同,差异不是很大,都集中在服务端rpc底层读写和编解码阶段。

3.性能瓶颈:新生代大小,默认设置为4G

五、参考资料
记不得了,如果有侵权的地方请联系,我会及时删除,感谢。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值