bingmous

欢迎交流,不吝赐教~

导航

gRPC

生态:https://github.com/grpc-ecosystem/awesome-grpc

概述

gRPC是由Google开源的一个高性能rpc框架,由内部的Stubby演化而来,2015年正式开源,是云原生时代的rpc标准(得益于Go的实现,容器技术/编排技术都是go实现)

核心设计思路

  • 网络通信:gRPC自己封装了网络通信的部分,提供多语言的网络通信的封装(c/java(netty),go)
  • 协议:http2,使用二进制传输、支持双向流(全双工)连接的多路复用
  • 序列化:(基于文本如json,基于二进制,如java原生序列化),protobuf,时间和空间效率是json的3-5倍,使用IDL语言定义
  • 代理的创建:让调用者像调用本地方法一样调用远程服务方法,stub

其他rpc框架

  • ThriftRPC,使用专属协议基于tcp,性能高于gRPC,但gRPC在云原生时代与其他组件合作顺利,所以gRPC应用更广泛

gRPC好处:

  • 高效的进程间通信(协议、序列化)
  • 支持多语言,原生支持c/go/java,其他c语言之上的语言也支持c++/c#/nodejs/python/ruby/php...,是c的实现上做适配,所以效率上比c/go/java要慢
  • 支持多平台,linux/macos/windows/android/ios
  • gRPC序列化方式采用protobuf,效率高
  • 目前rpc编程中,包括微服务开发,只有gRPC使用http2协议
  • 大厂背书(google)

Http2.0协议

1.x协议

  • 1.0:请求响应模式
    • 短连接(无状态),由于早期硬件性能不足,http在请求响应后就断开连接。在技术层面使用HttpSession解决服务端与客户端(cookie)连接问题
    • 文本传输、单工(无法实现服务器推送,只能采用客户端轮询方式实现)
  • 1.1:请求响应模式,实现了有限的长连接(keepalived头),可以升级为websocket协议实现双工
  • 1.x协议共性
    • 文本格式传输数据,可读性好但是效率差
    • 本质上无法实现双工
    • 资源请求,需要发送多次请求,建立多个连接才能完成(css/js),优化方法(动静资源分离,cdn)

2.x协议

  • 二进制协议,效率高于http1.x,可读性差
  • 可以实现双工通信
  • 一个请求,一个连接可以请求多个数据(多路复用)

http2.0的三个概念

  • 数据流,stream,一个连接上可以发送多个流,每个流里面有一个消息,消息里面有两个帧,包含了head数据、body数据(如果是Get请求就没有了body,只有head)
  • 消息,message
  • 帧,frame

http2.0其他概念

  • 数据流的优先级,可以为不同的stream设置权重,限制不同流的传输顺序
  • 流控,如client发送数据太快,server处理不过来,可以通知clien暂停发送

Protocol Buffers (protobuf)

  • protobuf是一种与编程语言无关(IDL),与具体的平台无关(OS),定义中间语言,可以方便的在client与server之间传输
  • 有两种版本,proto2,proto3,主流应用使用proto3
  • 需要安装protobuf编译器,将proto的IDL语言转换成某一种开发语言

github地址:https://github.com/protocolbuffers/protobuf
protobuf官网:https://protobuf.dev/

protobuf语法

idea中安装下protobuf插件(idea官方的即可,2021版本自带)

文件格式:.proto

版本声明:

syntax = "proto3"; //有2和3 使用3即可

注释:单行//,多行/* */

与java相关的语法:

option java_multiple_files = false; //生成一个文件还是多个文件
option_java_package = "org.example"; //生成的类放在哪个包下
option java_outer_classname = "HelloService"; //生成的外部类的名字,管理内部类,开发实际使用的是内部类,针对的是message

逻辑包:

package xxx; //protobuf对于文件内容的管理

导入:

import "xxx/Xxx.proto"; //在一个proto文件中导入另外一个定义的文件

基本类型:参考官网,https://protobuf.dev/programming-guides/proto3/#scalar

枚举:

enum Season {
	SPRING = 0; //必须从0开始
	SUMMER = 1;
}

消息message:

message LoginRequest {
	string username = 1; //1表示字段序号,范围1-2^29-1,19000-19999是proto保留,不能使用,不一定连续,唯一即可
	stirng password = 2;
}

//singlular:这个字段的值只能有0个或1个(默认关键字)
//repeated:这个字段返回的数据是多个,等价于java中的list,实际使用时是一个list

//一个proto文件中可以定义多个消息

//消息是可以嵌套的

//oneof关键字,其中一个,表示该字段只能是定义的其中一个
message MyMessage {
	string f1 = 1; //第一个字段
	oneof abc { //第二个字段 是其中一个
		string name = 2;
		int32 age = 3;
	}
}

服务定义:

service HelloService {
	rpc hello(HelloRequest) resturns(HelloResponse) {}
	//可以定义多个方法
}

//可以定义多个service

//有4个服务方式

proto文件生成的java类结构:

  • 一个proto文件生成一个类(不使用多文件),里面的每个message都是一个内部类,使用时使用内部类的builder方式
  • 一个proto文件中的service会生成一个类,名字为XxxGrpc,它有多个内部类为要使用的各个功能
    • XxxImplBase,需要继承实现的父类,提供服务的具体实现
    • XxxStub、这些以stub结尾的都是client的代理对象(client用这些对象rpc访问service),区别是网络通信方式不同
    • XxxBlockingStub
    • XxxFutureStub

gRPC开发

项目结构

xxx-api模块 //定义protobuf IDL语言(message、service),并且通过命令创建具体的代码,其他client、service引入使用

xxx-server模块 //实现api中定义的接口,发布rpc服务(创建服务端程序)

xxx-client模块 //创建服务stub(代理),基于代理进行rpc调用

一些开发经验:

  • proto文件一般放在main下的proto目录下
  • 一个proto对应一个文件,关闭多文件生成
  • 外部类名称一般与proto对应,如HelloProto对应Hello.proto,实际使用的是HelloProto的内部类

maven插件:https://github.com/grpc/grpc-java ,直接在工程中编译proto

服务端:

  • 服务端实现定义的服务Grpc基类,实现实际的service实现
    • 调用onNext方法回传响应数据
    • 调用onCompleted方法标记响应结束(服务端),如果不调用,客户端rpc不会结束
  • 构建server,并注册服务
    • ServerBuilder
  • 启动server

客户端:

  • 通过代理对象完成rpc调用
  • java中grpc是用netty实现的,需要获取channel
    • ManagedChannelBuilder
  • 通过channel获取stub(代理)
    • HelloServiceGrpc.newBlockingStub(managedChannel)
  • 准备调用参数
  • 通过stub完成rpc调用
  • 关闭channel

注意:

  • proto生成的java类,在gRPC传输时,接收的请求参数不会为null,string字段类型参数不会为null
  • 实际上,不允许为string类型字段赋值为null,否则会报NPE

gRPC的四种通信方式

  • 简单rpc,一元rpc(Unary RPC)
  • 服务端流式rpc(Server Streaming RPC)
  • 客户端流式rpc(Client Streaming RPC)
  • 双向流rpc(Bi-directional Stream RPC)
service HelloService {
  //一元rpc 客户端请求一次 服务器返回一个结果
  rpc hello(HelloRequest) returns (HelloResponse);

  //服务端流式rpc 客户端请求一次 服务端可以返回多个结果(不要认为可以返回一个List)
  rpc helloServerStream(HelloRequest) returns (stream HelloResponse);

  //客户端流式rpc 客户端可以发送多个数据 服务端返回一个结果
  rpc helloClientStream(stream HelloRequest) returns (HelloResponse);

  //双向流rpc 客户端可以发送多个数据 服务端可以返回多个结果
  rpc helloClientStreamServerStream(stream HelloRequest) returns (stream HelloResponse);
}

一元RPC

当client发起调用后,提交数据,等待服务端响应。也即是传统的服务端客户端通信方式

可以使用阻塞、异步、future方式处理响应

注意:

  • 如果服务端没有调用onComplete,客户端一直阻塞

服务端流式RPC

客户端发送一个请求对象,服务端返回多个结果对象。服务端可以在不同的时刻,返回结果

  • 客户端的响应结果为一个迭代器用于获取服务端的多个结果(同步情况)
  • 客户端异步情况下,参数中多了一个处理响应的异步观察者,用于处理服务端返回的结果(客户端可以控制参数,所以处理结果的异步逻辑在参数中)

在定义service时,需要在返回值前加stream

应用场景:如请求股票信息,只请求一次,后续返回多个结果

注意:

客户端流式RPC

客户端发送多个请求对象,服务端只返回一个结果

  • 服务端在返回值中返回一个异步处理客户端请求的逻辑(客户端request的观察者,服务端可以控制返回值,所以处理客户端请求的逻辑在返回值中,跟同步相比,request的处理放在了返回值中)
  • 客户端在rpc调用时返回值是一个request的观察者,与服务端的返回值对应,服务端返回客户端流的处理逻辑,客户端拿着服务端返回的客户端流进行发送数据,服务端和客户端用同一个Observer

在定义service时,需要在请求参数前加stream

应用场景:iot设备给服务端发送数据

双向流RPC

客户端可以发送多个消息给服务端,服务端也可以发送多个消息给客户端。

  • 与客户端流rpc结构相同(是因为不管是服务端流rpc还是客户端流rpc,服务端处理响应都是StreamObserver)

应用场景:聊天室

api总结

客户端

  • 同步处理

异步处理响应时,一般都是在参数中传入StreamObserver,响应时被框架自动调用。

发送多个请求时,一般都是在返回值中为一个StreamObserver,用于发送多个数据,自己调用。

服务端

  • 返回响应数据:服务端在做RPC接口的实现时,响应给客户端使用StreamObserver,放在参数里,因为需要调用框架接口,告诉框架要返回的数据,(一元RPC,服务端流RPC,客户端流RPC,双向流RPC,都是使用StreamObserver返回数据)
    • 一元RPC和客户端流RPC时为什么没有把响应放在方法的返回值上?
      我的理解是,为了保持操作的灵活性及接口的统一,因为返回数据不仅仅是一个值,还有其他接口调用(onComplete,onError);服务端在处理单个响应和多个响应时接口也可以保持一致
  • 获取请求数据:
    • 当客户端传递的请求为一个时(一元RPC,服务端流RPC),直接放在了参数里,由框架直接传递给用户直接使用
    • 当传递的请求为多个时(客户端流RPC,双向流RPC),放在了返回值里,返回值是一个StreamObserver,因为需要告诉框架如何处理多个异步请求。
      • 为什么没有放在请求参数里?
        服务端在响应时及客户端发送数据时,都可以发送多个请求(http2中不同的stream),这些操作被设计成了不同的接口(onNext/onComplete/onError),那么在处理客户端的请求时,也需要处理不同的操作情况下如何处理(onNext/onComplete/onError),每个接口的逻辑需要用户实现,这与用户发送多个请求时的接口是一一对应的;那么这些逻辑就封装在了StreamObserver里给到框架(框架如何处理null的StreamObserver?会报NPE)
  • 请求数据为null,服务端收到的请求对象也不为null,服务端响应为null,请求端接收的响应对象也不为null
  • 在No-Blocking情况下,客户端和服务端调用的接口参数形式都是相同的,好像客户端调用的就是服务端接收的一样

gRPC的代理方式

  • BlockingStub,阻塞(只能用于客户端发送一个数据,一元RPC、服务端流RPC)
  • Stub,异步,通过监听处理,(可用于所有RPC方式,因为不管是发送数据还是接收数据都是异步方式,StreamObserver可以发送多个数据也可以接收多个数据)
  • FutureStub,可以同步,也可以异步,类似NettyFuture,只能用于一元RPC,(只能接收数据,且只能处理一个)
    • 同步就使用get获取结果
    • 异步可以通过addListener,这个无法获取返回值
    • 异步可以使用工具类Futures.addCallback,可以获取返回值
Blocking(BlockingStub) Non-Blocking(Stub) Future(FutureStub)
Unary RPC
ServerStream RPC
ClientStream RPC
Bi-Direction Stream RPC

gRPC与SpringBoot整合

整合思想:

  • grpc-server
  • grpc-client
  • api层不管是不是用框架都要使用proto文件生成

SpringBoot与grpc整合过程中,对于服务端做了什么封装

  • 原生的grpc
    • 实现服务接口
    • 创建服务端,发布grpc功能
  • 如何封装
    • 实现服务接口与具体业务相关,无法封装
    • 服务端创建功能可以封装,但需要额外配置(端口 --> 配置;具体的服务实现 --> 注解@GrpcService)

注意:

  • 使用的grpc-spring-boot-starter版本内部的grpc版本与外部的要一致,否则依赖冲突
  • protoc-gen-grpc-java的版本要与使用的grpc的版本一致,protoc的版本要使用protobuf的版本,且要与grpc内部引用的protbobuf版本一致,否则编译的类报错

SpringBoot与grpc整合过程总,对客户端做了什么封装

  • 原生grpc
    • 通过channel设置服务端ip和port
    • 获取stub,即远程服务代理,进行调用
  • 如何封装
    • 连接服务端,通过配置封装
    • stub代理,通过spring容器封装(连接服务端 --> 配置;服务端代理对象 --> 注解@GrpcClient)

gRPC高级应用

  • 拦截器 (一元拦截器)
  • Stream Tracer (监听流,流拦截器)
  • Retry Policy (客户端重试)
  • NameResolver (配置中心)
  • 负载均衡 (pick-first,轮询)
  • grpc与微服务整合
    • 序列化(protobuf)与dubbo整合
    • grpc与dubbo整合,使用grpc通信
    • grpc与GateWay整合
    • grpc与JWT整合,做认证
    • grpc与Nacos2.0整合,自定义配置中心
    • grpc可以替换OpenFeign
    • k8s可以与grpc无缝对接(k8s就是使用的grpc)

源码

查看linux是否支持epoll:

cat /usr/include/bits/syscall.h | grep epoll # 查看是否有epoll系统调用

eventLoop的创建

服务端使用ServerBuilder.forPort()返回的ServerBuilder实际是NettyServerBuilder,在这个类中会加载bossGroup和workerGroup:

    static {
        MIN_KEEPALIVE_TIME_NANO = TimeUnit.MILLISECONDS.toNanos(1L);
        MIN_KEEPALIVE_TIMEOUT_NANO = TimeUnit.MICROSECONDS.toNanos(499L);
        MIN_MAX_CONNECTION_IDLE_NANO = TimeUnit.SECONDS.toNanos(1L);
        MIN_MAX_CONNECTION_AGE_NANO = TimeUnit.SECONDS.toNanos(1L);
        AS_LARGE_AS_INFINITE = TimeUnit.DAYS.toNanos(1000L);
        DEFAULT_BOSS_EVENT_LOOP_GROUP_POOL = SharedResourcePool.forResource(Utils.DEFAULT_BOSS_EVENT_LOOP_GROUP);
        DEFAULT_WORKER_EVENT_LOOP_GROUP_POOL = SharedResourcePool.forResource(Utils.DEFAULT_WORKER_EVENT_LOOP_GROUP);
    }

Utils.class中,会创建默认的eventLoop,默认bossGroup是1个,workerGroup是0个(0,后面会根据实际核心数创建NettyRuntime.availableProcessors()),默认poolName为grpc-nio-boss-ELG/grpc-nio-worker-ELG,如果支持epoll为grpc-default-boss-ELG/grpc-default-worker-ELG

在创建bossGroup时,会在MultithreadEventExecutorGroup.class中创建this.terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);

    static {
        CONTENT_TYPE_HEADER = AsciiString.of(GrpcUtil.CONTENT_TYPE_KEY.name());
        CONTENT_TYPE_GRPC = AsciiString.of("application/grpc");
        TE_HEADER = AsciiString.of(GrpcUtil.TE_HEADER.name());
        TE_TRAILERS = AsciiString.of("trailers");
        USER_AGENT = AsciiString.of(GrpcUtil.USER_AGENT_KEY.name());
        NIO_BOSS_EVENT_LOOP_GROUP = new Utils.DefaultEventLoopGroupResource(1, "grpc-nio-boss-ELG", Utils.EventLoopGroupType.NIO);
        NIO_WORKER_EVENT_LOOP_GROUP = new Utils.DefaultEventLoopGroupResource(0, "grpc-nio-worker-ELG", Utils.EventLoopGroupType.NIO);
        if (isEpollAvailable()) {
            DEFAULT_CLIENT_CHANNEL_TYPE = epollChannelType();
            EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE = epollDomainSocketChannelType();
            DEFAULT_SERVER_CHANNEL_FACTORY = new ReflectiveChannelFactory(epollServerChannelType());
            EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = epollEventLoopGroupConstructor();
            DEFAULT_BOSS_EVENT_LOOP_GROUP = new Utils.DefaultEventLoopGroupResource(1, "grpc-default-boss-ELG", Utils.EventLoopGroupType.EPOLL);
            DEFAULT_WORKER_EVENT_LOOP_GROUP = new Utils.DefaultEventLoopGroupResource(0, "grpc-default-worker-ELG", Utils.EventLoopGroupType.EPOLL);
        } else {
            logger.log(Level.FINE, "Epoll is not available, using Nio.", getEpollUnavailabilityCause());
            DEFAULT_SERVER_CHANNEL_FACTORY = nioServerChannelFactory();
            DEFAULT_CLIENT_CHANNEL_TYPE = NioSocketChannel.class;
            EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE = null;
            DEFAULT_BOSS_EVENT_LOOP_GROUP = NIO_BOSS_EVENT_LOOP_GROUP;
            DEFAULT_WORKER_EVENT_LOOP_GROUP = NIO_WORKER_EVENT_LOOP_GROUP;
            EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = null;
        }
    }

实际Resource实现是其内部类DefaultEventLoopGroupResource,通过create方法创建EventLoopGroup,并使用DefaultThreadFactory,线程名称为poolName-poolId-nextId,poolId和nextId(池中线程数)都是从1开始递增

NettyServerBuilder会使用ServerImplBuilder创建一个默认的executor,用于执行每个连接后gRPC中的方法调用:

    static {
        DEFAULT_EXECUTOR_POOL = SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR);
        DEFAULT_FALLBACK_REGISTRY = new ServerImplBuilder.DefaultFallbackRegistry();
        DEFAULT_DECOMPRESSOR_REGISTRY = DecompressorRegistry.getDefaultInstance();
        DEFAULT_COMPRESSOR_REGISTRY = CompressorRegistry.getDefaultInstance();
        DEFAULT_HANDSHAKE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(120L);
    }

GrpcUtil中创建channel使用的executor,这里的threadFactory创建thread从grpc-default-executor-0开始

        SHARED_CHANNEL_EXECUTOR = new Resource<Executor>() {
            private static final String NAME = "grpc-default-executor";

            public Executor create() {
                return Executors.newCachedThreadPool(GrpcUtil.getThreadFactory("grpc-default-executor-%d", true));
            }

            public void close(Executor instance) {
                ((ExecutorService)instance).shutdown();
            }

            public String toString() {
                return "grpc-default-executor";
            }
        };

posted on 2023-06-08 20:14  Bingmous  阅读(264)  评论(0编辑  收藏  举报