草庐IT

深入浅出gRPC

技术灭霸 2023-03-28 原文

一、gRPC介绍

gRPC 是在 HTTP/2 之上实现的 RPC 框架,HTTP/2 是第 7 层(应用层)协议,它运行在 TCP(第 4 层 - 传输层)协议之上,相比于传统的 REST/JSON 机制有诸多的优点:

  1. 基于 HTTP/2 之上的二进制协议(Protobuf 序列化机制);
  2. 一个连接上可以多路复用,并发处理多个请求和响应;
  3. 多种语言的类库实现;
  4. 服务定义文件和自动代码生成(.proto 文件和 Protobuf 编译工具)。

此外,gRPC 还提供了很多扩展点,用于对框架进行功能定制和扩展,例如,通过开放负载均衡接口可以无缝的与第三方组件进行集成对接(Zookeeper、域名解析服务、SLB 服务等)。

二、gRPC服务调用原理

一个完整的 RPC 调用流程示例如下:

RPC 请求消息发送流程

gRPC 默认基于 Netty HTTP/2 + PB 进行 RPC 调用,请求消息发送流程如下所示:

image

RPC 响应接收和处理流程

gRPC 客户端响应消息的接收入口是 NettyClientHandler,它的处理流程如下所示:

并行调用和异步调用

要解决串行调用效率低的问题,有两个解决对策:

  1. 并行服务调用,一次 I/O 操作,可以发起批量调用,然后同步等待响应;
  2. 异步服务调用,在同一个业务线程中异步执行多个服务调用,不阻塞业务线程。

采用并行服务调用的伪代码示例:

ParallelFuture future = ParallelService.invoke(serviceName [], methodName[], args []);
List<Object> results = future.get(timeout);// 同步阻塞式获取批量服务调用的响应列表

并行服务调用的一种实现策略如下所示:

异步服务调用的工作原理如下:


异步服务调用相比于同步服务调用有两个优点:

  1. 化串行为并行,提升服务调用效率,减少业务线程阻塞时间。
  2. 化同步为异步,避免业务线程阻塞。

基于 Future-Listener 的纯异步服务调用代码示例如下:

xxxService1.xxxMethod(Req);
Future f1 = RpcContext.getContext().getFuture();
Listener l = new xxxListener();
f1.addListener(l);
class xxxListener{
public void operationComplete(F future)
{ // 判断是否执行成功,执行后续业务流程}
     }

理解误区

1、异步服务就是异步吗?

实际上,通信框架基于 NIO 实现,并不意味着服务框架就支持异步服务调用了,两者本质上不是同一个层面的事情。在 RPC/ 微服务框架中,引入 NIO 带来的好处是显而易见的:

  • 所有的 I/O 操作都是非阻塞的,避免有限的 I/O 线程因为网络、对方处理慢等原因被阻塞;
  • 多路复用的 Reactor 线程模型:基于 Linux 的 epoll 和 Selector,一个 I/O 线程可以并行处理成百上千条链路,解决了传统同步 I/O 通信线程膨胀的问题。

NIO 只解决了通信层面的异步问题,跟服务调用的异步没有必然关系,也就是说,即便采用传统的 BIO 通信,依然可以实现异步服务调用,只不过通信效率和可靠性比较差而已。

对异步服务调用和通信框架的关系进行说明:


用户发起远程服务调用之后,经历层层业务逻辑处理、消息编码,最终序列化后的消息会被放入到通信框架的消息队列中。业务线程可以选择同步等待、也可以选择直接返回,通过消息队列的方式实现业务层和通信层的分离是比较成熟、典型的做法,目前主流的 RPC 框架或者 Web 服务器很少直接使用业务线程进行网络读写。

通过上图可以看出,采用 NIO 还是 BIO 对上层的业务是不可见的,双方的汇聚点就是消息队列,在 Java 实现中它通常就是个 Queue。业务线程将消息放入到发送队列中,可以选择主动等待或者立即返回,跟通信框架是否是 NIO 没有任何关系。因此不能认为 I/O 异步就代表服务调用也是异步的。

2、异步服务调用性能肯定更高吗?

对于 I/O 密集型,资源不是瓶颈,大部分时间都在同步等应答的场景,异步服务调用会带来巨大的吞吐量提升,资源使用率也可以提高,更加充分的利用硬件资源提升性能。

另外,对于时延不稳定的接口,例如依赖第三方服务的响应速度、数据库操作类等,通常异步服务调用也会带来性能提升。

但是,如果接口调用时延本身都非常小(例如毫秒级),内存计算型,不依赖第三方服务,内部也没有 I/O 操作,则异步服务调用并不会提升性能。能否提升性能,主要取决于业务的应用场景。

普通 RPC 调用

普通的 RPC 调用提供了三种实现方式:

  1. 同步阻塞式服务调用,通常实现类是 xxxBlockingStub(基于 proto 定义生成)。
  2. 异步非阻塞调用,基于 Future-Listener 机制,通常实现类是 xxxFutureStub。
  3. 异步非阻塞调用,基于 Reactive 的响应式编程模式,通常实现类是 xxxStub。

Streaming 模式服务调用

gRPC 服务调用支持同步和异步方式,同时也支持普通的 RPC 和 streaming 模式,可以最大程度满足业务的需求。
对于 streaming 模式,可以充分利用 HTTP/2.0 协议的多路复用功能,实现在一条 HTTP 链路上并行双向传输数据,有效的解决了 HTTP/1.X 的数据单向传输问题,在大幅减少 HTTP 连接的情况下,充分利用单条链路的性能,可以媲美传统的 RPC 私有长连接协议:更少的链路、更高的性能:

gRPC 的网络 I/O 通信基于 Netty 构建,服务调用底层统一使用异步方式,同步调用是在异步的基础上做了上层封装。因此,gRPC 的异步化是比较彻底的,对于提升 I/O 密集型业务的吞吐量和可靠性有很大的帮助。

三、gRPC线程模型

影响 RPC 框架性能的三个核心要素如下:

  1. I/O 模型:用什么样的通道将数据发送给对方,BIO、NIO 或者 AIO,IO 模型在很大程度上决定了框架的性能;
  2. 协议:采用什么样的通信协议,Rest+ JSON 或者基于 TCP 的私有二进制协议,协议的选择不同,性能模型也不同,相比于公有协议,内部私有二进制协议的性能通常可以被设计的更优;
  3. 线程:数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,通信线程模型的不同,对性能的影响也非常大。

gRPC 线程模型

消息的序列化和反序列化均由 gRPC 线程负责,而没有在 Netty 的 Handler 中做 CodeC,原因如下:Netty4 优化了线程模型,所有业务 Handler 都由 Netty 的 I/O 线程负责,通过串行化的方式消除锁竞争,原理如下所示:

如果大量的 Handler 都在 Netty I/O 线程中执行,一旦某些 Handler 执行比较耗时,则可能会反向影响 I/O 操作的执行,像序列化和反序列化操作,都是 CPU 密集型操作,更适合在业务应用线程池中执行,提升并发处理能力。因此,gRPC 并没有在 I/O 线程中做消息的序列化和反序列化。

改进点思考

1、时间可控的接口调用直接在 I/O 线程上处理

gRPC 采用的是网络 I/O 线程和业务调用线程分离的策略,大部分场景下该策略是最优的。但是,对于那些接口逻辑非常简单,执行时间很短,不需要与外部网元交互、访问数据库和磁盘,也不需要等待其它资源的,则建议接口调用直接在 Netty /O 线程中执行,不需要再投递到后端的服务线程池。避免线程上下文切换,同时也消除了线程并发问题。

例如提供配置项或者接口,系统默认将消息投递到后端服务调度线程,但是也支持短路策略,直接在 Netty 的 NioEventLoop 中执行消息的序列化和反序列化、以及服务接口调用。

2、减少锁竞争

当前 gRPC 的线程切换策略如下:

优化之后的 gRPC 线程切换策略:

通过线程绑定技术(例如采用一致性 hash 做映射), 将 Netty 的 I/O 线程与后端的服务调度线程做绑定,1 个 I/O 线程绑定一个或者多个服务调用线程,降低锁竞争,提升性能。

四、gRPC 安全性设计

RPC 调用安全主要涉及如下三点:

  1. 个人 / 企业敏感数据加密:例如针对个人的账号、密码、手机号等敏感信息进行加密传输,打印接口日志时需要做数据模糊化处理等,不能明文打印;
  2. 对调用方的身份认证:调用来源是否合法,是否有访问某个资源的权限,防止越权访问;
  3. 数据防篡改和完整性:通过对请求参数、消息头和消息体做签名,防止请求消息在传输过程中被非法篡改。

敏感数据加密传输

1. 基于 SSL/TLS 的通道加密
2. 针对敏感数据的单独加密

有些 RPC 调用并不涉及敏感数据的传输,或者敏感字段占比较低,为了最大程度的提升吞吐量,降低调用时延,通常会采用 HTTP/TCP + 敏感字段单独加密的方式,既保障了敏感信息的传输安全,同时也降低了采用 SSL/TLS 加密通道带来的性能损耗,对于 JDK 原生的 SSL 类库,这种性能提升尤其明显。

它的工作原理如下所示:

image

通常使用 Handler 拦截机制,对请求和响应消息进行统一拦截,根据注解或者加解密标识对敏感字段进行加解密,这样可以避免侵入业务。

认证和鉴权

1. 身份认证
2. 权限管控

在 RPC 调用领域比较流行的是基于 OAuth2.0 的权限认证机制,它的工作原理如下:

数据完整性和一致性

利用消息摘要可以保障数据的完整性和一致性,它的特点如下:

  • 单向 Hash 算法,从明文到密文的不可逆过程,即只能加密而不能解密;
  • 无论消息大小,经过消息摘要算法加密之后得到的密文长度都是固定的;
  • 输入相同,则输出一定相同。

目前常用的消息摘要算法是 SHA-1、MD5 和 MAC,MD5 可产生一个 128 位的散列值。 SHA-1 则是以 MD5 为原型设计的安全散列算法,可产生一个 160 位的散列值,安全性更高一些。MAC 除了能够保证消息的完整性,还能够保证来源的真实性。

有关深入浅出gRPC的更多相关文章

  1. ChatGPT教程之深入了解魔术背后的技术 - 2

    解开谜团:深入探索ChatGPT的技术奇迹。ChatGpt无处不在,无论是在播客、博客、YouTube还是社交媒体上。当我注意到这项新技术如此受欢迎时,我决定试一试,我被震惊了!有很多关于ChatGpt及其魔力的博客,但在这篇博客中,我将深入探讨其内部技术及其工作原理!ChatGpt简介根据OpenAI,ChatGpt被描述为:“我们训练了一个名为ChatGpt的模型,它以对话方式进行交互。对话格式使ChatGpt可以回答后续问题、承认错误、挑战不正确的前提并拒绝不适当的请求。ChatGPT是InstructGPT的兄弟模型,它经过训练可以按照提示中的说明进行操作并提供详细的响应。”OpenA

  2. 科大讯飞刘聪:由ChatGPT浪潮引发的深入思考与落地展望 - 2

    近期,以“生成式人工智能”(GenerativeAI)为核心技术的聊天机器人ChatGPT火爆全球。百度、阿里巴巴、科大讯飞、360等国内企业纷纷抛出ChatGPT相关进展,打造中国版的ChatGPT。科大讯飞此前在投资者互动平台表示,ChatGPT主要涉及到自然语言处理相关技术,属于认知智能领域的应用之一,公司在该方向技术和应用具备长期深厚的积累。并称2022年12月已进一步启动生成式预训练大模型任务攻关,类ChatGPT技术将在今年5月率先落地科大讯飞AI学习机产品。近日,科大讯飞副总裁、研究院执行院长刘聪围绕什么是ChatGPT,它强在哪里?会对未来世界带来哪些颠覆性影响?进一步阐述Ch

  3. 深入理解C++中的move和forward! - 2

    导语 |  在C++11标准之前,C++中默认的传值类型均为Copy语义,即:不论是指针类型还是值类型,都将会在进行函数调用时被完整的复制一份!对于非指针而言,开销及其巨大!因此在C++11以后,引入了右值和Move语义,极大地提高了效率。本文介绍了在此场景下两个常用的标准库函数:move和forward。一、特性背景(一)Copy语义简述C++中默认为Copy语义,因此存在大量开销。以下面的代码为例:0_copy_semantics.cc#include#includeclassObject{public:Object(){std::coutv;v.push_back(obj);}最终的输出

  4. 深入理解Linux文件系统与日志分析 - 2

    目录引言:一、inode和block1、inode和block概述2、inode的内容1.inode包含文件的元信息(文件属性)2.用stat命令可以查看某个文件的inode信息3.Linux系统文件三个主要的时间属性  4.目录文件的结构3、inode的号码​5、硬盘分区后的结构6、inode的大小7、inode的特殊作用 二、链接文件三、案例:恢复EXT类型的文件四、案例:恢复XFS类型的文件五、日志文件1.日志的功能2.日志文件的分类3.日志保存位置1.常见的一些日志文件:2.扩展:日志检查3.小结:​4.日志消息的级别5.用户日志分析六、总结引言:inode是一个重要概念,是理解Uni

  5. javascript - 对于 JavaScript 多维数组的深拷贝,深入一层似乎就足够了。这是真的吗? - 2

    注意:我只是一个编码新手,所以这个问题的核心可能存在明显的错误或误解。本质上,我需要在JavaScript中“按值”深度复制多维数组到未知深度。我原以为这需要一些复杂的递归,但似乎在JavaScript中您只需要深复制一个级别就可以按值复制整个数组。举个例子,这是我的测试代码,使用了一个故意复杂的数组。functiontest(){vararr=[['ok1'],[],[[],[],[[],[[['ok2'],[]]]]]];varcloned=cloneArray(arr);arr='';//Deletetheoriginalalert(cloned);}functioncloneA

  6. 深入解析 pycocotools 的安装和运行报错 && 安装 mmcv-full and mmpycocotools - 2

    pycocotools库的主要作用:下载coco数据集,并使得操作数据集的数据更加方便。MMCV是一个面向计算机视觉的基础库,它支持了很多开源项目。好的习惯:学会在官方文档中解决的问题。目录一、安装VisualStudio2022(其他版本也可以)二、下载pycocotools三、解析Why?四、安装mmpycocotools库(mmcv有用到)五、安装mmcv-full库(1)介绍mmcv(2)安装mmcv一、安装VisualStudio2022(其他版本也可以)直接去官方下载:VisualStudio2022IDE-适用于软件开发人员的编程工具(microsoft.com)注意:网上有很多

  7. javascript - 深入理解 : How code structure affects the content of date arrays created with loops - 2

    背景说明我问了一个关于使用循环定义日期数组的问题。数组是根据名为“dateinterval”的已声明变量定义的。我设计代码的方式导致了与另一个循环相关的错误消息,另一个用户为我提供了另一个循环来解决这个问题。既然我已经仔细比较了两种不同的解决方案,我就是不明白为什么它们不会产生相同的结果。我的代码我开发了以下代码来定义UTC格式的日期数组。然而,结果是自1970年1月1日00:00:00以来以毫秒为单位的日期数组。换句话说,一个数字。for(vari=0;i正确的解决方案下面的代码是另一位用户提供给我的正确代码(再次感谢您!)此代码定义了一组UTC日期。for(vari=0;i我不明白

  8. Android 深入系统完全讲解(30) - 2

    下图是生命周期的说明图:如图可以看到:当创建编解码器的时候处于未初始化状态。首先你需要调用configure(…)方法让它处于Configured状态,然后调用start()方法让其处于Executing状态。在Executing状态下,你就可以使用上面提到的缓冲区来处理数据。Executing的状态下也分为三种子状态:Flushed,Running、End-of-Stream。在start()调用后,编解码器处于Flushed状态,这个状态下它保存着所有的缓冲区。一旦第一个输入buffer出现了,编解码器就会自动运行到Running的状态。当带有end-of-stream标志的buffer进

  9. javascript - 深入了解 Ember App Kit - 与使用普通 Ember 的区别 - 2

    我已经开始使用emberappkit并认真阅读itsguides.然而,我无法理解常规应用程序与EmberAppKit使用ES6模块构建各种位的方式之间的差异,而不是将所有内容填充到用作命名空间(例如App)的全局变量中。我发现这方面解释的不是很清楚:Ember如何在自动生成模型、View、路由和Controller方面发挥其魔力?它希望在哪里找到它们?我应该遵循哪些命名约定?如果我已经创建了一个模板、路由或Controller,而Ember没有找到/检测到它,而只是在它的位置生成一个默认的,我如何找到它正在寻找的地方;或者在这种情况下进行调试?与使用EmberAppKit进行开发相比

  10. javascript - 深入了解 Ember.js - 2

    我刚刚被告知我必须从事的一些项目使用Ember.js框架。它看起来很有趣,我想获得更多关于它的知识。我也看过官方网站,但我认为仍然缺乏适合初学者的适当教程。此外,我只是对Javascript有基本的了解。我应该从哪里开始?Javascript还是直接使用Ember.js?编辑:我很乐意看到所有Javascript开发人员和初学者如何开始学习Javascript的建议。我打算做的是阅读EloquentJavascript并直接进入Ember.js。如果我遇到任何问题,我可以引用SO。 最佳答案 就个人而言,作为@sl7_7,我开始使用

随机推荐