在学校里总要拿点什么东西练练手,看了些文章像什么《从零开始写Rpc》,《从零开始写搜索引擎》都是可以一试的。其实前些日子的tiny4j还有些要改进继续完善的东西,奈何人都是喜新厌旧的,总忍不住再开一坑,所以这次是RPC。

在开始写之前做了不少准备,了解RPC的一些基本的东西,也找了一些RPC框架在试用,诸如GRPC,thrift,它们都提供跨语言调用的能力,根据写的一些配置便可自动生成一些代码。除了这两个国产框架里也有非常优秀的如阿里开源的dubbo,微博的motan;微博的motan文档很简洁,而且直接支持Spring的注解形式。马上尝试了一下helloworld,Api使用简单方便,几个注解搞定所有服务,对于Java这个特定的语言,这个RPC真的已经非常好用了!代码clone下来一看,传输层用的是netty,为了能够完成这个又跑去搞了本《netty 权威指南》啃了大半本,算是入了个门。

准备工作差不多了于是开始尝试写,毕竟是第一次写,就不考虑跨语言了,像motan一样配合Spring使用能顺手简单就行。简单梳理的整个RPC调用过程,可以分成以下几层:

rpc层次

通信包括连接建立、消息传输, 编解码要传输的内容,序列化和反序列化,最后是客户端和服务端的实现,客户调用一个服务的代理实现,发起对服务端的请求。
基于netty5.0,protostuff、kryo序列化库,commons-pool2实现连接池,服务代理使用jdk自身的Proxy或Cglib,在这样一堆优秀的框架和类库上,整个RPC的构建就简单了不少。

也算是自己从零编写一个RPC的一个实践吧.源码请戳 github ,通过这个项目可以了解RPC的方方面面;对了大家要是有更好的思路欢迎大家一起交流。

现有功能

  • 基本的客户端、服务端请求调用
  • 客户端连接池,多个服务的接入
  • SpringBoot集成
  • 基于Consul和Zookeeper的服务注册发现
  • 断路器

简单调用示例

定义服务和服务实现

public interface EchoService {
    String hello(String s);
}

public class EchoServiceImpl implements EchoService {
    @Override
    public String hello(String s) {
        return s;
    }
}

Server:

//配置:端口号,最大的传输容量(单位M),服务响应的Handle
Server server = new Server.Builder()
             .port(9001)
             .serviceName("test")//服务名,全局唯一
             .serviceId("dev")//服务id,区分不同环境,建议 dev,test,prod
             .maxCapacity(3)
             .build();
//发布服务
server.addService(EchoService.class, new EchoServiceImpl())
       .addService(TestService.class, new TestServiceImpl());
server.start();

Client:

ClientProperty client = new ClientProperty();
client.serviceName("test")
        .provider("127.0.0.1:9001")
        .interfaces("com.tg.rpc.example.service.EchoService");
Client client = new Client.Builder().maxCapacity(3)
        .requestTimeoutMillis(3500)
        .connectionMaxTotal(10)
        .connectionMaxIdle(6)
        .client(client)
        .build();
DefaultClientInterceptor interceptor = new DefaultClientInterceptor(client);
ClientProxy clientProxy = new JdkClientProxy(interceptor);
EchoService echoService = clientProxy.getProxy(EchoService.class);
System.out.println(echoService.echo("twogoods"));

客户端可以接入多组服务

ClientProperty clientA = new ClientProperty();
clientA.serviceName("A")
        .provider("127.0.0.1:9001")
        .interfaces("com.tg.rpc.xxx.EchoService");
       
ClientProperty clientB = new ClientProperty();
clientB.serviceName("B")
        .provider("127.0.0.1:8090").provider("127.0.0.1:8080")//同一服务的多个实例
        .interfaces("com.tg.rpc.xxx.TestAService")//一个服务下的多个接口
        .interfaces("com.tg.rpc.xxx.TestBService");
        
Client client = new Client.Builder().maxCapacity(3)
        .requestTimeoutMillis(3500)
        .connectionMaxTotal(10)
        .connectionMaxIdle(6)
        .enableBreaker()
        .client(clientA)//加入两组服务
        .client(clientB)
        .build();

服务注册与发现

支持Consul和Zookeeper,只需在server和client里增加相应的组件即可

//server端使用服务注册组件
ServiceRegistry serviceRegistry = ConsulCompentFactory.getRegistry("localhost", 8500);
Server server = new Server.Builder()
        .port(9001)
        .serviceName("testService")
        .serviceId("dev")
        .maxCapacity(3)
        .serviceRegistry(serviceRegistry)
        .build();
        
//client端使用服务发现组件     
ServiceDiscovery serviceDiscovery = ConsulCompentFactory.getDiscovery("localhost", 8500);
Client client = new Client.Builder()
        .serviceDiscovery(serviceDiscovery)
        .connectionMinIdle(1)
        .maxCapacity(3)
        .client(clientA)
        .build();

以上是基于consul的配置,使用zookeeper只需更换使用不同的组件即可

ServiceDiscovery serviceDiscovery = ZookeeperCompentFactory.getDiscovery("localhost",2181);
ServiceRegistry serviceRegistry = ZookeeperCompentFactory.getRegistry("localhost", 2181);

整合SpringBoot

服务端配置

使用@RpcService注解

public interface EchoService {
    String echo(String s);
}

@RpcService
public class EchoServiceImpl implements EchoService {
    @Override
    public String echo(String s) {
        return s;
    }
}

application.yml配置

tgrpc:
    server:
        port: 9001
        serviceName: testService
        serviceId: dev
        registery: consul
        consulHost: 127.0.0.1
        consulPort: 8500

启动server:

@SpringBootApplication
@EnableAutoConfiguration
@EnableRpcServer //启用Server
@ComponentScan()
public class ServerApplication {
    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(ServerApplication.class);
    }
}
client配置

调用方使用@RpcReferer注解.

@Component
public class ServiceCall {

    @RpcReferer
    private EchoService echoService;

    public String echo(String s) {
        return echoService.echo(s);
    }
}

application.yml配置

tgrpc:
    client:
        registery: consul
        consulHost: 127.0.0.1
        consulPort: 8500
        maxCapacity: 3
        maxTotal: 3
        maxIdle: 3
        minIdle: 0
        borrowMaxWaitMillis: 5000
        clients:
            - serviceName: testService
              requestTimeoutMillis: 2000
              interfaces:
                    - com.tg.rpc.example.service.EchoService
                    - com.tg.rpc.example.service.TestService
              providerList: 127.0.0.1:8080

Client发起调用

@SpringBootApplication
@EnableAutoConfiguration
@EnableRpcClient //启用Client
@ComponentScan(basePackages = {"com.tg.rpc.springsupport.bean.client"})
public class ClientApplication {
    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(ClientApplication.class);
        ServiceCall serviceCall = (ServiceCall) applicationContext.getBean("serviceCall");
        System.out.println("echo return :" + serviceCall.echo("TgRPC"));
    }
}

断路器

熔断发生在客户端,默认的熔断策略:30秒内请求数大于60并且错误率超过50%触发熔断,熔断后每20秒过一个请求测试后端服务是否正常,调用成功则关闭熔断。
熔断期间的客户端不发送实际请求到服务端,如果你的服务接口使用了Java8接口里的默认方法,那么执行此默认方法,否则抛出RequestRejectedException异常,因此建议在定义接口的时候使用默认方法:

public interface TestServiceIface {
    default String echo(String str) {
        return str;
    }
}

熔断组件真正执行方法的时候有两种方式

1、利用反射执行
BreakerProperty breakerProperty = new BreakerProperty().addClass("com.tg.rpc.breaker.TestServiceIface");//要监控的类
Breaker breaker = new Breaker(breakerProperty);
Method metricsMethod = TestServiceIface.class.getMethod("echo", String.class);//熔断发生在方法级别
Object obj = new TestServiceIfaceImpl();
ReflectTask task = new ReflectTask(metricsMethod, new Object[]{"twogoods"}, obj, 100l);
Object res = breaker.execute(task);
System.out.println(res);
2、函数式思想传入具体行为
BreakerProperty breakerProperty = new BreakerProperty().addClass("com.tg.rpc.breaker.TestServiceIface");
Breaker breaker = new Breaker(breakerProperty);
Method metricsMethod = TestServiceIface.class.getMethod("echo", String.class);
TestServiceIface testServiceIface = new TestServiceIfaceImpl();
TaskExecuteHook<String, String> taskExecuteHook = s -> testServiceIface.echo(s);//真正的执行行为
Object res = breaker.execute(new HookTask<>(taskExecuteHook, "twogoods", () -> {
    return new Object[]{"twogoods"};
}, metricsMethod));
System.out.println(res);

RPC框架提供了对熔断的支持,默认是关闭熔断的,开启只需修改配置

Client client = new Client.Builder()
        .requestTimeoutMillis(3500)
        .enableBreaker()//开启断路器
        .client(client)
        .build();

或者在SpringBoot的配置文件里增加

tgrpc:
    client:
        breakerable: true

更多使用请参考example模块

TODO

  • http调用的支持、异步编程、超时与重试、监控、限流(server)、降级开关(server)
  • netty优化

twogoods
574 声望2 粉丝