文章目录
- 1. 什么是远程过程调用(RPC)?
- 2. RPC的流程
- 3. RPC实践
- 4. RPC与REST的区别
- 4.1 RPC与REST的相似之处
- 4.2 RPC与REST的架构原则
- 4.3 RPC与REST的主要区别
- 5. RPC与服务发现
- 5.1 以zookeeper为服务注册中心
- 5.2 以etcd为服务注册中心
- 6. 小结
- 参考
1. 什么是远程过程调用(RPC)?
在分布式计算中,远程过程调用(RPC)是指计算机程序导致过程(子程序)在不同的地址空间(通常为一个开放网络中的另一台计算机)执行,其编写方式就像是普通(本地)过程调用一样,程序员无需明确编写远程交互的细节。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受响应进行信息交互的系统。
RPC是一种进程间通信的模式,程序分布在不同的地址空间中。如果在同一主机里,RPC可以通过不同的虚拟地址空间(即使使用相同的物理地址)进行通讯,而在不同主机间,则通过不同的物理地址进行交互。许多技术(通常是不兼容)都是基于这种概念而实现的。
RPC调用本质上是一种协议,允许一台计算机上的程序在另一天计算机上执行代码,而无需程序员明确编写此交互的代码。RPC类似于调用不同系统中可用的函数或方法,因此叫做远程过程调用。
RPC的一个显著特点是它能够掩盖网络接口的复杂性,使得开发人员可以专注于应用程序的功能,而无需深入研究网络协议的复杂性。
2. RPC的流程
- 客户端调用客户端stub(client stub),这个调用是在本地,并交调用参数push到栈(stack)中
- 客户端stub(client stub)将这些参数包装,并通过系统调用发送到服务端机器。打包的过程叫做marshalling。
- 客户端本地操作系统发送信息到服务器(可通过自定义TCP协议或者HTTP传输)
- 服务器将信息传送至服务端stub(server stub)
- 服务端stub(server stub)解析信息。该过程叫做unmarshalling
- 服务端stub(server stub)调用程序,并通过类似的方式返回给客户端
3. RPC实践
RPC实践,主要是以gRPC为例进行实践,因为我用的是go语言,所以选择的是grpc-go来进行实践。
gRPC的原理图如下,具体可参考:Introduction to gRPC
具体可以参考:gRPC for Go Quick Start
可以直接按照官方文档提供的步骤来,先完成前置条件,安装好protoc-gen-go
和progoc-gen-go-grpc
即可。
然后编写proto文件
syntax = "proto3";
option go_package="go-study/blogs/grpc_study/helloworld/hello";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
使用protoc直接生成对应的pb文件即可,具体的命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative hello.proto
生成的文件如下:
最后我们主要使用hello_grpc.pb.go文件里面对应的Client和Server的方法即可。
关于client和server的代码,可以参考:grpc-go examples helloworld
分别运行client和server,当client运行时会发送消息给server,收到消息后,server会处理消息并返回响应。
$ go run .\server.go
2024/07/14 15:42:30 server listening at [::]:50051
2024/07/14 15:42:42 Received: testing
$ go run .\client.go --name=testing
2024/07/14 15:42:42 Greeting: Hello testing
4. RPC与REST的区别
RPC与REST是API设计中的两种不同的架构风格。API则是允许两个软件组件使用一组定义和协议相互通信的机制。软件开发人员使用之前开发的组件或者第三方的组件来执行功能,因此不必从头开始编写所有内容。RPC API允许开发人员在外部服务器中调用远程函数,就好像它们在本地环境一样。
4.1 RPC与REST的相似之处
- 抽象
- 虽然网络通信是API的主要目标,但较低级别的通信本身是从API开发人员那里抽象出来的,这使得开发人员可以专注与功能而不是技术实施。
- 交流
- REST与RPC均可使用HTTP作为底层数据传输协议,RPC和REST中最常见的消息格式是JSON和XML。
- 跨语言兼容性
- 开发人员可以使用任何语言实现RESTful或者RPC API,只要API的网络通信元素符合RESTful或者RPC接口标准,就可以使用任何编程语言编写逻辑代码。
4.2 RPC与REST的架构原则
在RPC中,客户端在服务器上进行远程函数(也称为方法或过程)调用,通常在调用期间会向服务器传递一个或者多个数据值。
而REST客户端则是请求服务器针对服务器特定资源执行操作,操作仅限于创建、读取、更新和删除,并以HTTP动词或者HTTP方法的形式传达。
RPC侧重函数或操作,而REST侧重于资源或对象。
RPC原则:
- 远程调用
- RPC调用是由客户端对远程服务器上的函数进行的,就像该函数是在本地调用到客户端一样
- 传递参数
- 客户端通常向服务器函数发送参数,与本地函数大致相同
- 存根(stub)
- 函数存根同时存在于客户端和服务器上,在客户端上,它进行函数调用,在服务器上,它调用实际函数。(这个在hello world的实践处也可以看到,对于Client发送请求,进行的是定义好的pb函数的调用,而服务端启动的时候,是注册了服务的方法,而这个方法就是实际函数。)
REST原则
- 客户端-服务器
- REST的客户端与服务器架构将两者分离开来,即将客户端和服务器视为独立系统
- 无状态
- 服务器不会保留两次客户端请求之间的客户端记录
- 可缓存
- 客户端或者中间件系统可能会根据客户端是否指定了可以缓存的响应来缓存服务器响应
- 分层系统
- 中间系统可以存在于客户端与服务器之间,而客户端与服务器对中间系统无感知,像是它们之间连在一起一样
- 统一接口
- 客户端和服务器通过一组标准化指令和消息收发格式与REST API通信
4.3 RPC与REST的主要区别
- 开发时间
- RPC与REST诞生时间不同
- 操作格式
- REST拥有一套标准化服务器操作,但RPC API没有,某些RPC实施为标准化操作提供了框架
- 数据传递格式
- REST可在统一API内传递任何数据格式和多种格式的数据,RPC API的数据格式则由服务器选择,在实施过程中是固定的。
- 状态
- REST系统始终是无状态的,但RPC系统可以有状态,也可以无状态,具体取决于设计
当下REST API已经成为了主流,因为它更易于开发人员理解和实施,但RPC并没有消失,依然会在适合的应用场景中使用(如gRPC,允许客户端与服务器之间流式通信,而非请求和相应数据交换模式)。
5. RPC与服务发现
为了高可用,在生产环境中服务提供方都是以集群的方式对外提供服务,集群里面的这些IP随时会发生变化,此时我们需要能够实时获取对应服务节点,而这个获取的过程我们称作“服务发现”。
5.1 以zookeeper为服务注册中心
类似于Dubbo,采用的是以Zookeeper作为服务注册中心,注册中心在RPC场景下负责保存服务端应用的信息,服务端注册接口信息和自身地址到注册中心,客户端从注册中心读取和订阅需要调用的地址列表,框架如图所示:
5.2 以etcd为服务注册中心
在etcd中,etcd提供了一个gRPC解析器来支持备用名称系统,该系统从etcd获取端点以发现gRPC服务,底层基于监听以服务名称为前缀的秘钥更新机制实现。
具体实现则是通过etcd提供的etcdnaming的能力实现获取对应的grpc resolver,从而获取到对应的存储在etcd的服务器信息,从而获取到服务器的请求地址。
import (
"go.etcd.io/etcd/client/v3"
etcdnaming "go.etcd.io/etcd/client/v3/naming/resolver"
"google.golang.org/grpc"
)
func main() {
cli, err := clientv3.NewFromURL("http://localhost:2379")
if err != nil {
// ...
}
builder, err := etcdnaming.NewBuilder(cli)
if err != nil {
// ...
}
conn, gerr := grpc.Dial("my-service", grpc.WithResolvers(builder), grpc.WithBlock(), ...)
}
而对应的管理服务端点以及添加/删除服务端点则是通过操作etcd来实现。
添加端点
ETCDCTL_API=3 etcdctl put my-service/1.2.3.4 '{"Addr":"1.2.3.4","Metadata":"..."}'
删除端点
ETCDCTL_API=3 etcdctl del my-service/1.2.3.4
使用租约注册端点
使用租约注册端点可以确保如果主机无法保持活动心跳(例如,机器发生故障),它将从服务中删除。
lease=`ETCDCTL_API=3 etcdctl lease grant 5 | cut -f2 -d' '`
ETCDCTL_API=3 etcdctl put --lease=$lease my-service/1.2.3.4 '{"Addr":"1.2.3.4","Metadata":"..."}'
ETCDCTL_API=3 etcdctl lease keep-alive $lease
通过etcd加持gRPC可以实现RPC服务的注册发现,从而使得服务在保证高可用的情况下,RPC服务请求调用也能顺利完成。
6. 小结
很早之前就接触过RPC,也接触过服务的注册与发现,最早是学习Java的时候接触的Dubbo,工作后转到了Go,最近是看到了有etcd+gRPC的使用,带着看一看的想法就把RPC顺带着给一起复习下了。
在当下的分布式系统的情况下,RPC服务基本都需要考虑高可用,所以注册中心就成为了必须要解决的一个问题。
参考
- Remote procedure call
- 服务注册发现
- RPC与REST的区别
- gRPC命名和发现 - etcd