网易有钱RPC框架介绍

阿凡达2018-06-28 16:07

1. 背景

随着项目规模的不断扩大,分解项目的复杂度就变成一个必须要面对的问题,在诸多的解决方案中,服务化架构被很多项目采纳。本文不对服务化做过多的展开,主要聊一下网易有钱在服务化过程中RPC框架的选型与设计。

RPC(Remote Procedure Call)即远程方法调用,让应用可以像调用本地方法一样调用远端的方法

主流RPC框架存在的问题:

  1. dubbo、spring cloud

配置、依赖复杂,他们提供了一整套服务化的框架,但各个框架之间有比较大的耦合,也就意味着需要使用这些框架,必须要对整个全家桶有所了解,也增大了开发人员的学习成本和项目的维护成本

  1. thrift、gRpc

引入IDL,需要增加大量的定义和编译工作,学习成本高,可维护性差,影响编码效率

为了解决上述问题,我们打算基于Spring和通用的序列化协议设计一个轻量级松耦合的RPC框架

2. 设计原则

结合项目中的实际场景,我们定义如下的设计原则:

  1. 配置简洁,不破坏现有代码结构和开发模式,学习和使用成本低
  2. 序列化协议的选择对应用透明
  3. 基于HTTP/S,与通用负载均衡组件、服务发现组件实现解耦
  4. 打通系统内所有服务的请求跟踪链路
  5. 暴露的RPC接口有比较便捷的方式进行调试验证
  6. 支持分离开发,开发阶段允许本地mock各种测试用例
  7. 自定义灰度流量控制:可以灵活的选择参与测试的请求或者用户

3. 基本架构


  1. Protocol:RPC服务的描述和配置信息

    1. 通过接口定义阶段的声明来描述RPC服务的默认行为
    2. 通过客户端增加配置的形式来调整RPC Client的行为,如灰度流量控制、本地Mock返回等
  2. Serialize/Deserialize:序列化与反序列化层

    主要用于对请求的方法、参数和返回的结果、异常进行序列化与反序列化

  3. Transport:网络传输层,基于HTTP/S

    1. Client采用HttpClient:将序列化的数据及本地的请求跟踪信息发送到服务端,接收服务端返回结果
    2. Server利用Spring框架暴露Servlet API
  4. Consul Nginx:服务发现组件

    1. Transport将请求转发至Consul Nginx,再由Consul Nginx根据注册的服务按配置的策略转发到服务端
    2. Consul Nginx与RPC框架实现解耦
  5. TraceFilter:服务端采用Filter拦截,将HTTP请求中的跟踪信息设置到本地session
  6. Consul:服务发现管理
  7. Register:服务提供方 与服务本身解耦,向Consul发送注册请求,注册后的服务可以在Consul Nginx中被发现

4. 常用功能介绍

4.1 基础功能支持

  1. 注解@RemoteMethod修饰interface,如

    @RemoteMethod(mapping = {
     @RpcUrlMapping(beanName = "hello", path = "/helloService.s") }, 
     baseUrl = "http://${testService.domain}/rpc-framework")
    public interface Hello {
     HelloResponse sayHello(String name);
    }
         
         
         
    

    说明:

    1. baseUrl:服务所在的域;支持占位符
    2. betaBaseUrl:用于灰度流量控制,表示beta版本所在的域
    3. beanName:Client端的实例名称,不声明则使用接口名称的小驼峰式表达,如:接口mypackage.MyService对应的默认beanName为myService
    4. path:访问路径,baseUrl + path 为接口的实际访问地址
  2. 注解@RpcService修饰接口的实现类,如:

     @RpcService("/helloService.s")
    

    说明: value值必须和接口的@RpcUrlMapping#path保持一致

  3. RpcScannerConfigurer

    调用方通过BeanDefinitionRegistryPostProcessor在实例定义阶段扫描RPC接口,并将它们的代理类注册到ApplicationContext中

    说明:

    1. basePackage:必选参数。声明rpc接口所在的包。
    2. markerInterface、annotation:选填参数。可通过这两个配置项对basePackage下面的接口进行过滤。
    3. mockDir:通过该参数实现调用方mock接口的返回结果

4.2 请求跟踪

需要进行跟踪的请求需配置TraceFilter

主要目的是将HTTP请求中的跟踪信息设置到本地session,并通过MDC实现日志打印时输出跟踪信息对开发人员是透明的

4.3 接口调试

所有暴露的接口默认支持JSON模式,因此可以通过命令行、rest client等工具进行测试

如:

curl -H "Content-Type: application/json" -X POST -d '{"sign":"addUser","args":["wgy@163.com","wgy"]}' http://127.0.0.1:8080/rpc-framework/v3/userService.s

4.4 本地MOCK

调用方可以mock接口的返回结果是实现分离开发和服务降级的基础,可通过增加配置RpcScannerConfigurer#mockDir实现该功能。

说明:

  1. 打开配置后调用一次指定的接口,会在mockDir下面产生一个文件(mockFile demo)
  2. mockFile的文件名为:$mockDir/$RpcUrlMapping#path/$methodSign
  3. $methodSign为方法签名,由方法名和参数类型等构造
  4. mockFile的内容为一个JSON对象,如{"tip":null,"priority":null}
  5. 开发过程中可以根据测试的需要调整mockFile的内容,无需重启应用

4.5 自定义灰度流量控制

服务提供方可通过声明@RemoteMethod#betaBaseUrl,并根据自己的需求实现接口BetaSwitcher,将符合条件的流量指向到Beta版本

如:

网易有钱的自动分类版本升级后,通过这种方式测试新版本的实际分类效果

5. 示例

环境限制:

  1. 使用Spring Framework(>= 3.1.1.RELEASE)或者Spring Boot
  2. HttpClient 4.x
  3. RPC框架
    <groupId>com.netease.qian</groupId>
    <artifactId>qian-rpc-framework</artifactId>
    <version>${qian-rpc-framework.version}</version>
    

RPC Server

  1. 定义接口、模型以及异常

     public interface Hello {
     HelloResponse sayHello(String name);
     }
    
  2. 为上述定义的接口声明暴露方式:

    @RemoteMethod(mapping = {
     @RpcUrlMapping(beanName = "hello", path = "/helloService.s")}, 
     baseUrl = "http://${testService.domain}/rpc-framework")
    public interface Hello {
     HelloResponse sayHello(String name);
    }
    
  3. 实现接口:

         @RpcService("/helloService.s")
         public class HelloImpl implements Hello {
    
             @Override
             public HelloResponse sayHello(String name) {
                 HelloResponse response = new HelloResponse();
                 response.setTip("hello," + name + "!");
                 return response;
             }
         }
    
  4. 配置servlet,暴露RPC接口(以SpringMVC为例,Spring Boot类似)

    1. web.xml添加servlet配置

      <servlet>
       <servlet-name>rpc</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <init-param>
           <param-name>contextConfigLocation</param-name>
           <param-value>classpath:rpc-servlet.xml</param-value>
       </init-param>
       <load-on-startup>0</load-on-startup>
      </servlet>
      
    2. rpc-servlet.xml

      <context:component-scan
       base-package="com.netease.mail.test.server1,com.netease.qian.rpc.provider">
      </context:component-scan>
      
  5. 启动应用,测试接口可用 如执行

     curl -H "Content-Type: application/json" -X POST -d '{"sign":"sayHello","args":["wgy"]}' http://127.0.0.1:8080/rpc-framework/helloService.s
    

    返回

     {"value":{"tip":"hello,wgy!"}}
    

Rpc Client


  1. 调用方引入Server定义的接口,如jar包
  2. 增加配置

    1. 占位符定义:server.properties

       testService.domain=127.0.0.1:8080
      
    2. 扫描RPC Client实例(applicationContext-hello-client.xml)

      <bean class="com.netease.qian.rpc.consumer.RpcScannerConfigurer">
       <property name="basePackage" value="com.netease.mail.test.api1" />
      </bean>
      
  3. 测试用例

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({ "/applicationContext-hello-client.xml" })
    public class HttpClientTester {
    
     @Autowired
     private Hello hello;
    
     @Test
     public void test() {
         System.out.println(hello.sayHello("wgy"));
     }
    }
    


总结


RPC框架的设计初衷是简洁,易用:

  1. 对一般基于Spring框架开发的应用来说,如果需要搭建一个RPC Server,只要在接口和实现类上添加注解即可完成
  2. 对接口的调用方来说,只需要在原有代码的基础上,增加RpcScannerConfigurer扫描即可

目前框架还有不少未完善的地方,比如接口文档的维护管理,欢迎大家一起交流。

本文来自网易实践者社区,经作者王国云授权发布。