RPC(远程过程调用)简介

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

之前听过这个名词,但是也只是大概记住了“远程调用”之类的关键词,而其他并没有太多了解。

来到TX实习,确实如别人所说的那样,公司内部有自己的开发框架。我所在部门使用的是一个叫做TAF(Tencent Application Framwork)的后台框架,它本质上是一个分布式系统,提供了良好的RPC封装。

那么问题来了——RPC到底是什么?

在校期间,大家多多少少也都写过一些程序,比如写个hello world服务类,然后本地调用下,如下所示。这些程序的特点是服务消费方和服务提供方是本地调用关系。

//服务接口
public interface HelloWorldService {
    String sayHello(String msg);
}
//服务实现
public class HelloWorldServiceImpl implements HelloWorldService {
    @Override
    public String sayHello(String msg) {
        String result = "hello world " + msg;
        System.out.println(result);
        return result;
    }
}
//本地服务调用
public class Test {
     public static void main(String[] args) {
         HelloWorldService helloWorldService = new HelloWorldServiceImpl();
         helloWorldService.sayHello("test");
     }
 }

而在大型互联网公司,公司的系统都由成千上万大大小小的服务组成,各个服务部署在不同的机器上,由不同的团队负责。

这时就会遇到两个问题:

  1. 要搭建一个新服务,免不了需要依赖他人的服务,而现在他人的服务都在远端(而不是在本地),怎么调用?
  2. 其它团队要使用我们的服务,我们的服务该怎么发布以便他人调用?

RPC可以解决上面两个问题。简单来说,RPC就是说,假设有两台服务器A和B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

1 如何调用他人的远程服务?

由于各服务部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都要写一大堆网络通信相关的代码,不仅复杂而且极易出错。

如果有一种方式能让我们像调用本地服务一样调用远程服务,而让调用者对网络通信这些细节透明,那么将大大提高生产力,比如服务消费方在执行helloWorldService.sayHello(“test”)时,实质上调用的是远端的服务

RPC的例子有:阿里巴巴的hsf、dubbo(开源)、Facebook的thrift(开源)、Google grpc(开源)、Twitter的finagle等。

首先我们先看下一个RPC调用的流程:

1)服务消费方(client)调用以本地调用方式调用服务;
2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(编码);
3)client stub找到服务地址,并将消息发送到服务端;
4)server stub收到消息后进行解码;
5)server stub根据解码结果调用本地的服务;
6)本地服务执行并将结果返回给server stub;
7)server stub将返回结果打包成消息(编码)并发送至消费方;
8)client stub接收到消息,并进行解码;
9)服务消费方得到最终结果。

RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。

Q:怎么做到透明化远程服务调用?

答:使用代理!

Q:为什么使用代理呢?

举个例子:假设你有一套房子要卖,一种方法是你直接去网上发布出售信息,然后直接带要买房子的人来看房子等等,另一种方法是去找中介,中介实际上就是你的代理——本来是你要做的事情,现在中介帮助你一一处理。对于买方来说跟你直接交易跟同中介直接交易没有任何差异,买方甚至可能觉察不到你的存在,这就是代理最大的好处。(当然,还有另外一个好处就是:在你很忙的时候,你可以把事情交给代理去做!)

下面简单介绍下动态代理怎么实现我们的需求。我们需要实现RPCProxyClient代理类,代理类的invoke方法中封装了与远端服务通信的细节,消费方首先从RPCProxyClient获得服务提供方的接口,当执行helloWorldService.sayHello(“test”)方法时就会调用invoke方法。

Q:怎么对消息进行编码和解码?

首先,要确定消息数据结构。通信的第一步就是要确定客户端和服务端相互通信的消息结构。客户端的请求消息结构一般需要包括以下内容:

1)接口名称
比如“HelloWorldService”,如果不传,服务端就不知道调用哪个接口了;

2)方法名
一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;

3)参数类型&参数值
参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class);

4)超时时间

5)requestID,标识唯一请求id。

同理服务端返回的消息结构一般包括以下内容:

1)返回值

2)状态code

3)requestID

第二步,就是序列化与反序列化

序列化:将数据结构或对象转换成二进制串的过程,也就是编码的过程。

反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。

为什么需要序列化:转换为二进制串后便于网络传输。

序列化方案在选择的时候,主要看三点:
1)通用性,比如是否能支持Map等复杂的数据结构;
2)性能,包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;
3)可扩展性,对互联网公司而言,业务变化快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,删除老的字段,而不影响老的服务,这将大大提供系统的健壮性。

目前国内各大互联网公司广泛使用hessian、protobuf、thrift、avro等成熟的序列化解决方案来搭建RPC框架。

2 如何发布自己的服务?

如何让别人使用我们的服务呢?

最基本的,我们需要告诉使用者服务的IP以及端口。但是问题在于,如果是直接通过告知IP+port的方式,将会有一系列问题:如果你发现你的服务一台机器不够,要再添加一台,这个时候就要告诉调用者我现在有两个ip了,你们要轮询调用来实现负载均衡;调用者咬咬牙改了,结果某天一台机器挂了,调用者发现服务有一半不可用,他又只能手动修改代码来删除挂掉那台机器的ip——这是非常不可取的。

机智的做法是:调用者不写死服务提供方地址,并做到机器的增添、剔除对调用方透明。比如zookeeper被广泛用于实现服务自动注册与发现功能。

简单来讲,zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(ip+端口)去访问具体的服务提供者。

具体来说,zookeeper就是个分布式文件系统,每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上: /{service}/{version}/{ip:port}, 比如我们的HelloWorldService部署到两台机器,那么zookeeper上就会创建两条目录:分别为/HelloWorldService/1.0.0/100.19.20.01:16888 /HelloWorldService/1.0.0/100.19.20.02:16888。

zookeeper提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除,比如100.19.20.02这台机器如果宕机了,那么zookeeper上的路径就会只剩/HelloWorldService/1.0.0/100.19.20.01:16888。

服务消费者会去监听相应路径(/HelloWorldService/1.0.0),一旦路径上的数据有任务变化(增加或减少),zookeeper都会通知服务消费方服务提供者地址列表已经发生改变,从而进行更新。

更为重要的是zookeeper 与生俱来的容错容灾能力,可以确保服务注册表的高可用性。

你可能感兴趣的:(服务器)