rpc

RPC : Remote Procedure Call ,即远程过程调用。是分布式系统常见的一-种通信方法,从跨进程到跨物理机已经有几十年历史。

从一个方法调用开始

System.our.println("hello world");

在本机上,完成这么样的一次方法调用需要:

  1. 传递方法参数:将字符串hello world的引用地址压栈
  2. 确定方法版本:像在JVM上 这个过程使用invokexxx指令来完成
  3. 执行被调方法:从栈中弹出Parameter的值或引用,以此为输入,执行Callee内部的逻辑
  4. 返回执行结果:将Callee的执行结果压栈

为了完成这些过程,就需要通过内存来传递数据 如果两个方法不在同一个进程,要如何传递数据?

通信的成本

通过网络进行分布式运算的8宗罪

The network is reliable —— 网络是可靠的。Latency is zero —— 延迟是不存在的。Bandwidth is infinite —— 带宽是无限的。The network is secure —— 网络是安全的。Topology doesn't change —— 拓扑结构是一成不变的。There is one administrator —— 总会有一个管理员。Transport cost is zero —— 不必考虑传输成本。The network is homogeneous —— 网络是同质化的。

RPC的三个基本问题

跨进程交互形式: RESTful、 WebService、 HTTP、 基于DB做数据交换、基于MQ做数据交换,以及RPC。

通信协议

可扩展的RPC协议必备要素:

  1. 协议头:版本、首部长度、消息ID
  2. 协议头扩展字段
  3. 协议体

序列化协议

RPC选择序列化协议需要考量的:

  1. 安全 像某些JSON的实现三天两头爆出漏洞
  2. 性能
  3. 空间
  4. 通用与兼容

Hessian 与 Protobuf 是综合这些考量较优的选择

Thrift

通过预先定义一个IDL:

struct SearchClick{  1:string user_id,  2:string search_term,  3:i16 rank,  4:string landing_url,  // 5:i32 click_timestamp, deprecated 已废弃  6:i64 click_long_timestamp,  7:string ip_address}

Thrift 里的 TBinaryProtocol 在顺序写入数据的过程中,不仅会写入数据的值(field-value),还会写入数据的编号(field-id)和类型(field-type)

20230327204002

有了编号,就可以实现向下兼容旧数据,同时部分字段也可以不用有值。

为了压榨存储空间,Thrift 使用两种方式:

网络模型

多路IO复用、零拷贝是实现高性能的关键

交互形式

批注 2020-05-08 204733

批注 2020-05-08 204746

核心原理

批注 2020-05-08 205456

另外一个角度

如果跳出程序方法调用的视角 不再以传递参数-调用方法-获取结果这样的思路思考 就会有焕然一新的视角

客户端负载均衡

// 自己实现一个随机负载均衡List<ServiceInstance> list = discoveryClient.getInstances("producer");Random random = new Random();ServiceInstance serviceInstance = list.get(random.nextInt(list.size()));
// Ribbon的负载均衡选择@RestControllerpublic class Controller {    @Autowired    LoadBalancerClient client;    @RequestMapping("/user")    public String user(){        return new RestTemplate().getForObject(                client.choose("user-service").getUri().toString()+"/user",String.class);    }}

负载均衡策略

简单的实现有随机策略,但有时需要根据压力的负载情况来选择压力最小的节点,从而实现压力的均衡,一种实现是监控节点的资源指标,压力越高,权重越小。

SpringCloudFeign

feign通过调用方自己定义一个提供方的接口来进行RPC:

// 调用方@FeignClient("producer")public interface HelloRemote {    @RequestMapping("/hello")    String hello(@RequestParam String name);}