什么是RPC?
Remote Procedure Call,即远程过程调用
为什么需要RPC?(一切开发技术的出现都是为了简化某些东西,为了让开发提效)
假设我们的应用由两个服务组成,彼此如何进行通信呢
- 方案一:请求方将服务方的服务器地址以配置文件的形式写死在代码中,并且适配好对应的请求响应结构体,并通过HTTP请求的方式进行请求
- 方案二:请求方将服务方的服务器地址以配置文件的形式写死在代码中,服务提供方自己维护一份SDK文件,消费方只需要引入该SDK并调用
如果我们在一个服务内进行一次函数调用(本地过程调用)是如何进行呢
- 本地调用:引入该函数,输入参数并调用,接收结果。。
通过对比这两者的区别我们会发现,你如果想要做到将远程过程调用做到如同本地调用一样,你需要解决很多问题
第一,解决服务提供方的地址问题。事实上,如果服务方的地址变化不频繁,这一点的影响其实不大,写死在代码中就写死在代码中,如果要变的时候我再改一下不就行了。但如果你的微服务很多,且服务提供的地址经常变化,那将是你的噩梦,例如你一个应用共有5个微服务,4个服务都依赖其中一个叫userCenter的服务,且userCenter的地址已经写死在其他服务的配置文件中了,那么当userCenter的服务地址发生变更,你要去手动修改另外4个微服务的地址。而RPC框架可以通过注册中心来很快的更新服务地址,并且可以注册多个地址来启动负载均衡功能。
第二,解决服务提供方接口经常变更的问题,这是对开发效率影响较大的一点,一个接口发布之后是会经常变更的。如果按照方案一的形式完成了服务接入,接口的请求响应一旦变更,那么客户端可能就要去进行维护,这十分的不方便。按照方案二来说,会更好一点,因为服务提供方在修改完对应接口后,往往也会更新对应的SDK,消费者们只需要更新他们依赖的SDK版本,便可以较小代价的完成接口适配。但方案二方便了消费者,对于服务提供者来说却痛苦了很多,修改一个接口就要发布一个SDK,如果要求多语言支持,那么该一行接口代码,可能要修改五,六份SDK代码,十分的痛苦。而RPC框架可以通过IDL(接口定义语言) 来实现跨语言的SDK接入,解放了服务提供方的开发工作。
第三,远程调用比本地调用多走了一趟网络,这中间会发生很多意外,服务提供方也会时不时宕机,所以还要有重试,超时,限流,熔断等功能保证请求的成功率,这些如果都由业务方自己来开发会浪费很多时间,RPC框架一般都提供这些功能。
另外一提,如果您所涉及的微服务数量不多,可以选择方案二,各个服务提供方都将自己的SDK放在一个公共代码仓库中,其他服务都去引入该仓库。这可以说是最简单和最实用的方案。
当您的微服务数量变多,且各服务之间的语言变得不再统一,上述方案可能就会带来开发负担,此时可以尝试我们下面引入的PRC框架。
RPC怎么解决问题的?
关于第一个问题,即服务地址的问题,个人认为这并不是RPC框架所带来的最大的改变,因为就算没有RPC框架,我们完全可以通过nigix,提供一个固定的访问ip,并自带负载均衡,或者通过k8s中的service功能来解决后端服务器ip经常变化的问题。个人认为PRC框架解决的最关键也是微服务开发中最令人头疼的是第二点,即服务提供方接口经常变更的问题。
之前提到过,我们可以在发布服务的时候,同时发布一套SDK,并在修改服务接口的时候同时修改对应SDK的实现,多个消费者引入该SDK的最新依赖来及时的更新接口版本。不同的PRC框架都是在此基础上去做的一些改进,来更方便我们的开发者,更快地提高我们的开发效率。
以Dubbo为例先来介绍一下他的RPC使用方式
Dubbo
Dubbo是一个由Alibaba开发,开源的、基于Java的高性能、轻量级的分布式服务框架。
Dubbo在SDK的基础上更进一步,以往SDK的方式,服务端还仍需要自己去写代码。Dubbo在写好接口的定义后直接将接口的实现类注册在Dubbo中,消费者直接调用对应的服务即可。省去了服务端开发SDK的成本。缺点是将语言限制死了在java上,不支持跨语言的远程过程调用。
以下是Dubbo通过Java接口定义发送请求的基本步骤:
1. 定义服务接口:首先,你需要定义一个Java接口,这个接口包含了你想要提供的服务的方法和参数。
public interface GreetingService {
String sayHello(String name);
}
2. 实现服务接口:然后,你需要创建一个类来实现这个接口。这个类是服务的实现,它需要完成实际的业务逻辑。
public class GreetingServiceImpl implements GreetingService {
public String sayHello(String name) {
return "Hello, " + name;
}
}
3. 发布服务:接着,你需要使用Dubbo的API来发布这个服务。Dubbo会将这个服务注册到注册中心,这样服务的消费者就可以发现这个服务。
ServiceConfig<GreetingService> service = new ServiceConfig<>();
service.setApplication(new ApplicationConfig("greeting-service"));
service.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
service.setInterface(GreetingService.class);
service.setRef(new GreetingServiceImpl());
service.export();
4. 调用服务:最后,服务的消费者可以使用Dubbo的API来调用这个服务。Dubbo会从注册中心获取到服务的地址,然后通过网络调用服务。
ReferenceConfig<GreetingService> reference = new ReferenceConfig<>();
reference.setApplication(new ApplicationConfig("greeting-consumer"));
reference.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
reference.setInterface(GreetingService.class);
GreetingService service = reference.get();
String message = service.sayHello("Dubbo");
当然服务端还是要将服务接口打包成一个jar包,然后在消费者的项目中引入这个jar包。
例如,如果你的服务接口定义在一个名为greeting-service-api的项目中,你可以在你的服务提供者和消费者的pom.xml(如果你使用的是Maven)中添加如下的依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>greeting-service-api</artifactId>
<version>1.0.0</version>
</dependency>
就像上面所说Dubbo省去了服务端开发SDK的成本。缺点是将语言限制死了在java上,不支持跨语言的远程过程调用。
下面介绍的gRPC是一款跨语言RPC框架
gRPC
gRPC是一个高性能、开源的通用远程过程调用(RPC)框架,由Google开发。它可以在任何环境中运行,允许你在不同设备、应用、语言和平台之间进行通信。
他是怎么做到跨语言的呢,Dubbo是通过java interface来定义接口的形式的,这限定了必须使用java语言,gRPC使用的是与编程语言无关的IDL(接口定义语言),具体使用的是Protocol Buffers(protobuf),这里不详细展开介绍,这里只给一个示例,大家只需要明白IDL可以抽象的描述一个接口以及其请求响应结构,而与具体的语言无关。客户端和服务端只需要引入该IDL,并通过与编程语言对应的编译器生成对应的具体代码。
// greet.proto
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
在go语言中,我们引入该proto文件定义后,通过下面指令进行编译
protoc --go_out=plugins=grpc:. greet.proto
具体来说,greet.pb.go文件中会包含以下内容:
- 消息类型:对于
.proto文件中定义的每个message,greet.pb.go文件中都会有一个对应的Go结构体。这个结构体包含了消息中的所有字段,以及一些方法,例如序列化和反序列化方法。 - 服务接口:对于
.proto文件中定义的每个service,greet.pb.go文件中都会有一个对应的Go接口。这个接口包含了服务中的所有方法。 - 服务注册函数:
greet.pb.go文件中还会包含一个服务注册函数,这个函数可以将你的服务实现注册到一个gRPC服务器。 - 客户端接口:
greet.pb.go文件中还会包含一个客户端接口,这个类型提供了调用服务方法的函数。
这些内容都是根据.proto文件自动生成的,你可以直接在你的代码中使用,无需手动编写。
其中消息类型和服务接口不再解释,来介绍一下服务注册函数和客户端接口
- 服务注册函数:这是一个用于将你的服务实现注册到gRPC服务器的函数。例如,如果你的
.proto文件中定义 了一个名为Greeter的服务,那么生成的greet.pb.go文件中就会有一个名为RegisterGreeterServer的函数。这个函数接收两个参数:一个是gRPC服务器,一个是Greeter服务的实现。你可以使用这个函数将你的服务实现注册到gRPC服务器,然后gRPC服务器就可以处理对Greeter服务的请求了。
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}
- 客户端类型:这是一个用于调用gRPC服务的客户端类型。例如,如果你的
.proto文件中定义了一个名为Greeter的服务,那么生成的greet.pb.go文件中就会有一个名为GreeterClient的接口。这个接口包含了Greeter服务中的所有方法,你可以通过这个接口调用Greeter服务。
type GreeterClient interface {
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
接着,我们需要实现服务。以下是在Go语言中实现服务的代码:
// server.go
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/greet.pb.go"
)
type server struct{}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
客户端代码
// client.go
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
pb "path/to/greet.pb.go"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
通过protobuf,我们可以仅维护一套proto文件,就可以快速实现跨语言的PRC方法调用
除此之外,PRC框架往往还提供功能,来加速我们的日常开发效率:
常见RPC框架都能提供的功能:
-
通信:RPC框架提供了客户端和服务端之间的通信机制。这通常包括连接管理,消息序列化和反序列化,以及请求和响应的传输。
-
服务发现:在微服务架构中,服务可能会动态地上线和下线,因此RPC框架通常会提供服务发现的机制,使得客户端能够找到当前可用的服务实例。
-
负载均衡:当有多个服务实例可用时,RPC框架通常会提供负载均衡的机制,以均匀地分配请求到各个服务实例。
-
容错处理:RPC框架通常会提供容错处理的机制,例如重试,超时,熔断等,以提高系统的可用性和稳定性。
-
跟踪和监控:为了方便排查问题和优化性能,RPC框架通常会提供跟踪和监控的功能,例如请求日志,性能指标,调用链跟踪等。
-
安全:RPC框架可能会提供安全相关的功能,例如身份验证,权限控制,数据加密等。
以上仅代表我个人对RPC的个人理解,由于认知有限,不免有很多错误,恳请大家进行指正,并欢迎在评论区一起讨论有关RPC框架的相关内容