gRPC

HTTP调用与RPC(Remote Procedure Call)

RPC是远端过程调用,其调用协议通常包含传输协议和序列化协议,如:传统的http1.1和json(传统的HTTP也属于RPC)、gRPC的http2和protobuf。

调用远程计算机上提供的函数。服务端实现具体的函数功能,客户端本地留有远程服务接口定义存根,表现形式是函数调用,但实际上与HTTP调用一样是向远程计算器发起的网络请求,函数的入参是请求,函数的返回值是响应。RPC函数调用的形式封装了跨语言的服务访问,大大简化了应用层编程(HTTP调用需要显式的序列化和反序列化)。

RPC与HTTP调用的不同点是:传输协议、序列化方式、代码的调用方式。

为什么要用gRPC?

通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性。

序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化协议。使用 HTTP/2 + PB, 保障了 RPC 调用的高性能。如果是一个大型服务,内部子系统较多,微服务架构,接口非常多的情况下,gRPC将体现出性能优势。

gRPC还可以很简单的插入身份认证、负载均衡、日志和监控等功能。

环境安装

$ brew install protobuf protoc-gen-go protoc-gen-go-grpc
$ export GO111MODULE=off # 有的包必须要装到$GOPATH/src下
$ cd $GOPATH/src
$ git clone https://github.com/golang/text.git ./golang.org/x/text
$ git clone https://github.com/golang/net.git ./golang.org/x/net
$ git clone https://github.com/grpc/grpc-go ./google.golang.org/grpc
$ git clone https://github.com/google/go-genproto.git ./google.golang.org/genproto
$ git clone https://github.com/protocolbuffers/protobuf-go.git ./google.golang.org/protobuf
$ git clone https://github.com/golang/protobuf.git ./github.com/golang/protobuf
$ git clone https://github.com/googleapis/googleapis ./googleapis # 这个包直接放src目录下
$ export GO111MODULE=on # 开启 go mod 才能安装指定版本
# $ go install github.com/golang/protobuf/[email protected] # 这个代码生成的包不装太新的

golang工程如果配好了GOPROXY=https://goproxy.io,direct,仍然有包下不下来,go.mod可用replace来替换代码库地址,类似如下这样:

Protocol Buffers

可以理解为与 json、xml 作用相类似。

为什么使用 Protocol Buffer?

  • 更小:它可以在序列化数据的同时对数据进行压缩,所以它生成的字节流,通常都要比相同数据的其他格式(例如 XML 和 JSON)占用的空间明显小很多,小3-10倍。

  • 更快:序列化速度更快,比xml和JSON快20-100倍,体积缩小后,传输时,带宽也会优化

  • 更简单:proto编译器,自动进行序列化和反序列化

  • 维护成本低:跨平台、跨语言,多平台仅需要维护一套对象协议(.proto)

  • “向后”兼容性好:允许在保证向后兼容的前提下更新字段

Protocol Buffer 的缺点是调试不便,以二进制数据流做传输,速度是快了,但可读性变差了。

gRPC默认使用 Protocol Buffer 作为接口设计语言(IDL,interface design language),这个 .proto 文件包括两部分:

  • gRPC服务的定义

  • 服务端和客户端之间传递的消息

定义好服务之后,执行:protoc --go_out=plugins=grpc:. helloworld/helloworld.proto 可以自动生成gRPC接口代码xxx.pb.go文件。

向前/向后兼容

所谓的“向后兼容”(backward compatible),就是说,当模块 B 升级了之后,它能够正确识别模块 A 发出的老版本的协议。 所谓的“向前兼容”(forward compatible),就是说,当模块A升级了之后,模块 B 能够正常识别模块 A 发出的新版本的协议。

这个特性依赖于字段编号始终表示相同的数据项。如果从服务的新版本的消息中删除字段,则永远不应重复使用该字段编号。 可以通过使用reserved关键字强制执行此行为。

gRPC四种通信方式

  1. 简单RPC(Simple RPC):就是一般的rpc调用,一个请求对象对应一个返回对象。

  2. 服务端流式RPC(Server-side streaming RPC):一个请求对象,服务端返回数据流(数组,每次传一个元素)。

  3. 客户端流式RPC(Client-side streaming RPC):客户端传入连续的请求对象(数组),服务端返回一个响应结果。

  4. 双向流式RPC(Bidirectional streaming RPC):结合客户端流式RPC和服务端流式RPC,可以传入多个对象,返回多个响应对象。

流式接口的使用场景:一个接口要发送大量数据时,一次只传输一部分数据,分批传输数据,比如文件的传输,用流式接口可以降低服务器的瞬时压力,对客户端的响应也更快。

gRPC拦截器

与HTTP服务的拦截器功能类似,可以在RPC方法前、后执行一些操作。

拦截器分类

  • 一元拦截器 UnaryInterceptor

  • 流式拦截器 StreamInterceptor

典型应用场景:统一接口身份认证。

gRPC支持拦截器链。

进阶使用之import其他proto

import 同一级或子级的 proto

import 其他自己项目的proto

各个项目都放到同级目录,按如上这样import,调整 protoc 编译命令即可,protoc可以有多个--proto_path参数指定,编译时就会到指定的多个路径下找package。

import google 提供的 proto

不管引用了哪个包,直接 import 它下面的代码即可。

与 import 其他自己项目的proto不同的是,import 自己的项目要写上项目名

而 import google 提供的 proto 不写,这样写是不对的:import "googleapis/google/api/annotations.proto"

注意这个protoc编译指令指定的多个proto_path,除了指定的当前项目目录的.,其他添加的路径都要是全路径,不可以写成../,也不能有~/这种路径

传递metadata(类似于HTTP调用当中的Header)

需要导入 google.golang.org/grpc/metadata 这个包

metadata的Header与Trailer

Header在 RPC 开始时(在任何数据之前)发送,而 Trailer 则在结束时(在发送完所有数据之后)发送,参考

客户端设置Request的metadata

发起请求时需要传递 context,使用创建的这个ctxH 即可。

服务端读取Request的metadata

一元模式 数据的读取

流模式 数据的读取

服务端设置Response的metadata

server 端会把 metadata 分为 header 和 trailer 发送给 client

一元模式 数据的写入

流模式 数据的写入

客户端读取Response的metadata

一元模式读取 header 信息

流模式读取 header 信息

超时处理

gRPC 服务的客户端可以通过 context 来控制超时时间。

请求失败重试

gRPC 的重试策略有两种,分别是:重试(retryPolicy)和对冲(hedging),在客户端创建 gRPC 连接时,下面以通过 service config 来配置 retryPolicy 为例:

MaxAttempts:最大重试次数。

RetryableStatusCodes:配置哪些错误是允许重试的。

InitialBackoff:第一次重试等待的间隔。

BackoffMultiplier:每次间隔的指数因子。

gPRC 采用指数避退+随机间隔 组合起来的方式进行重试。

指数避退:重试间隔时间按照指数增长,如等 3s 9s 27s后重试。指数避退能有效防止对对端造成不必要的冲击。

MaxBackoff:等待的最大时长,随着重试次数的增加,不希望第N次重试等待的时间变成30分钟这样不切实际的值。

retryThrottling限流配置是针对整个服务器的,当客户端的失败和成功比超过某个阈值时,gRPC 会通过禁用这些重试策略来防止由于重试导致服务器过载。

错误处理与状态码

以下是客户端的错误处理示例:

gRPC 服务端要像这样返回 err:

单元测试这样断言:

调试工具

https://github.com/fullstorydev/grpcui

使用Wireshark抓包

默认不会识别HTTP2,要设置一下,在第一个报文上右击,选择解码为(Decode As),会出现一个FieldTCP的行,把port后面的Value字段设置为50051(启动的gRPC服务的端口号),把当前(Current)字段设置为HTTP2,再点OK即可。接下来即可看到gRPCHTTP2的报文,下面展开的是一个请求的报文,发送的是string类型的字段,值为world

浅谈gRPC服务端理解

如上这样一个 gRPC 服务端的启动原理是什么呢?参考

初始化

NewServer

注册

用 Protobuf 生成出来的 .pb.go 文件中,会定义出 Service APIs interface 作为 server 的具体实现约束,必须要实现所定义接口包含的所有方法。

服务端注册代码:pb.RegisterSimpleServiceServer(s, &server{}),接口的具体实现是server{}实例的,如上 gRPC 源码中register方法把接口的具体实现注册到内部service实例,使接口方法名与其具体实现一一对应,以便于后续实际调用的使用。

  • server:服务的接口信息

  • md:一元服务的 RPC 方法集

  • sd:流式服务的 RPC 方法集

  • mdata:metadata,元数据

监听

监听/处理阶段,核心代码如下:

  • 循环处理客户端请求,通过 lis.Accept 取出新的客户端请求,如果队列中没有需处理的连接时,会形成阻塞等待。

  • 若 lis.Accept 失败,则触发休眠机制,第一次休眠 5ms,不断翻倍,最大 1s。

  • 若 lis.Accept 成功,代表监听到请求,重置休眠的时间计数和启动一个新的 goroutine 调用 handleRawConn 方法去执行/处理新的请求,也就是说每一个请求都是不同的 goroutine 在处理

  • 加入 waitGroup 用来处理优雅重启或退出,等待所有 goroutine 执行结束之后才会退出。

注1:listen()函数可以让套接字进入被动监听状态(当没有客户端请求时,套接字处于“睡眠”状态;当接收到客户端请求时,套接字才会被“唤醒”来响应请求)。

注2:当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

注3:listen()只是让套接字进入监听状态,并没有真正接收客户端请求,listen()不会阻塞。通过accept()函数来接收客户端请求,accept()会阻塞程序执行,直到有新的请求到来,accept()每次接受一个请求。

浅谈gRPC客户端理解

创建连接

grpc.Dial方法是对grpc.DialContext的封装,DialContext是异步建立连接的,也就是并不是马上生效,处于Connecting状态,而要到达Ready状态才可用。

如果想通过Dial方法就立刻打通与服务端的连接,需要在grpc.Dial方法多传一个opt参数:grpc.WithBlock()

创建客户端实例

调用

proto 生成的 RPC 方法更像是一个包装盒,把需要的东西放进去,而实际上调用的还是 grpc.invoke 方法。如下:

通过概览,可以关注到三块调用。如下:

  • newClientStream:获取传输层 Trasport 并组合封装到 ClientStream 中返回,在这块会涉及负载均衡、超时控制、 Encoding、 Stream 的动作,与服务端基本一致的行为。

  • cs.SendMsg:发送 RPC 请求出去,但其并不承担等待响应的功能。

  • cs.RecvMsg:阻塞等待接受到的 RPC 方法响应结果。

关闭连接

conn.Close方法会取消ClientConn上下文,同时关闭所有底层传输。涉及如下:

  • Context Cancel

  • 清空并关闭客户端连接

  • 清空并关闭解析器连接

  • 清空并关闭负载均衡连接

  • 添加跟踪引用

  • 移除当前通道信息

gRPC-Gateway

安装

参考

使用示例

protobuf定义:

编写如下服务端源码,执行即可。更丰富的例子参考

测试:curl -X POST -d '{"name": "will"}' 127.0.0.1:9090/helloworld

Last updated

Was this helpful?