RPC理念,高性能RPC框架gRpc核心概念及示例

Posted 架构师是怎样炼成的

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RPC理念,高性能RPC框架gRpc核心概念及示例相关的知识,希望对你有一定的参考价值。

RPC理念

RPC(Remote Procedure Call)即远程过程调用,RPC框架的目标就是让远程服务调用更加简单,透明,RPC框架负责屏蔽底层的传输方式(TCP或UDP),序列化方式(XML/JSON/二进制)和通信细节,服务调用者可以像调用本地接口一样调用远程服务的提供者,而不必关心底层通信细节和调用过程,RPC调用的基本原理图:

图注:RPC调用基本原理

一次完整的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这些步骤都封装起来,让用户对这些细节透明。

目前在RPC领域可谓百花齐放,各大厂均有重复造轮子的迹象,业界主流的RPC框架主要分三类:

  1. 只支持特定语言的RPC框架,例如新浪微博的Moton

  2. 支持多种语言RPC框架,如Google的gRpc,Facebook的Thrift

  3. 支持服务治理等服务化特性的分布式服务框架,底层仍然是RPC框架,如阿里的Dubbo,华为的ServiceComb,目前这两项目均在Apache的孵化器中孵化

gRpc简介

gRPC  是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, php 和 C# 支持

gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用,gRpc采用Google内部长期使用,经大量生产流量验证的protobuf序列化,具有很高的性能,有了 gRPC, 我们可以一次性的在一个 .proto 文件中定义服务并使用任何支持它的语言去实现客户端和服务器,反过来,它们可以在各种环境中,从Google的服务器到你自己的平板电脑—— gRPC 帮你解决了不同语言及环境间通信的复杂性。使用 protocol buffers 还能获得其他好处,包括高效的序列号,简单的 IDL 以及容易进行接口更新

在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

RPC理念,高性能RPC框架gRpc核心概念及示例
图注:gRpc调用原理

gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。所以,你可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。此外,Google 最新 API 将有 gRPC 版本的接口,使你很容易地将 Google 的功能集成到你的应用里

另外在学习gRpc是强烈建议掌握的知识点:
  • JavaNIO

  • Netty

  • Google protobuff

  • Http2协议

  • Reactor模式

  • 多线程

gRpc核心概念

注:本文列举的概念并不是全部的gRpc概念,如需了解更多,请参阅其官网
gRPc官网文档:https://grpc.io/docs/guides/

定义服务

正如其他 RPC 系统,gRPC 基于如下思想:定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型。gRPC 默认使用 protocol buffers 作为接口定义语言(如果尚不熟悉protobuff,可以先参考Google protobuff, protocol-buffers文档:https://developers.google.com/protocol-buffers/),来描述服务接口和有效载荷消息结构。如果有需要的话,可以使用其他替代方案。

service HelloService {
 rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
 required string greeting = 1;
}
message HelloResponse {
 required string reply = 1;
}

gRPC 允许你定义四类服务方法:

  • 单项 RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

rpc SayHello(HelloRequest) returns (HelloResponse){
}
  • 服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
  • 客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
  • 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

使用 API 接口

gRPC 提供 protocol buffer 编译插件,能够从一个服务定义的 .proto 文件生成客户端和服务端代码。通常 gRPC 用户可以在服务端实现这些API,并从客户端调用它们。

  • 在服务侧,服务端实现服务接口,运行一个 gRPC 服务器来处理客户端调用。gRPC 底层架构会解码传入的请求,执行服务方法,编码服务应答。

  • 在客户侧,客户端有一个存根实现了服务端同样的方法。客户端可以在本地存根调用这些方法,用合适的 protocol buffer 消息类型封装这些参数— gRPC 来负责发送请求给服务端并返回服务端 protocol buffer 响应。

RPC 生命周期

现在让我们来仔细了解一下当 gRPC 客户端调用 gRPC 服务端的方法时到底发生了什么

单向RPC

首先我们来了解一下最简单的 RPC 形式:客户端发出单个请求,获得单个响应。

  • 一旦客户端通过桩调用一个方法,服务端会得到相关通知 ,通知包括客户端的元数据,方法名,允许的响应期限(如果可以的话)

  • 服务端既可以在任何响应之前直接发送回初始的元数据,也可以等待客户端的请求信息,到底哪个先发生,取决于具体的应用。

  • 一旦服务端获得客户端的请求信息,就会做所需的任何工作来创建或组装对应的响应。如果成功的话,这个响应会和包含状态码以及可选的状态信息等状态明细及可选的追踪信息返回给客户端 。
    假如状态是 OK 的话,客户端会得到应答,这将结束客户端的调用。

服务端流式 RPC

服务端流式 RPC 除了在得到客户端请求信息后发送回一个应答流之外,与我们的简单例子一样。在发送完所有应答后,服务端的状态详情(状态码和可选的状态信息)和可选的跟踪元数据被发送回客户端,以此来完成服务端的工作。客户端在接收到所有服务端的应答后也完成了工作。

客户端流式 RPC

客户端流式 RPC 也基本与我们的简单例子一样,区别在于客户端通过发送一个请求流给服务端,取代了原先发送的单个请求。服务端通常(但并不必须)会在接收到客户端所有的请求后发送回一个应答,其中附带有它的状态详情和可选的跟踪数据。

双向流式 RPC

双向流式 RPC ,调用由客户端调用方法来初始化,而服务端则接收到客户端的元数据,方法名和截止时间。服务端可以选择发送回它的初始元数据或等待客户端发送请求。
下一步怎样发展取决于应用,因为客户端和服务端能在任意顺序上读写 - 这些流的操作是完全独立的。例如服务端可以一直等直到它接收到所有客户端的消息才写应答,或者服务端和客户端可以像"乒乓球"一样:服务端后得到一个请求就回送一个应答,接着客户端根据应答来发送另一个请求,以此类推。

gRpc示例

本示例模拟了日常开发中,不同服务间的间的调用,具有很强的实战性,项目代码已上传至githug,如有需要clone到本地及可运行

RPC理念,高性能RPC框架gRpc核心概念及示例

grpc-api:存放由.proto文件生产的服务接口及实体类,因为客户端及服务端均需要此模块,故将其抽取成公共模块(此处注意:proto编译器能生成我们需要的文件,但每次执行命令是比较烦人的事情,maven中有专门生产protobuff文件的插件,根据.proto文件生成需要的服务接口及实体类的工程我已上传到github,如需要自取: https://github.com/JavaDebugMan/Protobuf)
grpc-commom:公共模块
grpc-task:客户端模块
grpc-user:服务端模块

服务端

服务端的工作有两个:
1.实现我们服务定义的生成的服务接口:做我们的服务的实际的“工作”。
2.运行一个 gRPC 服务器,监听来自客户端的请求并返回服务的响应。

实现 GreeterGrpc.GreeterImplBase

    static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
       @Override
       public void sayHelloAgain(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
           HelloReply reply = HelloReply.newBuilder().setMessage("Hello again " + request.getName()).build();
           responseObserver.onNext(reply);
           responseObserver.onCompleted();
       }
       @Override
       public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
           HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
           responseObserver.onNext(reply);
           responseObserver.onCompleted();
       }
   }

简单rpc

    private Server server;
   private void start() throws IOException {
       /* The port on which the server should run */
       int port = 50051;
       server = ServerBuilder.forPort(port)
               .addService(new GreeterImpl())
               .build()
               .start();
       logger.info("Server started, listening on " + port);
       Runtime.getRuntime().addShutdownHook(new Thread() {
           @Override
           public void run() {
               // Use stderr here since the logger may have been reset by its JVM shutdown hook.
               System.err.println("*** shutting down gRPC server since JVM is shutting down");
               HelloWorldServer.this.stop();
               System.err.println("*** server shut down");
           }
       });
   }
   private void stop() {
       if (server != null) {
           server.shutdown();
       }
   }
   /**
    * Await termination on the main thread since the grpc library uses daemon threads.
    */

   private void blockUntilShutdown() throws InterruptedException {
       if (server != null) {
           server.awaitTermination();
       }
   }
   /**
    * Main launches the server from the command line.
    */

   public static void main(String[] args) throws IOException, InterruptedException {
       final HelloWorldServer server = new HelloWorldServer();
       server.start();
       server.blockUntilShutdown();
   }

客户端

创建存根

为了调用服务方法,我们需要首先创建一个 存根,或者两个存根:

channel = NettyChannelBuilder.forAddress(host, port)
       .negotiationType(NegotiationType.PLAINTEXT)
       .build();

如你所见,我们用一个 NettyServerBuilder 构建和启动服务器。这个服务器的生成器基于 Netty 传输框架。

我们使用 Netty 传输框架,所以我们用一个 NettyServerBuilder 启动服务器。

调用服务方法

 private final ManagedChannel channel;
   private final GreeterGrpc.GreeterBlockingStub blockingStub;
   /**
    * Construct client connecting to HelloWorld server at {@code host:port}.
    */

   public HelloWorldClient(String host, int port) {
       this(ManagedChannelBuilder.forAddress(host, port)
               // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
               // needing certificates.
               .usePlaintext(true)
               .build());
   }
   /**
    * Construct client for accessing RouteGuide server using the existing channel.
    */

   HelloWorldClient(ManagedChannel channel) {
       this.channel = channel;
       blockingStub = GreeterGrpc.newBlockingStub(channel);
   }
   public void shutdown() throws InterruptedException {
       channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
   }
   /**
    * Say hello to server.
    */

   public void greet(String name) {
       logger.info("Will try to greet " + name + " ...");
       HelloRequest request = HelloRequest.newBuilder().setName(name).build();
       HelloReply response;
       try {
           response = blockingStub.sayHello(request);
       } catch (StatusRuntimeException e) {
           logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
           return;
       }
       logger.info("Greeting: " + response.getMessage());
       try {
           response = blockingStub.sayHelloAgain(request);
       } catch (StatusRuntimeException e) {
           logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
           return;
       }
       logger.info("Greeting: " + response.getMessage());
   }
   /**
    * Greet server. If provided, the first element of {@code args} is the name to use in the
    * greeting.
    */

   public static void main(String[] args) throws Exception {
       HelloWorldClient client = new HelloWorldClient("localhost", 50051);
       try {
           /* Access a service running on the local machine on port 50051 */
           String user = "world";
           if (args.length > 0) {
               user = args[0]; /* Use the arg as the name to greet if provided */
           }
           client.greet(user);
       } finally {
           client.shutdown();
       }
   }


运行服务端及客户端

服务端:


RPC理念,高性能RPC框架gRpc核心概念及示例

可以发现服务端已在50051端口监听客户端连接


客户端:

运行客户端已答应出我们希望出现的Hello world


关于gRpc的服务注册与发现及负载均衡

gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展

我们完全可以使用etcd,consul等注册中心实现gRpc服务的注册于发现及 负载均衡


gRpc与Srpingboot的集成

https://github.com/LogNet/grpc-spring-boot-starter

以上是关于RPC理念,高性能RPC框架gRpc核心概念及示例的主要内容,如果未能解决你的问题,请参考以下文章

GRPC 1.15.0 发布,Google 高性能 RPC 框架

Google高性能RPC框架gRPC 1.0.0发布

gRPC原理详解

rpc概念及nfs的基本应用

RPC框架实践之:Google gRPC

gRPC 1.36.0 发布,高性能 RPC 框架