27 gRPC 与分布式系统
[TOC]
gRPC 与分布式系统
采用微服务架构或其它分布式架构开发的分布式系统,将业务拆分成一个独立的服务或进程,进程间通信就是必须的。进程间通信通常使用消息传递方式实现,同步的请求-响应或是异步事件回调。同步请求-响应方式是客户端向服务端发送请求消息并等待服务端的响应。异步事件回调则是进程间通过异步消息传递进行通信。
分布式系统进程间通信采用同步请求-响应方式,最常见的是将服务资源构建为 RESTful 服务,通过 HTTP 进程网络调用进行访问和状态变更。但对大多数场景来说 RESTful 服务实现进程间通信是繁琐和低效的。相比 RESTful 服务 gRPC 要更高效,gRPC 在建立连接以后可以以异步模式或流模式进行操作。
微服务架构
微服务架构是一种分布式系统架构模式。将程序构建为一组独立开发、部署、扩展和松耦合、面向业务的服务。每一组服务都是独立运行的,有独立的进程。优点是每个服务足够内聚,业务粒度细,一个微服务聚焦一个指定的业务功能或业务需求。微服务的主要优势就是在于架构大型系统,可以有效的减少系统复杂度,使系统架构的逻辑更清晰明了。
进程间通信
分布式系统实现进程间通信常见通信框架的有RPC、SOAP 和 REST,gRPC 是 RPC 框架的一种。
RPC 远程过程调用
RPC 远程过程调用是进程间通信请求-响应方式最常用的。RPC 的通信方式客户端可以像调用本地方法那样远程调用某个方法。比如 Java 的远程方法调用 RMI 框架是构建在 TCP 通信协议之上的 RPC 框架,这种基于 TCP 之上的 RPC 框架使用上都有大量的规范限制,实现颇为复杂。
SOAP 简单对象访问协议
SOAP 简单对象访问协议,是面向服务的架构 SOA 中的标准通信技术,用于在服务之间交换基于 XML 的结构化数据,能够基于任意应用层通信协议进行通信,最常用的是 HTTP 。
SOAP 曾是一项非常流行的技术,但消息格式和它所构建的各种规范都比较复杂,影响了构建分布式系统的敏捷性。如今构建分布式系统,SOAP Web 服务被认为是一种遗留技术,大多数都采用 REST 风格,不再使用 SOAP。
REST 描述性状态迁移
REST 是面向资源的架构的基础,这种架构中需要将分布式应用程序建模为资源集合,访问这些资源的客户端可以变更这些资源的状态。
REST 的通用实现是 HTTP。根据 HTTP 请求方法 GET、POST、PUT、DELETE、PATCH 等变更资源的状态。资源状态可以以 JSON、XML、HTML、YAML 等文本的格式来表述。HTTP 和 JSON 构建 RESTful 服务是构建微服务的标准方法,但还有一些局限性,这些局限限制了 REST 作为消息协议在微服务架构中的应用。
REST 基于文本协议是它的主要局限性。RESTful 服务建立在基于文本的传输协议 HTTP1 之上,这是低效的。
RESTful 服务之间缺乏强类型接口也是它的局限性。服务使用不同的语言构建,缺乏明确定义和强类型的服务接口。RESTful 中现有的各种服务定义技术,如 OpenAPI、Swagger 等都是补救措施,没有和底层的架构风格或消息协议紧密集成在一起。
gRPC
gRPC 是 google 发布的开源 RPC 框架,基础设施具有标准化、通用和跨平台的特点。gRPC 是支持大规模进程间通信的技术,可以弥补传统进程间通信技术的大多数缺点。gRPC 以及广泛应用于微服务和云原生程序的构建。
gRPC 的主要优势
gRPC 主要有高效、强约定、强类型、多语言、支持双工流等优势。 gRPC 没有使用 JSON、XML 这样的文本化格式,使用基于 protocol buffers 的二进制协议进行通信,gRPC 在 HTTP2 之上实现了 protocol buffers ,从而能够更快地处理进程间通信,使 gRPC 成为最高效的进程间通信技术之一。gRPC 为应用程序开发提供了一种契约优先的方式,首先必须定义服务接口然后才能去实现细节,这点上与 RESTful 服务定义中的 OpenAPI、Swagger 或 SOAP Web 服务中的 WSDL 不同,gRPC 这种方式更简单、可靠和可扩展。gRPC 采用 protocol buffers 定义,明确了通信使用的类型,在多团队协作开发中因为类型错误导致的不稳定因素会减少。gRPC 是支持多语言的,基于 protocol buffers 的服务定义都是语言中立的,任意语言都能实现与现有 gRPC 服务或客户端进行互操作。gRPC 在客户端和服务端都支持原生流,这个功能被整合到服务定义本身之中,所以开发流服务或流客户端都很容易,与 RESTful 相比流服务是 gRPC 的关键优势之一。
gRPC 的主要劣势
gRPC 不适合构建面向外部的服务,因为 gRPC 服务的强类型和强约定限制了向外暴露服务的灵活性,通过 gRPC 网关是解决这个问题的方案。gRPC 的生态系统还不如 REST 或 HTTP 等协议,客户端浏览器或 APP 对 gRPC 的支持还处于初级阶段。
gRPC 与 Thrift 比较
Thrift 是与 gRPC 类似的 RPC 框架。Thrift 拥有自己的接口定义语言并提供了多种编程语言的支持,可以在定义文件中定义数据类型和服务接口。Thrift 的传输层为网络 I/O 提供了抽象,可以使用 HTTP 或其它协议通信。两者相比,在性能上差距不大,社区支持度 gRPC 要稍高,在传输协议上 Thrift 同样可以使用 HTTP2 但是 gRPC 倾向性更强。
gRPC 与 GraphQL
GraphQL 是一门针对 API 的查询语言,是一项发展势头较好的进程间通信技术。为传统的 C/S 通信提供了一种新的方法,它允许客户端定义希望获得的数据、获取数据的方式以及数据格式。gRPC 则通过针对远程方法的特定约定实现 C/S 之间的通信。GraphQL 更适合面向外部的服务或API。
实现一个 gRPC 程序
gRPC 程序开发先定义服务接口,服务接口中包含允许远程调用的方法、方法参数以及调用这些方法所使用的消息格式等。定义的服务都以 protocol buffers 定义的形式进行记录,protocol buffers 是 gRPC 使用的接口定义语言,源码文件名以 .proto 形式扩展,IntelliJ IDEA 可以安装 protocol buffers 插件加快快发速度。
以这个身份信息录入和查询的 IdentityInfoService 服务为例,使用 Go 实现 gRPC 服务端, Java 实现 gRPC 客户端,体现 gRPC 跨语言的特性。服务端实现 addIdentityInfo(info) 实名信息录入方法 和 getInfo(infoId) 获取实名信息方法,客户端实现远程调用。

程序整体示意图
定义消息类型
消息是客户端和服务端交换数据的数据结构,定义消息类型就是约定双方交互数据的格式。IdentityInfoService 就需要定义 IdentityInfo 和 IdentityInfoId 结构体,因为按照 protocol buffers 的规则,远程方法只能有一个参数,返回值也只能有一个,如果要传递或返回更多参数就需要定义消息类型,并对所有的值进行分组,因此要分别定义 IdentityInfo 和 IdentityInfoId 消息类型。
消息类型定义使用 message 关键字,源码文件名以 .proto 为扩展名。定义每一个参数都分配编号,在 XML 或 JSON 等数据描述语言中一般通过成员名称绑定对应的数据,但是 protobuf 是通过字段的唯一编号绑定对应的数据。例如:IdentityInfo.proto
定义服务类型
服务是暴露给客户端的远程方法集合。IdentityInfoService 服务中 addIdentityInfo(info) 和 getInfo(infoId) 方法就是暴露给客户端的远程方法。
定义服务
定义服务类型第一步是编写 service 定义服务,例如在 IdentityInfo.proto 文件中追加服务的定义,服务定义使用 service 关键字,远程方法定义使用 rpc 关键字。
编译 proto
定义服务类型第二步根据定义的服务编译 protobuf 定义,可以使用 protoc-gen-go 编译出相应 Go 代码,也可使用一些集成开发环境可以快速为服务创建 protocol buffers 定义。例如 IntelliJ IDEA 安装 protocol buffers 插件后为服务创建 protocol buffers 定义。使用 protoc-gen 工具生成相应语言的代码可以参考文档 https://developers.google.com/protocol-buffers/docs/gotutorial 。
Windows 下编译 protobuf 要先搭建编译环境,先下载编译工具 https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-win64.zip 。解压工具包后将 proto bin 配置至环境变量 path。编译成 Go 代码需要 protoc-gen-go 编译器,下载 Go 版本编译器源码 go get github.com/golang/protobuf ,使用 go build 编译 mod\github.com\golang\[email protected]\protoc-gen-go 得到可执行文件 protoc-gen-go.exe 并且配置到环境变量 path 。
编译环境搭建完成后,使用命令 protoc --proto_path=./ --go_out=plugins=grpc:./ ./*.proto 编译获得 Go 版本源码 IdentityInfo.pb.go 。
服务实现
服务定义完成后,可以基于编译 protobuf 定义所得的源码实现服务。复制生成的 IdentityInfo.pb.go 文件到 server 模块 pb 目录下并将包名改成 pb 。先实现服务中远程方法的业务逻辑,再创建服务器监听特定端口并注册服务。
Service 实现
编写 IdentityInfoService.go,实现远程方法业务逻辑。Go 实现 service,要先引入相应的依赖 go get github.com/golang/protobuf go get -u google.golang.org/grpc。Go 实现的远程方法都会有第二个为 error 类型的返回值,这个返回值有特殊的生成方式,会返回给调用者(或者称为消费者),这个值也作为调用结果的依据。Go 实现的远程方法的第一个参数是 context.Context,Context 对象包含一些元数据,比如终端用户授权的标识和请求截止的时间等,这些数据会在请求生命周期内一直存在。
启动服务
将 grpc 服务绑定到指定的 tcp 监听器创建服务器程序。gRPC 客户端可以访问 8081 端口远程调用方法。启动服务会用到 pb 文件的 pb.RegisterIdentityInfoServiceServer(server,new (service.IdentityInfoService)) 方法。
gRPC 客户端
Go 实现
客户端的实现同样要引入 go get -u google.golang.org/grpc 依赖,也需要编译的 pb 文件的支持,通过 client := pb.NewIdentityInfoServiceClient(conn) 注册客户端实现远程调用。
Java 实现
将 protobuf 服务定义编译成 Java 版本也要安装相应的编译环境 protoc 和 protoc-gen-grpc-java,protoc-gen-grpc-java 下载 https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/1.9.1/ ,protoc-gen-grpc-java 下载后放到一个容易找到的位置,这个在编译时必须配置成绝对路径。
环境配置完成后,编译 protobuf 服务成 Java 源码,与 Go 略有不同,Java 的消息类型和 gRPC 服务是分开编译并且编译成两个文件的。使用命令 protoc --java_out=./ ./*.proto 编译 Java 的消息类型源码 IdentityInfoOuterClass.java,使用命令 protoc --plugin=protoc-gen-grpc-java=E:\c\dev2\bins\protoc-gen-grpc-java.exe --grpc-java_out=. --proto_path=. IdentityInfo.proto 编译出 Java 的 gRPC 服务源码 IdentityInfoServiceGrpc.java,无论服务端实现还是客户端实现都需要这两个文件作为依赖。编译出的源码如果出现 @Override 报错的情况要检查模块的 Java 版本是否太低,有的 IDE 在不设置情况下使用 Java5,应采用 Java8 以上版本。
需要引入的主要依赖:
Java客户端主要代码:
程序监控
系统监控一般监控系统级的指标数据和应用级指标数据两类数据。系统级指标数据包括 CPU 使用情况、内存使用情况等,应用级指标数据包括请求数量、请求错误率等。系统级的指标数据是程序运行期间捕获的。应用级指标数据根据程序的不同而采集不同的指标,例如采集 gRPC 远程方法的被调用次数。gRPC 中可以使用 OpenCensus 或 Prometheus 收集指标数据并可视化。
gRPC 服务端启用 Prometheus
启用 Prometheus 监控 gRPC 远程方法被调用次数,先引入依赖 go get github.com/grpc-ecosystem/go-grpc-prometheus 。
对 service 改造,远程方法中加入 CustomMetricCounter.WithLabelValues(in.Name).Inc(),增加访问的统计。在使用客户端调用远程方法后再访问 http://localhost:9010/metrics 可以查看到采集的指标数据。
gRPC 程序压力测试
Go gRPC 程序压力测试工具 bojand/ghz ,下载地址 https://github.com/bojand/ghz/releases 。解压得到可执行文件后,执行命令 ./ghz.exe --insecure --proto IdentityInfo.proto --call example1.IdentityInfoService.addIdentityInfo -d '{\"id\":5,\"name\":\"W5\"}' -n 80000 -c 500 localhost:8082 进行测试。命令中 -n 设置请求数,-c 设置并发线程数量。测试结果参数主要有 Total 总耗时、Slowest 最慢的耗时、Fastest 最快的耗时、Average 平均耗时等。
gRPC 生态
gRPC 网关
gRPC 网关插件能够让 protocol buffers 编译器读取 gRPC 服务定义,并生成反向代理服务器端,服务端能够将 RESTful JSON API 转换为 gRPC。这是转为 Go 编写的支持从 gRPC 和 HTTP 客户端调用 gRPC 服务。
常用中间件
分布式系统中的中间件是不同功能的组件。大部分常用中间件都是基于拦截器的,使用时注册拦截器就集成完成。常用的有日志、监控、重试、错误处理、速率限制等。日志库有 grpc_ctxtags、grpc_zap、grpc_logrus 等,监控 grpc_prometheus,客户端重试机制 grpc_retry 是客户端中间件,服务端错误转换将 panic 转出成 gRPC 错误中间件 grpc_recovery,限制器 retelimit 对 gRPC 速率限制。
健康检查协议
gRPC 定义了一个健康检查协议,gRPC 服务通过它暴露服务端的状态,调用者就可以查看服务端状态。健康检查的用途是客户端在检测到服务端不健康后可以在负载均衡列表中不优先访问这个服务端甚至移除。健康检查协议是定义一个 proto 服务,编译后使用多路复用的方式集成到服务中。
gRPC 健康探针
grpc_health_probe 是社区提供的 gRPC 健康探针工具,用于检查服务健康状态。服务端通过健康检查协议暴露服务端状态,健康探针能够与 gRPC 标准的监控检查服务通信。可以通过 CLI 工具的方式使用 grpc_health_probe ,若在 Kubernetes 上运行 gRPC 服务就可以通过运行 grpc_health_probe 作为服务端 pod 的存活性和就绪性状态检查。
Last updated